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,99 @@
# @suaveplan/opentelemetry - OpenTelemetry SDK setup and configuration helpers: tracer/meter/logger providers, resource detection, exporter configuration, and sampling strategies
## Summary
OpenTelemetry SDK setup and configuration helpers for Suaveplan. Provides batteries-included OTel initialization helpers, resource detection, ergonomic span/metrics wrappers, W3C trace-context propagation utilities, and a no-op SDK façade that works without any heavyweight `@opentelemetry/sdk-*` packages.
<!-- begin:auto-docs -->
## Documentation tree
- [propagation](./src/core/propagation/propagation.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [propagation](./src/core/propagation/propagation.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [noop-sdk](./src/core/sdk/noop-sdk.md) — Provides `NoopSdk`, a zero-dependency implementation of the `OtelSdk` interface that delegates all tracer and meter loo…
- [sdk-factory](./src/core/sdk/sdk-factory.md) — Provides the `createSdk` factory function — the primary entry point for constructing an `OtelSdk` instance from a valid…
- [errors](./src/errors/opentelemetry-error.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [opentelemetry-error](./src/errors/opentelemetry-error.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [schemas](./src/schemas/otel-config.schema.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [otel-config.schema](./src/schemas/otel-config.schema.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [metrics-utils](./src/utils/metrics-utils.md) — Provides thin factory and recording helpers that wrap the `@opentelemetry/api` metrics API with sensible defaults and e…
- [resource](./src/utils/resource.md) — Builds a frozen OpenTelemetry resource attribute map from service identity configuration fields. Resource attributes ar…
- [span-utils](./src/utils/span-utils.md) — Provides ergonomic helper functions for working with OpenTelemetry spans. Rather than writing the same try/catch/finall…
<!-- end:auto-docs --> - [propagation](./src/core/propagation/propagation.md) — <!-- begin:auto-docs --> _(no submodules)_- [noop-sdk](./src/core/sdk/noop-sdk.md) — Provides `NoopSdk`, a zero-dependency implementation of the `OtelSdk` interface that delegates all tracer and meter loo…
- [sdk-factory](./src/core/sdk/sdk-factory.md) — Provides the `createSdk` factory function — the primary entry point for constructing an `OtelSdk` instance from a valid…
- [errors](./src/errors/opentelemetry-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [opentelemetry-error](./src/errors/opentelemetry-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/otel-config.schema.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel-config.schema](./src/schemas/otel-config.schema.md) — <!-- begin:auto-docs --> _(no submodules)_- [metrics-utils](./src/utils/metrics-utils.md) — Provides thin factory and recording helpers that wrap the `@opentelemetry/api` metrics API with sensible defaults and e…
- [resource](./src/utils/resource.md) — Builds a frozen OpenTelemetry resource attribute map from service identity configuration fields. Resource attributes ar…
- [span-utils](./src/utils/span-utils.md) — Provides ergonomic helper functions for working with OpenTelemetry spans. Rather than writing the same try/catch/finall…
- [propagation](./src/core/propagation/propagation.md) — <!-- begin:auto-docs --> _(no submodules)_- [noop-sdk](./src/core/sdk/noop-sdk.md) — Provides `NoopSdk`, a zero-dependency implementation of the `OtelSdk` interface that delegates all tracer and meter loo…
- [sdk-factory](./src/core/sdk/sdk-factory.md) — Provides the `createSdk` factory function — the primary entry point for constructing an `OtelSdk` instance from a valid…
- [errors](./src/errors/opentelemetry-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [opentelemetry-error](./src/errors/opentelemetry-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/otel-config.schema.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel-config.schema](./src/schemas/otel-config.schema.md) — <!-- begin:auto-docs --> _(no submodules)_- [metrics-utils](./src/utils/metrics-utils.md) — Provides thin factory and recording helpers that wrap the `@opentelemetry/api` metrics API with sensible defaults and e…
- [resource](./src/utils/resource.md) — Builds a frozen OpenTelemetry resource attribute map from service identity configuration fields. Resource attributes ar…
- [span-utils](./src/utils/span-utils.md) — Provides ergonomic helper functions for working with OpenTelemetry spans. Rather than writing the same try/catch/finall…
- [propagation](./src/core/propagation/propagation.md) — <!-- begin:auto-docs --> _(no submodules)_- [noop-sdk](./src/core/sdk/noop-sdk.md) — Provides `NoopSdk`, a zero-dependency implementation of the `OtelSdk` interface that delegates all tracer and meter loo…
- [sdk-factory](./src/core/sdk/sdk-factory.md) — Provides the `createSdk` factory function — the primary entry point for constructing an `OtelSdk` instance from a valid…
- [errors](./src/errors/opentelemetry-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [opentelemetry-error](./src/errors/opentelemetry-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/otel-config.schema.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel-config.schema](./src/schemas/otel-config.schema.md) — <!-- begin:auto-docs --> _(no submodules)_- [metrics-utils](./src/utils/metrics-utils.md) — Provides thin factory and recording helpers that wrap the `@opentelemetry/api` metrics API with sensible defaults and e…
- [resource](./src/utils/resource.md) — Builds a frozen OpenTelemetry resource attribute map from service identity configuration fields. Resource attributes ar…
- [span-utils](./src/utils/span-utils.md) — Provides ergonomic helper functions for working with OpenTelemetry spans. Rather than writing the same try/catch/finall…
- [propagation](./src/core/propagation/propagation.md) — <!-- begin:auto-docs --> _(no submodules)_- [noop-sdk](./src/core/sdk/noop-sdk.md) — Provides `NoopSdk`, a zero-dependency implementation of the `OtelSdk` interface that delegates all tracer and meter loo…
- [sdk-factory](./src/core/sdk/sdk-factory.md) — Provides the `createSdk` factory function — the primary entry point for constructing an `OtelSdk` instance from a valid…
- [errors](./src/errors/opentelemetry-error.md) — <!-- begin:auto-docs --> _(no submodules)_ - [opentelemetry-error](./src/errors/opentelemetry-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/otel-config.schema.md) — <!-- begin:auto-docs --> _(no submodules)_ - [otel-config.schema](./src/schemas/otel-config.schema.md) — <!-- begin:auto-docs --> _(no submodules)_- [metrics-utils](./src/utils/metrics-utils.md) — Provides thin factory and recording helpers that wrap the `@opentelemetry/api` metrics API with sensible defaults and e…
- [resource](./src/utils/resource.md) — Builds a frozen OpenTelemetry resource attribute map from service identity configuration fields. Resource attributes ar…
- [span-utils](./src/utils/span-utils.md) — Provides ergonomic helper functions for working with OpenTelemetry spans. Rather than writing the same try/catch/finall…
## Packages
| Module | Path | Description |
|--------|------|-------------|
| `errors` | [src/errors/opentelemetry-error.md](src/errors/opentelemetry-error.md) | Error class and code registry |
| `sdk` | [src/core/sdk/noop-sdk.md](src/core/sdk/noop-sdk.md) | `OtelSdk` interface and `NoopSdk` implementation |
| `sdk-factory` | [src/core/sdk/sdk-factory.md](src/core/sdk/sdk-factory.md) | `createSdk` — validated config → SDK instance |
| `span-utils` | [src/utils/span-utils.md](src/utils/span-utils.md) | `withSpan`, `recordException`, `addAttributes`, `getTraceContext` |
| `metrics-utils` | [src/utils/metrics-utils.md](src/utils/metrics-utils.md) | `createCounter`, `createHistogram`, `createGauge`, `recordDuration` |
| `resource` | [src/utils/resource.md](src/utils/resource.md) | `createResource` — OTel resource attribute builder |
| `propagation` | [src/core/propagation/propagation.md](src/core/propagation/propagation.md) | W3C traceparent inject/extract/parse/build |
## Quick Start
```ts
import { createSdk } from "@suaveplan/opentelemetry/sdk";
import { withSpan } from "@suaveplan/opentelemetry/span-utils";
import { createCounter, recordDuration } from "@suaveplan/opentelemetry/metrics-utils";
const sdk = createSdk({
serviceName: "my-service",
serviceVersion: "1.0.0",
environment: "production",
});
await sdk.start();
const tracer = sdk.getTracer("my-service");
const meter = sdk.getMeter("my-service");
const requestCounter = createCounter(meter, "http.request.total", "Total HTTP requests");
const latency = createHistogram(meter, "http.request.duration", "Request latency", "ms");
async function handleRequest(req: Request) {
return withSpan(tracer, "http.handle", async (span) => {
span.setAttribute("http.method", req.method);
const start = Date.now();
const response = await processRequest(req);
requestCounter.add(1, { "http.status": response.status });
recordDuration(latency, start, { "http.route": "/api" });
return response;
});
}
// Graceful shutdown
process.on("SIGTERM", () => sdk.shutdown());
```
## Subpath Exports
```ts
import { ... } from "@suaveplan/opentelemetry"; // all exports
import { createSdk, NoopSdk } from "@suaveplan/opentelemetry/sdk";
import { withSpan, recordException } from "@suaveplan/opentelemetry/span-utils";
import { createCounter, createHistogram } from "@suaveplan/opentelemetry/metrics-utils";
import { injectTraceContext, parseTraceParent } from "@suaveplan/opentelemetry/propagation";
import { OpenTelemetryError, OTEL_CODES } from "@suaveplan/opentelemetry/errors";
```

View File

@@ -0,0 +1,108 @@
# propagation
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides helpers for W3C Trace Context header injection and extraction, enabling distributed tracing across service boundaries. When a service makes an outgoing HTTP call it injects the current trace context into the request headers; when a service receives an inbound request it extracts the upstream trace context from the headers so subsequent spans are automatically parented to the upstream caller's span.
These utilities wrap `@opentelemetry/api`'s global propagator, meaning they respect whatever propagator implementation (or no-op) the application has registered — library code using these helpers does not need to know which propagation format is in use.
## Features
- `injectTraceContext` — adds `traceparent`/`tracestate` headers to an outgoing headers map
- `extractTraceContext` — parses incoming headers and activates the remote span context
- `buildTraceParent` — formats a W3C `traceparent` header string from parts
- `parseTraceParent` — validates and parses a `traceparent` header into structured fields
- `getActiveSpanIds` — returns the active span's trace ID and span ID for manual use
## Usage: Basic Example
```ts
import { injectTraceContext, extractTraceContext } from "@suaveplan/opentelemetry/propagation";
// Outgoing HTTP call
const headers = injectTraceContext({ "content-type": "application/json" });
await fetch("https://downstream.service/api", { headers });
// Incoming HTTP request handler
extractTraceContext(req.headers as Record<string, string>);
// All spans created after this call will be children of the upstream span
```
## Usage: Advanced Example
```ts
import {
buildTraceParent,
parseTraceParent,
injectTraceContext,
} from "@suaveplan/opentelemetry/propagation";
// Build a traceparent manually for synthetic test traces
const traceparent = buildTraceParent(
"4bf92f3577b34da6a3ce929d0e0e4736",
"00f067aa0ba902b7",
1 // sampled
);
// "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
// Parse and validate a received header
const parsed = parseTraceParent(traceparent);
if (parsed !== null) {
console.log(`trace=${parsed.traceId} span=${parsed.spanId} sampled=${parsed.flags & 1}`);
}
// null for invalid headers
parseTraceParent("not-a-header"); // null
parseTraceParent("01-invalid-version-format"); // null
```
## API Reference
### `injectTraceContext(headers)`
```ts
function injectTraceContext(headers: Record<string, string>): Record<string, string>
```
Injects the active W3C trace context into `headers` via the global propagator. Returns the same object with headers added. Safe to call with no active span — the propagator no-ops gracefully.
### `extractTraceContext(headers)`
```ts
function extractTraceContext(headers: Record<string, string>): void
```
Extracts a remote span context from `headers` using the global propagator. No-op when no recognisable headers are present.
### `buildTraceParent(traceId, spanId, flags?)`
```ts
function buildTraceParent(traceId: string, spanId: string, flags?: number): string
```
Formats a W3C `traceparent` header: `"00-{traceId}-{spanId}-{flags:02x}"`. `flags` defaults to `1` (sampled bit set).
### `parseTraceParent(header)`
```ts
function parseTraceParent(header: string): { traceId: string; spanId: string; flags: number } | null
```
Parses and validates a `traceparent` header. Returns `null` for any invalid format, including wrong version byte, wrong lengths, or non-hex characters.
### `getActiveSpanIds()`
```ts
function getActiveSpanIds(): { traceId?: string; spanId?: string }
```
Returns the currently active span's `traceId` and `spanId`. Returns an empty object when no span is active.
## Implementation Notes
`injectTraceContext` and `extractTraceContext` use custom `TextMapSetter` / `TextMapGetter` implementations that operate directly on `Record<string, string>` objects rather than requiring a specific carrier type. This avoids a dependency on `@opentelemetry/core`'s `defaultTextMapGetter`/`defaultTextMapSetter`.
`parseTraceParent` uses a strict regex (`/^00-.../`) that requires version `00` — the only currently defined version in the W3C spec. Future spec versions would need explicit handling. The `TRACE_ID_RE` and `SPAN_ID_RE` guards after the regex match are redundant for inputs that passed the outer regex but are kept for defence-in-depth in case the outer pattern is ever loosened.

View File

@@ -0,0 +1,96 @@
# noop-sdk
## Purpose
Provides `NoopSdk`, a zero-dependency implementation of the `OtelSdk` interface that delegates all tracer and meter lookups to `@opentelemetry/api`'s built-in no-op providers. It also exports the `OtelSdk` interface itself, which is the abstraction callers program against regardless of which concrete implementation they use.
Having a no-op default means library packages can instrument their code with spans and metrics without forcing a specific OpenTelemetry SDK on their consumers. Applications that want real telemetry simply register an SDK before `start()` is called; libraries that don't need it continue working unchanged with zero overhead.
## Features
- `OtelSdk` interface — the contract for all SDK implementations in this package
- `NoopSdk` class — fully functional no-op implementation; start/shutdown are immediate no-ops
- `getTracer` / `getMeter` — delegate to `@opentelemetry/api` global providers (no-op by default)
- Lifecycle tracking via `isStarted()` — useful for guard checks and health probes
- Zero external dependencies beyond `@opentelemetry/api`
## Usage: Basic Example
```ts
import { NoopSdk } from "@suaveplan/opentelemetry/sdk";
const sdk = new NoopSdk();
await sdk.start();
const tracer = sdk.getTracer("my-service");
// Spans created with this tracer are silently discarded
await sdk.shutdown();
```
## Usage: Advanced Example
Injecting `NoopSdk` in tests while using a real SDK in production:
```ts
import type { OtelSdk } from "@suaveplan/opentelemetry/sdk";
import { NoopSdk } from "@suaveplan/opentelemetry/sdk";
class PaymentService {
constructor(private readonly sdk: OtelSdk) {}
async charge(amount: number): Promise<void> {
const tracer = this.sdk.getTracer("payment-service");
const span = tracer.startSpan("payment.charge");
try {
await processCharge(amount);
} finally {
span.end();
}
}
}
// In tests:
const svc = new PaymentService(new NoopSdk());
// In production (with a real SDK implementation):
// const realSdk = createSdk({ serviceName: "payment-service", ... });
// const svc = new PaymentService(realSdk);
```
## API Reference
### `OtelSdk` interface
```ts
interface OtelSdk {
start(): Promise<void>;
shutdown(): Promise<void>;
isStarted(): boolean;
getTracer(name: string, version?: string): Tracer;
getMeter(name: string, version?: string): Meter;
}
```
The contract all SDK implementations satisfy.
### `NoopSdk`
```ts
class NoopSdk implements OtelSdk {
start(): Promise<void>;
shutdown(): Promise<void>;
isStarted(): boolean;
getTracer(name: string, version?: string): Tracer;
getMeter(name: string, version?: string): Meter;
}
```
- `start()` — sets `isStarted()` to `true` and resolves immediately
- `shutdown()` — sets `isStarted()` to `false` and resolves immediately
- `getTracer(name, version?)` — returns `trace.getTracer(name, version)` from the `@opentelemetry/api` global
- `getMeter(name, version?)` — returns `metrics.getMeter(name, version)` from the `@opentelemetry/api` global
## Implementation Notes
The `started` field is `protected` rather than `private` to allow subclasses (e.g. a real SDK wrapper that extends `NoopSdk`) to read and modify it. This follows the project's preference for `protected` over `private`. `getTracer` and `getMeter` are not guarded by `isStarted()` by design — the no-op API already handles calls made before registration silently, and guarding would add overhead in the hot path without benefit.

View File

@@ -0,0 +1,87 @@
# sdk-factory
## Purpose
Provides the `createSdk` factory function — the primary entry point for constructing an `OtelSdk` instance from a validated `OtelConfig`. It performs Zod schema validation before any SDK construction, so callers get a clear, structured error if the configuration is malformed rather than a cryptic runtime failure deep inside an SDK constructor.
The factory intentionally returns a `NoopSdk` today. This is not a limitation — it is a deliberate separation of concerns: this package is responsible for OTel configuration helpers and ergonomic wrappers, not for pulling in heavyweight `@opentelemetry/sdk-*` packages. Applications that need real telemetry install the SDK packages themselves and either extend `NoopSdk` or register providers directly.
## Features
- Validates the full `OtelConfig` structure with Zod before constructing the SDK
- Throws a typed `OpenTelemetryError` with `OTEL.INVALID_CONFIG` and structured issue context on validation failure
- Development-mode config logging via `console.debug` when `NODE_ENV === "development"`
- Returns an `OtelSdk`-typed value — callers are decoupled from the concrete implementation
## Usage: Basic Example
```ts
import { createSdk } from "@suaveplan/opentelemetry/sdk";
const sdk = createSdk({ serviceName: "my-service" });
await sdk.start();
const tracer = sdk.getTracer("my-service");
```
## Usage: Advanced Example
```ts
import { createSdk } from "@suaveplan/opentelemetry/sdk";
import { OpenTelemetryError, OTEL_CODES } from "@suaveplan/opentelemetry/errors";
// Full config with traces, metrics, and logs
const sdk = createSdk({
serviceName: "payment-service",
serviceVersion: "2.0.0",
environment: "staging",
traces: {
exporter: "otlp-http",
exporterUrl: "https://otel.internal.example.com/v1/traces",
sampler: "trace-id-ratio",
sampleRate: 0.1,
propagators: ["w3c-tracecontext", "baggage"],
batchSize: 512,
flushInterval: 5000,
},
metrics: {
exporter: "otlp-http",
exporterUrl: "https://otel.internal.example.com/v1/metrics",
exportIntervalMs: 30000,
},
logs: {
exporter: "otlp-http",
exporterUrl: "https://otel.internal.example.com/v1/logs",
},
});
// Invalid config — throws OpenTelemetryError
try {
createSdk({ serviceName: "" }); // empty string fails min(1)
} catch (err) {
if (err instanceof OpenTelemetryError && err.code === OTEL_CODES.INVALID_CONFIG) {
console.error("Config invalid:", err.context?.issues);
}
}
```
## API Reference
### `createSdk(config)`
```ts
function createSdk(config: OtelConfig): OtelSdk
```
Validates `config` with `OtelConfigSchema` and returns an `OtelSdk` instance.
- `config` — must satisfy the `OtelConfig` shape; see `otel-config.schema.ts` for full validation rules
- **Returns** an `OtelSdk` (currently `NoopSdk`)
- **Throws** `OpenTelemetryError` with code `OTEL.INVALID_CONFIG` when Zod validation fails; `err.context.issues` contains an array of `{ path, message }` objects
## Implementation Notes
The factory uses `OtelConfigSchema.safeParse` (not `.parse`) so validation failures are caught locally and re-thrown as `OpenTelemetryError` instances rather than Zod errors. This ensures callers always get typed errors from this package regardless of which validation library is in use.
Development logging is gated behind both `typeof process !== "undefined"` (safe in edge runtimes) and `process.env.NODE_ENV === "development"` (never logs in production or test). The logged object is `result.data` — the parsed and normalised output, not the raw input, so any Zod transformations are reflected.
The `NoopSdk` return is intentional and documented as a design contract rather than a TODO: applications that need real telemetry register their own providers and optionally wrap or replace the returned instance. This keeps `@suaveplan/opentelemetry` free of `@opentelemetry/sdk-*` dependencies.

View File

@@ -0,0 +1,101 @@
# opentelemetry-error
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides the stable error class and code registry for all `@suaveplan/opentelemetry` failures. Every error thrown by this package is an `OpenTelemetryError` instance with a `code` drawn from `OTEL_CODES`, enabling callers to branch on machine-readable codes rather than string-matching error messages.
The code registry follows the `defineCodes` convention shared across the project: a frozen record mapping short reason keys to fully-qualified dot-separated strings (`OTEL.<REASON>`), narrowing the `TCode` type parameter so TypeScript knows the exact union at compile time.
## Features
- Four stable error codes covering the full SDK lifecycle: initialization, shutdown, configuration, and state guard
- Extends `SuaveplanError` for cause-chain traversal, wire-format serialization, OTel span-event adapter, and redaction support inherited from `@suaveplan/error`
- Fully narrowed `TCode` type — `instanceof OpenTelemetryError` narrows `.code` to `OtelCode`
- Frozen code registry prevents accidental mutation
## Usage: Basic Example
```ts
import { OpenTelemetryError, OTEL_CODES } from "@suaveplan/opentelemetry/errors";
throw new OpenTelemetryError(
OTEL_CODES.INIT_FAILED,
"opentelemetry.init_failed"
);
```
## Usage: Advanced Example
```ts
import { OpenTelemetryError, OTEL_CODES } from "@suaveplan/opentelemetry/errors";
try {
await provider.start();
} catch (cause) {
throw new OpenTelemetryError(
OTEL_CODES.INIT_FAILED,
"opentelemetry.init_failed",
{
cause: cause instanceof Error ? cause : new Error(String(cause)),
context: { provider: "otlp-http", endpoint: "https://otel.example.com" },
}
);
}
```
Catching the code:
```ts
import { OpenTelemetryError, OTEL_CODES } from "@suaveplan/opentelemetry/errors";
try {
sdk.getTracer("my-service");
} catch (err) {
if (err instanceof OpenTelemetryError && err.code === OTEL_CODES.NOT_STARTED) {
console.warn("SDK not yet started — call sdk.start() first");
}
}
```
## API Reference
### `OTEL_CODES`
```ts
const OTEL_CODES: {
readonly INIT_FAILED: "OTEL.INIT_FAILED";
readonly SHUTDOWN_FAILED: "OTEL.SHUTDOWN_FAILED";
readonly INVALID_CONFIG: "OTEL.INVALID_CONFIG";
readonly NOT_STARTED: "OTEL.NOT_STARTED";
};
```
Frozen record mapping short reason keys to fully-qualified `OTEL.*` code strings.
### `OtelCode`
```ts
type OtelCode = "OTEL.INIT_FAILED" | "OTEL.SHUTDOWN_FAILED" | "OTEL.INVALID_CONFIG" | "OTEL.NOT_STARTED";
```
Union of all valid OTel error codes.
### `OpenTelemetryError`
```ts
class OpenTelemetryError extends SuaveplanError<OtelCode> {
constructor(code: OtelCode, messageKey: string, options?: SuaveplanErrorOptions): void;
}
```
- `code` — one of the `OTEL_CODES` values; available as `.code` on the thrown instance
- `messageKey` — i18n resolver key (falls back to the key itself when no resolver is registered)
- `options.cause` — original error to attach as cause
- `options.context` — arbitrary key/value bag for structured logging
## Implementation Notes
The constructor is marked with a biome suppress comment for `noUselessConstructor` because the sole purpose is to widen the `protected` base constructor to `public` — a required pattern across all `@suaveplan/*` error subclasses. The base `SuaveplanError` sets `this.name = new.target.name` automatically, so `error.name` will be `"OpenTelemetryError"` without needing a manual assignment.

View File

@@ -0,0 +1,159 @@
# otel-config.schema
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides the Zod schemas and inferred TypeScript types that define the complete OpenTelemetry SDK configuration contract for `@suaveplan/opentelemetry`. All runtime configuration objects are validated against these schemas before they reach any SDK lifecycle code, ensuring that schema violations surface as structured `OpenTelemetryError` instances rather than silent misbehavior or runtime crashes.
The schemas serve as the single source of truth for what constitutes a valid `OtelConfig`. Any change to the accepted configuration shape is made here first, and all downstream code (e.g. `createSdk`, test fixtures) derives from these exports.
## Features
- **`ExporterTypeSchema`** — enum of the four supported exporter backends: `console`, `otlp-http`, `otlp-grpc`, and `noop`.
- **`SamplerTypeSchema`** — enum of four trace-sampling strategies: `always-on`, `always-off`, `trace-id-ratio`, and `parent-based`.
- **`PropagatorTypeSchema`** — enum of four context-propagation formats: `w3c-tracecontext`, `baggage`, `b3`, and `b3-multi`.
- **`TracesConfigSchema`** — optional per-signal object covering exporter, URL, sampler type and rate, propagators list, batch size, and flush interval.
- **`MetricsConfigSchema`** — optional metrics signal object covering exporter, URL, and export interval.
- **`LogsConfigSchema`** — optional logs signal object covering exporter and URL.
- **`OtelConfigSchema`** — top-level schema requiring `serviceName` and allowing optional `serviceVersion`, `environment`, and per-signal sub-configs.
- **Inferred TypeScript types** — all schemas export a corresponding `z.infer<>` type alias so callers can use fully-typed config objects without re-declaring shapes.
## Usage: Basic Example
```ts
import { OtelConfigSchema } from "@suaveplan/opentelemetry";
const result = OtelConfigSchema.safeParse({
serviceName: "my-service",
serviceVersion: "1.0.0",
environment: "production",
});
if (!result.success) {
console.error(result.error.issues);
} else {
const config = result.data; // typed as OtelConfig
console.log(config.serviceName); // "my-service"
}
```
## Usage: Advanced Example
Using the schema to validate a rich configuration that covers all three signals, then feeding the validated data into `createSdk`:
```ts
import { createSdk, OtelConfigSchema, type OtelConfig } from "@suaveplan/opentelemetry";
const raw: unknown = {
serviceName: "payment-service",
serviceVersion: "2.3.1",
environment: "staging",
traces: {
exporter: "otlp-http",
exporterUrl: "https://otel.example.com/v1/traces",
sampler: "trace-id-ratio",
sampleRate: 0.25,
propagators: ["w3c-tracecontext", "baggage"],
batchSize: 512,
flushInterval: 5000,
},
metrics: {
exporter: "otlp-http",
exporterUrl: "https://otel.example.com/v1/metrics",
exportIntervalMs: 30000,
},
logs: {
exporter: "console",
},
};
const parsed = OtelConfigSchema.parse(raw); // throws ZodError on failure
const sdk = createSdk(parsed as OtelConfig);
await sdk.start();
```
You can also validate individual sub-schemas independently:
```ts
import { TracesConfigSchema } from "@suaveplan/opentelemetry";
const tracesResult = TracesConfigSchema.safeParse({
exporter: "noop",
sampler: "always-off",
});
// tracesResult.success === true
```
## API Reference
### `ExporterTypeSchema`
`z.ZodEnum` — validates that a value is one of `"console" | "otlp-http" | "otlp-grpc" | "noop"`.
### `SamplerTypeSchema`
`z.ZodEnum` — validates that a value is one of `"always-on" | "always-off" | "trace-id-ratio" | "parent-based"`.
### `PropagatorTypeSchema`
`z.ZodEnum` — validates that a value is one of `"w3c-tracecontext" | "baggage" | "b3" | "b3-multi"`.
### `TracesConfigSchema`
`z.ZodObject` — all fields optional:
- `exporter?: ExporterType`
- `exporterUrl?: string` (validated as URL)
- `sampler?: SamplerType`
- `sampleRate?: number` (0.0–1.0 inclusive)
- `propagators?: PropagatorType[]`
- `batchSize?: number` (positive integer)
- `flushInterval?: number` (positive integer, milliseconds)
### `MetricsConfigSchema`
`z.ZodObject` — all fields optional:
- `exporter?: ExporterType`
- `exporterUrl?: string` (validated as URL)
- `exportIntervalMs?: number` (positive integer, milliseconds)
### `LogsConfigSchema`
`z.ZodObject` — all fields optional:
- `exporter?: ExporterType`
- `exporterUrl?: string` (validated as URL)
### `OtelConfigSchema`
`z.ZodObject` — top-level config schema:
- `serviceName: string` (required, minimum length 1)
- `serviceVersion?: string`
- `environment?: string`
- `traces?: TracesConfig`
- `metrics?: MetricsConfig`
- `logs?: LogsConfig`
### Type Aliases
- `OtelConfig` — `z.infer<typeof OtelConfigSchema>`
- `TracesConfig` — `z.infer<typeof TracesConfigSchema>`
- `MetricsConfig` — `z.infer<typeof MetricsConfigSchema>`
- `LogsConfig` — `z.infer<typeof LogsConfigSchema>`
- `ExporterType` — `z.infer<typeof ExporterTypeSchema>`
- `SamplerType` — `z.infer<typeof SamplerTypeSchema>`
- `PropagatorType` — `z.infer<typeof PropagatorTypeSchema>`
## Implementation Notes
The schema file is intentionally kept free of business logic — it contains only Zod definitions and their `z.infer<>` type exports. Validation errors from `OtelConfigSchema.safeParse` are surfaced to callers by `createSdk`, which maps them into `OpenTelemetryError` instances with the `OTEL.INVALID_CONFIG` code.
The `sampleRate` field uses `z.number().min(0).max(1)` with both bounds inclusive so that `0.0` (never sample) and `1.0` (always sample) are valid — these boundary values are important for feature-flag-driven sampling.
All URL fields use `z.string().url()` rather than a looser regex to ensure that only well-formed absolute URLs are accepted, preventing misconfigured exporter endpoints from reaching the network layer at runtime.
The enum schemas (`ExporterTypeSchema`, `SamplerTypeSchema`, `PropagatorTypeSchema`) are exported as named constants rather than inlined inside the parent schemas so that they can be reused independently for validation of partial configs or for driving UI option lists without pulling in the full parent schema.

View File

@@ -0,0 +1,108 @@
# metrics-utils
## Purpose
Provides thin factory and recording helpers that wrap the `@opentelemetry/api` metrics API with sensible defaults and ergonomic call signatures. These utilities reduce boilerplate at call sites, enforce consistent instrument creation patterns, and make the most common metrics pattern — recording a duration — a single-line operation.
Like all utilities in this package, these depend only on `@opentelemetry/api`, so they work equally well with any SDK implementation or the built-in no-op fallback used in tests and library code.
## Features
- `createCounter` — monotonically increasing counter for totals (requests, errors, events)
- `createHistogram` — value distribution tracking for latencies and sizes (supports unit string)
- `createGauge` — observable gauge for current-value snapshots (memory, queue depth)
- `recordDuration` — records elapsed `Date.now() - startMs` on a histogram in one call
## Usage: Basic Example
```ts
import { createCounter, createHistogram, recordDuration } from "@suaveplan/opentelemetry/metrics-utils";
import { metrics } from "@opentelemetry/api";
const meter = metrics.getMeter("my-service");
const errors = createCounter(meter, "app.errors", "Total application errors");
errors.add(1, { "error.type": "validation" });
const latency = createHistogram(meter, "app.request.duration", "Request duration", "ms");
const start = Date.now();
await handleRequest();
recordDuration(latency, start, { "http.route": "/api/users" });
```
## Usage: Advanced Example
```ts
import { createGauge, createHistogram, createCounter } from "@suaveplan/opentelemetry/metrics-utils";
import { metrics } from "@opentelemetry/api";
const meter = metrics.getMeter("worker-service", "1.0.0");
// Observable gauge for current queue depth (polled at collection time)
const queueGauge = createGauge(meter, "worker.queue.depth", "Current items in the work queue");
let currentQueueDepth = 0;
queueGauge.addCallback((result) => {
result.observe(currentQueueDepth, { "queue.name": "orders" });
});
// Histogram with explicit unit
const processingTime = createHistogram(
meter,
"worker.job.processing_time",
"Time spent processing each job",
"ms"
);
// Counter for tracking retries
const retryCounter = createCounter(meter, "worker.job.retries", "Total job retry attempts");
async function processJob(job: Job) {
const start = Date.now();
try {
await job.execute();
} catch {
retryCounter.add(1, { "job.type": job.type });
throw;
} finally {
recordDuration(processingTime, start, { "job.type": job.type });
}
}
```
## API Reference
### `createCounter(meter, name, description?)`
```ts
function createCounter(meter: Meter, name: string, description?: string): Counter
```
Creates a synchronous, monotonically increasing counter. Call `.add(value, attributes?)` to increment.
### `createHistogram(meter, name, description?, unit?)`
```ts
function createHistogram(meter: Meter, name: string, description?: string, unit?: string): Histogram
```
Creates a synchronous histogram instrument. Call `.record(value, attributes?)` to record an observation. `unit` follows the OTel unit convention (e.g. `"ms"`, `"By"`, `"1"`).
### `createGauge(meter, name, description?)`
```ts
function createGauge(meter: Meter, name: string, description?: string): ObservableGauge
```
Creates an asynchronous observable gauge. Register a callback via `.addCallback(fn)` to supply the current value at collection time.
### `recordDuration(histogram, startMs, attributes?)`
```ts
function recordDuration(histogram: Histogram, startMs: number, attributes?: Attributes): void
```
Records `Date.now() - startMs` on `histogram`. `startMs` should be captured with `Date.now()` immediately before the operation being measured.
## Implementation Notes
All factory functions pass the `description` to the underlying API even when it is `undefined` — the API ignores undefined values gracefully. The decision to use `createObservableGauge` (asynchronous) over `createGauge` (synchronous, upDownCounter-style) aligns with the OTel recommendation that current-value snapshots should use the observable API so the SDK can poll them at collection time rather than storing every intermediate value. `recordDuration` always uses `Date.now()` as the wall-clock source; for high-resolution measurement use `performance.now()` and convert to milliseconds before passing as `startMs`.

View File

@@ -0,0 +1,66 @@
# resource
## Purpose
Builds a frozen OpenTelemetry resource attribute map from service identity configuration fields. Resource attributes are attached to every span, metric, and log record produced by the SDK, providing the correlation anchor that ties telemetry signals back to a specific deployed service instance.
This utility abstracts the mapping from `OtelConfig` fields to OTEL Semantic Conventions keys (`service.name`, `service.version`, `deployment.environment`), so callers never have to remember which convention strings to use.
## Features
- Maps `serviceName` → `service.name` (always present)
- Maps `serviceVersion` → `service.version` (omitted when undefined)
- Maps `environment` → `deployment.environment` (omitted when undefined)
- Returns a frozen `Attributes` object — safe to share across SDK, tests, and exporters
- Zero side effects; pure function
## Usage: Basic Example
```ts
import { createResource } from "@suaveplan/opentelemetry/span-utils";
const attrs = createResource({ serviceName: "payment-service" });
// { "service.name": "payment-service" }
```
## Usage: Advanced Example
```ts
import { createResource } from "@suaveplan/opentelemetry/span-utils";
const attrs = createResource({
serviceName: "payment-service",
serviceVersion: "2.1.0",
environment: "production",
});
// {
// "service.name": "payment-service",
// "service.version": "2.1.0",
// "deployment.environment": "production",
// }
// Verify immutability
Object.assign(attrs, { extra: "ignored" }); // throws in strict mode (frozen)
```
## API Reference
### `createResource(config)`
```ts
function createResource(
config: Pick<OtelConfig, "serviceName" | "serviceVersion" | "environment">
): Attributes
```
Builds and freezes an OTel resource attribute map.
- `config.serviceName` — required; mapped to `service.name`
- `config.serviceVersion` — optional; mapped to `service.version` when present
- `config.environment` — optional; mapped to `deployment.environment` when present
**Returns** a frozen `Attributes` object (`Record<string, AttributeValue>`).
## Implementation Notes
The object is frozen with `Object.freeze` to prevent accidental mutation after construction. Optional fields are only included when they are not `undefined` — this matches the OpenTelemetry convention where absent attributes should be omitted rather than set to empty strings. The function does not accept a full `OtelConfig` in order to remain decoupled from signal-level configuration and avoid pulling in Zod validation at call time.

View File

@@ -0,0 +1,102 @@
# span-utils
## Purpose
Provides ergonomic helper functions for working with OpenTelemetry spans. Rather than writing the same try/catch/finally/recordException/setStatus boilerplate in every traced function, callers use `withSpan` to declaratively wrap work in a span with correct error handling built in.
These utilities depend only on `@opentelemetry/api` and are therefore compatible with any OTel SDK implementation (or the no-op fallback), making them safe to import in library code that should not force a specific SDK on its consumers.
## Features
- `withSpan` — wraps an async function in an active span; records exceptions and ends the span unconditionally
- `recordException` — normalizes any thrown value to an Error and records it with ERROR status
- `addAttributes` — sets a bulk `Attributes` map on a span in a single call
- `getTraceContext` — extracts `traceId`, `spanId`, and `traceFlags` from the currently active span
## Usage: Basic Example
```ts
import { withSpan } from "@suaveplan/opentelemetry/span-utils";
import { trace } from "@opentelemetry/api";
const tracer = trace.getTracer("my-service");
const result = await withSpan(tracer, "user.fetch", async (span) => {
span.setAttribute("user.id", userId);
return await db.findUser(userId);
});
```
## Usage: Advanced Example
```ts
import { withSpan, addAttributes, getTraceContext } from "@suaveplan/opentelemetry/span-utils";
import { trace } from "@opentelemetry/api";
const tracer = trace.getTracer("payment-service", "1.0.0");
async function processPayment(orderId: string, amount: number) {
return withSpan(
tracer,
"payment.process",
async (span) => {
addAttributes(span, {
"payment.order_id": orderId,
"payment.amount": amount,
"payment.currency": "USD",
});
const result = await chargeGateway(orderId, amount);
const ctx = getTraceContext();
console.log("trace:", ctx.traceId, "span:", ctx.spanId);
return result;
},
{ attributes: { "payment.initial": true } }
);
}
```
## API Reference
### `withSpan(tracer, name, fn, options?)`
```ts
function withSpan<T>(
tracer: Tracer,
name: string,
fn: (span: Span) => Promise<T>,
options?: { attributes?: Attributes }
): Promise<T>
```
Starts an active child span, calls `fn` with it, ends the span in `finally`, and records any exception + sets ERROR status before re-throwing.
### `recordException(span, error)`
```ts
function recordException(span: Span, error: unknown): void
```
Records `error` on `span` as a span event and sets `SpanStatusCode.ERROR`. Non-Error values are wrapped in `new Error(String(error))` before recording.
### `addAttributes(span, attrs)`
```ts
function addAttributes(span: Span, attrs: Attributes): void
```
Sets each key/value pair from `attrs` on the span. `undefined` values are skipped.
### `getTraceContext()`
```ts
function getTraceContext(): { traceId?: string; spanId?: string; traceFlags?: number }
```
Returns the active span's context fields. All fields are `undefined` when no span is active.
## Implementation Notes
`withSpan` uses `tracer.startActiveSpan` so that child spans created inside `fn` are automatically parented to this span via the context API — no manual context propagation required. The `finally` block ensures `span.end()` is always called even when `fn` throws, which is the single most common OTel correctness bug in hand-written tracing code. `recordException` follows the OTel spec by calling the API's built-in `span.recordException` rather than manually building the event, ensuring the `exception.type`, `exception.message`, and `exception.stacktrace` attributes are populated correctly.