End-to-end OpenTelemetry instrumentation for the framework — traces, logs, and metrics over OTLP for @cleverbrush/server, @cleverbrush/orm, and @cleverbrush/log.
Wiring OpenTelemetry into a Node.js service involves bootstrapping the SDK before anything else loads, choosing exporters per signal, and finding the right seam to open spans for HTTP requests, database queries, and outbound calls. Doing this consistently across services is repetitive and easy to get wrong.
@cleverbrush/otel provides one SDK bootstrap ( setupOtel ), middleware for @cleverbrush/server, a Knex hook for @cleverbrush/orm, an OTLP log sink + enricher for @cleverbrush/log, and DI registration for the active Tracer / Meter. All optional pieces are declared as optional peer dependencies — pull in only what you need.
Create a telemetry.ts module that loads before anything else, then start Node with --import ./dist/telemetry.js.
// telemetry.ts
import { setupOtel } from '@cleverbrush/otel';
import {
outboundHttpInstrumentations,
runtimeMetrics
} from '@cleverbrush/otel/instrumentations';
export const otel = setupOtel({
serviceName: 'todo-backend',
serviceVersion: '1.0.0',
environment: process.env.NODE_ENV,
otlpEndpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
instrumentations: [
...outboundHttpInstrumentations(),
...runtimeMetrics()
]
});import { tracingMiddleware } from '@cleverbrush/otel';
import { createServer } from '@cleverbrush/server';
const server = createServer()
.use(tracingMiddleware({ excludePaths: ['/health'] })) // first!
.use(corsMiddleware);A SpanKind.SERVER span is opened per request, named from the endpoint metadata (operationId or METHOD route), and tagged with HTTP semantic-convention attributes. Inbound W3C traceparent headers are extracted automatically.
import { instrumentKnex } from '@cleverbrush/otel';
import knex from 'knex';
const db = instrumentKnex(
knex({ client: 'pg', connection: '...' })
);Every query becomes a SpanKind.CLIENT span with db.system.name, db.namespace, db.operation.name, and db.query.text, parented under the active server span.
import { createLogger, consoleSink } from '@cleverbrush/log';
import { otelLogSink, traceEnricher } from '@cleverbrush/otel';
const logger = createLogger({
minimumLevel: 'information',
sinks: [consoleSink({ theme: 'dark' }), otelLogSink()],
enrichers: [traceEnricher()] // attaches TraceId/SpanId
});| Export | Purpose |
|---|---|
setupOtel(config) | Boot the Node SDK; returns { shutdown(), sdk } |
tracingMiddleware(opts?) | @cleverbrush/server middleware; opens SpanKind.SERVER span per request |
instrumentKnex(knex, opts?) | Hook a Knex instance; emits SpanKind.CLIENT span per query |
otelLogSink(opts?) | @cleverbrush/log sink → OTLP log records (severity, body, attributes, exception info) |
traceEnricher() | Log enricher → adds TraceId / SpanId / TraceFlags |
configureOtel(services, opts?) | Register ITracer / IMeter in @cleverbrush/di |
outboundHttpInstrumentations() | Lazy-load HTTP / undici client auto-instrumentations |
runtimeMetrics() | Lazy-load Node runtime metrics |
tracingMiddleware stores the active server span on ctx.items under OTEL_SPAN_ITEM_KEY. Use it to attach custom attributes or events directly from endpoint handlers without calling trace.getActiveSpan().
import { OTEL_SPAN_ITEM_KEY } from '@cleverbrush/otel';
import type { Span } from '@opentelemetry/api';
// Inside a @cleverbrush/server endpoint handler
const span = ctx.items.get(OTEL_SPAN_ITEM_KEY) as Span | undefined;
span?.setAttribute('app.user_id', userId);
span?.addEvent('cache.miss', { key: cacheKey });tracingMiddleware does not record query strings by default (recordQuery: false).instrumentKnex accepts a sanitizeStatement hook to redact SQL.otelLogSink accepts a sanitizeAttribute hook to drop or rewrite sensitive fields per event.The todo-backend demo is fully wired and ships traces, logs, and metrics to a ClickStack container included in demos/docker-compose.yml. Run docker compose up from demos/ and open http://localhost:8091 for the ClickStack UI.