# 3pm-backend route `metricsInventory`

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: 3060
  o200k: 1864
# GET /api/buckets/{clusterId}/metrics/inventory (metricsInventory)

What a dashboard or an LLM starts from, in one call: the metrics the bucket holds series for, with their descriptions, its hosts and their processes — text for an LLM by default, JSON with `?format=json` or `Accept: application/json` (what this client sends).

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

## Details

Text for an LLM by default; the generated client asks for the JSON (`Accept: application/json`), and so does `?format=json`. The JSON is `{ tokens, metric_groups, label_values, hosts }`, shaped for few tokens: nothing empty, nothing derivable, no join to make. `tokens` comes first: what the whole answer costs an LLM, estimated from its bytes with the ratio measured for its shape on Claude's tokenizer (about 2.4 bytes a token; OpenAI's counts 1.5–1.7× fewer). `metric_groups` are the catalog lines that have at least one series stored over the last two UTC days — the catalog says what a query may name, this says what would draw something — grouped by the labels they carry: `{ labels, metrics: [{ id, kind, unit?, default_time?, description?, apps? }] }`. `default_time` is left out when it is the kind's (`avg_over_time` for a gauge, `rate` for a counter, `quantile` for a histogram), `unit` and `description` when empty; a line is an application's exactly when its id starts with `app.`, and `apps` lists the apps whose series carry it (one read of every application series but a histogram's buckets, at most 5000: past that, the lists may be short). `label_values` are `{ label, values, more? }`: what every other label of those lines holds over the same window (not `node`, `app`, `instance`, `ns` — `hosts` has them — nor `pid`), sorted, at most 50 values, `more` counting the rest. `hosts` are every enrolled node, `{ node_id, hostname, os, arch, state, processes: [{ app, instance, ns? }] }`: the instances the store holds a series for over the same window, label values that go into a query's `filters` as they are, next to `node` = `node_id`. Every series carries `node` = a `node_id`: a query names a host by it, never by hostname, and `hosts` turns a legend's `node` into a name. `?detail=index` leaves the descriptions out, about 40 % fewer tokens. The text (`?format=llm`, the default) is the same answer as TOON-style `text/plain` after an `about:` line saying how the lists join, about a fifth fewer tokens than the JSON (more than half fewer with `detail=index`): a header per list with its count and fields (`metrics[140]{id,kind,unit,default_time,description}:`), one row per entry, every `default_time` spelled out. An unknown parameter or value is a 400. The store is read for at most 5000 series names and 5000 processes. Without `--metrics-url` the route answers 409 `ERR_STORE_DISABLED`.

## Types

```ts
/** `metricsInventory(clusterId)`: what a dashboard or an LLM starts from. The route answers TOON-style text by default; this client asks for this JSON (`Accept: application/json`), as does `?format=json`. `?detail=index` leaves the descriptions out. */
interface MetricsInventory {
  /** What this whole answer costs an LLM; first, so a reader knows before the rest. */
  tokens: TokenCount;
  /** The catalog lines with a series stored over the last two UTC days, grouped by the labels they carry; groups and lines in catalog order. */
  metric_groups: MetricGroup[];
  /** What every other label of those lines holds over the same window — not `node`, `app`, `instance`, `ns` (`hosts` has them) nor `pid` —, in the order the lines name them; a label that holds nothing is left out. */
  label_values: InventoryLabel[];
  /** Every enrolled node of the bucket, whatever its state, with its processes; a node only the store knows comes last, `state: "unknown"`. Every series carries `node` = `node_id`: a query names a host by it, never by hostname, and this list names a legend's `node`. */
  hosts: InventoryHost[];
}

/** What an answer costs to put in a prompt: the first field of every answer that carries it (`metricsInventory`, `GET /api/v1/routes`). */
interface TokenCount {
  /** Claude's tokenizer, estimated from the bytes with a ratio measured per shape through the `count_tokens` API — within a few percent. */
  claude: number;
  /** OpenAI's `o200k_base`, exact. Claude counts 1.5–1.7× more on this backend's answers. */
  o200k: number;
}

interface MetricGroup {
  /** What `filters` and `aggregate.by` may name for every metric of the group. */
  labels: QueryLabel[];
  metrics: InventoryMetric[];
}

interface InventoryLabel {
  label: QueryLabel;
  /** Sorted, at most 50: values a filter takes as written. */
  values: string[];
  /** How many more there were past the 50; absent when none. */
  more?: number;
}

interface InventoryHost {
  node_id: NodeId;
  hostname: string;
  os: string;
  arch: string;
  /** As `NodeView.state`; `"unknown"` for a node only the store knows. */
  state: DisplayState | "unknown";
  /** The instances the store holds a series for, sorted: label values, ready for `filters` next to `node` = `node_id`. */
  processes: InventoryProcess[];
}

/** The labels a query may filter or group on; the bucket itself is never one of them. */
/** A built-in label, or an application's own label name (`^[a-z][a-z0-9_]{0,31}$`, not reserved) on a metric that carries it. */
type QueryLabel = "node" | "app" | "instance" | "ns" | "mount" | "window" | "src" | "core" | "iface" | "blockdev" | "sensor" | "gpu" | "array" | "proc" | (string & {});

/** A catalog line, minus what is empty or derivable: it is an application's exactly when `id` starts with `app.`. */
interface InventoryMetric {
  id: string;
  kind: MetricKind;
  /** Absent when the metric has none. */
  unit?: string;
  /** Absent when it is the kind's: `avg_over_time` for a gauge, `rate` for a counter, `quantile` for a histogram. */
  default_time?: TimeFnName;
  /** Absent when the application gave none, and with `?detail=index`. */
  description?: string;
  /** An application line only: the apps whose series carry it, sorted — absent when none did. With several, put `app` in `aggregate.by` or a filter. */
  apps?: string[];
}

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

/** `NodeView.state`: the enrollment state, or the presence of an active node. */
type DisplayState = NodeState | "online" | "stale" | "offline";

interface InventoryProcess {
  app: string;
  instance: string;
  ns?: string;
}

type MetricKind = "gauge" | "counter" | "histogram";

/** `rate`, `increase`, `delta` for counters; the `*_over_time` family for gauges; `raw` for either; `quantile` (with `q`), `rate` or `increase` for a histogram. */
type TimeFnName =
  | "raw"
  | "rate"
  | "increase"
  | "delta"
  | "avg_over_time"
  | "min_over_time"
  | "max_over_time"
  | "last_over_time"
  | "quantile_over_time"
  | "quantile";

/** Enrollment state in the database, presence aside. */
type NodeState = "active" | "pending" | "revoked" | "unknown";
```
