OpenTelemetry instrumentation for the Cleverbrush framework — traces, logs, and metrics over OTLP for @cleverbrush/server, @cleverbrush/orm, and @cleverbrush/log. Designed to ship straight into SigNoz, Grafana Tempo, Jaeger, or any OTLP-compatible backend.
npm install @cleverbrush/otel @opentelemetry/api
# Optional auto-instrumentations (only if you want them):
npm install @opentelemetry/instrumentation-http \
@opentelemetry/instrumentation-undici \
@opentelemetry/instrumentation-runtime-node
// telemetry.ts — load FIRST
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()
]
});
Run with:
node --import ./dist/telemetry.js dist/index.js
Don't forget to await otel.shutdown() on SIGTERM / SIGINT so batched data flushes.
import { tracingMiddleware } from '@cleverbrush/otel';
import { createServer } from '@cleverbrush/server';
const server = createServer()
.use(tracingMiddleware({ excludePaths: ['/health'] })) // first!
.use(corsMiddleware)
// ... rest of the chain
A SpanKind.SERVER span is opened per request, named operationId or METHOD route and tagged with the standard HTTP semantic-convention attributes. W3C traceparent is extracted, so spans link to upstream callers.
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(), SigNoz and other OTel backends show both services under one distributed trace.
Put the 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 Knex query becomes a SpanKind.CLIENT span with db.system.name, db.namespace, db.operation.name, db.query.text, and parented under the active server span automatically.
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 to every event
});
| Export | Purpose |
|---|---|
setupOtel(config) |
Boot the Node SDK; returns { shutdown(), sdk } |
tracingMiddleware(opts?) |
@cleverbrush/server middleware; opens SERVER span |
clientTracingMiddleware(opts?) |
@cleverbrush/client middleware; opens CLIENT span |
instrumentKnex(knex, opts?) |
Hook a Knex instance; emits CLIENT span per query |
otelLogSink(opts?) |
@cleverbrush/log sink → OTLP log records |
traceEnricher() |
@cleverbrush/log enricher → adds TraceId / SpanId |
configureOtel(services, opts?) |
Register ITracer / IMeter in @cleverbrush/di |
outboundHttpInstrumentations() |
Lazy-load HTTP / undici client auto-instrumentations |
runtimeMetrics() |
Lazy-load Node runtime metrics |
OTEL_SPAN_ITEM_KEY |
ctx.items key under which the request span is stashed |
All optional auto-instrumentations and knex are declared as optional peer dependencies, so the package is usable with only @opentelemetry/api installed.
tracingMiddleware stores the active server span on ctx.items under OTEL_SPAN_ITEM_KEY. Use this to attach custom attributes or events from inside endpoint handlers:
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 });
setupOtel reads OTEL_EXPORTER_OTLP_ENDPOINT from the environment by default (recommended). Per-signal endpoints (tracesEndpoint / logsEndpoint / metricsEndpoint) and signal toggles (disableTraces, disableLogs, disableMetrics) let you wire up split collectors. Headers (e.g. for SaaS tokens) are passed via headers: { authorization: 'Bearer …' }.
tracingMiddleware does not record query strings (recordQuery: false by default) and instrumentKnex lets you redact SQL via sanitizeStatement. otelLogSink accepts a sanitizeAttribute hook to drop sensitive fields per event.
BSD-3-Clause