Files
suaveplan/openspec/archive/2026-04-24-enterprise-events-otel/proposal.md
2026-05-27 23:29:53 +01:00

6.1 KiB
Raw Blame History

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).