6.1 KiB
OpenSpec Change: enterprise-events-otel
- Change ID: enterprise-events-otel
- Package:
@suaveplan/events-otel - Category: universal
- Target path:
universal/events/events-otel - Status: PROPOSED
- Date: 2026-04-24
- Reference spec:
/mnt/s/web/suaveplan/enterprise/events-otel.md
Summary
Deliver @suaveplan/events-otel on the genesis branch from scratch at enterprise quality. The package is an opt-in OpenTelemetry tracing companion for @suaveplan/events: it wraps each dispatch in a span, propagates W3C Trace Context across synchronous and async subscriber invocations, records event-level attributes on each span, and correctly marks listener exceptions as span errors. Attaches via the public @suaveplan/events interceptor API. Sampling-aware: honours the parent sampling decision and supports rule-based head sampling.
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/error (workspace:^) |
Base class for EventsOtelError; defineCodes for EVENTS_OTEL.* codes |
@suaveplan/types (workspace:^) |
LoggerLike, Clock injection interfaces |
zod (catalog:forms) |
Zod schemas for all public option types |
@opentelemetry/api (catalog:observability) |
Tracer, Span, SpanStatusCode, W3C context propagation |
tslib (catalog:build) |
Runtime helpers |
Dev deps: vitest, @vitest/coverage-istanbul, fast-check (all catalog:testing).
Public API surface
Setup
// Attaches OTel tracing companion to an existing EventBus
function attachEventTracing(bus: EventBusLike, options?: EventTracingOptions): EventTracingHandle
interface EventTracingOptions {
tracer?: Tracer // default: global tracer from @opentelemetry/api
propagator?: TextMapPropagator // default: W3CTraceContextPropagator
eventFilter?: (name: string) => boolean
samplingRule?: SamplingRule
clock?: Clock
logger?: LoggerLike
}
interface EventTracingHandle {
detach(): void
}
Span attribute convention
// All span attribute keys as stable constants
const SPAN_ATTRIBUTES = {
EVENT_NAME: "suaveplan.event.name",
EVENT_ID: "suaveplan.event.id",
EVENT_CATEGORY: "suaveplan.event.category",
LISTENER_ID: "suaveplan.listener.id",
BUS_ID: "suaveplan.bus.id",
} as const
// Span names
const SPAN_NAMES = {
DISPATCH: "events.dispatch",
LISTENER: "events.listener",
} as const
Sampling rule
type SamplingRule =
| { type: "always" }
| { type: "never" }
| { type: "rate"; rate: number } // 0.0–1.0
| { type: "event-name"; names: string[] } // sample only named events
Trace context envelope (for transport interop)
// Serialize current trace context into a plain object for transport
function serializeTraceContext(): Record<string, string>
// Restore trace context from a serialized envelope
function deserializeTraceContext(headers: Record<string, string>): Con```text
### Error handling
```ts
class EventsOtelError extends SuaveplanError<EventsOtelErrorCode>
type EventsOtelErrorCode =
| "EVENTS_OTEL_ATTACH_FAILED"
| "EVENTS_OTEL_ALREADY_ATTACHED"
| "EVENTS_OTEL_PROPAGATION_FAILED"
function registerEventsOtelErrorCodes(): void
Zod schemas
const EventTracingOptionsSchema: ZodSchema
const SamplingRuleSchema: ZodSchema
New additions for genesis
1. W3C Trace Context propagation across async subscribers
The interceptor captures the active Context at dispatch time. Each listener invocation runs with that context restored as the active context, so any nested OTel operations inside listeners are correctly parented to the dispatch span.
2. Listener exception → span error
If a listener throws, the dispatch span's status is set to SpanStatusCode.ERROR with the exception recorded via span.recordException(err). The error is re-thrown after recording.
3. Sampling-aware dispatch
When samplingRule is "rate" or "event-name", spans are only created for the matching subset. The companion checks the parent sampling decision first; if the parent is sampled, the rule is bypassed to maintain trace completeness.
4. serializeTraceContext / deserializeTraceContext
These helpers allow @suaveplan/events-transport to inject and extract trace context from transport envelopes, enabling cross-context trace propagation without a direct package dependency on OTel internals.
5. Property-based tests (fast-check)
- Parent-child relationship: every listener span is a child of its dispatch span
- Context propagation under reentrancy: nested dispatches inside listeners produce correct span trees
- Sampling rate accuracy: at sampling rate 0.5, sampled span count is within 5% of 50% over 10k dispatches
File layout
universal/events/events-otel/
package.json
tsconfig.json
vitest.config.ts
src/
index.ts
error-codes.ts
constants/
index.ts
core/
attach-event-tracing/
attach-event-tracing.ts
attach-event-tracing.md
attach-event-tracing.types.ts
attach-event-tracing.test.ts
index.ts
propagation/
trace-context/
trace-context.ts
trace-context.md
trace-context.test.ts
index.ts
sampling/
sampling-rule/
sampling-rule.ts
sampling-rule.md
sampling-rule.types.ts
sampling-rule.test.ts
index.ts
tests/
integration.test.ts
property.test.ts
Impact
Downstream packages: @suaveplan/events-transport uses serializeTraceContext/deserializeTraceContext for cross-context propagation. Any application using @suaveplan/events with production tracing can add this companion without code changes to publishers or subscribers.
Workspace dep prerequisites: @suaveplan/events, @suaveplan/error, @suaveplan/types must be published to Verdaccio.
Breaking changes: none (new package).