Files
skills/docs/api-stack/api/api-host/src/otel/otel.md
2026-05-29 08:20:56 +01:00

3.8 KiB

otel — host instrument registry

(no submodules)

Purpose

createHostInstruments builds the frozen bundle of every OpenTelemetry counter and histogram the host emits on a production path. The instrument list mirrors design doc §12.3 exactly — every name is a published surface and removing one is a breaking change. The package never installs an OTel SDK; the meter-provider source is, in priority order, the explicit meterProvider override and then the OTel global provider (which falls back to a no-op when no SDK is wired). This keeps SDK initialisation an operator concern, not a library concern.

The registry also vends per-plugin meters via forPlugin(name) — each call returns a cached meter namespaced under @suaveplan/api-host/plugin/<name> with the plugin label pre-attached. Stage-4 packages plug their plugin-declared manifest.telemetry instruments onto these meters.

Features

  • Twelve counters (HOST_COUNTER_NAMES) and four histograms (HOST_HISTOGRAM_NAMES) covering the full §12.3 metric surface.
  • Pre-tagged host meter with host.id + host.version attributes.
  • Histogram units (ms, By) set at construction time.
  • Cached per-plugin meters with plugin.name pre-attached.
  • tenantLabelOptIn flag — defaults to false to bound cardinality.
  • dispose() clears the per-plugin meter cache for graceful teardown.

Usage: Basic

import { createHostInstruments } from "@suaveplan/api-host/otel";

const instruments = createHostInstruments({
    hostId: "host-1",
    hostVersion: "0.1.0",
});

instruments.counters["api.requests_total"].add(1, { route: "/billing/invoices" });
instruments.histograms["api.request_duration_ms"].record(42, {
    route: "/billing/invoices",
});

Usage: Advanced

Plug an explicit meter-provider (test harness or operator-supplied SDK):

import {
    HOST_COUNTER_NAMES,
    createHostInstruments,
} from "@suaveplan/api-host/otel";
import { MeterProvider } from "@opentelemetry/sdk-metrics";

const provider = new MeterProvider({
    /* readers / views configured by the operator */
});

const instruments = createHostInstruments({
    hostId: "host-1",
    hostVersion: "0.1.0",
    tenantLabelOptIn: true,
    meterProvider: provider,
});

// Per-plugin meter — caches per `pluginName`.
const billingMeter = instruments.forPlugin("billing").meter;
const errorsByReason = billingMeter.createCounter("billing.errors_total");

// Verify the surface is complete.
for (const name of HOST_COUNTER_NAMES) {
    instruments.counters[name]; // present.
}

API Reference

  • createHostInstruments(config: HostInstrumentsConfig): HostInstruments
  • HOST_COUNTER_NAMES — readonly tuple of every counter name.
  • HOST_HISTOGRAM_NAMES — readonly tuple of every histogram name.
  • HostInstruments:
    • meter: Meter
    • tenantLabelOptIn: boolean
    • counters: Readonly<Record<HostCounterName, Counter>>
    • histograms: Readonly<Record<HostHistogramName, Histogram>>
    • forPlugin(name): PluginInstruments
    • dispose(): void
  • PluginInstruments: { meter: Meter; pluginName: string }.

Implementation Notes

The counter / histogram lists are exported as readonly tuples so consumers can iterate and assert against them in tests — a missing instrument shows up as a test failure, not a runtime no-op. Per-plugin meters are cached by name to avoid leaking meter objects on every reload (one meter per plugin is the design invariant — disposal happens at uninstall, mirrored by dispose() here clearing the whole cache). Tenant-label opt-in is gated at the call site rather than the registry level: the registry exposes the flag and the request pipeline (Chunk B) decides whether to attach tenant to a given measurement. This keeps the cardinality story explicit at every emit site rather than hidden behind a registry policy.