@cleverbrush/otel 0.0.0-beta-20260424142030

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,127 @@
1
+ # @cleverbrush/otel
2
+
3
+ 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 ClickStack, Grafana Tempo, Jaeger, or any OTLP-compatible backend.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @cleverbrush/otel @opentelemetry/api
9
+ # Optional auto-instrumentations (only if you want them):
10
+ npm install @opentelemetry/instrumentation-http \
11
+ @opentelemetry/instrumentation-undici \
12
+ @opentelemetry/instrumentation-runtime-node
13
+ ```
14
+
15
+ ## Quick Start
16
+
17
+ ### 1. Bootstrap the SDK (must run before anything else)
18
+
19
+ ```ts
20
+ // telemetry.ts — load FIRST
21
+ import { setupOtel } from '@cleverbrush/otel';
22
+ import {
23
+ outboundHttpInstrumentations,
24
+ runtimeMetrics
25
+ } from '@cleverbrush/otel/instrumentations';
26
+
27
+ export const otel = setupOtel({
28
+ serviceName: 'todo-backend',
29
+ serviceVersion: '1.0.0',
30
+ environment: process.env.NODE_ENV,
31
+ otlpEndpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
32
+ instrumentations: [
33
+ ...outboundHttpInstrumentations(),
34
+ ...runtimeMetrics()
35
+ ]
36
+ });
37
+ ```
38
+
39
+ Run with:
40
+
41
+ ```bash
42
+ node --import ./dist/telemetry.js dist/index.js
43
+ ```
44
+
45
+ Don't forget to await `otel.shutdown()` on `SIGTERM` / `SIGINT` so batched data flushes.
46
+
47
+ ### 2. Trace HTTP requests
48
+
49
+ ```ts
50
+ import { tracingMiddleware } from '@cleverbrush/otel';
51
+ import { createServer } from '@cleverbrush/server';
52
+
53
+ const server = createServer()
54
+ .use(tracingMiddleware({ excludePaths: ['/health'] })) // first!
55
+ .use(corsMiddleware)
56
+ // ... rest of the chain
57
+ ```
58
+
59
+ 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.
60
+
61
+ ### 3. Trace SQL queries
62
+
63
+ ```ts
64
+ import { instrumentKnex } from '@cleverbrush/otel';
65
+ import knex from 'knex';
66
+
67
+ const db = instrumentKnex(
68
+ knex({ client: 'pg', connection: '...' })
69
+ );
70
+ ```
71
+
72
+ 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.
73
+
74
+ ### 4. Send logs as OTLP records (with trace correlation)
75
+
76
+ ```ts
77
+ import { createLogger, consoleSink } from '@cleverbrush/log';
78
+ import { otelLogSink, traceEnricher } from '@cleverbrush/otel';
79
+
80
+ const logger = createLogger({
81
+ minimumLevel: 'information',
82
+ sinks: [consoleSink({ theme: 'dark' }), otelLogSink()],
83
+ enrichers: [traceEnricher()] // attaches TraceId/SpanId to every event
84
+ });
85
+ ```
86
+
87
+ ## API
88
+
89
+ | Export | Purpose |
90
+ | --------------------------------- | --------------------------------------------------------- |
91
+ | `setupOtel(config)` | Boot the Node SDK; returns `{ shutdown(), sdk }` |
92
+ | `tracingMiddleware(opts?)` | `@cleverbrush/server` middleware; opens SERVER span |
93
+ | `instrumentKnex(knex, opts?)` | Hook a Knex instance; emits CLIENT span per query |
94
+ | `otelLogSink(opts?)` | `@cleverbrush/log` sink → OTLP log records |
95
+ | `traceEnricher()` | `@cleverbrush/log` enricher → adds `TraceId` / `SpanId` |
96
+ | `configureOtel(services, opts?)` | Register `ITracer` / `IMeter` in `@cleverbrush/di` |
97
+ | `outboundHttpInstrumentations()` | Lazy-load HTTP / undici client auto-instrumentations |
98
+ | `runtimeMetrics()` | Lazy-load Node runtime metrics |
99
+ | `OTEL_SPAN_ITEM_KEY` | `ctx.items` key under which the request span is stashed |
100
+
101
+ All optional auto-instrumentations and `knex` are declared as **optional peer dependencies**, so the package is usable with only `@opentelemetry/api` installed.
102
+
103
+ ## Accessing the Request Span from Handlers
104
+
105
+ `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:
106
+
107
+ ```ts
108
+ import { OTEL_SPAN_ITEM_KEY } from '@cleverbrush/otel';
109
+ import type { Span } from '@opentelemetry/api';
110
+
111
+ // Inside a @cleverbrush/server endpoint handler
112
+ const span = ctx.items.get(OTEL_SPAN_ITEM_KEY) as Span | undefined;
113
+ span?.setAttribute('app.user_id', userId);
114
+ span?.addEvent('cache.miss', { key: cacheKey });
115
+ ```
116
+
117
+ ## Configuration
118
+
119
+ `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 …' }`.
120
+
121
+ ## Privacy
122
+
123
+ `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.
124
+
125
+ ## License
126
+
127
+ BSD-3-Clause
package/dist/di.d.ts ADDED
@@ -0,0 +1,62 @@
1
+ /**
2
+ * DI service key for an OpenTelemetry `Tracer`.
3
+ *
4
+ * Resolved from the global `TracerProvider` set by
5
+ * {@link import('./setupOtel.js').setupOtel}. Components that prefer
6
+ * dependency injection over the global API can inject this token.
7
+ */
8
+ export declare const ITracer: {
9
+ __brand: "ITracer";
10
+ };
11
+ /**
12
+ * DI service key for an OpenTelemetry `Meter`.
13
+ *
14
+ * Resolved from the global `MeterProvider` set by
15
+ * {@link import('./setupOtel.js').setupOtel}.
16
+ */
17
+ export declare const IMeter: {
18
+ __brand: "IMeter";
19
+ };
20
+ /**
21
+ * Configuration for {@link configureOtel}.
22
+ */
23
+ export interface ConfigureOtelOptions {
24
+ /**
25
+ * Tracer name.
26
+ *
27
+ * @default '@cleverbrush/otel'
28
+ */
29
+ tracerName?: string;
30
+ /**
31
+ * Meter name.
32
+ *
33
+ * @default '@cleverbrush/otel'
34
+ */
35
+ meterName?: string;
36
+ /** Optional version string used for both tracer and meter. */
37
+ version?: string;
38
+ }
39
+ /**
40
+ * Registers OTel `Tracer` and `Meter` instances in the DI container.
41
+ *
42
+ * Both are resolved lazily from the global providers configured by
43
+ * {@link import('./setupOtel.js').setupOtel}, so this helper can be
44
+ * called at DI setup time even before the SDK has fully started.
45
+ *
46
+ * @param services - the `ServiceCollection` to register with
47
+ * @param options - tracer / meter naming overrides
48
+ *
49
+ * @example
50
+ * ```ts
51
+ * import { configureOtel, ITracer } from '@cleverbrush/otel';
52
+ *
53
+ * configureOtel(services, { tracerName: 'todo-backend' });
54
+ *
55
+ * const tracer = provider.get(ITracer);
56
+ * tracer.startActiveSpan('custom-work', span => {
57
+ * // …
58
+ * span.end();
59
+ * });
60
+ * ```
61
+ */
62
+ export declare function configureOtel(services: any, options?: ConfigureOtelOptions): void;
@@ -0,0 +1,25 @@
1
+ import type { Enricher } from '@cleverbrush/log';
2
+ /**
3
+ * Creates a log enricher that attaches the active span's
4
+ * `TraceId`, `SpanId`, and `TraceFlags` to every log event.
5
+ *
6
+ * Reads from the OpenTelemetry context via `@opentelemetry/api`,
7
+ * so it works with any tracer provider — including the one
8
+ * configured by {@link import('../setupOtel.js').setupOtel}.
9
+ *
10
+ * No-op when no span is active.
11
+ *
12
+ * @returns an enricher that adds `{ TraceId, SpanId, TraceFlags }` if a span is active
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * import { createLogger, consoleSink } from '@cleverbrush/log';
17
+ * import { traceEnricher } from '@cleverbrush/otel';
18
+ *
19
+ * const logger = createLogger({
20
+ * sinks: [consoleSink()],
21
+ * enrichers: [traceEnricher()],
22
+ * });
23
+ * ```
24
+ */
25
+ export declare function traceEnricher(): Enricher;
@@ -0,0 +1,6 @@
1
+ export { type ConfigureOtelOptions, configureOtel, IMeter, ITracer } from './di.js';
2
+ export { traceEnricher } from './enrichers/trace.js';
3
+ export { type InstrumentKnexOptions, instrumentKnex } from './knex/instrumentKnex.js';
4
+ export { OTEL_SPAN_ITEM_KEY, type TracingMiddlewareOptions, tracingMiddleware } from './middleware/tracing.js';
5
+ export { type OtelConfig, type OtelHandle, setupOtel } from './setupOtel.js';
6
+ export { type OtelLogSinkOptions, otelLogSink } from './sinks/OtelLogSink.js';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ import{metrics as k,trace as C}from"@opentelemetry/api";var L=Symbol.for("ITracer"),w=Symbol.for("IMeter");function D(e,r){let n=r?.tracerName??"@cleverbrush/otel",s=r?.meterName??"@cleverbrush/otel",i=r?.version;typeof e?.addSingleton=="function"&&(e.addSingleton(L,()=>C.getTracer(n,i)),e.addSingleton(w,()=>k.getMeter(s,i)))}import{trace as U}from"@opentelemetry/api";function K(){return e=>{let r=U.getActiveSpan();if(!r)return e;let n=r.spanContext();return n?.traceId?{...e,properties:{...e.properties,TraceId:n.traceId,SpanId:n.spanId,TraceFlags:n.traceFlags}}:e}}import{SpanKind as V,SpanStatusCode as v,trace as H}from"@opentelemetry/api";import{ATTR_DB_NAMESPACE as q,ATTR_DB_OPERATION_NAME as Q,ATTR_DB_QUERY_TEXT as $,ATTR_DB_SYSTEM_NAME as B,ATTR_SERVER_ADDRESS as Y,ATTR_SERVER_PORT as F}from"@opentelemetry/semantic-conventions/incubating";var z=/^\s*([A-Za-z]+)/;function j(e,r){return r?r.toUpperCase():z.exec(e)?.[1]?.toUpperCase()}function G(e){let r=e?.dialect??e?.config?.client;if(!r)return;let n=String(r).toLowerCase();return n.startsWith("pg")||n.includes("postgres")?"postgresql":n.includes("mysql")||n==="mysql2"?"mysql":n.includes("sqlite")||n==="better-sqlite3"?"sqlite":n.includes("mssql")?"mssql":n.includes("oracle")?"oracle":n}function W(e,r){let n=r?.tracerName??"@cleverbrush/otel/knex",s=r?.recordStatement??!0,i=r?.sanitizeStatement,T,E=()=>(T||(T=H.getTracer(n)),T),g=e.client,o=r?.dbSystem??G(g)??"other_sql",t=g?.config?.connection??{},u=typeof t=="object"?t.database:void 0,c=typeof t=="object"?t.host:void 0,R=typeof t=="object"&&typeof t.port=="number"?t.port:void 0,b=new Map,S={[B]:o};return u&&(S[q]=u),c&&(S[Y]=c),typeof R=="number"&&(S[F]=R),e.on?.("query",p=>{let m=p.__knexQueryUid;if(!m)return;let a=j(p.sql,p.method),d=a??"db.query",O={...S};a&&(O[Q]=a),s&&p.sql&&(O[$]=i?i(p.sql):p.sql);let y=E().startSpan(d,{kind:V.CLIENT,attributes:O});b.set(m,y)}),e.on?.("query-response",(p,m)=>{let a=m?.__knexQueryUid;if(!a)return;let d=b.get(a);d&&(b.delete(a),d.setStatus({code:v.OK}),d.end())}),e.on?.("query-error",(p,m)=>{let a=m?.__knexQueryUid;if(!a)return;let d=b.get(a);d&&(b.delete(a),p instanceof Error?(d.recordException(p),d.setStatus({code:v.ERROR,message:p.message})):d.setStatus({code:v.ERROR,message:String(p)}),d.end())}),e}import{context as X,propagation as J,SpanKind as Z,SpanStatusCode as A,trace as ee}from"@opentelemetry/api";import{ATTR_HTTP_REQUEST_METHOD as te,ATTR_HTTP_RESPONSE_STATUS_CODE as I,ATTR_HTTP_ROUTE as re,ATTR_SERVER_ADDRESS as ne,ATTR_SERVER_PORT as oe,ATTR_URL_PATH as se,ATTR_URL_QUERY as ie,ATTR_URL_SCHEME as ae,ATTR_USER_AGENT_ORIGINAL as ce}from"@opentelemetry/semantic-conventions";var M="otel.span";function de(e){return e?.items?.get("__endpoint_meta")}function ue(e){if(!e)return;let r=e.basePath??"",n=e.pathTemplate;if(typeof n=="string")return`${r}${n}`;let s=n?.template??n?.pattern??n?.toString?.()??null;return typeof s=="string"?`${r}${s}`:r||void 0}function pe(e){let r=e?.tracerName??"@cleverbrush/otel",n=e?.tracerVersion,s=new Set((e?.excludePaths??[]).map(t=>typeof t=="string"?t:t.path)),i=e?.enrichSpan,T=e?.recordQuery??!1,E=e?.responseTraceHeader===!1?!1:e?.responseTraceHeader??"X-Trace-Id",g,o=()=>(g||(g=ee.getTracer(r,n)),g);return async(t,u)=>{let c=t.url,R=c?.pathname??t.path??"";if(s.has(R)){await u();return}let b=o(),S=t.headers??{},p=J.extract(X.active(),S),m=de(t),a=ue(m),d=(t.method??"GET").toUpperCase(),O=m?.operationId||(a?`${d} ${a}`:`${d} ${R}`),y={[te]:d,[se]:R};if(c?.protocol&&(y[ae]=c.protocol.replace(/:$/,"")),c?.hostname&&(y[ne]=c.hostname),c?.port){let l=Number(c.port);Number.isFinite(l)&&(y[oe]=l)}a&&(y[re]=a),S["user-agent"]&&(y[ce]=S["user-agent"]),T&&c?.search&&(y[ie]=c.search.replace(/^\?/,"")),m?.tags?.length&&(y["cleverbrush.endpoint.tags"]=m.tags.join(",")),m?.operationId&&(y["cleverbrush.endpoint.operationId"]=m.operationId),await b.startActiveSpan(O,{kind:Z.SERVER,attributes:y},p,async l=>{if(t.items?.set?.(M,l),E){let{traceId:f}=l.spanContext();if(f){let x=t.response;x?.setHeader?x.setHeader(E,f):t.setHeader&&t.setHeader(E,f)}}if(i)try{i(l,t)}catch{}try{await u();let f=t.response?.statusCode??t.statusCode??200;l.setAttribute(I,f),f>=500?l.setStatus({code:A.ERROR}):l.setStatus({code:A.OK})}catch(f){let x=t.response?.statusCode??t.statusCode??500;throw l.setAttribute(I,x),f instanceof Error?(l.recordException(f),l.setStatus({code:A.ERROR,message:f.message})):l.setStatus({code:A.ERROR,message:String(f)}),f}finally{l.end()}})}}import{DiagConsoleLogger as le,DiagLogLevel as me,diag as ge}from"@opentelemetry/api";import{OTLPLogExporter as fe}from"@opentelemetry/exporter-logs-otlp-http";import{OTLPMetricExporter as Te}from"@opentelemetry/exporter-metrics-otlp-http";import{OTLPTraceExporter as Ee}from"@opentelemetry/exporter-trace-otlp-http";import{resourceFromAttributes as ye}from"@opentelemetry/resources";import{BatchLogRecordProcessor as Re}from"@opentelemetry/sdk-logs";import{PeriodicExportingMetricReader as Se}from"@opentelemetry/sdk-metrics";import{NodeSDK as be}from"@opentelemetry/sdk-node";import{BatchSpanProcessor as _e}from"@opentelemetry/sdk-trace-node";import{ATTR_DEPLOYMENT_ENVIRONMENT_NAME as he,ATTR_SERVICE_NAME as Oe,ATTR_SERVICE_VERSION as xe}from"@opentelemetry/semantic-conventions/incubating";function N(e,r,n){if(r)return r;if(e)return`${e.replace(/\/$/,"")}${n}`}function Ae(e){if(!e.serviceName)throw new Error("setupOtel: `serviceName` is required");e.debug&&ge.setLogger(new le,me.DEBUG);let r={[Oe]:e.serviceName,...e.serviceVersion?{[xe]:e.serviceVersion}:{},...e.environment?{[he]:e.environment}:{},...e.resourceAttributes??{}},n=ye(r),s=e.headers,i;if(!e.disableTraces){let t=N(e.otlpEndpoint,e.tracesEndpoint,"/v1/traces"),u=new Ee({...t?{url:t}:{},...s?{headers:s}:{}});i=[new _e(u)]}let T;if(!e.disableLogs){let t=N(e.otlpEndpoint,e.logsEndpoint,"/v1/logs"),u=new fe({...t?{url:t}:{},...s?{headers:s}:{}});T=[new Re(u)]}let E;if(!e.disableMetrics){let t=N(e.otlpEndpoint,e.metricsEndpoint,"/v1/metrics"),u=new Te({...t?{url:t}:{},...s?{headers:s}:{}});E=new Se({exporter:u,exportIntervalMillis:e.metricsExportIntervalMs??6e4})}let g=new be({resource:n,...i?{spanProcessors:i}:{},...T?{logRecordProcessors:T}:{},...E?{metricReader:E}:{},instrumentations:e.instrumentations??[]});g.start();let o=null;return{sdk:g,shutdown(){return o||(o=g.shutdown().catch(t=>{console.error("[otel] shutdown error:",t)})),o}}}import{LogLevel as _,levelToString as ve}from"@cleverbrush/log";import{logs as Ne,SeverityNumber as h}from"@opentelemetry/api-logs";var Le={[_.Trace]:h.TRACE,[_.Debug]:h.DEBUG,[_.Information]:h.INFO,[_.Warning]:h.WARN,[_.Error]:h.ERROR,[_.Fatal]:h.FATAL},we=new Set(["string","number","boolean","bigint"]);function P(e){if(e==null)return e;if(!(typeof e=="function"||typeof e=="symbol")){if(we.has(typeof e))return e;if(Array.isArray(e))return e.map(P).filter(r=>r!==void 0);try{return JSON.stringify(e)}catch{return String(e)}}}function Ie(e){let r=e?.loggerName??"@cleverbrush/otel",n=e?.loggerVersion,s=e?.sanitizeAttribute,i,T=()=>(i||(i=Ne.getLogger(r,n)),i);return{async emit(E){let g=T();for(let o of E){let t={};for(let[u,c]of Object.entries(o.properties)){let R=s?s(u,c):P(c);R!==void 0&&(t[u]=R)}o.messageTemplate&&(t["cleverbrush.message_template"]=o.messageTemplate),o.eventId&&(t["cleverbrush.event_id"]=o.eventId),o.exception&&(t["exception.type"]=o.exception.name,t["exception.message"]=o.exception.message,o.exception.stack&&(t["exception.stacktrace"]=o.exception.stack)),g.emit({timestamp:o.timestamp.getTime(),severityNumber:Le[o.level],severityText:ve(o.level).toUpperCase(),body:o.renderedMessage,attributes:t})}},async[Symbol.asyncDispose](){}}}export{w as IMeter,L as ITracer,M as OTEL_SPAN_ITEM_KEY,D as configureOtel,W as instrumentKnex,Ie as otelLogSink,Ae as setupOtel,K as traceEnricher,pe as tracingMiddleware};
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/di.ts","../src/enrichers/trace.ts","../src/knex/instrumentKnex.ts","../src/middleware/tracing.ts","../src/setupOtel.ts","../src/sinks/OtelLogSink.ts"],"sourcesContent":["import { type Meter, metrics, type Tracer, trace } from '@opentelemetry/api';\n\n/**\n * DI service key for an OpenTelemetry `Tracer`.\n *\n * Resolved from the global `TracerProvider` set by\n * {@link import('./setupOtel.js').setupOtel}. Components that prefer\n * dependency injection over the global API can inject this token.\n */\nexport const ITracer = Symbol.for('ITracer') as unknown as {\n __brand: 'ITracer';\n};\n\n/**\n * DI service key for an OpenTelemetry `Meter`.\n *\n * Resolved from the global `MeterProvider` set by\n * {@link import('./setupOtel.js').setupOtel}.\n */\nexport const IMeter = Symbol.for('IMeter') as unknown as {\n __brand: 'IMeter';\n};\n\n/**\n * Configuration for {@link configureOtel}.\n */\nexport interface ConfigureOtelOptions {\n /**\n * Tracer name.\n *\n * @default '@cleverbrush/otel'\n */\n tracerName?: string;\n\n /**\n * Meter name.\n *\n * @default '@cleverbrush/otel'\n */\n meterName?: string;\n\n /** Optional version string used for both tracer and meter. */\n version?: string;\n}\n\n/**\n * Registers OTel `Tracer` and `Meter` instances in the DI container.\n *\n * Both are resolved lazily from the global providers configured by\n * {@link import('./setupOtel.js').setupOtel}, so this helper can be\n * called at DI setup time even before the SDK has fully started.\n *\n * @param services - the `ServiceCollection` to register with\n * @param options - tracer / meter naming overrides\n *\n * @example\n * ```ts\n * import { configureOtel, ITracer } from '@cleverbrush/otel';\n *\n * configureOtel(services, { tracerName: 'todo-backend' });\n *\n * const tracer = provider.get(ITracer);\n * tracer.startActiveSpan('custom-work', span => {\n * // …\n * span.end();\n * });\n * ```\n */\nexport function configureOtel(\n services: any,\n options?: ConfigureOtelOptions\n): void {\n const tracerName = options?.tracerName ?? '@cleverbrush/otel';\n const meterName = options?.meterName ?? '@cleverbrush/otel';\n const version = options?.version;\n\n if (typeof services?.addSingleton === 'function') {\n services.addSingleton(\n ITracer,\n (): Tracer => trace.getTracer(tracerName, version)\n );\n services.addSingleton(\n IMeter,\n (): Meter => metrics.getMeter(meterName, version)\n );\n }\n}\n","import type { Enricher } from '@cleverbrush/log';\nimport { trace } from '@opentelemetry/api';\n\n/**\n * Creates a log enricher that attaches the active span's\n * `TraceId`, `SpanId`, and `TraceFlags` to every log event.\n *\n * Reads from the OpenTelemetry context via `@opentelemetry/api`,\n * so it works with any tracer provider — including the one\n * configured by {@link import('../setupOtel.js').setupOtel}.\n *\n * No-op when no span is active.\n *\n * @returns an enricher that adds `{ TraceId, SpanId, TraceFlags }` if a span is active\n *\n * @example\n * ```ts\n * import { createLogger, consoleSink } from '@cleverbrush/log';\n * import { traceEnricher } from '@cleverbrush/otel';\n *\n * const logger = createLogger({\n * sinks: [consoleSink()],\n * enrichers: [traceEnricher()],\n * });\n * ```\n */\nexport function traceEnricher(): Enricher {\n return event => {\n const span = trace.getActiveSpan();\n if (!span) return event;\n const ctx = span.spanContext();\n if (!ctx?.traceId) return event;\n return {\n ...event,\n properties: {\n ...event.properties,\n TraceId: ctx.traceId,\n SpanId: ctx.spanId,\n TraceFlags: ctx.traceFlags\n }\n };\n };\n}\n","import {\n type Span,\n SpanKind,\n SpanStatusCode,\n type Tracer,\n trace\n} from '@opentelemetry/api';\nimport {\n ATTR_DB_NAMESPACE,\n ATTR_DB_OPERATION_NAME,\n ATTR_DB_QUERY_TEXT,\n ATTR_DB_SYSTEM_NAME,\n ATTR_SERVER_ADDRESS,\n ATTR_SERVER_PORT\n} from '@opentelemetry/semantic-conventions/incubating';\nimport type { Knex } from 'knex';\n\n/**\n * Configuration for {@link instrumentKnex}.\n */\nexport interface InstrumentKnexOptions {\n /**\n * Value to record as `db.system.name` (e.g. `postgresql`, `mysql`,\n * `sqlite`). When omitted, inferred from the knex client.\n */\n dbSystem?: string;\n\n /**\n * Tracer name used when resolving the OTel tracer.\n *\n * @default '@cleverbrush/otel/knex'\n */\n tracerName?: string;\n\n /**\n * Whether to include the SQL statement as `db.query.text`.\n *\n * The statement is taken verbatim from knex (parameter placeholders\n * are kept; bound values are **not** included). Disable if your\n * SQL itself may contain sensitive identifiers.\n *\n * @default true\n */\n recordStatement?: boolean;\n\n /**\n * Optional hook to redact / rewrite the SQL before it is recorded.\n * Called only when {@link recordStatement} is enabled.\n */\n sanitizeStatement?: (sql: string) => string;\n}\n\ninterface KnexQueryEvent {\n sql: string;\n method?: string;\n bindings?: unknown[];\n __knexQueryUid?: string;\n}\n\nconst FIRST_KEYWORD_RE = /^\\s*([A-Za-z]+)/;\nfunction inferOperation(sql: string, method?: string): string | undefined {\n if (method) return method.toUpperCase();\n const m = FIRST_KEYWORD_RE.exec(sql);\n return m?.[1]?.toUpperCase();\n}\n\nfunction inferDbSystem(client: any): string | undefined {\n const dialect: string | undefined =\n client?.dialect ?? client?.config?.client;\n if (!dialect) return undefined;\n const norm = String(dialect).toLowerCase();\n if (norm.startsWith('pg') || norm.includes('postgres')) {\n return 'postgresql';\n }\n if (norm.includes('mysql') || norm === 'mysql2') return 'mysql';\n if (norm.includes('sqlite') || norm === 'better-sqlite3') return 'sqlite';\n if (norm.includes('mssql')) return 'mssql';\n if (norm.includes('oracle')) return 'oracle';\n return norm;\n}\n\n/**\n * Instruments a Knex instance to emit an OpenTelemetry `CLIENT` span\n * for every executed query.\n *\n * Hooks knex's built-in `query`, `query-response`, and `query-error`\n * events — every dbset read, change-tracker write, save-graph, and raw\n * `knex(...)` call is captured uniformly because they all flow through\n * the same knex instance.\n *\n * Spans automatically nest under any ambient OTel context, so DB spans\n * become children of the enclosing HTTP server span produced by\n * `tracingMiddleware`.\n *\n * Returns the same instance for fluent chaining:\n * `instrumentKnex(knex({...}))`.\n *\n * @param k - the knex instance to instrument (mutated in place)\n * @param options - optional overrides for db system, tracer name, redaction\n * @returns the same knex instance\n *\n * @example\n * ```ts\n * import knex from 'knex';\n * import { instrumentKnex } from '@cleverbrush/otel';\n *\n * services.addSingleton(KnexToken, () =>\n * instrumentKnex(\n * knex({ client: 'pg', connection: dbUrl }),\n * { dbSystem: 'postgresql' }\n * )\n * );\n * ```\n */\nexport function instrumentKnex<T extends Knex>(\n k: T,\n options?: InstrumentKnexOptions\n): T {\n const tracerName = options?.tracerName ?? '@cleverbrush/otel/knex';\n const recordStatement = options?.recordStatement ?? true;\n const sanitize = options?.sanitizeStatement;\n\n let cachedTracer: Tracer | undefined;\n const getTracer = (): Tracer => {\n if (!cachedTracer) cachedTracer = trace.getTracer(tracerName);\n return cachedTracer;\n };\n\n const client: any = (k as any).client;\n const dbSystem = options?.dbSystem ?? inferDbSystem(client) ?? 'other_sql';\n const connection: any = client?.config?.connection ?? {};\n const dbName: string | undefined =\n typeof connection === 'object' ? connection.database : undefined;\n const host: string | undefined =\n typeof connection === 'object' ? connection.host : undefined;\n const port: number | undefined =\n typeof connection === 'object' && typeof connection.port === 'number'\n ? connection.port\n : undefined;\n\n const active = new Map<string, Span>();\n\n const baseAttrs: Record<string, string | number> = {\n [ATTR_DB_SYSTEM_NAME]: dbSystem\n };\n if (dbName) baseAttrs[ATTR_DB_NAMESPACE] = dbName;\n if (host) baseAttrs[ATTR_SERVER_ADDRESS] = host;\n if (typeof port === 'number') baseAttrs[ATTR_SERVER_PORT] = port;\n\n (k as any).on?.('query', (q: KnexQueryEvent) => {\n const uid = q.__knexQueryUid;\n if (!uid) return;\n const operation = inferOperation(q.sql, q.method);\n const spanName = operation ?? 'db.query';\n\n const attrs: Record<string, string | number> = { ...baseAttrs };\n if (operation) attrs[ATTR_DB_OPERATION_NAME] = operation;\n if (recordStatement && q.sql) {\n attrs[ATTR_DB_QUERY_TEXT] = sanitize ? sanitize(q.sql) : q.sql;\n }\n\n const span = getTracer().startSpan(spanName, {\n kind: SpanKind.CLIENT,\n attributes: attrs\n });\n active.set(uid, span);\n });\n\n (k as any).on?.('query-response', (_resp: unknown, q: KnexQueryEvent) => {\n const uid = q?.__knexQueryUid;\n if (!uid) return;\n const span = active.get(uid);\n if (!span) return;\n active.delete(uid);\n span.setStatus({ code: SpanStatusCode.OK });\n span.end();\n });\n\n (k as any).on?.('query-error', (err: unknown, q: KnexQueryEvent) => {\n const uid = q?.__knexQueryUid;\n if (!uid) return;\n const span = active.get(uid);\n if (!span) return;\n active.delete(uid);\n if (err instanceof Error) {\n span.recordException(err);\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: err.message\n });\n } else {\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: String(err)\n });\n }\n span.end();\n });\n\n return k;\n}\n","import {\n context,\n propagation,\n type Span,\n SpanKind,\n SpanStatusCode,\n type Tracer,\n trace\n} from '@opentelemetry/api';\nimport {\n ATTR_HTTP_REQUEST_METHOD,\n ATTR_HTTP_RESPONSE_STATUS_CODE,\n ATTR_HTTP_ROUTE,\n ATTR_SERVER_ADDRESS,\n ATTR_SERVER_PORT,\n ATTR_URL_PATH,\n ATTR_URL_QUERY,\n ATTR_URL_SCHEME,\n ATTR_USER_AGENT_ORIGINAL\n} from '@opentelemetry/semantic-conventions';\n\n/**\n * Attribute key under which the OTel server span is stashed on the\n * per-request `RequestContext.items` map.\n *\n * Downstream middleware/handlers can read this with\n * `ctx.items.get(OTEL_SPAN_ITEM_KEY)` to attach custom attributes\n * or events to the active server span.\n */\nexport const OTEL_SPAN_ITEM_KEY = 'otel.span';\n\n/**\n * Configuration for {@link tracingMiddleware}.\n */\nexport interface TracingMiddlewareOptions {\n /**\n * Tracer name used when resolving the OTel tracer.\n *\n * @default '@cleverbrush/otel'\n */\n tracerName?: string;\n\n /** Tracer version. */\n tracerVersion?: string;\n\n /**\n * Paths to exclude from tracing entirely (no span created).\n *\n * Accepts plain strings or objects with a `path` property\n * (e.g. an `EndpointBuilder`). Useful for `/health` and other\n * high-frequency, low-value endpoints.\n */\n excludePaths?: (string | { readonly path: string })[];\n\n /**\n * Hook for adding custom attributes to the server span just before\n * the inner pipeline runs. Errors thrown here are swallowed.\n */\n enrichSpan?: (span: Span, ctx: any) => void;\n\n /**\n * Whether to record the URL query string as `url.query`.\n *\n * Disabled by default because query strings frequently contain PII\n * (search terms, tokens). Enable explicitly only when you have\n * verified your URLs are safe to record.\n *\n * @default false\n */\n recordQuery?: boolean;\n\n /**\n * Name of the response header that carries the W3C trace ID for the\n * current request span.\n *\n * Expose this so API consumers can include it in bug reports and you\n * can look up the exact trace in ClickStack / Jaeger / etc.\n *\n * Set to `false` to disable the header entirely.\n *\n * @default 'X-Trace-Id'\n */\n responseTraceHeader?: string | false;\n}\n\nfunction getEndpointMeta(ctx: any): any | undefined {\n const items: Map<string, unknown> | undefined = ctx?.items;\n return items?.get('__endpoint_meta');\n}\n\nfunction getRouteTemplate(meta: any): string | undefined {\n if (!meta) return undefined;\n const base: string = meta.basePath ?? '';\n const tpl = meta.pathTemplate;\n if (typeof tpl === 'string') return `${base}${tpl}`;\n // ParseStringSchemaBuilder — try its `template` getter, otherwise toString\n const candidate =\n tpl?.template ?? tpl?.pattern ?? tpl?.toString?.() ?? null;\n if (typeof candidate === 'string') return `${base}${candidate}`;\n return base || undefined;\n}\n\n/**\n * Creates a `@cleverbrush/server` middleware that opens an OpenTelemetry\n * `SERVER` span for every incoming request.\n *\n * Should be registered as the **first** middleware so that the span\n * wraps CORS, auth, request logging, and the handler — capturing the\n * full request lifetime.\n *\n * Behavior:\n * - Extracts inbound trace context from request headers\n * (W3C `traceparent`, `baggage`).\n * - Names the span `${operationId}` if available, otherwise\n * `${method} ${http.route}`, otherwise `${method} ${url.path}`.\n * - Sets HTTP semantic-convention attributes\n * (`http.request.method`, `url.path`, `url.scheme`,\n * `server.address`, `user_agent.original`, `http.route`).\n * - Records `http.response.status_code` after `next()` completes.\n * - Marks the span `ERROR` and records the exception on uncaught errors.\n * - Stashes the span at `ctx.items.get(OTEL_SPAN_ITEM_KEY)` for\n * downstream code to enrich.\n *\n * @param options - tracing configuration\n * @returns a `Middleware` compatible with `@cleverbrush/server`\n *\n * @example\n * ```ts\n * import { tracingMiddleware } from '@cleverbrush/otel';\n *\n * createServer()\n * .use(tracingMiddleware({ excludePaths: ['/health'] }))\n * .use(corsMiddleware)\n * .use(authMiddleware)\n * .listen(3000);\n * ```\n */\nexport function tracingMiddleware(options?: TracingMiddlewareOptions) {\n const tracerName = options?.tracerName ?? '@cleverbrush/otel';\n const tracerVersion = options?.tracerVersion;\n const excludePaths = new Set(\n (options?.excludePaths ?? []).map(p =>\n typeof p === 'string' ? p : p.path\n )\n );\n const enrichSpan = options?.enrichSpan;\n const recordQuery = options?.recordQuery ?? false;\n const responseTraceHeader =\n options?.responseTraceHeader === false\n ? false\n : (options?.responseTraceHeader ?? 'X-Trace-Id');\n\n let cachedTracer: Tracer | undefined;\n const getTracer = (): Tracer => {\n if (!cachedTracer) {\n cachedTracer = trace.getTracer(tracerName, tracerVersion);\n }\n return cachedTracer;\n };\n\n return async (ctx: any, next: () => Promise<void>): Promise<void> => {\n const url: URL | undefined = ctx.url;\n const pathname = url?.pathname ?? ctx.path ?? '';\n\n if (excludePaths.has(pathname)) {\n await next();\n return;\n }\n\n const tracer = getTracer();\n const headers: Record<string, string> = ctx.headers ?? {};\n\n // Extract inbound trace context (W3C traceparent + baggage).\n const parentCtx = propagation.extract(context.active(), headers);\n\n const meta = getEndpointMeta(ctx);\n const route = getRouteTemplate(meta);\n const method: string = (ctx.method ?? 'GET').toUpperCase();\n const spanName: string =\n meta?.operationId ||\n (route ? `${method} ${route}` : `${method} ${pathname}`);\n\n const attributes: Record<string, string | number> = {\n [ATTR_HTTP_REQUEST_METHOD]: method,\n [ATTR_URL_PATH]: pathname\n };\n if (url?.protocol) {\n attributes[ATTR_URL_SCHEME] = url.protocol.replace(/:$/, '');\n }\n if (url?.hostname) {\n attributes[ATTR_SERVER_ADDRESS] = url.hostname;\n }\n if (url?.port) {\n const port = Number(url.port);\n if (Number.isFinite(port)) attributes[ATTR_SERVER_PORT] = port;\n }\n if (route) attributes[ATTR_HTTP_ROUTE] = route;\n if (headers['user-agent']) {\n attributes[ATTR_USER_AGENT_ORIGINAL] = headers['user-agent'];\n }\n if (recordQuery && url?.search) {\n attributes[ATTR_URL_QUERY] = url.search.replace(/^\\?/, '');\n }\n if (meta?.tags?.length) {\n attributes['cleverbrush.endpoint.tags'] = meta.tags.join(',');\n }\n if (meta?.operationId) {\n attributes['cleverbrush.endpoint.operationId'] = meta.operationId;\n }\n\n await tracer.startActiveSpan(\n spanName,\n { kind: SpanKind.SERVER, attributes },\n parentCtx,\n async (span: Span) => {\n ctx.items?.set?.(OTEL_SPAN_ITEM_KEY, span);\n\n // Write the trace ID to the response so consumers can look\n // up the exact trace in ClickStack / Jaeger / any backend.\n if (responseTraceHeader) {\n const { traceId } = span.spanContext();\n if (traceId) {\n const res = ctx.response;\n if (res?.setHeader) {\n res.setHeader(responseTraceHeader, traceId);\n } else if (ctx.setHeader) {\n ctx.setHeader(responseTraceHeader, traceId);\n }\n }\n }\n\n if (enrichSpan) {\n try {\n enrichSpan(span, ctx);\n } catch {\n // ignore enrichment errors\n }\n }\n\n try {\n await next();\n const status: number =\n ctx.response?.statusCode ?? ctx.statusCode ?? 200;\n span.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, status);\n if (status >= 500) {\n span.setStatus({ code: SpanStatusCode.ERROR });\n } else {\n span.setStatus({ code: SpanStatusCode.OK });\n }\n } catch (err) {\n const status: number =\n ctx.response?.statusCode ?? ctx.statusCode ?? 500;\n span.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, status);\n if (err instanceof Error) {\n span.recordException(err);\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: err.message\n });\n } else {\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: String(err)\n });\n }\n throw err;\n } finally {\n span.end();\n }\n }\n );\n };\n}\n","import { DiagConsoleLogger, DiagLogLevel, diag } from '@opentelemetry/api';\nimport { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http';\nimport { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';\nimport { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';\nimport { resourceFromAttributes } from '@opentelemetry/resources';\nimport { BatchLogRecordProcessor } from '@opentelemetry/sdk-logs';\nimport { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';\nimport { NodeSDK } from '@opentelemetry/sdk-node';\nimport {\n BatchSpanProcessor,\n type SpanProcessor\n} from '@opentelemetry/sdk-trace-node';\nimport {\n ATTR_DEPLOYMENT_ENVIRONMENT_NAME,\n ATTR_SERVICE_NAME,\n ATTR_SERVICE_VERSION\n} from '@opentelemetry/semantic-conventions/incubating';\n\n/**\n * Configuration for {@link setupOtel}.\n */\nexport interface OtelConfig {\n /**\n * Logical name of the service emitting telemetry.\n * Becomes the `service.name` resource attribute and is the primary\n * identifier in observability backends.\n */\n serviceName: string;\n\n /** Optional service version → `service.version` resource attribute. */\n serviceVersion?: string;\n\n /**\n * Deployment environment name (e.g. `production`, `staging`, `dev`).\n * Becomes the `deployment.environment.name` resource attribute.\n */\n environment?: string;\n\n /**\n * Additional resource attributes merged onto the default resource.\n * Useful for `host.name`, `cloud.region`, custom team tags, etc.\n */\n resourceAttributes?: Record<string, string | number | boolean>;\n\n /**\n * Base OTLP/HTTP endpoint for traces, logs, and metrics.\n *\n * If not provided, falls back to the standard OTel environment\n * variables (`OTEL_EXPORTER_OTLP_ENDPOINT`,\n * `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`, etc.).\n *\n * Per-signal endpoints below take precedence over this value.\n *\n * @default process.env.OTEL_EXPORTER_OTLP_ENDPOINT\n */\n otlpEndpoint?: string;\n\n /** Override the OTLP traces endpoint (`/v1/traces` is appended). */\n tracesEndpoint?: string;\n\n /** Override the OTLP logs endpoint (`/v1/logs` is appended). */\n logsEndpoint?: string;\n\n /** Override the OTLP metrics endpoint (`/v1/metrics` is appended). */\n metricsEndpoint?: string;\n\n /**\n * Optional headers to send with every OTLP export request\n * (e.g. authentication tokens for hosted backends).\n */\n headers?: Record<string, string>;\n\n /** Disable trace export. @default false */\n disableTraces?: boolean;\n\n /** Disable log export. @default false */\n disableLogs?: boolean;\n\n /** Disable metrics export. @default false */\n disableMetrics?: boolean;\n\n /**\n * Metric export interval in milliseconds.\n * @default 60000\n */\n metricsExportIntervalMs?: number;\n\n /**\n * Auto-instrumentations to register at SDK startup.\n *\n * Use the helpers from `@cleverbrush/otel/instrumentations`\n * (e.g. `outboundHttpInstrumentations()`, `runtimeMetrics()`).\n */\n instrumentations?: unknown[];\n\n /**\n * Enable verbose OTel SDK diagnostics (sets the global `diag` logger).\n *\n * @default false\n */\n debug?: boolean;\n}\n\n/**\n * Handle returned by {@link setupOtel} for lifecycle management.\n */\nexport interface OtelHandle {\n /**\n * Gracefully flushes and shuts down all exporters.\n *\n * Idempotent — safe to call multiple times.\n * Should be invoked from your process's shutdown hook\n * (`SIGTERM`/`SIGINT`) before `process.exit`.\n */\n shutdown(): Promise<void>;\n\n /**\n * The underlying `NodeSDK` instance.\n *\n * Exposed for advanced use cases (custom span processors, runtime\n * configuration). Most consumers do not need to touch this directly.\n */\n sdk: NodeSDK;\n}\n\nfunction buildExporterUrl(\n base: string | undefined,\n perSignal: string | undefined,\n suffix: string\n): string | undefined {\n if (perSignal) return perSignal;\n if (!base) return undefined;\n return `${base.replace(/\\/$/, '')}${suffix}`;\n}\n\n/**\n * Initializes the OpenTelemetry Node SDK with sensible defaults for\n * the Cleverbrush framework.\n *\n * This must be called **before** any instrumented modules are imported\n * — typically via `node --import ./telemetry.js entrypoint.js`.\n *\n * Configures W3C Trace Context propagation, OTLP/HTTP exporters for\n * traces, logs, and metrics, and the resource attributes that identify\n * the service in observability backends.\n *\n * @param config - service identity and exporter configuration\n * @returns a handle exposing `shutdown()` and the underlying SDK\n *\n * @example\n * ```ts\n * // telemetry.ts — loaded via `node --import ./telemetry.js`\n * import { setupOtel } from '@cleverbrush/otel';\n * import { outboundHttpInstrumentations, runtimeMetrics } from '@cleverbrush/otel/instrumentations';\n *\n * export const otel = setupOtel({\n * serviceName: 'todo-backend',\n * serviceVersion: '1.0.0',\n * environment: process.env.NODE_ENV,\n * otlpEndpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,\n * instrumentations: [...outboundHttpInstrumentations(), runtimeMetrics()],\n * });\n *\n * process.on('SIGTERM', () => otel.shutdown());\n * ```\n */\nexport function setupOtel(config: OtelConfig): OtelHandle {\n if (!config.serviceName) {\n throw new Error('setupOtel: `serviceName` is required');\n }\n\n if (config.debug) {\n diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG);\n }\n\n const attrs: Record<string, string | number | boolean> = {\n [ATTR_SERVICE_NAME]: config.serviceName,\n ...(config.serviceVersion\n ? { [ATTR_SERVICE_VERSION]: config.serviceVersion }\n : {}),\n ...(config.environment\n ? { [ATTR_DEPLOYMENT_ENVIRONMENT_NAME]: config.environment }\n : {}),\n ...(config.resourceAttributes ?? {})\n };\n const resource = resourceFromAttributes(attrs);\n\n const headers = config.headers;\n\n let spanProcessors: SpanProcessor[] | undefined;\n if (!config.disableTraces) {\n const url = buildExporterUrl(\n config.otlpEndpoint,\n config.tracesEndpoint,\n '/v1/traces'\n );\n const traceExporter = new OTLPTraceExporter({\n ...(url ? { url } : {}),\n ...(headers ? { headers } : {})\n });\n spanProcessors = [new BatchSpanProcessor(traceExporter)];\n }\n\n let logRecordProcessors: BatchLogRecordProcessor[] | undefined;\n if (!config.disableLogs) {\n const url = buildExporterUrl(\n config.otlpEndpoint,\n config.logsEndpoint,\n '/v1/logs'\n );\n const logExporter = new OTLPLogExporter({\n ...(url ? { url } : {}),\n ...(headers ? { headers } : {})\n });\n logRecordProcessors = [new BatchLogRecordProcessor(logExporter)];\n }\n\n let metricReader: PeriodicExportingMetricReader | undefined;\n if (!config.disableMetrics) {\n const url = buildExporterUrl(\n config.otlpEndpoint,\n config.metricsEndpoint,\n '/v1/metrics'\n );\n const metricExporter = new OTLPMetricExporter({\n ...(url ? { url } : {}),\n ...(headers ? { headers } : {})\n });\n metricReader = new PeriodicExportingMetricReader({\n exporter: metricExporter,\n exportIntervalMillis: config.metricsExportIntervalMs ?? 60_000\n });\n }\n\n const sdk = new NodeSDK({\n resource,\n ...(spanProcessors ? { spanProcessors } : {}),\n ...(logRecordProcessors ? { logRecordProcessors } : {}),\n ...(metricReader ? { metricReader } : {}),\n instrumentations: (config.instrumentations ?? []) as any\n });\n\n sdk.start();\n\n let shuttingDown: Promise<void> | null = null;\n\n return {\n sdk,\n shutdown(): Promise<void> {\n if (!shuttingDown) {\n shuttingDown = sdk.shutdown().catch(err => {\n // Best-effort shutdown — log but don't throw\n console.error('[otel] shutdown error:', err);\n });\n }\n return shuttingDown;\n }\n };\n}\n","import type { LogEvent, LogSink } from '@cleverbrush/log';\nimport { LogLevel, levelToString } from '@cleverbrush/log';\nimport {\n logs,\n type Logger as OtelLogger,\n SeverityNumber\n} from '@opentelemetry/api-logs';\n\n/**\n * Configuration for {@link otelLogSink}.\n */\nexport interface OtelLogSinkOptions {\n /**\n * Logger name (`InstrumentationScope`) under which records are\n * emitted via the OTel Logs API.\n *\n * @default '@cleverbrush/otel'\n */\n loggerName?: string;\n\n /** Optional logger version. */\n loggerVersion?: string;\n\n /**\n * Hook for redacting / dropping properties before they become\n * OTel log record attributes. Return `undefined` to drop the\n * attribute entirely.\n */\n sanitizeAttribute?: (key: string, value: unknown) => unknown | undefined;\n}\n\nconst SEVERITY_NUMBER: Record<LogLevel, SeverityNumber> = {\n [LogLevel.Trace]: SeverityNumber.TRACE,\n [LogLevel.Debug]: SeverityNumber.DEBUG,\n [LogLevel.Information]: SeverityNumber.INFO,\n [LogLevel.Warning]: SeverityNumber.WARN,\n [LogLevel.Error]: SeverityNumber.ERROR,\n [LogLevel.Fatal]: SeverityNumber.FATAL\n};\n\nconst SCALAR_TYPES = new Set(['string', 'number', 'boolean', 'bigint']);\n\nfunction toAttributeValue(value: unknown): unknown {\n if (value === null || value === undefined) return value;\n if (typeof value === 'function' || typeof value === 'symbol') {\n return undefined;\n }\n if (SCALAR_TYPES.has(typeof value)) return value;\n if (Array.isArray(value)) {\n return value.map(toAttributeValue).filter(v => v !== undefined);\n }\n try {\n return JSON.stringify(value);\n } catch {\n return String(value);\n }\n}\n\n/**\n * Creates a {@link LogSink} that forwards every event to the\n * OpenTelemetry Logs API.\n *\n * Maps each `LogEvent` to an OTel `LogRecord`:\n * - `timestamp` → nanoseconds via `Date.getTime()` × 1e6\n * - `level` → `severityNumber` and `severityText`\n * - `renderedMessage` → `body`\n * - `properties` → flat attribute map (functions / symbols dropped,\n * nested objects JSON-stringified)\n * - `messageTemplate` → `cleverbrush.message_template` attribute\n * - `eventId` → `cleverbrush.event_id` attribute\n * - `exception.*` attributes when the event carries an `Error`\n *\n * Trace correlation (`traceId`, `spanId`) is filled in automatically\n * by the OTel SDK from the active context — typically established by\n * `tracingMiddleware`.\n *\n * The sink itself is per-event; for high-throughput services wrap it\n * with `BatchingSink` from `@cleverbrush/log`.\n *\n * Requires that `setupOtel({ ... })` has been called so the global\n * `LoggerProvider` is set; otherwise emissions become no-ops.\n *\n * @param options - logger name and attribute sanitization\n * @returns a `LogSink` that emits to the OTel Logs pipeline\n *\n * @example\n * ```ts\n * import { createLogger, consoleSink } from '@cleverbrush/log';\n * import { otelLogSink } from '@cleverbrush/otel';\n *\n * const logger = createLogger({\n * minimumLevel: 'information',\n * sinks: [consoleSink(), otelLogSink()],\n * });\n * ```\n */\nexport function otelLogSink(options?: OtelLogSinkOptions): LogSink {\n const loggerName = options?.loggerName ?? '@cleverbrush/otel';\n const loggerVersion = options?.loggerVersion;\n const sanitize = options?.sanitizeAttribute;\n\n let cached: OtelLogger | undefined;\n const getLogger = (): OtelLogger => {\n if (!cached) cached = logs.getLogger(loggerName, loggerVersion);\n return cached;\n };\n\n return {\n async emit(events: LogEvent[]): Promise<void> {\n const otelLogger = getLogger();\n for (const event of events) {\n const attributes: Record<string, unknown> = {};\n for (const [key, raw] of Object.entries(event.properties)) {\n const value = sanitize\n ? sanitize(key, raw)\n : toAttributeValue(raw);\n if (value !== undefined) attributes[key] = value;\n }\n\n if (event.messageTemplate) {\n attributes['cleverbrush.message_template'] =\n event.messageTemplate;\n }\n if (event.eventId) {\n attributes['cleverbrush.event_id'] = event.eventId;\n }\n if (event.exception) {\n attributes['exception.type'] = event.exception.name;\n attributes['exception.message'] = event.exception.message;\n if (event.exception.stack) {\n attributes['exception.stacktrace'] =\n event.exception.stack;\n }\n }\n\n otelLogger.emit({\n timestamp: event.timestamp.getTime(), // milliseconds; OTel SDK converts to ns internally\n severityNumber: SEVERITY_NUMBER[event.level],\n severityText: levelToString(event.level).toUpperCase(),\n body: event.renderedMessage,\n attributes: attributes as any\n });\n }\n },\n\n async [Symbol.asyncDispose](): Promise<void> {\n // The OTel SDK owns the LoggerProvider lifecycle —\n // it is shut down via `OtelHandle.shutdown()`.\n }\n };\n}\n"],"mappings":"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","names":["metrics","trace","ITracer","IMeter","configureOtel","services","options","tracerName","meterName","version","trace","traceEnricher","event","span","ctx","SpanKind","SpanStatusCode","trace","ATTR_DB_NAMESPACE","ATTR_DB_OPERATION_NAME","ATTR_DB_QUERY_TEXT","ATTR_DB_SYSTEM_NAME","ATTR_SERVER_ADDRESS","ATTR_SERVER_PORT","FIRST_KEYWORD_RE","inferOperation","sql","method","inferDbSystem","client","dialect","norm","instrumentKnex","k","options","tracerName","recordStatement","sanitize","cachedTracer","getTracer","dbSystem","connection","dbName","host","port","active","baseAttrs","q","uid","operation","spanName","attrs","span","_resp","err","context","propagation","SpanKind","SpanStatusCode","trace","ATTR_HTTP_REQUEST_METHOD","ATTR_HTTP_RESPONSE_STATUS_CODE","ATTR_HTTP_ROUTE","ATTR_SERVER_ADDRESS","ATTR_SERVER_PORT","ATTR_URL_PATH","ATTR_URL_QUERY","ATTR_URL_SCHEME","ATTR_USER_AGENT_ORIGINAL","OTEL_SPAN_ITEM_KEY","getEndpointMeta","ctx","getRouteTemplate","meta","base","tpl","candidate","tracingMiddleware","options","tracerName","tracerVersion","excludePaths","p","enrichSpan","recordQuery","responseTraceHeader","cachedTracer","getTracer","next","url","pathname","tracer","headers","parentCtx","route","method","spanName","attributes","port","span","traceId","res","status","err","DiagConsoleLogger","DiagLogLevel","diag","OTLPLogExporter","OTLPMetricExporter","OTLPTraceExporter","resourceFromAttributes","BatchLogRecordProcessor","PeriodicExportingMetricReader","NodeSDK","BatchSpanProcessor","ATTR_DEPLOYMENT_ENVIRONMENT_NAME","ATTR_SERVICE_NAME","ATTR_SERVICE_VERSION","buildExporterUrl","base","perSignal","suffix","setupOtel","config","attrs","resource","headers","spanProcessors","url","traceExporter","logRecordProcessors","logExporter","metricReader","metricExporter","sdk","shuttingDown","err","LogLevel","levelToString","logs","SeverityNumber","SEVERITY_NUMBER","SCALAR_TYPES","toAttributeValue","value","v","otelLogSink","options","loggerName","loggerVersion","sanitize","cached","getLogger","events","otelLogger","event","attributes","key","raw"]}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Returns instrumentations for outbound HTTP traffic — Node `http`/`https`
3
+ * (covers `node-fetch` etc.) and `undici` (covers global `fetch`).
4
+ *
5
+ * Both packages are declared as optional peer dependencies of
6
+ * `@cleverbrush/otel`. Install them in the host project to use:
7
+ *
8
+ * ```sh
9
+ * npm install @opentelemetry/instrumentation-http @opentelemetry/instrumentation-undici
10
+ * ```
11
+ *
12
+ * @returns an array of instrumentation instances ready to pass to {@link import('./setupOtel.js').setupOtel}
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * import { setupOtel } from '@cleverbrush/otel';
17
+ * import { outboundHttpInstrumentations } from '@cleverbrush/otel/instrumentations';
18
+ *
19
+ * setupOtel({
20
+ * serviceName: 'todo-backend',
21
+ * instrumentations: outboundHttpInstrumentations(),
22
+ * });
23
+ * ```
24
+ */
25
+ export declare function outboundHttpInstrumentations(): unknown[];
26
+ /**
27
+ * Returns the Node.js runtime metrics instrumentation, which emits
28
+ * basic process gauges (event loop lag, GC, heap size).
29
+ *
30
+ * Requires `@opentelemetry/instrumentation-runtime-node` to be
31
+ * installed in the host project.
32
+ *
33
+ * @returns an array containing one instrumentation instance, or empty if the package is not installed
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * import { setupOtel } from '@cleverbrush/otel';
38
+ * import { runtimeMetrics } from '@cleverbrush/otel/instrumentations';
39
+ *
40
+ * setupOtel({
41
+ * serviceName: 'todo-backend',
42
+ * instrumentations: runtimeMetrics(),
43
+ * });
44
+ * ```
45
+ */
46
+ export declare function runtimeMetrics(): unknown[];
@@ -0,0 +1,2 @@
1
+ import{createRequire as r}from"module";var o=r(import.meta.url);function i(){let n=[];try{let t=o("@opentelemetry/instrumentation-http"),e=t.HttpInstrumentation??t.default;e&&n.push(new e)}catch{}try{let t=o("@opentelemetry/instrumentation-undici"),e=t.UndiciInstrumentation??t.default;e&&n.push(new e)}catch{}return n}function s(){try{let n=o("@opentelemetry/instrumentation-runtime-node"),t=n.RuntimeNodeInstrumentation??n.default;if(t)return[new t]}catch{}return[]}export{i as outboundHttpInstrumentations,s as runtimeMetrics};
2
+ //# sourceMappingURL=instrumentations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/instrumentations.ts"],"sourcesContent":["/**\n * Opt-in auto-instrumentations.\n *\n * These helpers wrap upstream OpenTelemetry instrumentation packages\n * for outbound HTTP and Node.js runtime metrics. They are kept in a\n * separate entry point so projects that don't want them avoid the\n * extra dependencies and the `require-in-the-middle` patching at\n * startup.\n *\n * Pass the returned arrays to `setupOtel({ instrumentations: [...] })`.\n *\n * @module\n */\nimport { createRequire } from 'node:module';\n\nconst requireFromHere = createRequire(import.meta.url);\n\n/**\n * Returns instrumentations for outbound HTTP traffic — Node `http`/`https`\n * (covers `node-fetch` etc.) and `undici` (covers global `fetch`).\n *\n * Both packages are declared as optional peer dependencies of\n * `@cleverbrush/otel`. Install them in the host project to use:\n *\n * ```sh\n * npm install @opentelemetry/instrumentation-http @opentelemetry/instrumentation-undici\n * ```\n *\n * @returns an array of instrumentation instances ready to pass to {@link import('./setupOtel.js').setupOtel}\n *\n * @example\n * ```ts\n * import { setupOtel } from '@cleverbrush/otel';\n * import { outboundHttpInstrumentations } from '@cleverbrush/otel/instrumentations';\n *\n * setupOtel({\n * serviceName: 'todo-backend',\n * instrumentations: outboundHttpInstrumentations(),\n * });\n * ```\n */\nexport function outboundHttpInstrumentations(): unknown[] {\n const result: unknown[] = [];\n try {\n const mod = requireFromHere(\n '@opentelemetry/instrumentation-http'\n ) as any;\n const Cls = mod.HttpInstrumentation ?? mod.default;\n if (Cls) result.push(new Cls());\n } catch {\n // optional peer dependency not installed\n }\n try {\n const mod = requireFromHere(\n '@opentelemetry/instrumentation-undici'\n ) as any;\n const Cls = mod.UndiciInstrumentation ?? mod.default;\n if (Cls) result.push(new Cls());\n } catch {\n // optional peer dependency not installed\n }\n return result;\n}\n\n/**\n * Returns the Node.js runtime metrics instrumentation, which emits\n * basic process gauges (event loop lag, GC, heap size).\n *\n * Requires `@opentelemetry/instrumentation-runtime-node` to be\n * installed in the host project.\n *\n * @returns an array containing one instrumentation instance, or empty if the package is not installed\n *\n * @example\n * ```ts\n * import { setupOtel } from '@cleverbrush/otel';\n * import { runtimeMetrics } from '@cleverbrush/otel/instrumentations';\n *\n * setupOtel({\n * serviceName: 'todo-backend',\n * instrumentations: runtimeMetrics(),\n * });\n * ```\n */\nexport function runtimeMetrics(): unknown[] {\n try {\n const mod = requireFromHere(\n '@opentelemetry/instrumentation-runtime-node'\n ) as any;\n const Cls = mod.RuntimeNodeInstrumentation ?? mod.default;\n if (Cls) return [new Cls()];\n } catch {\n // optional peer dependency not installed\n }\n return [];\n}\n"],"mappings":"AAaA,OAAS,iBAAAA,MAAqB,SAE9B,IAAMC,EAAkBD,EAAc,YAAY,GAAG,EA0B9C,SAASE,GAA0C,CACtD,IAAMC,EAAoB,CAAC,EAC3B,GAAI,CACA,IAAMC,EAAMH,EACR,qCACJ,EACMI,EAAMD,EAAI,qBAAuBA,EAAI,QACvCC,GAAKF,EAAO,KAAK,IAAIE,CAAK,CAClC,MAAQ,CAER,CACA,GAAI,CACA,IAAMD,EAAMH,EACR,uCACJ,EACMI,EAAMD,EAAI,uBAAyBA,EAAI,QACzCC,GAAKF,EAAO,KAAK,IAAIE,CAAK,CAClC,MAAQ,CAER,CACA,OAAOF,CACX,CAsBO,SAASG,GAA4B,CACxC,GAAI,CACA,IAAMF,EAAMH,EACR,6CACJ,EACMI,EAAMD,EAAI,4BAA8BA,EAAI,QAClD,GAAIC,EAAK,MAAO,CAAC,IAAIA,CAAK,CAC9B,MAAQ,CAER,CACA,MAAO,CAAC,CACZ","names":["createRequire","requireFromHere","outboundHttpInstrumentations","result","mod","Cls","runtimeMetrics"]}
@@ -0,0 +1,66 @@
1
+ import type { Knex } from 'knex';
2
+ /**
3
+ * Configuration for {@link instrumentKnex}.
4
+ */
5
+ export interface InstrumentKnexOptions {
6
+ /**
7
+ * Value to record as `db.system.name` (e.g. `postgresql`, `mysql`,
8
+ * `sqlite`). When omitted, inferred from the knex client.
9
+ */
10
+ dbSystem?: string;
11
+ /**
12
+ * Tracer name used when resolving the OTel tracer.
13
+ *
14
+ * @default '@cleverbrush/otel/knex'
15
+ */
16
+ tracerName?: string;
17
+ /**
18
+ * Whether to include the SQL statement as `db.query.text`.
19
+ *
20
+ * The statement is taken verbatim from knex (parameter placeholders
21
+ * are kept; bound values are **not** included). Disable if your
22
+ * SQL itself may contain sensitive identifiers.
23
+ *
24
+ * @default true
25
+ */
26
+ recordStatement?: boolean;
27
+ /**
28
+ * Optional hook to redact / rewrite the SQL before it is recorded.
29
+ * Called only when {@link recordStatement} is enabled.
30
+ */
31
+ sanitizeStatement?: (sql: string) => string;
32
+ }
33
+ /**
34
+ * Instruments a Knex instance to emit an OpenTelemetry `CLIENT` span
35
+ * for every executed query.
36
+ *
37
+ * Hooks knex's built-in `query`, `query-response`, and `query-error`
38
+ * events — every dbset read, change-tracker write, save-graph, and raw
39
+ * `knex(...)` call is captured uniformly because they all flow through
40
+ * the same knex instance.
41
+ *
42
+ * Spans automatically nest under any ambient OTel context, so DB spans
43
+ * become children of the enclosing HTTP server span produced by
44
+ * `tracingMiddleware`.
45
+ *
46
+ * Returns the same instance for fluent chaining:
47
+ * `instrumentKnex(knex({...}))`.
48
+ *
49
+ * @param k - the knex instance to instrument (mutated in place)
50
+ * @param options - optional overrides for db system, tracer name, redaction
51
+ * @returns the same knex instance
52
+ *
53
+ * @example
54
+ * ```ts
55
+ * import knex from 'knex';
56
+ * import { instrumentKnex } from '@cleverbrush/otel';
57
+ *
58
+ * services.addSingleton(KnexToken, () =>
59
+ * instrumentKnex(
60
+ * knex({ client: 'pg', connection: dbUrl }),
61
+ * { dbSystem: 'postgresql' }
62
+ * )
63
+ * );
64
+ * ```
65
+ */
66
+ export declare function instrumentKnex<T extends Knex>(k: T, options?: InstrumentKnexOptions): T;
@@ -0,0 +1,96 @@
1
+ import { type Span } from '@opentelemetry/api';
2
+ /**
3
+ * Attribute key under which the OTel server span is stashed on the
4
+ * per-request `RequestContext.items` map.
5
+ *
6
+ * Downstream middleware/handlers can read this with
7
+ * `ctx.items.get(OTEL_SPAN_ITEM_KEY)` to attach custom attributes
8
+ * or events to the active server span.
9
+ */
10
+ export declare const OTEL_SPAN_ITEM_KEY = "otel.span";
11
+ /**
12
+ * Configuration for {@link tracingMiddleware}.
13
+ */
14
+ export interface TracingMiddlewareOptions {
15
+ /**
16
+ * Tracer name used when resolving the OTel tracer.
17
+ *
18
+ * @default '@cleverbrush/otel'
19
+ */
20
+ tracerName?: string;
21
+ /** Tracer version. */
22
+ tracerVersion?: string;
23
+ /**
24
+ * Paths to exclude from tracing entirely (no span created).
25
+ *
26
+ * Accepts plain strings or objects with a `path` property
27
+ * (e.g. an `EndpointBuilder`). Useful for `/health` and other
28
+ * high-frequency, low-value endpoints.
29
+ */
30
+ excludePaths?: (string | {
31
+ readonly path: string;
32
+ })[];
33
+ /**
34
+ * Hook for adding custom attributes to the server span just before
35
+ * the inner pipeline runs. Errors thrown here are swallowed.
36
+ */
37
+ enrichSpan?: (span: Span, ctx: any) => void;
38
+ /**
39
+ * Whether to record the URL query string as `url.query`.
40
+ *
41
+ * Disabled by default because query strings frequently contain PII
42
+ * (search terms, tokens). Enable explicitly only when you have
43
+ * verified your URLs are safe to record.
44
+ *
45
+ * @default false
46
+ */
47
+ recordQuery?: boolean;
48
+ /**
49
+ * Name of the response header that carries the W3C trace ID for the
50
+ * current request span.
51
+ *
52
+ * Expose this so API consumers can include it in bug reports and you
53
+ * can look up the exact trace in ClickStack / Jaeger / etc.
54
+ *
55
+ * Set to `false` to disable the header entirely.
56
+ *
57
+ * @default 'X-Trace-Id'
58
+ */
59
+ responseTraceHeader?: string | false;
60
+ }
61
+ /**
62
+ * Creates a `@cleverbrush/server` middleware that opens an OpenTelemetry
63
+ * `SERVER` span for every incoming request.
64
+ *
65
+ * Should be registered as the **first** middleware so that the span
66
+ * wraps CORS, auth, request logging, and the handler — capturing the
67
+ * full request lifetime.
68
+ *
69
+ * Behavior:
70
+ * - Extracts inbound trace context from request headers
71
+ * (W3C `traceparent`, `baggage`).
72
+ * - Names the span `${operationId}` if available, otherwise
73
+ * `${method} ${http.route}`, otherwise `${method} ${url.path}`.
74
+ * - Sets HTTP semantic-convention attributes
75
+ * (`http.request.method`, `url.path`, `url.scheme`,
76
+ * `server.address`, `user_agent.original`, `http.route`).
77
+ * - Records `http.response.status_code` after `next()` completes.
78
+ * - Marks the span `ERROR` and records the exception on uncaught errors.
79
+ * - Stashes the span at `ctx.items.get(OTEL_SPAN_ITEM_KEY)` for
80
+ * downstream code to enrich.
81
+ *
82
+ * @param options - tracing configuration
83
+ * @returns a `Middleware` compatible with `@cleverbrush/server`
84
+ *
85
+ * @example
86
+ * ```ts
87
+ * import { tracingMiddleware } from '@cleverbrush/otel';
88
+ *
89
+ * createServer()
90
+ * .use(tracingMiddleware({ excludePaths: ['/health'] }))
91
+ * .use(corsMiddleware)
92
+ * .use(authMiddleware)
93
+ * .listen(3000);
94
+ * ```
95
+ */
96
+ export declare function tracingMiddleware(options?: TracingMiddlewareOptions): (ctx: any, next: () => Promise<void>) => Promise<void>;
@@ -0,0 +1,123 @@
1
+ import { NodeSDK } from '@opentelemetry/sdk-node';
2
+ /**
3
+ * Configuration for {@link setupOtel}.
4
+ */
5
+ export interface OtelConfig {
6
+ /**
7
+ * Logical name of the service emitting telemetry.
8
+ * Becomes the `service.name` resource attribute and is the primary
9
+ * identifier in observability backends.
10
+ */
11
+ serviceName: string;
12
+ /** Optional service version → `service.version` resource attribute. */
13
+ serviceVersion?: string;
14
+ /**
15
+ * Deployment environment name (e.g. `production`, `staging`, `dev`).
16
+ * Becomes the `deployment.environment.name` resource attribute.
17
+ */
18
+ environment?: string;
19
+ /**
20
+ * Additional resource attributes merged onto the default resource.
21
+ * Useful for `host.name`, `cloud.region`, custom team tags, etc.
22
+ */
23
+ resourceAttributes?: Record<string, string | number | boolean>;
24
+ /**
25
+ * Base OTLP/HTTP endpoint for traces, logs, and metrics.
26
+ *
27
+ * If not provided, falls back to the standard OTel environment
28
+ * variables (`OTEL_EXPORTER_OTLP_ENDPOINT`,
29
+ * `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`, etc.).
30
+ *
31
+ * Per-signal endpoints below take precedence over this value.
32
+ *
33
+ * @default process.env.OTEL_EXPORTER_OTLP_ENDPOINT
34
+ */
35
+ otlpEndpoint?: string;
36
+ /** Override the OTLP traces endpoint (`/v1/traces` is appended). */
37
+ tracesEndpoint?: string;
38
+ /** Override the OTLP logs endpoint (`/v1/logs` is appended). */
39
+ logsEndpoint?: string;
40
+ /** Override the OTLP metrics endpoint (`/v1/metrics` is appended). */
41
+ metricsEndpoint?: string;
42
+ /**
43
+ * Optional headers to send with every OTLP export request
44
+ * (e.g. authentication tokens for hosted backends).
45
+ */
46
+ headers?: Record<string, string>;
47
+ /** Disable trace export. @default false */
48
+ disableTraces?: boolean;
49
+ /** Disable log export. @default false */
50
+ disableLogs?: boolean;
51
+ /** Disable metrics export. @default false */
52
+ disableMetrics?: boolean;
53
+ /**
54
+ * Metric export interval in milliseconds.
55
+ * @default 60000
56
+ */
57
+ metricsExportIntervalMs?: number;
58
+ /**
59
+ * Auto-instrumentations to register at SDK startup.
60
+ *
61
+ * Use the helpers from `@cleverbrush/otel/instrumentations`
62
+ * (e.g. `outboundHttpInstrumentations()`, `runtimeMetrics()`).
63
+ */
64
+ instrumentations?: unknown[];
65
+ /**
66
+ * Enable verbose OTel SDK diagnostics (sets the global `diag` logger).
67
+ *
68
+ * @default false
69
+ */
70
+ debug?: boolean;
71
+ }
72
+ /**
73
+ * Handle returned by {@link setupOtel} for lifecycle management.
74
+ */
75
+ export interface OtelHandle {
76
+ /**
77
+ * Gracefully flushes and shuts down all exporters.
78
+ *
79
+ * Idempotent — safe to call multiple times.
80
+ * Should be invoked from your process's shutdown hook
81
+ * (`SIGTERM`/`SIGINT`) before `process.exit`.
82
+ */
83
+ shutdown(): Promise<void>;
84
+ /**
85
+ * The underlying `NodeSDK` instance.
86
+ *
87
+ * Exposed for advanced use cases (custom span processors, runtime
88
+ * configuration). Most consumers do not need to touch this directly.
89
+ */
90
+ sdk: NodeSDK;
91
+ }
92
+ /**
93
+ * Initializes the OpenTelemetry Node SDK with sensible defaults for
94
+ * the Cleverbrush framework.
95
+ *
96
+ * This must be called **before** any instrumented modules are imported
97
+ * — typically via `node --import ./telemetry.js entrypoint.js`.
98
+ *
99
+ * Configures W3C Trace Context propagation, OTLP/HTTP exporters for
100
+ * traces, logs, and metrics, and the resource attributes that identify
101
+ * the service in observability backends.
102
+ *
103
+ * @param config - service identity and exporter configuration
104
+ * @returns a handle exposing `shutdown()` and the underlying SDK
105
+ *
106
+ * @example
107
+ * ```ts
108
+ * // telemetry.ts — loaded via `node --import ./telemetry.js`
109
+ * import { setupOtel } from '@cleverbrush/otel';
110
+ * import { outboundHttpInstrumentations, runtimeMetrics } from '@cleverbrush/otel/instrumentations';
111
+ *
112
+ * export const otel = setupOtel({
113
+ * serviceName: 'todo-backend',
114
+ * serviceVersion: '1.0.0',
115
+ * environment: process.env.NODE_ENV,
116
+ * otlpEndpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
117
+ * instrumentations: [...outboundHttpInstrumentations(), runtimeMetrics()],
118
+ * });
119
+ *
120
+ * process.on('SIGTERM', () => otel.shutdown());
121
+ * ```
122
+ */
123
+ export declare function setupOtel(config: OtelConfig): OtelHandle;
@@ -0,0 +1,60 @@
1
+ import type { LogSink } from '@cleverbrush/log';
2
+ /**
3
+ * Configuration for {@link otelLogSink}.
4
+ */
5
+ export interface OtelLogSinkOptions {
6
+ /**
7
+ * Logger name (`InstrumentationScope`) under which records are
8
+ * emitted via the OTel Logs API.
9
+ *
10
+ * @default '@cleverbrush/otel'
11
+ */
12
+ loggerName?: string;
13
+ /** Optional logger version. */
14
+ loggerVersion?: string;
15
+ /**
16
+ * Hook for redacting / dropping properties before they become
17
+ * OTel log record attributes. Return `undefined` to drop the
18
+ * attribute entirely.
19
+ */
20
+ sanitizeAttribute?: (key: string, value: unknown) => unknown | undefined;
21
+ }
22
+ /**
23
+ * Creates a {@link LogSink} that forwards every event to the
24
+ * OpenTelemetry Logs API.
25
+ *
26
+ * Maps each `LogEvent` to an OTel `LogRecord`:
27
+ * - `timestamp` → nanoseconds via `Date.getTime()` × 1e6
28
+ * - `level` → `severityNumber` and `severityText`
29
+ * - `renderedMessage` → `body`
30
+ * - `properties` → flat attribute map (functions / symbols dropped,
31
+ * nested objects JSON-stringified)
32
+ * - `messageTemplate` → `cleverbrush.message_template` attribute
33
+ * - `eventId` → `cleverbrush.event_id` attribute
34
+ * - `exception.*` attributes when the event carries an `Error`
35
+ *
36
+ * Trace correlation (`traceId`, `spanId`) is filled in automatically
37
+ * by the OTel SDK from the active context — typically established by
38
+ * `tracingMiddleware`.
39
+ *
40
+ * The sink itself is per-event; for high-throughput services wrap it
41
+ * with `BatchingSink` from `@cleverbrush/log`.
42
+ *
43
+ * Requires that `setupOtel({ ... })` has been called so the global
44
+ * `LoggerProvider` is set; otherwise emissions become no-ops.
45
+ *
46
+ * @param options - logger name and attribute sanitization
47
+ * @returns a `LogSink` that emits to the OTel Logs pipeline
48
+ *
49
+ * @example
50
+ * ```ts
51
+ * import { createLogger, consoleSink } from '@cleverbrush/log';
52
+ * import { otelLogSink } from '@cleverbrush/otel';
53
+ *
54
+ * const logger = createLogger({
55
+ * minimumLevel: 'information',
56
+ * sinks: [consoleSink(), otelLogSink()],
57
+ * });
58
+ * ```
59
+ */
60
+ export declare function otelLogSink(options?: OtelLogSinkOptions): LogSink;
package/package.json ADDED
@@ -0,0 +1,107 @@
1
+ {
2
+ "author": "Andrew Zolotukhin <andrew_zol@cleverbrush.com>",
3
+ "bugs": {
4
+ "url": "https://github.com/cleverbrush/framework/issues",
5
+ "email": "andrew_zol@cleverbrush.com"
6
+ },
7
+ "description": "OpenTelemetry instrumentation for the Cleverbrush framework — traces for HTTP, Knex/SQL, outbound HTTP; structured logs and metrics over OTLP",
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "homepage": "https://docs.cleverbrush.com/",
12
+ "keywords": [
13
+ "opentelemetry",
14
+ "otel",
15
+ "tracing",
16
+ "observability",
17
+ "cleverbrush",
18
+ "knex",
19
+ "http",
20
+ "logs",
21
+ "metrics"
22
+ ],
23
+ "license": "BSD 3-Clause",
24
+ "main": "./dist/index.js",
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/index.d.ts",
28
+ "import": "./dist/index.js"
29
+ },
30
+ "./instrumentations": {
31
+ "types": "./dist/instrumentations.d.ts",
32
+ "import": "./dist/instrumentations.js"
33
+ }
34
+ },
35
+ "sideEffects": false,
36
+ "name": "@cleverbrush/otel",
37
+ "readme": "https://github.com/cleverbrush/framework/tree/master/libs/otel#readme",
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "github:cleverbrush/framework"
41
+ },
42
+ "scripts": {
43
+ "watch": "tsc --build --watch",
44
+ "build": "tsup && rm -f tsconfig.build.tsbuildinfo && tsc --project tsconfig.build.json --emitDeclarationOnly",
45
+ "clean": "rm -rf dist tsconfig.tsbuildinfo"
46
+ },
47
+ "dependencies": {
48
+ "@opentelemetry/api": "^1.9.0",
49
+ "@opentelemetry/api-logs": "^0.215.0",
50
+ "@opentelemetry/context-async-hooks": "^2.7.0",
51
+ "@opentelemetry/core": "^2.7.0",
52
+ "@opentelemetry/exporter-logs-otlp-http": "^0.215.0",
53
+ "@opentelemetry/exporter-metrics-otlp-http": "^0.215.0",
54
+ "@opentelemetry/exporter-trace-otlp-http": "^0.215.0",
55
+ "@opentelemetry/resources": "^2.7.0",
56
+ "@opentelemetry/sdk-logs": "^0.215.0",
57
+ "@opentelemetry/sdk-metrics": "^2.7.0",
58
+ "@opentelemetry/sdk-node": "^0.215.0",
59
+ "@opentelemetry/sdk-trace-node": "^2.7.0",
60
+ "@opentelemetry/semantic-conventions": "^1.40.0"
61
+ },
62
+ "peerDependencies": {
63
+ "@cleverbrush/di": "0.0.0-beta-20260424142030",
64
+ "@cleverbrush/log": "0.0.0-beta-20260424142030",
65
+ "@cleverbrush/server": "0.0.0-beta-20260424142030",
66
+ "@opentelemetry/instrumentation-http": "^0.215.0",
67
+ "@opentelemetry/instrumentation-runtime-node": "^0.28.0",
68
+ "@opentelemetry/instrumentation-undici": "^0.25.0",
69
+ "knex": "^3.0.0"
70
+ },
71
+ "peerDependenciesMeta": {
72
+ "@cleverbrush/di": {
73
+ "optional": true
74
+ },
75
+ "@cleverbrush/log": {
76
+ "optional": true
77
+ },
78
+ "@cleverbrush/server": {
79
+ "optional": true
80
+ },
81
+ "@opentelemetry/instrumentation-http": {
82
+ "optional": true
83
+ },
84
+ "@opentelemetry/instrumentation-runtime-node": {
85
+ "optional": true
86
+ },
87
+ "@opentelemetry/instrumentation-undici": {
88
+ "optional": true
89
+ },
90
+ "knex": {
91
+ "optional": true
92
+ }
93
+ },
94
+ "devDependencies": {
95
+ "@cleverbrush/di": "0.0.0-beta-20260424142030",
96
+ "@cleverbrush/log": "0.0.0-beta-20260424142030",
97
+ "@cleverbrush/server": "0.0.0-beta-20260424142030",
98
+ "@opentelemetry/instrumentation-http": "^0.215.0",
99
+ "@opentelemetry/instrumentation-runtime-node": "^0.28.0",
100
+ "@opentelemetry/instrumentation-undici": "^0.25.0",
101
+ "@types/node": "^25.4.0",
102
+ "knex": "^3.1.0"
103
+ },
104
+ "type": "module",
105
+ "types": "./dist/index.d.ts",
106
+ "version": "0.0.0-beta-20260424142030"
107
+ }