aboutsummaryrefslogtreecommitdiff
path: root/src/zenserver/sessions/httpsessions.cpp
diff options
context:
space:
mode:
authorStefan Boberg <[email protected]>2026-03-13 19:46:52 +0100
committerStefan Boberg <[email protected]>2026-03-13 19:46:52 +0100
commitab797d3a57786b01f95bcc1ed3e4091a211dca66 (patch)
tree2b25d53d6acce959082ffe92526f3ee12c0d29d8 /src/zenserver/sessions/httpsessions.cpp
parentAdd object store dashboard page (diff)
downloadzen-ab797d3a57786b01f95bcc1ed3e4091a211dca66.tar.xz
zen-ab797d3a57786b01f95bcc1ed3e4091a211dca66.zip
Add session log ingestion, dashboard visualization, and log forwarding sink
- Add POST/GET endpoints for session logs (/sessions/{id}/log) supporting raw text (newline-split) and structured JSON/CbObject with batch "entries" array - Add in-memory log storage per session (capped at 10k entries) with pagination - Add log panel to sessions dashboard with polling, auto-scroll, and level coloring - Add SessionLogSink that batches log messages on a background thread and forwards them to the sessions endpoint via HTTP, integrated into ZenStorageServer when a remote sessions URL is configured
Diffstat (limited to 'src/zenserver/sessions/httpsessions.cpp')
-rw-r--r--src/zenserver/sessions/httpsessions.cpp161
1 files changed, 161 insertions, 0 deletions
diff --git a/src/zenserver/sessions/httpsessions.cpp b/src/zenserver/sessions/httpsessions.cpp
index 6996ce5b5..f6eb77c15 100644
--- a/src/zenserver/sessions/httpsessions.cpp
+++ b/src/zenserver/sessions/httpsessions.cpp
@@ -111,6 +111,11 @@ HttpSessionsService::Initialize()
HttpVerb::kGet | HttpVerb::kPost | HttpVerb::kPut | HttpVerb::kDelete);
m_Router.RegisterRoute(
+ "{session_id}/log",
+ [this](HttpRouterRequest& Req) { SessionLogRequest(Req); },
+ HttpVerb::kGet | HttpVerb::kPost);
+
+ m_Router.RegisterRoute(
"",
[this](HttpRouterRequest& Req) { ListSessionsRequest(Req); },
HttpVerb::kGet);
@@ -315,6 +320,162 @@ HttpSessionsService::SessionRequest(HttpRouterRequest& Req)
//////////////////////////////////////////////////////////////////////////
//
+// Session log
+//
+
+static void
+WriteLogEntry(CbWriter& Writer, const SessionsService::LogEntry& Entry)
+{
+ Writer << "timestamp" << Entry.Timestamp;
+ if (!Entry.Level.empty())
+ {
+ Writer << "level" << Entry.Level;
+ }
+ if (!Entry.Message.empty())
+ {
+ Writer << "message" << Entry.Message;
+ }
+ if (Entry.Data.GetSize() > 0)
+ {
+ Writer.AddObject("data"sv, Entry.Data);
+ }
+}
+
+void
+HttpSessionsService::SessionLogRequest(HttpRouterRequest& Req)
+{
+ HttpServerRequest& ServerRequest = Req.ServerRequest();
+
+ const Oid SessionId = Oid::TryFromHexString(Req.GetCapture(1));
+ if (SessionId == Oid::Zero)
+ {
+ m_SessionsStats.BadRequestCount++;
+ return ServerRequest.WriteResponse(HttpResponseCode::BadRequest,
+ HttpContentType::kText,
+ fmt::format("Invalid session id '{}'", Req.GetCapture(1)));
+ }
+
+ m_SessionsStats.RequestCount++;
+
+ Ref<SessionsService::Session> Session = m_Sessions.GetSession(SessionId);
+ if (!Session)
+ {
+ return ServerRequest.WriteResponse(HttpResponseCode::NotFound,
+ HttpContentType::kText,
+ fmt::format("Session '{}' not found", SessionId));
+ }
+
+ if (ServerRequest.RequestVerb() == HttpVerb::kPost)
+ {
+ m_SessionsStats.SessionWriteCount++;
+
+ if (ServerRequest.RequestContentType() == HttpContentType::kText)
+ {
+ // Raw text — split by newlines, one entry per line
+ IoBuffer Payload = ServerRequest.ReadPayload();
+ std::string_view Text(reinterpret_cast<const char*>(Payload.GetData()), Payload.GetSize());
+ const DateTime Now = DateTime::Now();
+
+ size_t Pos = 0;
+ while (Pos < Text.size())
+ {
+ size_t End = Text.find('\n', Pos);
+ if (End == std::string_view::npos)
+ {
+ End = Text.size();
+ }
+
+ std::string_view Line = Text.substr(Pos, End - Pos);
+ // Strip trailing \r
+ if (!Line.empty() && Line.back() == '\r')
+ {
+ Line.remove_suffix(1);
+ }
+
+ if (!Line.empty())
+ {
+ Session->AppendLog(SessionsService::LogEntry{
+ .Timestamp = Now,
+ .Message = std::string(Line),
+ });
+ }
+
+ Pos = End + 1;
+ }
+ }
+ else
+ {
+ // Structured log (JSON or CbObject)
+ // Accepts a single record or an "entries" array of records
+ CbObject RequestObject = ServerRequest.ReadPayloadObject();
+ const DateTime Now = DateTime::Now();
+
+ auto AppendFromObject = [&](CbObjectView Obj) {
+ std::string Level(Obj["level"sv].AsString());
+ std::string Message(Obj["message"sv].AsString());
+ CbObjectView DataView = Obj["data"sv].AsObjectView();
+
+ Session->AppendLog(SessionsService::LogEntry{
+ .Timestamp = Now,
+ .Level = std::move(Level),
+ .Message = std::move(Message),
+ .Data = CbObject::Clone(DataView),
+ });
+ };
+
+ CbFieldView EntriesField = RequestObject["entries"sv];
+ if (EntriesField.IsArray())
+ {
+ for (CbFieldView Entry : EntriesField)
+ {
+ AppendFromObject(Entry.AsObjectView());
+ }
+ }
+ else
+ {
+ AppendFromObject(RequestObject);
+ }
+ }
+
+ return ServerRequest.WriteResponse(HttpResponseCode::OK);
+ }
+ else
+ {
+ // GET - return log entries
+ m_SessionsStats.SessionReadCount++;
+
+ HttpServerRequest::QueryParams Params = ServerRequest.GetQueryParams();
+ uint32_t Limit = 0;
+ uint32_t Offset = 0;
+
+ if (std::string_view LimitStr = Params.GetValue("limit"sv); !LimitStr.empty())
+ {
+ Limit = uint32_t(std::strtoul(std::string(LimitStr).c_str(), nullptr, 10));
+ }
+ if (std::string_view OffsetStr = Params.GetValue("offset"sv); !OffsetStr.empty())
+ {
+ Offset = uint32_t(std::strtoul(std::string(OffsetStr).c_str(), nullptr, 10));
+ }
+
+ std::vector<SessionsService::LogEntry> Entries = Session->GetLogEntries(Limit, Offset);
+
+ CbObjectWriter Response;
+ Response << "total" << Session->GetLogCount();
+ Response.BeginArray("entries");
+ for (const SessionsService::LogEntry& Entry : Entries)
+ {
+ Response.BeginObject();
+ WriteLogEntry(Response, Entry);
+ Response.EndObject();
+ }
+ Response.EndArray();
+
+ return ServerRequest.WriteResponse(HttpResponseCode::OK, Response.Save());
+ }
+}
+
+//////////////////////////////////////////////////////////////////////////
+//
// WebSocket push
//