# 3pm-backend route `metricsByServer`

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: 2678
  o200k: 1648
# GET /api/buckets/{clusterId}/metrics/servers (metricsByServer)

Which metric is stored on which server, for which app: each node with its host's own metrics, then each of its apps with theirs — 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
- node: also open to a node's own bearer (3pma_…) when its row is granted fleet:list; that node's bucket only, and for the calls it placed itself
- answer: MetricsByServer (JSON)

## Details

The inventory by server, for a question that starts from a host or an app: what can be queried with `node` = this node_id (and `app` = this app) without drawing nothing. The JSON is `{ tokens, metric_groups, servers }`. `tokens` comes first, priced as `metricsInventory`'s. `metric_groups` are the catalog lines with a series on some server over the last 24 hours, grouped by the labels they carry, as `metricsInventory` gives them (without `apps`): what each id below is — kind, unit, default time function, labels, description. `servers` is an object keyed by node_id — the value of the `node` label every series carries —: every enrolled node in the bucket's order, then a node only the store knows (`state: "unknown"`), each `{ hostname, os, arch, state, metrics?, apps? }`. `metrics` are the ids of the host's own lines stored on that server (series without `app`: `system.*`, `daemon.*`, `node.*`…), in catalog order; `apps` is an object keyed by app name, sorted, each the ids whose series carry `app` = that name on that server — `process.*`, `subprocess.*`, the app's own `app.*` — in catalog order. A list that would be empty is left out. Present means at least one series over the last 24 hours, not that it is reported right now. `?detail=index` leaves the descriptions out. The text (`?format=llm`, the default) is the same answer as TOON-style `text/plain`: an `about:` line, `metric_groups` as `metricsInventory` writes them, then `servers:` with a key per node_id and, under `apps:`, one `<app>[<count>]: <ids>` line per app. One instant query on the store answers for the whole bucket, cached a minute. An unknown parameter or value is a 400; without `--metrics-url` the route answers 409 `ERR_STORE_DISABLED`.

## Types

```ts
/** `metricsByServer(clusterId)`: which metric is stored on which server, for which app. 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 MetricsByServer {
  /** What this whole answer costs an LLM; first, so a reader knows before the rest. */
  tokens: TokenCount;
  /** The catalog lines with a series on some server over the last 24 hours, grouped by the labels they carry as `metricsInventory` gives them (without `apps`): what each id of `servers` is. */
  metric_groups: MetricGroup[];
  /** Keyed by node_id — the `node` label every series carries —: the enrolled nodes in the bucket's order, then a node only the store knows, `state: "unknown"`. */
  servers: Record<NodeId, ServerMetrics>;
}

/** 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[];
}

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

/** One server of `MetricsByServer`: ids only, in catalog order. Present means at least one series over the last 24 hours. */
interface ServerMetrics {
  hostname: string;
  os: string;
  arch: string;
  /** As `NodeView.state`; `"unknown"` for a node only the store knows. */
  state: DisplayState | "unknown";
  /** The host's own lines (series without `app`); absent when none. */
  metrics?: string[];
  /** Keyed by app name, sorted: the lines whose series carry `app` = that name on this server — `process.*`, `subprocess.*`, the app's own `app.*`; absent when none. */
  apps?: Record<string, string[]>;
}

/** 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[];
}

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

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";
```
