# 3pm-backend route `logsQuery`

Paths are relative to the backend's origin (https://fleet.edgecr4ft.net, or your own 3pm-backend), not to this site. Index: https://3pm.edgecr4ft.net/docs/backend/api.md

---

tokens:
  claude: 2596
  o200k: 1650
# POST /api/buckets/{clusterId}/logs/query (logsQuery)

The logs the bucket's nodes shipped over a range: their sources, the templates with their counts (worst first) and the shipped lines (newest first), filtered by node, source, owner, severity, template or text.

- auth: an account — Authorization: Bearer 3pmu_<token>, or the dashboard's session cookie
- body: LogsQuery (JSON)
- answer: LogsResult (JSON)

## Details

Body `{ range, filters?, text?, limits? }`. `range` is the events query's (`from` / `to` as epoch milliseconds or `now-<duration>`, no grid). `filters` are exact matches, AND-ed: `node` (a node id), `src` (the node's source id), `owner` (the writer's name, as the source declares it), `tpl` (a template id, unique per source of a node only: it needs `node` and `src`, else 400), `sev` — one word of `debug`, `info`, `notice`, `warning`, `err`, `crit`, a list of them, or `<word>+` for that level or worse (`warning+`). `text` keeps the shipped lines that contain it (case sensitive). `limits` = `{ templates, lines }`.

Answer `{ range, sources, templates, lines, truncated }`. `sources`: every source the nodes declared over the range, `{ node, hostname, src, kind, path?, app?, owner?, format?, lines, shipped, dropped, templates }` — the lines counted, shipped and stored, read and not shipped, and the templates with lines, over the range. `templates`: worst severity first, then by lines, `{ node, src, tpl, sev, text, lines, first_seen, last_seen, params: [{ var, distinct, top }] }`; the templates a node does not label on their own (beyond its 2 000 busiest) come back together per source and severity as `tpl: null`, `text: "(other templates)"`. `lines`: the lines shipped, newest first, `{ ts, node, hostname, src, tpl?, sev, why, line, ident?, unit? }`. `truncated` counts what the limits cut (`sources` past the 5 000 the catalog reads), and `counts` the nodes whose counts the engine would not give (asked a few nodes at a time; their templates and source `lines` are missing, the rest of the answer stands). Counts need `--metrics-url`; without it they are 0. The counts follow `node`, `src`, `owner`, `sev` and `tpl`; `text` filters the lines (and `shipped`) only — a count has no text. A template's `sev` is the worst severity counted for it over the range, the one it is ranked and filtered on. Counts come in windows of about 5 minutes stamped at their end: a window ending just after `from` counts whole, and the window still open at `to` is not counted yet, so the last 5 to 10 minutes lag and a short range may show lines with no counts. `params` are read for the templates returned only, and come back empty rather than failing the answer: `distinct` is the largest over the whole range, `top` the values most often in the tops of the latest 2 000 windows. Bounds: `limits.logsQuery` of `GET /api/v1/routes`.

Example — what went wrong on the bucket over the last six hours, errors and worse only:

{"range":{"from":"now-6h"},"filters":{"sev":"err+"},"limits":{"templates":50,"lines":100}}

## Bounds

- default_lines: 200
- default_templates: 200
- max_body_bytes: 16384
- max_lines: 1000
- max_range_ms: 2592000000
- max_templates: 500
- max_value_len: 256

## Types

```ts
/** `POST /api/buckets/{id}/logs/query`: the logs the bucket's nodes shipped. */
interface LogsQuery {
  /** `step` is ignored; 30 days at most. */
  range: TimeRange;
  filters?: LogFilters;
  /** A substring of the shipped lines (case sensitive). */
  text?: string;
  limits?: { templates?: number; lines?: number };
}

interface LogsResult {
  range: { from: EpochMs; to: EpochMs };
  sources: LogSourceView[];
  /** Worst severity first, then by lines. */
  templates: LogTemplateView[];
  /** Newest first. */
  lines: LogLineView[];
  /** How many sources, templates and lines the limits cut; `counts`: nodes whose counts the engine would not give (their templates and source `lines` missing), 0 when whole. */
  truncated: { sources: number; templates: number; lines: number; counts: number };
}

interface TimeRange {
  from: TimeValue;
  /** Default `now`. */
  to?: TimeValue;
  /** `30s`, `5m`, `1h` or milliseconds (what `step_ms` answers); omitted ⇒ derived from `options.max_points`. Both ends are aligned to it. */
  step?: DurationValue;
}

/** Exact matches; `sev` is one word, a list, or `warning+` (that level or worse). */
interface LogFilters {
  node?: NodeId;
  src?: string;
  owner?: string;
  sev?: string | string[];
  /** A template id: unique per source of a node only, so `node` and `src` are required with it. */
  tpl?: number;
}

/** Milliseconds since the Unix epoch. */
type EpochMs = number;

interface LogSourceView {
  node: NodeId;
  hostname: string;
  src: string;
  /** `app`, `journal`, `file`. */
  kind: string;
  path?: string;
  app?: string;
  owner?: string;
  format?: string;
  /** Lines counted over the range. */
  lines: number;
  /** Lines shipped and stored over the range. */
  shipped: number;
  /** Lines the node read and did not ship. */
  dropped: number;
  /** Templates with lines over the range. */
  templates: number;
}

interface LogTemplateView {
  node: NodeId;
  src: string;
  /** `null`: the templates without a label of their own, together (`"(other templates)"`). */
  tpl: number | null;
  sev: string;
  text: string;
  lines: number;
  first_seen: EpochMs | null;
  last_seen: EpochMs | null;
  params: { var: string; distinct: number; top: string[] }[];
}

interface LogLineView {
  ts: EpochMs;
  node: NodeId;
  hostname: string;
  src: string;
  tpl?: number;
  sev: string;
  /** Why it was shipped: `new`, `error`, `back`, `spike`, `return`, `context`. */
  why: string;
  line: string;
  ident?: string;
  unit?: string;
}

/** Epoch milliseconds, or `now` / `now-<duration>` (`now-1h`, `now-7d`). */
type TimeValue = EpochMs | string;

/** Milliseconds, or `30s` / `5m` / `1h` / `7d` / `2w`. */
type DurationValue = number | string;

/** `3pmn_…` — a node. */
type NodeId = string;
```
