Log as JSON for Loki #91
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The log is text, so the Grafana dashboard (grafana/dashboard.py) picks lines apart with regular expressions: the access log's 'GET /x -> 200 in 3ms' and the events' '<- {json}'. Logged as JSON with the request's method, path, status and time, and each event's fields, as fields, Loki's json parser reads them directly and a change of wording breaks nothing.
Fixed in
4f8b3d6: IPX_LOG_FORMAT=json logs one JSON object a line, with the access log's method, path, route, status and ms and each event's fields as fields. Production sets it; the Grafana dashboard reads the fields with Loki's json parser. Deployed.Follow-up from Dash0's structured logging guide (https://www.dash0.com/guides/structured-logging-for-modern-applications), checked against what production logs now (
4f8b3d6). These are the parts that apply to ipx, most useful first.1. Put the trace id on every log line. The guide's main point: log lines carry
trace_idandspan_idso a log line and its trace can be joined. Ours don't. A line hasspan(name, feed), but nothing that leads to Tempo.query.py traceonly shows log lines because tracing-opentelemetry also records them as span events, and that doesn't work in the other direction: from afeed_erroror a slowipx::httpline in Loki there is no way to find its trace. The fix is to add the OTel context to the JSON formatter's fields (tracing-opentelemetry'sOtelDataon the current span, via a customFormatEventor a layer that records it). A Loki derived field ontrace_idthen links each line to Tempo in Grafana. This also covers the guide's "propagate a request id": the trace id is that id, for requests and scans both.2. Record failures as fields, not as sentences. The guide wants errors structured (
exception.type,exception.messagein OTel's naming) rather than written into the message. Ours:ipx::scanwarnings are only a message:gizmodo: error: HTTP 404 Not Found. Theipx::ioline for the same event hasfeedandmsg, butmsgis still one string holding the status, the DNS failure or the redirect loop.msg. That's the same text-parsing #91 set out to remove.http.response.status_codewhere there is one, and anerror.typetaken from whatexplain_failurein src/feed.rs already works out (http_404,dns,redirect_loop,forbidden,timeout, ...), tofeed_erroranddownload_error.error = ?e(Debug) anderror = %e(Display) are mixed, in main.rs:466/472 and access.rs among others. Pick one, Display with{:#}for the anyhow chain, so the field reads the same everywhere.3. Units in field names. The guide recommends
duration_msoverms.bytesalready says what it is;msmostly does. Renamingmsbreaks the Grafana panels andipx-prod-check's queries (CLAUDE.md: rename a field and its panels go blank), so only do it together with regeneratinggrafana/dashboard.pyand updating the skill. Low value on its own. Any new field should carry its unit from the start, though.4. One event, one line. Every scan event is logged twice, once as
ipx::io(fields) and once asipx::scan(the same thing in words, and the only one at WARN). Theipx::iomessage is also the whole event again as JSON text (<- {"ev":...}) beside the flattened fields. In JSON mode, one line per event would do: theipx::ioline at the event's own level (WARN for failures), with a short human message. The text format for a terminal can stay as it is. That halves the volume Loki stores and makes check 1 inipx-prod-check(warnings grouped by message) group by feed properly.Not needed here:
severityNumberand a separatebody/attributessplit: that is the OTLP log data model, and ipx ships to Loki, whereleveland flattened fields are what LogQL wants.service.nameand resource attributes: Alloy labels the stream withcontainer. Worth adding only if logs ever go over OTLP next to the traces.feed,pathandtrace_idmust stay in the line, not in stream labels.Fixed in
448e557, all four follow-ups: JSON lines inside a traced span carry trace_id and span_id (the access log included, now written inside its request's span); feed and download failures carry error.type and http.response.status_code; each scan event is one line under ipx::scan (the wire copy is at debug, for the admin page); the access log's ms is duration_ms. grafana/dashboard.py and the ipx-prod-check skill follow, and the dashboard is regenerated. Deployed 2026-09-29 16:26 UTC. Tempo's link to Loki (filterByTraceID) now finds lines; the link the other way, Loki to Tempo, needs a derived field on trace_id in the monitoring project's loki.yaml, which is outside this repo.Loki to Tempo is linked too: /mnt/fast/arcane/projects/monitoring/grafana-provisioning/datasources/loki.yaml has a derived field matching "trace_id":"..." in a line, pointing at Tempo (uid P214B5B846CF3925F). Grafana restarted to load it.