# otel/metrics _(no submodules)_ ## Purpose This module declares the six OpenTelemetry counter instruments emitted by `@suaveplan/broadcast-channel`. Every counter is registered against the meter `@suaveplan/broadcast-channel@0.1.0` so a single global `MeterProvider` configuration in the host application picks all of them up. The instruments close the observability gap the enterprise audit flagged ("No structured logging / OTel counters"): each lifecycle event a channel observes is counted, attributed by channel name and either message type or drop reason. The `refreshBroadcastChannelMeters()` function re-resolves every instrument against the current global `MeterProvider`. Tests call it after wiring the in-memory exporter from `tests/setup/polyfills.ts` so subsequent record calls land on the exporter rather than the noop meter resolved at module load. ## Features - Six counters: `channel.message.{sent,received,dropped}`, `channel.tab.{joined,left}`, `channel.leader.elected`. - Stable attribute shape: `{ channel }` plus `{ type }` or `{ reason }`. - `refreshBroadcastChannelMeters()` for test rebinding. - No span / tracer surface — counters only, by design. - Module-load-safe under the noop meter when no `MeterProvider` is configured. ## Usage: Basic ```ts import { metrics } from "@opentelemetry/api"; import { MeterProvider } from "@opentelemetry/sdk-metrics"; import { refreshBroadcastChannelMeters, TypedBroadcastChannel, nativeChannelFactory, } from "@suaveplan/broadcast-channel"; metrics.setGlobalMeterProvider(new MeterProvider({ readers: [] })); refreshBroadcastChannelMeters(); const channel = new TypedBroadcastChannel({ config: { name: "app-bus" }, factory: nativeChannelFactory, }); channel.open(); channel.send("greet", "hi"); // increments channel.message.sent ``` ## Usage: Advanced ```ts import { metrics } from "@opentelemetry/api"; import { AggregationTemporality, InMemoryMetricExporter, MeterProvider, PeriodicExportingMetricReader, } from "@opentelemetry/sdk-metrics"; import { refreshBroadcastChannelMeters } from "@suaveplan/broadcast-channel"; const exporter = new InMemoryMetricExporter(AggregationTemporality.CUMULATIVE); const reader = new PeriodicExportingMetricReader({ exporter, exportIntervalMillis: 60_000, }); metrics.setGlobalMeterProvider(new MeterProvider({ readers: [reader] })); refreshBroadcastChannelMeters(); // Later, in a test assertion: const collected = exporter.getMetrics(); const names = collected.flatMap((rm) => rm.scopeMetrics.flatMap((sm) => sm.metrics.map((m) => m.descriptor.name)) ); expect(names).toContain("channel.message.sent"); ``` ## API Reference - `refreshBroadcastChannelMeters(): void` — re-resolve every instrument against the current global `MeterProvider`. - `recordMessageSent(type: string, channelName: string): void` - `recordMessageReceived(type: string, channelName: string): void` - `recordMessageDropped(reason: "malformed", channelName: string): void` - `recordTabJoined(channelName: string): void` - `recordTabLeft(channelName: string): void` - `recordLeaderElected(channelName: string): void` Each `record*` helper calls `.add(1, attrs)` on the corresponding counter. ## Implementation Notes The instruments are stored in module-local `let` bindings so `refreshBroadcastChannelMeters` can rebind them without forcing callers to hold a stable reference. This mirrors the pattern used by `@suaveplan/slack-client`: at module load the meter is whatever the global provider returns (typically the noop provider, in which case `.add()` is a no-op), and the explicit refresh after `setGlobalMeterProvider` swaps in real instruments. The attribute keys (`channel`, `type`, `reason`) are kept short to minimise cardinality blow-up; consumer dashboards group on `channel` first to slice per logical bus. No histograms are exposed — the package has no meaningful latency axis (`send` is synchronous, dispatch is in-process) so a histogram would be empty noise.