first commit
This commit is contained in:
33
docs/networking/events/events-otel/README.md
Normal file
33
docs/networking/events/events-otel/README.md
Normal 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`
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user