@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 +127 -0
- package/dist/di.d.ts +62 -0
- package/dist/enrichers/trace.d.ts +25 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/instrumentations.d.ts +46 -0
- package/dist/instrumentations.js +2 -0
- package/dist/instrumentations.js.map +1 -0
- package/dist/knex/instrumentKnex.d.ts +66 -0
- package/dist/middleware/tracing.d.ts +96 -0
- package/dist/setupOtel.d.ts +123 -0
- package/dist/sinks/OtelLogSink.d.ts +60 -0
- package/package.json +107 -0
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;
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|