End-to-end OpenTelemetry instrumentation for the framework — traces for inbound/outbound HTTP, SQL, and typed client calls; OTLP log sink with trace correlation; metrics — for @cleverbrush/server, @cleverbrush/client, @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 and @cleverbrush/client, 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 { createClient } from '@cleverbrush/client';
import { clientTracingMiddleware } from '@cleverbrush/otel/client';
const client = createClient(api, {
baseUrl: 'http://todo-backend:3000',
middlewares: [clientTracingMiddleware()]
});clientTracingMiddleware() opens a SpanKind.CLIENT span around each typed client call and injects W3C traceparent / tracestate / baggage headers. When the downstream Cleverbrush service uses tracingMiddleware(), OTel backends show both services under one distributed trace.
Put tracing middleware first in the client middleware list so it wraps retries, timeouts, and batching. If you use batching(), keep batching last so each logical subrequest carries its own trace context.
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 |
clientTracingMiddleware(opts?) | @cleverbrush/client middleware; opens SpanKind.CLIENT span per outbound call, injects W3C trace context |
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 SigNoz container included in demos/docker-compose.yml. Run docker compose up from demos/ and open http://localhost:8082 for the SigNoz UI.
Ready-made dashboard JSON files (HTTP traffic, DB calls, Node.js runtime, and business activity) live in demos/signoz/dashboards/. If they were not auto-provisioned on first boot, import them manually via Dashboards → Import in the SigNoz UI to visualise what the OTel instrumentation emits.