# `otel/instruments` — OpenTelemetry instruments _(no submodules)_ ## Purpose Builds the OpenTelemetry instrument bundle emitted by the canonical `ImportService` so consumers can wire production observability without hand-rolling counter names. The bundle is a single cached object keyed against the global `MeterProvider` — the first `getImportInstruments()` call resolves the meter; subsequent calls return the same reference. Tests rebind by installing their own `MeterProvider`, calling `resetImportInstruments()`, and reading back via an `InMemoryMetricExporter`. ## Features - Nine instruments: `jobsTotal`, `rowsTotal`, `rowErrorsTotal`, `durationMs` (histogram), `storeWritesTotal`, `storeReadsTotal`, `sseConnected`, `sseEventsSent`, `sseDisconnected`. - `storeWritesTotal{outcome}` and `storeReadsTotal{outcome}` are emitted by `SurrealImportStore` on every adapter call so dashboards can split backend health from upstream import success/failure. The in-memory adapter (`MemoryImportStore`) does not emit; durable backends do. - `sseConnected`, `sseEventsSent{kind}`, and `sseDisconnected{reason}` are emitted by `GET /imports/:id/progress` so dashboards can chart open SSE connections, frame throughput by event kind, and teardown causes (terminal `complete` / `failed` vs client abort). - Cached bundle — production code does not re-resolve the meter on every emit. - `resetImportInstruments()` drops the cache so a freshly-installed `MeterProvider` rebinds on the next call. - Asserted by `instruments.test.ts` against a real in-memory exporter (no mocks). ## Usage: Basic Example ```ts import { getImportInstruments } from "@suaveplan/api-plugin-import"; const instruments = getImportInstruments(); instruments.jobsTotal.add(1, { outcome: "success" }); instruments.rowsTotal.add(42, { outcome: "success" }); instruments.durationMs.record(123); ``` ## Usage: Advanced Example ```ts import { metrics } from "@opentelemetry/api"; import { AggregationTemporality, InMemoryMetricExporter, MeterProvider, PeriodicExportingMetricReader, } from "@opentelemetry/sdk-metrics"; import { getImportInstruments, resetImportInstruments } from "@suaveplan/api-plugin-import"; const exporter = new InMemoryMetricExporter(AggregationTemporality.CUMULATIVE); const reader = new PeriodicExportingMetricReader({ exporter, exportIntervalMillis: 60_000 }); const provider = new MeterProvider({ readers: [reader] }); metrics.setGlobalMeterProvider(provider); resetImportInstruments(); const instruments = getImportInstruments(); instruments.jobsTotal.add(1, { outcome: "success" }); await provider.forceFlush(); const collected = exporter.getMetrics(); // collected[0].scopeMetrics[0].metrics — read back the counter datapoint ``` ## API Reference - `interface ImportInstruments { jobsTotal: Counter; rowsTotal: Counter; rowErrorsTotal: Counter; durationMs: Histogram; storeWritesTotal: Counter; storeReadsTotal: Counter; sseConnected: Counter; sseEventsSent: Counter; sseDisconnected: Counter }` - `getImportInstruments(): ImportInstruments` - `resetImportInstruments(): void` ## Implementation Notes - The meter is named `@suaveplan/api-plugin-import` — matching the package name keeps cross-package metric reports readable. - Counters carry the `outcome` label so dashboards can split jobs/rows by success vs error without re-aggregating. - The histogram unit is set to `"ms"` so OTel collectors that respect unit annotation produce labelled charts; consumers MUST emit millisecond values. - The cache is module-scoped — every consumer of the package shares the same instrument set. This is the desired behaviour because the meter resolves against the global `MeterProvider`, which is a process-wide singleton.