# `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/` 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 ```ts 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): ```ts 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>` - `histograms: Readonly>` - `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.