@cleverbrush/otel
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.
💡 Why @cleverbrush/otel?
The Problem
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.
The Solution
@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.
🚀 Quick Start
1. Bootstrap the SDK first
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()
]
});2. Trace HTTP requests
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.
3. Trace typed client calls
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.
4. Trace SQL queries
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.
5. Send logs as OTLP records
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
});📚 API
| 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 |
🎯 Accessing the Request Span from Handlers
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 });🔒 Privacy & Redaction
tracingMiddlewaredoes not record query strings by default (recordQuery: false).instrumentKnexaccepts asanitizeStatementhook to redact SQL.otelLogSinkaccepts asanitizeAttributehook to drop or rewrite sensitive fields per event.
🎯 See it in action
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.