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.
interval=wholegives one exact-count bucket — the way to count logs, since log lists have nototal.- 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— trustexpected_bucket_countand each bucket’sstart/end. top_kapplies per bucket; it doesn’t enumerate a whole population. Use the attribute catalogue’sservice_namesfor a full service inventory.- An attribute group with
present: falseis the never-set cohort; filter it withNOT attribute.<key>:*. event.nameis 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):
attributes.truncated: false. Values come from a recent-row sample — values.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.
- Optional fields (
event_name,trace_id,span_id,scope_version,flags,dropped_attributes_count) may be omitted rather thannull. - A record carries no normalized
severityfield — only numericseverity_numberand the emitter’s rawseverity_text. Derive the band fromseverity_number: 1–4 trace, 5–8 debug, 9–12 info, 13–16 warn, 17–20 error, 21–24 fatal, 0 unspecified. - Parse
timeas an instant before sorting — fractional seconds have variable width, so lexical comparison is unsafe. Don’t usetimealone 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 rawseverity_text.warningaliaseswarn.unspecifiedhas no ordering, so ordered comparisons andseverity!="error"omit unspecified records unless explicitly ORed in. - Use
service.name, notattribute.service.name; usetrace_id/span_id, notattribute.trace_id— the normalized fields cover both promoted OTel context and same-named attributes.
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.
event: readyannounces the resolved filter/window, snapshot count, and whether the connection resumed.event: logcarriesdata: {"log": {...}, "replay": true|false}. The resume token is the SSEid:line, not a JSON field.event: heartbeatkeeps idle connections alive.event: errorannounces termination — reconnect after the SSEretry:delay (milliseconds), withLast-Event-IDand the same filter (a different filter is rejected).
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.
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