# `otel.ts` — OpenTelemetry surface for `audio-ir-loader` _(no submodules)_ ## Purpose This module owns the loader's OpenTelemetry instrumentation. It lazily builds a single, cached `AudioIrLoaderInstruments` bag bound to either the global `MeterProvider` or a per-process override installed via `setAudioIrLoaderMeterProvider`. The bag carries every counter and histogram the spec requires (`bytes_read_total`, `frames_out_total`, `cache_hits_total`, `cache_misses_total`, `errors_total`, plus the `load_duration_ms` histogram). The split between this module and the loader/cache modules is deliberate: all metric definitions live here so dashboards can grep for instrument names in one place, and the loader/cache modules call recorder helpers (`recordSuccess`, `recordCacheHit`, `recordCacheMiss`, `recordError`) that close over the cached bag. ## Features - Real `MeterProvider` override for tests — no mocks; tests install an in-memory recorder via `setAudioIrLoaderMeterProvider`. - Histogram + five counters covering success / cache-hit / cache-miss / error outcomes. - Span-attribute helper `uriKindOf(uri)` resolves the `audio_ir.uri_kind` dimension from a URI's scheme without re-parsing the whole URI. ## Usage: Basic ```ts import { recordSuccess, setAudioIrLoaderMeterProvider, } from "@suaveplan/audio-ir-loader"; import { metrics } from "@opentelemetry/api"; // In an app bootstrap: setAudioIrLoaderMeterProvider(metrics.getMeterProvider()); recordSuccess("room", 100000, 25000, 12.5); ``` ## Usage: Advanced Tests install a real in-memory provider that records every observation: ```ts import { getInstruments, setAudioIrLoaderMeterProvider, } from "@suaveplan/audio-ir-loader"; import type { MeterProvider } from "@opentelemetry/api"; const observed: { name: string; value: number; attrs: Record }[] = []; const provider = { getMeter: () => ({ createCounter: (name: string) => ({ add: (v: number, a?: Record) => observed.push({ name, value: v, attrs: a ?? {} }), }), createHistogram: (name: string) => ({ record: (v: number, a?: Record) => observed.push({ name, value: v, attrs: a ?? {} }), }), }), } as unknown as MeterProvider; setAudioIrLoaderMeterProvider(provider); getInstruments(true); ``` ## API Reference ### `setAudioIrLoaderMeterProvider(provider | undefined): void` Install a meter provider override. Pass `undefined` to revert. ### `getInstruments(force?: boolean): AudioIrLoaderInstruments` Lazily builds (and caches) the instrument bag. ### `resetInstruments(): void` Drops the cache — used between test provider swaps. ### `recordSuccess(family, bytes, frames, durationMs): void` Increments `bytes_read_total{family}` and `frames_out_total{family}`, then records `load_duration_ms{family, outcome:"success"}`. ### `recordCacheHit(family, durationMs): void` / `recordCacheMiss(family): void` Counter + duration bookkeeping for cache outcomes. ### `recordError(code, family, durationMs): void` Increments `errors_total{code}` and records the duration with `outcome:"error"`. ### `uriKindOf(uri): "bundle" | "fallback" | "https" | "drive" | "other"` Maps a URI's leading scheme to the span-attribute dimension. ## Implementation Notes The cached instrument bag is rebuilt every time `setAudioIrLoaderMeterProvider` swaps the provider — otherwise tests would observe stale instruments bound to the previous provider's meter. The `force` parameter on `getInstruments` exists exclusively to let tests immediately rebind after `setAudioIrLoaderMeterProvider` to avoid a wasted check on the next call. `uriKindOf` deliberately treats `http` and `https` identically because callers care about the on-the-wire transport, not the TLS bit.