@cleverbrush/otel

End-to-end OpenTelemetry instrumentation for the framework — traces, logs, and metrics over OTLP for @cleverbrush/server, @cleverbrush/orm, and @cleverbrush/log.

$npm install @cleverbrush/otel @opentelemetry/api

Optional auto-instrumentations: @opentelemetry/instrumentation-http, @opentelemetry/instrumentation-undici, @opentelemetry/instrumentation-runtime-node.

💡 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, 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 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.

4. 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

ExportPurpose
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

🎯 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

🎯 See it in action

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.