Files
skills/docs/networking/events/broadcast-channel/src/otel/metrics.md
2026-05-29 08:20:56 +01:00

4.0 KiB

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

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

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.