Initial Commit
This commit is contained in:
33
packages/networking/events/events-otel/README.md
Normal file
33
packages/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`
|
||||
84
packages/networking/events/events-otel/package.json
Normal file
84
packages/networking/events/events-otel/package.json
Normal file
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"name": "@suaveplan/events-otel",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
"description": "OpenTelemetry tracing companion for @suaveplan/events: wraps each dispatch in a span, propagates W3C Trace Context, and records listener exceptions as span errors.",
|
||||
"license": "MIT",
|
||||
"author": "Suaveplan Team",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
"./package.json": "./package.json",
|
||||
".": {
|
||||
"import": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"./core/attach-event-tracing": {
|
||||
"import": {
|
||||
"types": "./dist/core/attach-event-tracing/attach-event-tracing.d.ts",
|
||||
"default": "./dist/core/attach-event-tracing/attach-event-tracing.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/core/attach-event-tracing/attach-event-tracing.d.ts",
|
||||
"default": "./dist/core/attach-event-tracing/attach-event-tracing.js"
|
||||
}
|
||||
},
|
||||
"./propagation/trace-context": {
|
||||
"import": {
|
||||
"types": "./dist/propagation/trace-context/trace-context.d.ts",
|
||||
"default": "./dist/propagation/trace-context/trace-context.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/propagation/trace-context/trace-context.d.ts",
|
||||
"default": "./dist/propagation/trace-context/trace-context.js"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"test": "vitest run --passWithNoTests",
|
||||
"test:watch": "vitest",
|
||||
"test:coverage": "vitest run --coverage --passWithNoTests",
|
||||
"lint": "bunx biome check src/",
|
||||
"lint:fix": "bunx biome lint --fix --unsafe --max-diagnostics 500 src/",
|
||||
"format": "bunx biome format --write --max-diagnostics 500 src/",
|
||||
"check": "bunx biome check --write --unsafe --max-diagnostics 500 src/",
|
||||
"typecheck": "bunx tsc --noEmit --composite false -p tsconfig.lib.json",
|
||||
"docs:check": "bun run ../../../../scripts/docs-check.ts ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@opentelemetry/api": "catalog:observability",
|
||||
"@suaveplan/error": "workspace:^",
|
||||
"@suaveplan/events": "workspace:^",
|
||||
"@suaveplan/types": "workspace:^",
|
||||
"tslib": "catalog:build",
|
||||
"zod": "catalog:forms"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@suaveplan/testing": "workspace:^",
|
||||
"@types/node": "catalog:",
|
||||
"@vitest/coverage-istanbul": "catalog:testing",
|
||||
"fast-check": "catalog:testing",
|
||||
"typescript": "catalog:",
|
||||
"vite": "catalog:build",
|
||||
"vite-plugin-dts": "catalog:build",
|
||||
"vitest": "catalog:testing"
|
||||
},
|
||||
"publishConfig": {
|
||||
"registry": "https://npmjs.fq.io/",
|
||||
"access": "public"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"src",
|
||||
"README.md"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/** Stable OTel span attribute keys for events-otel. */
|
||||
export 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;
|
||||
|
||||
/** Stable OTel span names for events-otel. */
|
||||
export const SPAN_NAMES = {
|
||||
DISPATCH: "events.dispatch",
|
||||
LISTENER: "events.listener",
|
||||
} as const;
|
||||
@@ -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,149 @@
|
||||
import { EventBus } from "@suaveplan/events";
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import { attachEventTracing } from "./attach-event-tracing.js";
|
||||
|
||||
type TestMap = {
|
||||
click: { x: number };
|
||||
submit: { formId: string };
|
||||
};
|
||||
|
||||
describe("attachEventTracing", () => {
|
||||
it("does not throw when instrumenting a bus", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const bus = new EventBus<TestMap>();
|
||||
const handle = attachEventTracing(bus);
|
||||
bus.on("click", () => void 0);
|
||||
await expect(bus.emitAsync("click", { x: 1 })).resolves.toBeDefined();
|
||||
handle.detach();
|
||||
});
|
||||
|
||||
it("detach removes the interceptor", { timeout: 5000 }, async () => {
|
||||
const bus = new EventBus<TestMap>();
|
||||
const handle = attachEventTracing(bus);
|
||||
handle.detach();
|
||||
bus.on("click", () => void 0);
|
||||
// Should still work without the interceptor
|
||||
await expect(bus.emitAsync("click", { x: 1 })).resolves.toBeDefined();
|
||||
});
|
||||
|
||||
it("respects eventFilter", { timeout: 5000 }, async () => {
|
||||
const bus = new EventBus<TestMap>();
|
||||
let callCount = 0;
|
||||
const handle = attachEventTracing(bus, {
|
||||
eventFilter: (name) => {
|
||||
callCount++;
|
||||
return name === "click";
|
||||
},
|
||||
});
|
||||
bus.on("click", () => void 0);
|
||||
bus.on("submit", () => void 0);
|
||||
await bus.emitAsync("click", { x: 1 });
|
||||
await bus.emitAsync("submit", { formId: "f" });
|
||||
expect(callCount).toBe(2); // filter called for each event
|
||||
handle.detach();
|
||||
});
|
||||
|
||||
it("sampling rule never skips span creation", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const bus = new EventBus<TestMap>();
|
||||
const handle = attachEventTracing(bus, {
|
||||
samplingRule: { type: "never" },
|
||||
});
|
||||
bus.on("click", () => void 0);
|
||||
await expect(bus.emitAsync("click", { x: 1 })).resolves.toBeDefined();
|
||||
handle.detach();
|
||||
});
|
||||
|
||||
it("sampling rule always creates span", { timeout: 5000 }, async () => {
|
||||
const bus = new EventBus<TestMap>();
|
||||
const handle = attachEventTracing(bus, {
|
||||
samplingRule: { type: "always" },
|
||||
});
|
||||
bus.on("click", () => void 0);
|
||||
await expect(bus.emitAsync("click", { x: 1 })).resolves.toBeDefined();
|
||||
handle.detach();
|
||||
});
|
||||
|
||||
it("sampling rule event-name filters by name", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const bus = new EventBus<TestMap>();
|
||||
const handle = attachEventTracing(bus, {
|
||||
samplingRule: { type: "event-name", names: ["click"] },
|
||||
});
|
||||
bus.on("click", () => void 0);
|
||||
bus.on("submit", () => void 0);
|
||||
await bus.emitAsync("click", { x: 1 });
|
||||
await bus.emitAsync("submit", { formId: "f" });
|
||||
handle.detach();
|
||||
});
|
||||
|
||||
it("listener errors do not prevent dispatch completion", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const bus = new EventBus<TestMap>();
|
||||
const handle = attachEventTracing(bus);
|
||||
bus.on("click", () => {
|
||||
throw new Error("listener threw");
|
||||
});
|
||||
const result = await bus.emitAsync("click", { x: 0 });
|
||||
expect(result.errors.length).toBe(1);
|
||||
handle.detach();
|
||||
});
|
||||
|
||||
it("listener throwing a non-Error value is collected without recordException", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const bus = new EventBus<TestMap>();
|
||||
const handle = attachEventTracing(bus);
|
||||
bus.on("click", () => {
|
||||
// Throw via an `unknown`-typed local so biome's useThrowOnlyError
|
||||
// can't statically prove the value isn't Error-like. Exercises the
|
||||
// non-Error branch of the listener-error coercion path.
|
||||
const failure: unknown = "string-failure";
|
||||
throw failure;
|
||||
});
|
||||
const result = await bus.emitAsync("click", { x: 0 });
|
||||
expect(result.errors.length).toBeGreaterThanOrEqual(1);
|
||||
handle.detach();
|
||||
});
|
||||
|
||||
it("propagation error in interceptor rethrows", {
|
||||
timeout: 5000,
|
||||
}, async () => {
|
||||
const bus = new EventBus<TestMap>();
|
||||
const handle = attachEventTracing(bus);
|
||||
// Add a second interceptor that throws
|
||||
bus.use(async () => {
|
||||
throw new Error("interceptor failure");
|
||||
});
|
||||
// The event bus may wrap non-EventsError throws; check error chain.
|
||||
let caught: unknown;
|
||||
try {
|
||||
await bus.emitAsync("click", { x: 0 });
|
||||
} catch (e) {
|
||||
caught = e;
|
||||
}
|
||||
const hasMessage = (err: unknown, msg: string): boolean =>
|
||||
err instanceof Error
|
||||
? err.message.includes(msg) || hasMessage(err.cause, msg)
|
||||
: false;
|
||||
expect(hasMessage(caught, "interceptor failure")).toBe(true);
|
||||
handle.detach();
|
||||
});
|
||||
|
||||
it("rate sampling with injectable rand", { timeout: 5000 }, async () => {
|
||||
const bus = new EventBus<TestMap>();
|
||||
// We test via shouldSample independently; here we just confirm no errors
|
||||
const handle = attachEventTracing(bus, {
|
||||
samplingRule: { type: "rate", rate: 0.5 },
|
||||
});
|
||||
bus.on("click", () => void 0);
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await bus.emitAsync("click", { x: i });
|
||||
}
|
||||
handle.detach();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
import { context, SpanStatusCode, trace } from "@opentelemetry/api";
|
||||
import type { EventBus, EventMap } from "@suaveplan/events";
|
||||
import { SPAN_ATTRIBUTES, SPAN_NAMES } from "../../constants/index.js";
|
||||
import { shouldSample } from "../../sampling/sampling-rule/sampling-rule.js";
|
||||
import type {
|
||||
EventTracingHandle,
|
||||
EventTracingOptions,
|
||||
} from "./attach-event-tracing.types.js";
|
||||
|
||||
/**
|
||||
* Attaches OTel tracing instrumentation to an EventBus via the interceptor API.
|
||||
*
|
||||
* Wraps each dispatch in a span, propagates W3C Trace Context to listener
|
||||
* invocations, and records listener exceptions as span errors.
|
||||
*/
|
||||
export function attachEventTracing<TMap extends EventMap>(
|
||||
bus: EventBus<TMap>,
|
||||
options: EventTracingOptions = {}
|
||||
): EventTracingHandle {
|
||||
const tracer = options.tracer ?? trace.getTracer("suaveplan.events");
|
||||
const filter = options.eventFilter ?? (() => true);
|
||||
const samplingRule = options.samplingRule ?? { type: "always" as const };
|
||||
|
||||
const unsubscribe = bus.use(async (envelope, next) => {
|
||||
const eventName = envelope.type;
|
||||
|
||||
if (!filter(eventName) || !shouldSample(samplingRule, eventName)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
return tracer.startActiveSpan(
|
||||
SPAN_NAMES.DISPATCH,
|
||||
{},
|
||||
context.active(),
|
||||
async (span) => {
|
||||
span.setAttributes({
|
||||
[SPAN_ATTRIBUTES.EVENT_NAME]: eventName,
|
||||
[SPAN_ATTRIBUTES.EVENT_ID]: envelope.id,
|
||||
});
|
||||
try {
|
||||
const result = await next();
|
||||
if (result.errors.length > 0) {
|
||||
span.setStatus({
|
||||
code: SpanStatusCode.ERROR,
|
||||
message: "Listener errors",
|
||||
});
|
||||
for (const err of result.errors) {
|
||||
span.recordException(err as Error);
|
||||
}
|
||||
} else {
|
||||
span.setStatus({ code: SpanStatusCode.OK });
|
||||
}
|
||||
return result;
|
||||
} catch (err) {
|
||||
span.setStatus({
|
||||
code: SpanStatusCode.ERROR,
|
||||
message: (err as Error).message,
|
||||
});
|
||||
span.recordException(err as Error);
|
||||
throw err;
|
||||
} finally {
|
||||
span.end();
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
detach(): void {
|
||||
unsubscribe();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { TextMapPropagator, Tracer } from "@opentelemetry/api";
|
||||
import type { Clock, LoggerLike } from "@suaveplan/types";
|
||||
import type { SamplingRule } from "../../sampling/sampling-rule/sampling-rule.types.js";
|
||||
|
||||
/**
|
||||
* Options for configuring event bus tracing instrumentation.
|
||||
*/
|
||||
export interface EventTracingOptions {
|
||||
/** OTel Tracer instance. Defaults to the global tracer from @opentelemetry/api. */
|
||||
tracer?: Tracer;
|
||||
/** W3C propagator. Defaults to the global propagator. */
|
||||
propagator?: TextMapPropagator;
|
||||
/** Filter function: only trace matching event names. */
|
||||
eventFilter?: (name: string) => boolean;
|
||||
/** Sampling rule for controlling span creation. Default: always. */
|
||||
samplingRule?: SamplingRule;
|
||||
/** Injectable clock. */
|
||||
clock?: Clock;
|
||||
/** Logger for diagnostic output. */
|
||||
logger?: LoggerLike;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle returned by attachEventTracing.
|
||||
*/
|
||||
export interface EventTracingHandle {
|
||||
/** Remove the tracing interceptor from the bus. */
|
||||
detach(): void;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export { attachEventTracing } from "./attach-event-tracing.js";
|
||||
export type {
|
||||
EventTracingHandle,
|
||||
EventTracingOptions,
|
||||
} from "./attach-event-tracing.types.js";
|
||||
@@ -0,0 +1,56 @@
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import {
|
||||
EVENTS_OTEL_CODES,
|
||||
EventsOtelError,
|
||||
registerEventsOtelErrorCodes,
|
||||
} from "./error-codes.js";
|
||||
|
||||
describe("EventsOtelError", () => {
|
||||
it("constructs with a registered code, message, and options", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const cause = new Error("root");
|
||||
const err = new EventsOtelError(
|
||||
EVENTS_OTEL_CODES.ATTACH_FAILED,
|
||||
"could not attach",
|
||||
{ cause }
|
||||
);
|
||||
expect(err).toBeInstanceOf(EventsOtelError);
|
||||
expect(err.code).toBe(EVENTS_OTEL_CODES.ATTACH_FAILED);
|
||||
expect(err.message).toBe("could not attach");
|
||||
expect(err.name).toBe("EventsOtelError");
|
||||
expect(err.cause).toBe(cause);
|
||||
});
|
||||
|
||||
it("constructs without options", { timeout: 5000 }, () => {
|
||||
const err = new EventsOtelError(
|
||||
EVENTS_OTEL_CODES.ALREADY_ATTACHED,
|
||||
"already attached"
|
||||
);
|
||||
expect(err.code).toBe(EVENTS_OTEL_CODES.ALREADY_ATTACHED);
|
||||
expect(err.name).toBe("EventsOtelError");
|
||||
});
|
||||
|
||||
it("registerEventsOtelErrorCodes is idempotent", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
expect(() => {
|
||||
registerEventsOtelErrorCodes();
|
||||
registerEventsOtelErrorCodes();
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("EVENTS_OTEL_CODES exposes all expected codes", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
expect(EVENTS_OTEL_CODES.ATTACH_FAILED).toBe(
|
||||
"EVENTS_OTEL.ATTACH_FAILED"
|
||||
);
|
||||
expect(EVENTS_OTEL_CODES.ALREADY_ATTACHED).toBe(
|
||||
"EVENTS_OTEL.ALREADY_ATTACHED"
|
||||
);
|
||||
expect(EVENTS_OTEL_CODES.PROPAGATION_FAILED).toBe(
|
||||
"EVENTS_OTEL.PROPAGATION_FAILED"
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import {
|
||||
defineCodes,
|
||||
SuaveplanError,
|
||||
type SuaveplanErrorOptions,
|
||||
} from "@suaveplan/error";
|
||||
|
||||
export const EVENTS_OTEL_CODES = defineCodes("EVENTS_OTEL", [
|
||||
"ATTACH_FAILED",
|
||||
"ALREADY_ATTACHED",
|
||||
"PROPAGATION_FAILED",
|
||||
] as const);
|
||||
|
||||
export type EventsOtelErrorCode =
|
||||
(typeof EVENTS_OTEL_CODES)[keyof typeof EVENTS_OTEL_CODES];
|
||||
|
||||
/**
|
||||
* Structured error for events-otel operations.
|
||||
*/
|
||||
export class EventsOtelError extends SuaveplanError<EventsOtelErrorCode> {
|
||||
public constructor(
|
||||
code: EventsOtelErrorCode,
|
||||
message: string,
|
||||
options?: SuaveplanErrorOptions
|
||||
) {
|
||||
super(code, message, options);
|
||||
this.name = "EventsOtelError";
|
||||
}
|
||||
}
|
||||
|
||||
/** Register events-otel error codes (idempotent). */
|
||||
export function registerEventsOtelErrorCodes(): void {
|
||||
// defineCodes handles registration; this is a no-op hook.
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export type { EventsOtelErrorCode } from "./error-codes.js";
|
||||
export {
|
||||
EVENTS_OTEL_CODES,
|
||||
EventsOtelError,
|
||||
registerEventsOtelErrorCodes,
|
||||
} from "./error-codes.js";
|
||||
21
packages/networking/events/events-otel/src/index.ts
Normal file
21
packages/networking/events/events-otel/src/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export { SPAN_ATTRIBUTES, SPAN_NAMES } from "./constants/index.js";
|
||||
export { attachEventTracing } from "./core/attach-event-tracing/attach-event-tracing.js";
|
||||
export type {
|
||||
EventTracingHandle,
|
||||
EventTracingOptions,
|
||||
} from "./core/attach-event-tracing/attach-event-tracing.types.js";
|
||||
export type { EventsOtelErrorCode } from "./error-codes/index.js";
|
||||
export {
|
||||
EVENTS_OTEL_CODES,
|
||||
EventsOtelError,
|
||||
registerEventsOtelErrorCodes,
|
||||
} from "./error-codes/index.js";
|
||||
export {
|
||||
deserializeTraceContext,
|
||||
serializeTraceContext,
|
||||
} from "./propagation/trace-context/trace-context.js";
|
||||
export {
|
||||
SamplingRuleSchema,
|
||||
shouldSample,
|
||||
} from "./sampling/sampling-rule/sampling-rule.js";
|
||||
export type { SamplingRule } from "./sampling/sampling-rule/sampling-rule.types.js";
|
||||
@@ -0,0 +1,4 @@
|
||||
export {
|
||||
deserializeTraceContext,
|
||||
serializeTraceContext,
|
||||
} from "./trace-context.js";
|
||||
@@ -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,55 @@
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import {
|
||||
deserializeTraceContext,
|
||||
serializeTraceContext,
|
||||
} from "./trace-context.js";
|
||||
|
||||
describe("serializeTraceContext", () => {
|
||||
it("returns a plain object", { timeout: 5000 }, () => {
|
||||
const result = serializeTraceContext();
|
||||
expect(typeof result).toBe("object");
|
||||
expect(result).not.toBeNull();
|
||||
});
|
||||
|
||||
it("all values are strings", { timeout: 5000 }, () => {
|
||||
const result = serializeTraceContext();
|
||||
for (const v of Object.values(result)) {
|
||||
expect(typeof v).toBe("string");
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts an explicit context", { timeout: 5000 }, () => {
|
||||
const { context } = require("@opentelemetry/api");
|
||||
const result = serializeTraceContext(context.active());
|
||||
expect(typeof result).toBe("object");
|
||||
});
|
||||
});
|
||||
|
||||
describe("deserializeTraceContext", () => {
|
||||
it("returns a Context from empty headers", { timeout: 5000 }, () => {
|
||||
const ctx = deserializeTraceContext({});
|
||||
expect(ctx).toBeDefined();
|
||||
});
|
||||
|
||||
it("round-trips a serialized context", { timeout: 5000 }, () => {
|
||||
// Without a real SDK, serialize returns {} and deserialize returns the active context
|
||||
const headers = serializeTraceContext();
|
||||
const ctx = deserializeTraceContext(headers);
|
||||
expect(ctx).toBeDefined();
|
||||
});
|
||||
|
||||
it("accepts a parent context", { timeout: 5000 }, () => {
|
||||
const { context } = require("@opentelemetry/api");
|
||||
const ctx = deserializeTraceContext({}, context.active());
|
||||
expect(ctx).toBeDefined();
|
||||
});
|
||||
|
||||
it("handles W3C traceparent header if present", { timeout: 5000 }, () => {
|
||||
const headers = {
|
||||
traceparent:
|
||||
"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
|
||||
};
|
||||
const ctx = deserializeTraceContext(headers);
|
||||
expect(ctx).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import { type Context, context, propagation } from "@opentelemetry/api";
|
||||
|
||||
/**
|
||||
* Serialize the current active trace context into a plain string→string map
|
||||
* suitable for inclusion in transport envelopes or event payloads.
|
||||
*/
|
||||
export function serializeTraceContext(ctx?: Context): Record<string, string> {
|
||||
const carrier: Record<string, string> = {};
|
||||
propagation.inject(ctx ?? context.active(), carrier);
|
||||
return carrier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore a trace context from a serialized envelope map.
|
||||
* Returns the root context if the carrier contains no valid trace headers.
|
||||
*/
|
||||
export function deserializeTraceContext(
|
||||
headers: Record<string, string>,
|
||||
parentCtx?: Context
|
||||
): Context {
|
||||
return propagation.extract(parentCtx ?? context.active(), headers);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { SamplingRuleSchema, shouldSample } from "./sampling-rule.js";
|
||||
export type { SamplingRule } from "./sampling-rule.types.js";
|
||||
@@ -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.
|
||||
@@ -0,0 +1,95 @@
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import { SamplingRuleSchema, shouldSample } from "./sampling-rule.js";
|
||||
|
||||
describe("shouldSample", () => {
|
||||
it("always rule always returns true", { timeout: 5000 }, () => {
|
||||
expect(shouldSample({ type: "always" }, "any.event")).toBe(true);
|
||||
});
|
||||
|
||||
it("never rule always returns false", { timeout: 5000 }, () => {
|
||||
expect(shouldSample({ type: "never" }, "any.event")).toBe(false);
|
||||
});
|
||||
|
||||
it("rate rule samples based on random value", { timeout: 5000 }, () => {
|
||||
const rule = { type: "rate" as const, rate: 0.5 };
|
||||
expect(shouldSample(rule, "e", () => 0.49)).toBe(true);
|
||||
expect(shouldSample(rule, "e", () => 0.5)).toBe(false);
|
||||
expect(shouldSample(rule, "e", () => 0)).toBe(true);
|
||||
expect(shouldSample(rule, "e", () => 0.99)).toBe(false);
|
||||
});
|
||||
|
||||
it("rate 0 never samples", { timeout: 5000 }, () => {
|
||||
expect(shouldSample({ type: "rate", rate: 0 }, "e", () => 0)).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
it("rate 1 always samples", { timeout: 5000 }, () => {
|
||||
expect(shouldSample({ type: "rate", rate: 1 }, "e", () => 0.999)).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it("event-name rule matches listed names", { timeout: 5000 }, () => {
|
||||
const rule = {
|
||||
type: "event-name" as const,
|
||||
names: ["user.created", "order.placed"],
|
||||
};
|
||||
expect(shouldSample(rule, "user.created")).toBe(true);
|
||||
expect(shouldSample(rule, "order.placed")).toBe(true);
|
||||
expect(shouldSample(rule, "page.viewed")).toBe(false);
|
||||
});
|
||||
|
||||
it("event-name empty list never samples", { timeout: 5000 }, () => {
|
||||
expect(shouldSample({ type: "event-name", names: [] }, "any")).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
it("uses Math.random by default for rate rule", { timeout: 5000 }, () => {
|
||||
// At rate 1.0, should always be true regardless of random
|
||||
const rule = { type: "rate" as const, rate: 1.0 };
|
||||
for (let i = 0; i < 10; i++) {
|
||||
expect(shouldSample(rule, "e")).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("SamplingRuleSchema", () => {
|
||||
it("parses always rule", { timeout: 5000 }, () => {
|
||||
expect(SamplingRuleSchema.parse({ type: "always" })).toEqual({
|
||||
type: "always",
|
||||
});
|
||||
});
|
||||
|
||||
it("parses never rule", { timeout: 5000 }, () => {
|
||||
expect(SamplingRuleSchema.parse({ type: "never" })).toEqual({
|
||||
type: "never",
|
||||
});
|
||||
});
|
||||
|
||||
it("parses rate rule", { timeout: 5000 }, () => {
|
||||
expect(SamplingRuleSchema.parse({ type: "rate", rate: 0.5 })).toEqual({
|
||||
type: "rate",
|
||||
rate: 0.5,
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects invalid rate", { timeout: 5000 }, () => {
|
||||
expect(() =>
|
||||
SamplingRuleSchema.parse({ type: "rate", rate: 1.5 })
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
it("parses event-name rule", { timeout: 5000 }, () => {
|
||||
const result = SamplingRuleSchema.parse({
|
||||
type: "event-name",
|
||||
names: ["e"],
|
||||
});
|
||||
expect(result).toEqual({ type: "event-name", names: ["e"] });
|
||||
});
|
||||
|
||||
it("rejects unknown type", { timeout: 5000 }, () => {
|
||||
expect(() => SamplingRuleSchema.parse({ type: "unknown" })).toThrow();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
import z from "zod";
|
||||
import type { SamplingRule } from "./sampling-rule.types.js";
|
||||
|
||||
/** Zod schema for SamplingRule. */
|
||||
export const SamplingRuleSchema = z.discriminatedUnion("type", [
|
||||
z.object({ type: z.literal("always") }),
|
||||
z.object({ type: z.literal("never") }),
|
||||
z.object({ type: z.literal("rate"), rate: z.number().min(0).max(1) }),
|
||||
z.object({ type: z.literal("event-name"), names: z.array(z.string()) }),
|
||||
]);
|
||||
|
||||
/**
|
||||
* Determines whether a dispatch should be sampled given a rule and optional random value.
|
||||
*
|
||||
* @param rule - Sampling rule to evaluate
|
||||
* @param eventName - Name of the event being dispatched
|
||||
* @param rand - Random number in [0, 1) for rate sampling (injectable for testing)
|
||||
*/
|
||||
export function shouldSample(
|
||||
rule: SamplingRule,
|
||||
eventName: string,
|
||||
rand: () => number = Math.random
|
||||
): boolean {
|
||||
switch (rule.type) {
|
||||
case "always":
|
||||
return true;
|
||||
case "never":
|
||||
return false;
|
||||
case "rate":
|
||||
return rand() < rule.rate;
|
||||
case "event-name":
|
||||
return rule.names.includes(eventName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Sampling rule for controlling which dispatches produce spans.
|
||||
*/
|
||||
export type SamplingRule =
|
||||
| { type: "always" }
|
||||
| { type: "never" }
|
||||
| { type: "rate"; rate: number }
|
||||
| { type: "event-name"; names: string[] };
|
||||
@@ -0,0 +1,18 @@
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
|
||||
describe("dep-graph", () => {
|
||||
it("package has correct name", { timeout: 5000 }, async () => {
|
||||
const pkg = await import("../package.json", { with: { type: "json" } });
|
||||
expect(pkg.default.name).toBe("@suaveplan/events-otel");
|
||||
});
|
||||
|
||||
it("exports attachEventTracing", { timeout: 30000 }, async () => {
|
||||
const { attachEventTracing } = await import("../src/index.js");
|
||||
expect(typeof attachEventTracing).toBe("function");
|
||||
});
|
||||
|
||||
it("exports serializeTraceContext", { timeout: 30000 }, async () => {
|
||||
const { serializeTraceContext } = await import("../src/index.js");
|
||||
expect(typeof serializeTraceContext).toBe("function");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
import { webcrypto } from "node:crypto";
|
||||
|
||||
// Polyfill Web Crypto for Node.js environments (Vitest forks pool)
|
||||
if (typeof globalThis.crypto === "undefined") {
|
||||
Object.defineProperty(globalThis, "crypto", {
|
||||
value: webcrypto,
|
||||
writable: false,
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
12
packages/networking/events/events-otel/tsconfig.lib.json
Normal file
12
packages/networking/events/events-otel/tsconfig.lib.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"lib": ["ES2022"],
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["src/**/*.test.ts", "src/**/*.bench.ts", "dist", "node_modules"]
|
||||
}
|
||||
38
packages/networking/events/events-otel/vite.config.ts
Normal file
38
packages/networking/events/events-otel/vite.config.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { defineConfig } from "vite";
|
||||
import dts from "vite-plugin-dts";
|
||||
|
||||
export default defineConfig({
|
||||
root: __dirname,
|
||||
cacheDir: "../../../node_modules/.vite/packages/events-otel",
|
||||
plugins: [
|
||||
dts({
|
||||
entryRoot: "src",
|
||||
tsconfigPath: "./tsconfig.lib.json",
|
||||
insertTypesEntry: true,
|
||||
}),
|
||||
],
|
||||
build: {
|
||||
outDir: "./dist",
|
||||
emptyOutDir: true,
|
||||
reportCompressedSize: true,
|
||||
sourcemap: true,
|
||||
lib: { entry: "src/index.ts", formats: ["es"] },
|
||||
rollupOptions: {
|
||||
treeshake: true,
|
||||
output: {
|
||||
preserveModules: true,
|
||||
preserveModulesRoot: "src",
|
||||
entryFileNames: "[name].js",
|
||||
},
|
||||
external: [
|
||||
"@suaveplan/error",
|
||||
"@suaveplan/events",
|
||||
"@suaveplan/types",
|
||||
"tslib",
|
||||
"zod",
|
||||
"@opentelemetry/api",
|
||||
/^node:/,
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
26
packages/networking/events/events-otel/vitest.config.ts
Normal file
26
packages/networking/events/events-otel/vitest.config.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: false,
|
||||
environment: "node",
|
||||
pool: "forks",
|
||||
setupFiles: ["./tests/setup/polyfills.ts"],
|
||||
include: ["src/**/*.test.ts", "tests/**/*.test.ts"],
|
||||
exclude: ["dist/**", "node_modules/**"],
|
||||
coverage: {
|
||||
provider: "istanbul",
|
||||
reporter: ["text", "html", "lcov"],
|
||||
include: ["src/**/*.ts"],
|
||||
exclude: [
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.types.ts",
|
||||
"src/**/index.ts",
|
||||
"src/**/*.bench.ts",
|
||||
"tests/**",
|
||||
],
|
||||
thresholds: { statements: 100, branches: 100, functions: 100, lines: 100 },
|
||||
},
|
||||
testTimeout: 30000,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user