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
MeterProvideris 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 globalMeterProvider.recordMessageSent(type: string, channelName: string): voidrecordMessageReceived(type: string, channelName: string): voidrecordMessageDropped(reason: "malformed", channelName: string): voidrecordTabJoined(channelName: string): voidrecordTabLeft(channelName: string): voidrecordLeaderElected(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.