> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mirador.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Logs

> Exact log stats, attribute discovery, filtered records, and a live SSE tail

Logs are OpenTelemetry log records, [ingested via OTLP](/opentelemetry/logs) and queried here. Investigate **from coarse to fine**:

<Steps>
  <Step title="Count first">
    `GET /v1/logs/stats` — exact bucketed counts, groups, and exemplars.
  </Step>

  <Step title="Discover the field universe">
    `GET /v1/logs/attributes` — the service inventory and real attribute keys.
  </Step>

  <Step title="Read records">
    `GET /v1/logs` — filtered records, newest-first, cursor-paged.
  </Step>

  <Step title="Tail live (optional)">
    `GET /v1/logs/stream` — best-effort SSE tail, when live is genuinely needed.
  </Step>
</Steps>

## Stats

### `GET /v1/logs/stats`

Exact counts over time buckets, optionally grouped, with sample records attached.

| Query param      | Description                                                                                                                               |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `filter`         | AIP-160 expression (see [Log filters](#log-filters))                                                                                      |
| `since`, `until` | Default: last 1 hour. Max span: 7 days.                                                                                                   |
| `interval`       | `auto` (default), `whole`, `1m`, `5m`, `15m`, `30m`, `1h`, `6h`, `1d`                                                                     |
| `group_by`       | Repeatable up to **two** dimensions: `service.name`, `severity`, `event.name`, or one `attribute.<key>` (at most one attribute dimension) |
| `top_k`          | 1–20 groups per bucket (default 10)                                                                                                       |
| `exemplars`      | 0–3 recent sample records per group (default 1); requires `group_by`                                                                      |

```bash theme={null}
curl -sS --fail-with-body -G \
  -H "Authorization: Bearer $MIRADOR_SERVER_KEY" \
  --data-urlencode 'since=1h' \
  --data-urlencode 'interval=5m' \
  --data-urlencode 'group_by=severity' \
  --data-urlencode 'group_by=service.name' \
  https://api.mirador.org/v1/logs/stats
```

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):

```json theme={null}
{
  "project_id": "prj_...",
  "range_start": "...",
  "range_end": "...",
  "attributes": { "items": [ { "key": "...", "values": { "items": [...], "truncated": false } } ], "truncated": false },
  "service_names": { "items": ["checkout", "payments"], "truncated": false }
}
```

Keys are complete for the window only when `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](/api/endpoints#pagination).

```bash theme={null}
curl -sS --fail-with-body -G \
  -H "Authorization: Bearer $MIRADOR_SERVER_KEY" \
  --data-urlencode 'since=1h' \
  --data-urlencode 'filter=severity="error"' \
  --data-urlencode 'page_size=10' \
  https://api.mirador.org/v1/logs
```

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

| Field                 | Operators                       | `OR`     | `NOT` | Meaning                                                  |
| --------------------- | ------------------------------- | -------- | ----- | -------------------------------------------------------- |
| `body`                | `:`                             | no       | no    | **Token** search, not substring — all tokens must occur  |
| `severity`            | `=`, `!=`, `<`, `<=`, `>`, `>=` | yes      | yes   | Normalized `trace < debug < info < warn < error < fatal` |
| `service.name`        | `=`, `!=`                       | equality | yes   | Canonical promoted service field                         |
| `event.name`          | `=`, `!=`                       | equality | yes   | Structured event/failure class, when emitters set it     |
| `trace_id`, `span_id` | `=`                             | no       | no    | Case-insensitive hex, normalized correlation IDs         |
| `attribute.<key>`     | `=`, `!=`, `:*`                 | equality | yes   | Log attributes take precedence over resource attributes  |

* `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](/api/endpoints#filtering).

## 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.

```bash theme={null}
# Bound exploratory tails; curl exit 28 is expected when --max-time expires.
curl -sS --fail-with-body -N --max-time 20 -G \
  -H "Authorization: Bearer $MIRADOR_SERVER_KEY" \
  --data-urlencode 'filter=severity>="error"' \
  --data-urlencode 'window=5m' \
  https://api.mirador.org/v1/logs/stream
```

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).

<Warning>
  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.
</Warning>

## 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`.

```bash theme={null}
GET /v1/logs?filter=trace_id="4bf92f3577b34da6a3ce929d0e0e4736"
  &since=2026-06-18T17:01:11Z&until=2026-06-18T17:03:23Z
```

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

<CardGroup cols={2}>
  <Card title="Metrics" icon="chart-line" href="/api/metrics">
    Discover metrics and evaluate PromQL
  </Card>

  <Card title="Sending logs via OTLP" icon="right-to-bracket" href="/opentelemetry/logs">
    Get logs into Mirador in the first place
  </Card>
</CardGroup>
