Skip to main content
Logs are OpenTelemetry log records, ingested via OTLP and queried here. Investigate from coarse to fine:
1

Count first

GET /v1/logs/stats — exact bucketed counts, groups, and exemplars.
2

Discover the field universe

GET /v1/logs/attributes — the service inventory and real attribute keys.
3

Read records

GET /v1/logs — filtered records, newest-first, cursor-paged.
4

Tail live (optional)

GET /v1/logs/stream — best-effort SSE tail, when live is genuinely needed.

Stats

GET /v1/logs/stats

Exact counts over time buckets, optionally grouped, with sample records attached.
Semantics worth knowing:
  • interval=whole gives one exact-count bucket — the way to count logs, since log lists have no total.
  • For every bucket, total_count == sum(groups[].count) + other_count. Empty expected buckets are returned explicitly.
  • Bucket boundaries are aligned independently of the request boundary, so clipped first/last buckets can make the bucket count differ from naive duration / interval — trust expected_bucket_count and each bucket’s start/end.
  • top_k applies per bucket; it doesn’t enumerate a whole population. Use the attribute catalogue’s service_names for a full service inventory.
  • An attribute group with present: false is the never-set cohort; filter it with NOT attribute.<key>:*.
  • event.name is the best structured classifier when emitters populate it. If they don’t, group by service or a discovered attribute, then inspect exemplars — body text can’t be grouped by stats.

Discovery

GET /v1/logs/attributes

The service inventory and real attribute keys/values (default: last 24 hours, max 7 days):
Keys are complete for the window only when attributes.truncated: false. Values come from a recent-row samplevalues.truncated: true can accompany empty values.items, so test an unlisted value with a direct filter rather than concluding it doesn’t exist.

Records

GET /v1/logs

Filtered records, newest-first. since defaults to the last 1 hour (max 35 days); page_size is 1–1000 (default 100) with an opaque next_page_token — see Pagination.
Record semantics:
  • Optional fields (event_name, trace_id, span_id, scope_version, flags, dropped_attributes_count) may be omitted rather than null.
  • A record carries no normalized severity field — only numeric severity_number and the emitter’s raw severity_text. Derive the band from severity_number: 1–4 trace, 5–8 debug, 9–12 info, 13–16 warn, 17–20 error, 21–24 fatal, 0 unspecified.
  • Parse time as an instant before sorting — fractional seconds have variable width, so lexical comparison is unsafe. Don’t use time alone as a dedupe key.

Log filters

  • body: splits on non-alphanumeric characters and matches tokens; repeated body terms are ANDed and share one 256-byte budget.
  • Filter on the normalized severity, never raw severity_text. warning aliases warn. unspecified has no ordering, so ordered comparisons and severity!="error" omit unspecified records unless explicitly ORed in.
  • Use service.name, not attribute.service.name; use trace_id/span_id, not attribute.trace_id — the normalized fields cover both promoted OTel context and same-named attributes.
Shared grammar rules (quoting, OR-on-one-field, dotted keys, size caps) are on Endpoints & Conventions.

Live SSE stream

GET /v1/logs/stream

Tails logs as Server-Sent Events. Accepts the same filter, plus window (replay depth, default 5m, enum up to 30d), page_size (default 100), heartbeat seconds (5–60, default 15), and an optional Last-Event-ID header to resume.
SSE framing:
  • event: ready announces the resolved filter/window, snapshot count, and whether the connection resumed.
  • event: log carries data: {"log": {...}, "replay": true|false}. The resume token is the SSE id: line, not a JSON field.
  • event: heartbeat keeps idle connections alive.
  • event: error announces termination — reconnect after the SSE retry: delay (milliseconds), with Last-Event-ID and the same filter (a different filter is rejected).
The tail is best-effort, not at-least-once. Late visibility can fall behind the cursor, and bursts larger than a poll batch can drop the middle to stay current. The connection also closes after 1 hour. Dedupe by SSE event ID, and re-read the bounded /v1/logs window when completeness matters.

Correlating traces and logs

trace_id is the pivot. The log filter normalizes the promoted OTel trace field and a same-named log attribute, so one trace_id="..." predicate covers both storage forms. Only the promoted half is indexed — the attribute-borne half scans the selected window, so bound the window: start one minute before the trace’s created_at and end one minute after created_at + duration_ms.
If the pivot comes back empty for a trace that clearly has activity, widen the pad to a few minutes — created_at is stamped at ingestion while duration_ms spans producer-stamped timestamps, and the two clocks can drift. An empty pivot means no correlated logs in that project/window: logs written outside trace context have no trace ID, and trace/log retention can differ.

Next Steps

Metrics

Discover metrics and evaluate PromQL

Sending logs via OTLP

Get logs into Mirador in the first place