Metrics
Metrics — 11 operations on https://api.hanzo.ai.
Also for this capability: API · CLI · SDKs
The REST reference for Metrics — 11 operations, generated from the OpenAPI document.
| Base URL | https://api.hanzo.ai |
| Operations | 11 |
| Auth | Authorization: Bearer $HANZO_API_KEY |
Specification
HIP-1241 · Metrics — One Native Store, Three Signals — Draft · read the specification →
/v1/metrics is the cloud's native signal store: metrics, logs and traces,
written and queried per tenant against one WAL-durable store under the
deployment's data dir. The routes are registered by the hanzoai/metrics
module and mounted by hanzoai/cloud (build.go:1162, MountMetrics); the
capability's row is manifest/apps.go:59. This HIP replaces HIP-0064, which
specified a log stack — a collector fleet, a shared warehouse database and a
search service — that this codebase does not run.
Motivation
One app, one store, three top-level addresses: the module serves
/v1/metrics/*, /v1/logs/* and /v1/traces/* from the same store. That is
HIP-0139 §7.1's exact case — an app with one store is one capability however
many nouns it answers for — and the two foreign roots fold.
Specification
The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as in RFC 2119.
The addresses, and the two that fold
The capability answers eleven operations (plugin/metrics/openapi.json):
write, query and health for each signal, plus the metric batch door and the
single-trace read. Every route MUST be under /v1/metrics:
/v1/logs/{write,query,health}→/v1/metrics/logs/{write,query,health}/v1/traces/{write,query,trace,health}→/v1/metrics/traces/{…}
The two standing pairs are ledgered in cloud's openapi/misfiled.txt and
close by this fold, never by alias. A rename to a covering word was considered
and refused: HIP-0139 §7.3 permits taking only an EXISTING address's name, and
neither logs nor traces names the whole. o11y is not the owner — it
serves /v1/o11y/logs and /v1/o11y/traces against a different store
(HIP-1240).
Every operation is value-shaped, and none is typed in cloud: the module
registers its own routes and imports only zap-proto/zip and luxfi —
deliberately, it depends on what it uses — so it cannot reach the prose
registry. The prose is therefore declared beside the mount
(build.go:1197, describeMetrics), keyed to the addresses, registered only
when the routes are.
The store
The capability owns its own store and nothing else: a per-tenant, WAL-durable
native store under Deps.DataDir (hanzoai/metrics mount.go), ingesting
luxfi/metric.MetricBatch — the same shape the ZAP MsgMetricBatch transport
carries — behind a deliberately tiny storage API (Append, Query, SeriesCount).
It shares nothing with the event.* warehouse plane.
The tenant
The org is handed down, never read here: MountMetrics passes
Org: principal.Org — the identity boundary's one tenant decision (HIP-0026)
— into the module's own Deps (build.go:1167-1169). The module MUST NOT
resolve its own tenant from a request header. It once did, reading X-Org-Id
directly, and a header a caller sends meant an anonymous request could read
and write any org's telemetry (build.go:1141-1146); the rule lives once, at
the boundary that authenticates.
Price, events, emission, stage
It is free, in those words: Price: cloud.Free
(plugin/metrics/main.go:21). It publishes no events on the bus, so a
customer's webhooks receive nothing from it. It emits nothing to observability
beyond the request span every route gets: it is where signals land, not an
emitter of its own. The stage is ga — the manifest row declares none, and
absent is ga (HIP-0139 §8).
Upstream
The module is hanzoai/metrics v1.110.6 (go.mod:693), MIT, copyright 2026
Hanzo AI. It derives from none: the store is native Go, prometheus-free by
declaration (store.go package doc), speaking PromQL-compatible queries as a
wire fact. The v1.110.6 tag also carries the module's own NOTICE declaring the
repository archived and succeeded by hanzoai/o11y; hanzoai/cloud at HEAD
remains its importer and this surface remains served, so the succession is a
proposal this HIP's amendment process owns, not a fact of the router.
Rationale
The alternative to the fold is three sibling capabilities, one per signal. That is refused on the store: one writer, one directory, one registry — splitting it would put three names on one store, the defect HIP-0106 names. The alternative to declaring prose in cloud is having the module import the registry, which would trade its whole dependency discipline for docstrings.
Security Considerations
The wrong implementation here is the one the module shipped first: a tenant resolved from a caller-supplied header, which turns every write door into a cross-tenant write and every query door into a cross-tenant read. The fix is structural — the org function is injected by the boundary that validated the principal, and the module has no other way to name a tenant. Ingest doors accept unauthenticated-looking traffic shaped as batches; they still resolve the same injected org, so a batch without a validated principal lands nowhere.
Four surfaces
| Surface | Reaches this capability as | Coverage |
|---|---|---|
| REST | metrics at its own prefix | 11 operations |
| CLI | hanzo metrics … | 4 of 11 — the CLI pins the document on its own clock |
| SDK | MetricsApi in every published client | 4 of 11 — the clients are generated at their own release |
| MCP | tool metrics on https://api.hanzo.ai/v1/mcp | 11 operations, 0 under the document's own id — ask describe for the rest |
Quickstart
export HANZO_API_KEY=sk-... # console.hanzo.ai → API keysThen the first call — a read that needs nothing but the key. GET /v1/metrics/query, operation get_metrics_query:
hanzo metrics queryimport { Configuration, MetricsApi } from 'hanzoai';
const api = new MetricsApi(new Configuration({ accessToken: process.env.HANZO_API_KEY }));
const { data } = await api.getMetricsQuery();from hanzoai.cloud import ApiClient, Configuration
from hanzoai.cloud.api import MetricsApi
client = ApiClient(Configuration(access_token=os.environ["HANZO_API_KEY"]))
result = MetricsApi(client).get_metrics_query()cfg := cloud.NewConfiguration()
cfg.AddDefaultHeader("Authorization", "Bearer "+os.Getenv("HANZO_API_KEY"))
client := cloud.NewAPIClient(cfg)
resp, _, err := client.MetricsAPI.GetMetricsQuery(context.Background()).Execute()
if err != nil {
return err
}use hanzo_cloud::apis::{configuration::Configuration, metrics_api};
let mut cfg = Configuration::new();
cfg.bearer_access_token = std::env::var("HANZO_API_KEY").ok();
let result = metrics_api::get_metrics_query(&cfg, Default::default()).await?;import ai.hanzo.cloud.ApiClient;
import ai.hanzo.cloud.api.MetricsApi;
ApiClient client = new ApiClient();
client.setRequestInterceptor(b -> b.header("Authorization", "Bearer " + System.getenv("HANZO_API_KEY")));
var result = new MetricsApi(client).getMetricsQuery();curl https://api.hanzo.ai/v1/metrics/query \
-H "Authorization: Bearer $HANZO_API_KEY"MCP reaches metrics through the metrics tool, which names its 11 operations with its own verbs — this one among them, under a name only MCP declares. describe explains any of them:
curl -X POST https://api.hanzo.ai/v1/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "describe",
"arguments": {
"op": "get_log_health"
}
}
}'Endpoints
| Endpoint | What it does |
|---|---|
POST /v1/metrics/batch | Ingest a MetricBatch — the same payload the ZAP transport carries |
GET /v1/metrics/health | How many metric series this deployment holds for your org |
GET /v1/metrics/logs/health | How many log records this deployment holds for your org |
GET /v1/metrics/logs/query | Search your org's logs by label, time and substring |
POST /v1/metrics/logs/write | Append structured log records for your org |
GET /v1/metrics/query | Read your org's series back over a time range |
GET /v1/metrics/traces/health | How many spans this deployment holds for your org |
GET /v1/metrics/traces/query | Recent spans for your org over a time range |
GET /v1/metrics/traces/trace | Every span of one trace — the waterfall |
POST /v1/metrics/traces/write | Append spans for your org |
POST /v1/metrics/write | Append samples to your org's named, labelled series |
How is this guide?