5.8 KiB
OpenSpec Change: enterprise-events-metrics
- Change ID: enterprise-events-metrics
- Package:
@suaveplan/events-metrics - Category: universal
- Target path:
universal/events/events-metrics - Status: PROPOSED
- Date: 2026-04-24
- Reference spec:
/mnt/s/web/suaveplan/enterprise/events-metrics.md
Summary
Deliver @suaveplan/events-metrics on the genesis branch from scratch at enterprise quality. The package is an opt-in metrics companion for @suaveplan/events: it attaches to the event bus via the public interceptor/companion API (never monkey-patching), records dispatch totals, listener error counts, dispatch latency histograms, queue depth gauges, and active subscriber counts. All metric names follow the suaveplan.events.* convention with stable attribute keys. Sub-microsecond hot-path overhead is a hard requirement.
Current-state audit (genesis)
The package does not exist on genesis yet — fresh build. No prior source, no test results, no coverage data.
Available workspace deps on genesis
| Package | Role |
|---|---|
@suaveplan/events (workspace:^) |
Event bus and interceptor API |
@suaveplan/opentelemetry (workspace:^) |
MeterProvider factory, counter/histogram/gauge helpers |
@suaveplan/error (workspace:^) |
Base class for EventsMetricsError; defineCodes for EVENTS_METRICS.* codes |
@suaveplan/types (workspace:^) |
LoggerLike, Clock injection interfaces |
zod (catalog:forms) |
Zod schemas for all public option types |
@opentelemetry/api (catalog:observability) |
Meter, Counter, Histogram, ObservableGauge |
tslib (catalog:build) |
Runtime helpers |
Dev deps: vitest, @vitest/coverage-istanbul, fast-check, mitata (all catalog:testing).
Public API surface
Setup
// Attaches metrics companion to an existing EventBus
function attachEventMetrics(bus: EventBusLike, options?: EventMetricsOptions): EventMetricsHandle
interface EventMetricsOptions {
meter?: Meter // default: global meter from @opentelemetry/api
eventFilter?: (name: string) => boolean // optional: record only matching events
clock?: Clock
logger?: LoggerLike
}
interface EventMetricsHandle {
detach(): void
snapshot(): MetricsSnapshot
}
Metric definitions
// All metric names and attribute keys as stable constants
const METRIC_NAMES = {
DISPATCH_TOTAL: "suaveplan.events.dispatch.total",
LISTENER_ERROR_TOTAL: "suaveplan.events.listener.error.total",
DISPATCH_LATENCY_MS: "suaveplan.events.dispatch.latency_ms",
QUEUE_DEPTH: "suaveplan.events.queue.depth",
ACTIVE_SUBSCRIBERS: "suaveplan.events.subscribers.active",
} as const
const ATTRIBUTE_KEYS = {
EVENT_NAME: "event.name",
EVENT_CATEGORY: "event.category",
LISTENER_ID: "listener.id",
BUS_ID: "bus.id",
} as const
Snapshot
interface MetricsSnapshot {
dispatchTotal: Record<string, number> // keyed by event.name
listenerErrorTotal: Record<string, number>
avgDispatchLatencyMs: Record<string, number>
queueDepth: number
activeSubscribers: number
}
Error handling
class EventsMetricsError extends SuaveplanError<EventsMetricsErrorCode>
type EventsMetricsErrorCode =
| "EVENTS_METRICS_ATTACH_FAILED"
| "EVENTS_METRICS_ALREADY_ATTACHED"
| "EVENTS_METRICS_DETACHED"
function registerEventsMetricsErrorCodes(): void
Zod schemas
const EventMetricsOptionsSchema: ZodSchema
New additions for genesis
1. Companion-API only
attachEventMetrics uses only the public EventBus interceptor/companion surface from @suaveplan/events. No access to private fields, no prototype patching. If the bus is replaced or re-created, callers must call detach() and attachEventMetrics again.
2. Low-overhead hot path
The dispatch interceptor is a thin closure: one array lookup for the counter and one performance.now() call for the histogram. Allocation on the critical path is zero when the event name was previously seen (name-to-attributes map is pre-cached).
3. Observable gauge for queue depth and subscribers
Queue depth and active subscriber count are reported as OTel ObservableGauge (polled at collection time), not recorded on every dispatch, to avoid per-dispatch overhead.
4. Property-based tests (fast-check)
- Counter monotonicity:
dispatchTotal[name]never decreases after more dispatches - Error counter accuracy:
listenerErrorTotalequals the number of listener throws injected - Histogram latency: average latency is within 1ms of a synthetic injected delay
5. Benchmark suite (src/__benchmarks__/)
- Overhead of
attachEventMetricson dispatch hot path: compare instrumented vs uninstrumented bus at 1M events/sec - Snapshot retrieval latency at 1k distinct event names
File layout
universal/events/events-metrics/
package.json
tsconfig.json
vitest.config.ts
src/
index.ts
error-codes.ts
constants/
index.ts
core/
attach-event-metrics/
attach-event-metrics.ts
attach-event-metrics.md
attach-event-metrics.types.ts
attach-event-metrics.test.ts
index.ts
metrics/
metric-names/
metric-names.ts
metric-names.md
index.ts
snapshot/
snapshot.ts
snapshot.md
snapshot.test.ts
index.ts
__benchmarks__/
events-metrics.bench.ts
tests/
integration.test.ts
property.test.ts
Impact
Downstream packages: any application wiring @suaveplan/events that needs production observability without code changes to event publishers or subscribers.
Workspace dep prerequisites: @suaveplan/events, @suaveplan/opentelemetry, @suaveplan/error, @suaveplan/types must be published to Verdaccio.
Breaking changes: none (new package).