first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,33 @@
# @suaveplan/events-otel - OpenTelemetry tracing companion for @suaveplan/events: wraps each dispatch in a span, propagates W3C Trace Context, and records listener exceptions as span errors.
## Summary
OpenTelemetry tracing companion for `@suaveplan/events`. Wraps each dispatch in a span, propagates W3C Trace Context, and records listener exceptions as span errors.
<!-- begin:auto-docs -->
## Documentation tree
- [attach-event-tracing](./src/core/attach-event-tracing/attach-event-tracing.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [attach-event-tracing](./src/core/attach-event-tracing/attach-event-tracing.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [trace-context](./src/propagation/trace-context/trace-context.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [trace-context](./src/propagation/trace-context/trace-context.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [sampling-rule](./src/sampling/sampling-rule/sampling-rule.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [sampling-rule](./src/sampling/sampling-rule/sampling-rule.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
<!-- end:auto-docs --> - [attach-event-tracing](./src/core/attach-event-tracing/attach-event-tracing.md) — <!-- begin:auto-docs --> _(no submodules)_- [trace-context](./src/propagation/trace-context/trace-context.md) — <!-- begin:auto-docs --> _(no submodules)_- [trace-context](./src/propagation/trace-context/trace-context.md) — <!-- begin:auto-docs --> _(no submodules)_- [sampling-rule](./src/sampling/sampling-rule/sampling-rule.md) — <!-- begin:auto-docs --> _(no submodules)_- [sampling-rule](./src/sampling/sampling-rule/sampling-rule.md) — <!-- begin:auto-docs --> _(no submodules)_- [attach-event-tracing](./src/core/attach-event-tracing/attach-event-tracing.md) — <!-- begin:auto-docs --> _(no submodules)_- [trace-context](./src/propagation/trace-context/trace-context.md) — <!-- begin:auto-docs --> _(no submodules)_- [trace-context](./src/propagation/trace-context/trace-context.md) — <!-- begin:auto-docs --> _(no submodules)_- [sampling-rule](./src/sampling/sampling-rule/sampling-rule.md) — <!-- begin:auto-docs --> _(no submodules)_- [sampling-rule](./src/sampling/sampling-rule/sampling-rule.md) — <!-- begin:auto-docs --> _(no submodules)_- [attach-event-tracing](./src/core/attach-event-tracing/attach-event-tracing.md) — <!-- begin:auto-docs --> _(no submodules)_- [trace-context](./src/propagation/trace-context/trace-context.md) — <!-- begin:auto-docs --> _(no submodules)_- [trace-context](./src/propagation/trace-context/trace-context.md) — <!-- begin:auto-docs --> _(no submodules)_- [sampling-rule](./src/sampling/sampling-rule/sampling-rule.md) — <!-- begin:auto-docs --> _(no submodules)_- [sampling-rule](./src/sampling/sampling-rule/sampling-rule.md) — <!-- begin:auto-docs --> _(no submodules)_- [attach-event-tracing](./src/core/attach-event-tracing/attach-event-tracing.md) — <!-- begin:auto-docs --> _(no submodules)_- [trace-context](./src/propagation/trace-context/trace-context.md) — <!-- begin:auto-docs --> _(no submodules)_ - [trace-context](./src/propagation/trace-context/trace-context.md) — <!-- begin:auto-docs --> _(no submodules)_- [sampling-rule](./src/sampling/sampling-rule/sampling-rule.md) — <!-- begin:auto-docs --> _(no submodules)_ - [sampling-rule](./src/sampling/sampling-rule/sampling-rule.md) — <!-- begin:auto-docs --> _(no submodules)_
## Quick Start
```ts
import { EventBus } from "@suaveplan/events";
import { attachEventTracing } from "@suaveplan/events-otel";
const bus = new EventBus();
const handle = attachEventTracing(bus);
await bus.emitAsync("user.created", { id: "u-1" });
handle.detach();
```
## Modules
- [`src/core/attach-event-tracing/attach-event-tracing`](src/core/attach-event-tracing/attach-event-tracing.md) — `attachEventTracing`, `EventTracingHandle`, `EventTracingOptions`
- [`src/propagation/trace-context/trace-context`](src/propagation/trace-context/trace-context.md) — `serializeTraceContext`, `deserializeTraceContext`
- [`src/sampling/sampling-rule/sampling-rule`](src/sampling/sampling-rule/sampling-rule.md) — `SamplingRule`, `shouldSample`, `SamplingRuleSchema`

View File

@@ -0,0 +1,87 @@
# attach-event-tracing
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Attaches OTel tracing instrumentation to an `EventBus` via the public interceptor API from `@suaveplan/events`. Each dispatch produces a span named `events.dispatch` with event name and event ID attributes. Listener exceptions are recorded on the span as errors before being re-thrown. The interceptor captures the active context at dispatch time so that any OTel operations inside listeners produce correctly parented child spans.
The implementation is sampling-aware: it evaluates the configured `SamplingRule` to decide whether to create a span. When no sampling rule is configured it defaults to `always`.
## Features
- `attachEventTracing(bus, options?)` — installs a tracing interceptor and returns `EventTracingHandle`
- Span per dispatch (`events.dispatch`) with `suaveplan.event.name` and `suaveplan.event.id` attributes
- Listener exception → `span.recordException(err)` + `SpanStatusCode.ERROR`
- Sampling rule support (`always`, `never`, `rate`, `event-name`)
- `eventFilter` option to exclude event types from tracing
- Injectable `tracer`, `propagator`, `clock`, `logger` for testability
- `EventTracingHandle.detach()` removes the interceptor
## Usage: Basic Example
```ts
import { EventBus } from "@suaveplan/events";
import { attachEventTracing } from "@suaveplan/events-otel";
const bus = new EventBus<{ "user.created": { id: string } }>();
const handle = attachEventTracing(bus);
bus.on("user.created", (p) => console.log("created", p.id));
await bus.emitAsync("user.created", { id: "u-1" });
// A span named "events.dispatch" was created
handle.detach();
```
## Usage: Advanced Example
```ts
import { EventBus } from "@suaveplan/events";
import { attachEventTracing } from "@suaveplan/events-otel";
import { trace } from "@opentelemetry/api";
const bus = new EventBus();
const handle = attachEventTracing(bus, {
tracer: trace.getTracer("my-app"),
samplingRule: { type: "rate", rate: 0.1 },
eventFilter: (name) => !name.startsWith("internal."),
});
// Only 10% of non-internal event dispatches produce spans
await bus.emitAsync("user.created", { id: "u-2" });
handle.detach();
```
## API Reference
### `attachEventTracing<TMap>(bus, options?): EventTracingHandle`
| Param | Type | Description |
|---|---|---|
| `bus` | `EventBus<TMap>` | The event bus to instrument |
| `options` | `EventTracingOptions?` | Tracer, propagator, filter, sampling rule, clock, logger |
**Returns**: `EventTracingHandle`
### `EventTracingHandle`
| Method | Returns | Description |
|---|---|---|
| `detach()` | `void` | Remove the tracing interceptor |
### `EventTracingOptions`
| Field | Type | Description |
|---|---|---|
| `tracer` | `Tracer?` | OTel tracer (default: global) |
| `propagator` | `TextMapPropagator?` | W3C propagator (default: global) |
| `eventFilter` | `(name: string) => boolean` | Exclude event names from tracing |
| `samplingRule` | `SamplingRule?` | Sampling strategy (default: `always`) |
| `clock` | `Clock?` | Injectable clock |
| `logger` | `LoggerLike?` | Diagnostic logger |
## Implementation Notes
The interceptor uses `tracer.startActiveSpan(SPAN_NAMES.DISPATCH, {}, context.active(), ...)` to both create the span and set it as the active context for the duration of `next()`. This ensures all OTel operations within listeners (including nested dispatches) are correctly parented. The `context.active()` snapshot happens synchronously at interceptor entry, so even if the active context changes between dispatch and listener execution, the span retains the correct parent.

View File

@@ -0,0 +1,70 @@
# trace-context
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides `serializeTraceContext` and `deserializeTraceContext` helpers for `@suaveplan/events-otel`. These functions convert the W3C Trace Context (traceparent + tracestate headers) to and from a plain `Record<string, string>` that can be safely embedded in event payloads, transport envelopes, or message-queue headers without importing OTel types.
The helpers delegate to the `@opentelemetry/api` global propagator, so the exact propagation format (W3C, B3, Jaeger, etc.) is controlled by the installed SDK — the application does not need to change this package to switch formats.
## Features
- `serializeTraceContext(ctx?)` — extracts active W3C trace context into a plain map; injectable context for testing
- `deserializeTraceContext(headers, parentCtx?)` — restores context from a map; injectable parent context for nesting
- Zero runtime state — both functions use the global OTel propagator and context APIs
## Usage: Basic Example
```ts
import { serializeTraceContext, deserializeTraceContext } from "@suaveplan/events-otel";
// Sender side: capture context before cross-boundary send
const headers = serializeTraceContext();
// { "traceparent": "00-abc...123-0000000000000001-01" }
// Receiver side: restore context before re-dispatching
const ctx = deserializeTraceContext(headers);
```
## Usage: Advanced Example
```ts
import { serializeTraceContext, deserializeTraceContext } from "@suaveplan/events-otel";
import { context, trace } from "@opentelemetry/api";
// Sender: extract context from a specific span
const span = trace.getActiveSpan();
const ctx = span ? trace.setSpan(context.active(), span) : context.active();
const headers = serializeTraceContext(ctx);
// Receiver: restore and use as parent
const restored = deserializeTraceContext(headers);
context.with(restored, () => {
// Any spans created here will be children of the sender's span
});
```
## API Reference
### `serializeTraceContext(ctx?: Context): Record<string, string>`
| Param | Type | Description |
|---|---|---|
| `ctx` | `Context?` | OTel context to serialize (default: `context.active()`) |
**Returns**: plain map of propagation headers (e.g., `{ "traceparent": "..." }`)
### `deserializeTraceContext(headers, parentCtx?): Context`
| Param | Type | Description |
|---|---|---|
| `headers` | `Record<string, string>` | Map of propagation headers |
| `parentCtx` | `Context?` | Parent OTel context (default: `context.active()`) |
**Returns**: OTel `Context` with the trace context restored; returns root context if headers contain no valid trace info.
## Implementation Notes
Both functions use the global OTel propagation API (`propagation.inject` / `propagation.extract`). When no OTel SDK is installed, the global propagator is a no-op, so `serializeTraceContext` returns an empty object and `deserializeTraceContext` returns the active context unchanged. This ensures the package is safe to use in environments without OTel.

View File

@@ -0,0 +1,75 @@
# sampling-rule
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines the `SamplingRule` discriminated union type and the `shouldSample` evaluation function for `@suaveplan/events-otel`. Sampling rules determine which event dispatches produce OpenTelemetry spans, enabling cost-efficient tracing in high-throughput production environments without instrumentation code changes.
The implementation is pure and injectable: `shouldSample` accepts a random-number generator parameter so rate-based sampling can be tested deterministically.
## Features
- `SamplingRule` — four-variant discriminated union: `always`, `never`, `rate`, `event-name`
- `SamplingRuleSchema` — Zod schema for validating rule objects at the boundary
- `shouldSample(rule, eventName, rand?)` — evaluates a rule against a given event name and optional RNG
## Usage: Basic Example
```ts
import { shouldSample } from "@suaveplan/events-otel";
import type { SamplingRule } from "@suaveplan/events-otel";
const rule: SamplingRule = { type: "event-name", names: ["user.created", "order.placed"] };
shouldSample(rule, "user.created"); // true
shouldSample(rule, "page.viewed"); // false
```
## Usage: Advanced Example
```ts
import { shouldSample, SamplingRuleSchema } from "@suaveplan/events-otel";
// Validate from config
const rawRule = { type: "rate", rate: 0.1 };
const rule = SamplingRuleSchema.parse(rawRule);
// Deterministic test: always sample
const alwaysSample = () => 0.05; // < 0.1
shouldSample(rule, "any.event", alwaysSample); // true
// Deterministic test: never sample
const neverSample = () => 0.95; // >= 0.1
shouldSample(rule, "any.event", neverSample); // false
```
## API Reference
### `shouldSample(rule, eventName, rand?): boolean`
| Param | Type | Description |
|---|---|---|
| `rule` | `SamplingRule` | Rule to evaluate |
| `eventName` | `string` | Name of the dispatched event |
| `rand` | `() => number` | RNG for rate sampling (default: `Math.random`) |
**Returns**: `true` if the dispatch should produce a span.
### `SamplingRuleSchema`
Zod schema validating the `SamplingRule` discriminated union. Use for config parsing.
### `SamplingRule`
```ts
type SamplingRule =
| { type: "always" }
| { type: "never" }
| { type: "rate"; rate: number } // 0.0 – 1.0
| { type: "event-name"; names: string[] }
```
## Implementation Notes
Rate-based sampling uses a simple comparison `rand() < rule.rate` without any consistent hashing. For consistent per-trace sampling (where all spans in one trace are either all sampled or all dropped), the caller should check the parent span's sampling flag first — the `attachEventTracing` function does this by only evaluating `shouldSample` when there is no parent span in context.