Skip to content

HTTP API

For: anyone writing a client, a script or an agent against a running node.

One port serves all of it (listen.http, 4318 by default). OTLP/gRPC is a separate port (listen.grpc, 4317) and a separate protocol, so it is not in this table.

Request and response bodies are KYAML, of which JSON is a subset — so an ordinary JSON client needs nothing special, and content-type is not load-bearing.

Each group below is a module, and its rustdoc is the reasoning behind those routes rather than a restatement of this table: Ingest is receiver; Query is api; Agents is mcp; Operate is alert and main; Web UI is ui.

Ingest

Endpoint What it answers
POST /v1/logs OTLP/HTTP log export. Protobuf or JSON, gzip either way.
POST /v1/traces OTLP/HTTP trace export. Protobuf or JSON, gzip either way.
POST /v1/metrics OTLP/HTTP metric export. Protobuf or JSON, gzip either way.

Query

Endpoint What it answers
POST /api/v1/query Log records or spans matching a filter, newest first.
POST /api/v1/metrics/query One metric's series over a window, with the exemplars naming the traces behind the points.
POST /api/v1/metrics/names Which metric names a window holds. An empty body means right now.
POST /api/v1/correlate The frame around a filter: its time extent, the traces it touches and the services that took part — one round trip where a client makes three.
POST /api/v1/map The service map over a window.
POST /api/v1/entities Every service that produced anything in a window.

Agents

Endpoint What it answers
POST /mcp The MCP endpoint: JSON-RPC in, one of the eight tools in TOOLS out.

Operate

Endpoint What it answers
GET /health Liveness, plus what the ingest path has refused so far.
GET /readyz Readiness: 200 while exports can be made durable, 503 once they cannot.
GET /api/v1/stats Everything this node knows about itself, in one document.
GET /api/v1/alerts Every rule this node evaluates and what it is currently doing.

Web UI

Endpoint What it answers
GET / The web UI. One HTML document; every view lives under the location hash.
GET /{file} One of the three assets, or nothing.

Request bodies

Every query endpoint takes a KYAML document and enforces a closed key set: an unknown key is a 400 naming it, rather than a filter that was silently not applied.

from and to are each one of three things: "now", a signed duration relative to now ("-15m", "+2h"), or Unix nanoseconds as a number. Left out, the window is the last 1h ending now.

POST /api/v1/query

Log records or spans matching a filter, newest first.

Accepts signal, from, to, where, limit, after.

{
  "signal": "logs",
  "from": "-15m",
  "to": "now",
  "where": [
    { "attr": "service.name", "eq": "checkout" },
    { "field": "severity_number", "gte": 17 },
    { "attr": "http.route", "contains": "/api" },
  ],
  "limit": 100,
  "after": "1757241600000000000.2718281828.7.41",
}

Answers under rows, with the envelope below.

POST /api/v1/metrics/query

One metric's series over a window, with the exemplars naming the traces behind the points.

Accepts name, from, to, where, max_series, max_points.

{
  "name": "http.server.request.duration",
  "from": "-1h",
  "where": [ { "attr": "service.name", "eq": "checkout" } ],
  "max_series": 50,
}

Answers under series, with the envelope below.

POST /api/v1/metrics/names

Which metric names a window holds. An empty body means right now.

Accepts from, to. An empty body is valid, and means the last 1h.

{ "from": "-1h", "to": "now" }

Answers under names, with the envelope below.

POST /api/v1/correlate

The frame around a filter: its time extent, the traces it touches and the services that took part — one round trip where a client makes three.

Accepts signal, from, to, where, limit, after, expand.

{
  "signal": "logs",
  "from": "-15m",
  "where": [ { "field": "severity_number", "gte": 17 } ],
  "expand": [ "traces", "around:2s", "peers" ],
}

Answers under frame, with the envelope below.

POST /api/v1/map

The service map over a window.

Accepts from, to, max_spans. An empty body is valid, and means the last 1h.

{ "from": "-15m", "to": "now", "max_spans": "50000" }

Answers under map, with the envelope below.

POST /api/v1/entities

Every service that produced anything in a window.

Accepts from, to. An empty body is valid, and means the last 1h.

{ "from": "-15m", "to": "now" }

Answers under entities, with the envelope below.

where terms

A term names its target and its operator in two keys rather than three: {"attr": "service.name", "eq": "checkout"}, not a target/op/value triple. Terms in a list are ANDed; there is no or.

  • attr — an OTLP attribute key, on the record, its scope or its resource. Any key at all; nothing is declared in advance.
  • field — a column of the signal itself, from the tables below.

Operators: eq or = or ==, ne or !=, lt or <, lte or <=, gt or >, gte or >=, contains or ~.

field names for signal: logs:

Field Compare against
id number
time_unix_nano nanoseconds
observed_time_unix_nano nanoseconds
severity_number number
severity_text string
event_name string
body string
trace_id hex string
span_id hex string
flags number
dropped_attributes_count number
resource_id number
scope_id number

field names for signal: traces:

Field Compare against
id number
trace_id hex string
span_id hex string
parent_span_id hex string
trace_state string
flags number
name string
kind number
start_time_unix_nano nanoseconds
duration_nano number
status_code number
status_message string
dropped_attributes_count number
dropped_events_count number
dropped_links_count number
resource_id number
scope_id number

The response envelope

Every query answers with its own field plus one stats object, so a caller always learns what the read cost:

{
  "rows": [ ... ],           # or series, names, frame, map, entities
  "stats": {
    "blocks_total": 0,
    "blocks_scanned": 0,
    "rows_scanned": 0,
    "rows_matched": 0,
    "elapsed_us": 0,
  },
  "next": "...",             # absent on the last page
}

elapsed_us is wall time including the wait for a scan permit — the number the caller actually waited. next is absent rather than null when there is no page behind this one, so if (doc.next) is the whole of a reader's paging logic; pass it back as after.

Status codes

Code When
200 The answer, as the envelope below.
400 The document did not parse, named a key no endpoint accepts, or gave a value out of range. The body is {"error": "..."} — JSON, like the success, so a caller has one thing to parse.
500 The read failed or its thread panicked. The body is the error text.
413 The export was larger than ingest.max_request_bytes.
503 The signal's queue is full and the export was shed. Retry it: this is backpressure, not a failure, and ingest.queue is how much of it an operator buys away.
415 The export was not protobuf.
202 An OTLP export was queued, or an MCP notification was taken. Accepted means durable when ingest.wal is on and queued when it is off.
304 A UI asset the caller already has. The bundle is built into the binary, so its ETag is its content hash.
404 No such UI asset.