@foam-ai/node-cliengo 0.1.0-alpha.1 → 0.1.0-alpha.10
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/dist/node-cliengo/src/constants.d.ts +1 -1
- package/dist/node-cliengo/src/constants.js +1 -1
- package/dist/node-cliengo/src/http/express.d.ts +21 -0
- package/dist/node-cliengo/src/http/express.js +57 -3
- package/dist/node-cliengo/src/http/fastify.d.ts +10 -0
- package/dist/node-cliengo/src/http/fastify.js +36 -3
- package/dist/node-cliengo/src/index.d.ts +5 -5
- package/dist/node-cliengo/src/index.js +6 -3
- package/dist/node-cliengo/src/init.d.ts +14 -11
- package/dist/node-cliengo/src/init.js +57 -49
- package/dist/node-cliengo/src/job.d.ts +3 -3
- package/dist/node-cliengo/src/job.js +1 -5
- package/dist/node-cliengo/src/logs/pino-destination.d.ts +7 -2
- package/dist/node-cliengo/src/logs/pino-destination.js +34 -9
- package/dist/node-cliengo/src/logs/pino-mixin.d.ts +5 -4
- package/dist/node-cliengo/src/logs/pino-mixin.js +9 -5
- package/dist/node-cliengo/src/metrics/instruments.d.ts +9 -11
- package/dist/node-cliengo/src/metrics/instruments.js +19 -55
- package/dist/node-cliengo/src/nr.d.ts +1 -1
- package/dist/node-cliengo/src/nr.js +1 -1
- package/dist/node-cliengo/src/sns.d.ts +1 -4
- package/dist/node-cliengo/src/sns.js +2 -9
- package/dist/node-cliengo/src/sqs.d.ts +3 -3
- package/dist/node-cliengo/src/sqs.js +1 -1
- package/dist/node-cliengo/src/trace-bridge.d.ts +31 -27
- package/dist/node-cliengo/src/trace-bridge.js +67 -36
- package/dist/node-cliengo/src/types.d.ts +10 -10
- package/dist/node-cliengo/src/types.js +2 -2
- package/package.json +12 -10
|
@@ -22,17 +22,22 @@
|
|
|
22
22
|
* 60→FATAL. Unknown numeric levels default to INFO.
|
|
23
23
|
* - Pino uses `msg` for the message field (not `message` like Winston). We
|
|
24
24
|
* check both for compatibility with custom Pino configs.
|
|
25
|
-
* -
|
|
26
|
-
*
|
|
25
|
+
* - Trace context (traceId, spanId, traceparent) is read from the parsed
|
|
26
|
+
* JSON line, NOT from getTraceContext(). The pino mixin injects these at
|
|
27
|
+
* log-write time (when NR's transaction is still active). The stream write
|
|
28
|
+
* happens asynchronously — by then NR's transaction has ended, so calling
|
|
29
|
+
* getTraceContext() here would return empty strings.
|
|
30
|
+
* - Internal Pino fields (pid, hostname, time) and trace fields (traceId,
|
|
31
|
+
* spanId, traceparent) are excluded from forwarded attributes.
|
|
27
32
|
* - Callback always called — never blocks the Pino stream pipeline.
|
|
28
33
|
* - Buffer chunks: converted to string before parsing (handles both Buffer
|
|
29
34
|
* and string inputs from Node streams).
|
|
30
35
|
*/
|
|
31
36
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
37
|
exports.createPinoDestination = createPinoDestination;
|
|
38
|
+
const api_1 = require("@opentelemetry/api");
|
|
33
39
|
const api_logs_1 = require("@opentelemetry/api-logs");
|
|
34
40
|
const node_stream_1 = require("node:stream");
|
|
35
|
-
const trace_bridge_1 = require("../trace-bridge");
|
|
36
41
|
const PINO_LEVEL_MAP = {
|
|
37
42
|
10: api_logs_1.SeverityNumber.TRACE,
|
|
38
43
|
20: api_logs_1.SeverityNumber.DEBUG,
|
|
@@ -66,16 +71,23 @@ function createPinoDestination(loggerProvider, serviceName) {
|
|
|
66
71
|
const msg = parsed.msg ?? parsed.message ?? '';
|
|
67
72
|
const severity = PINO_LEVEL_MAP[level] ?? api_logs_1.SeverityNumber.INFO;
|
|
68
73
|
const severityText = PINO_LEVEL_TEXT[level] ?? 'INFO';
|
|
69
|
-
|
|
74
|
+
// Read trace context from the parsed JSON line — the pino mixin already
|
|
75
|
+
// injected traceId/spanId/traceparent at log-write time (when the NR
|
|
76
|
+
// transaction was still active). Calling getTraceContext() here would be
|
|
77
|
+
// too late — the stream write happens asynchronously after NR's
|
|
78
|
+
// transaction has ended.
|
|
79
|
+
const traceId = parsed.traceId ?? '';
|
|
80
|
+
const spanId = parsed.spanId ?? '';
|
|
70
81
|
const attributes = {};
|
|
71
|
-
if (
|
|
72
|
-
attributes['trace.id'] =
|
|
82
|
+
if (traceId) {
|
|
83
|
+
attributes['trace.id'] = traceId;
|
|
73
84
|
}
|
|
74
|
-
if (
|
|
75
|
-
attributes['span.id'] =
|
|
85
|
+
if (spanId) {
|
|
86
|
+
attributes['span.id'] = spanId;
|
|
76
87
|
}
|
|
88
|
+
const SKIP_KEYS = new Set(['level', 'msg', 'message', 'time', 'pid', 'hostname', 'traceId', 'spanId', 'traceparent']);
|
|
77
89
|
for (const [key, val] of Object.entries(parsed)) {
|
|
78
|
-
if (key
|
|
90
|
+
if (!SKIP_KEYS.has(key)) {
|
|
79
91
|
try {
|
|
80
92
|
attributes[key] = typeof val === 'string' ? val : JSON.stringify(val);
|
|
81
93
|
}
|
|
@@ -84,11 +96,24 @@ function createPinoDestination(loggerProvider, serviceName) {
|
|
|
84
96
|
}
|
|
85
97
|
}
|
|
86
98
|
}
|
|
99
|
+
// Build an OTel Context carrying the SpanContext so the SDK stamps the
|
|
100
|
+
// native traceId/spanId fields on the exported OTLP LogRecord. Without
|
|
101
|
+
// this, backends can't correlate logs to traces.
|
|
102
|
+
let logContext;
|
|
103
|
+
if (traceId && spanId) {
|
|
104
|
+
logContext = api_1.trace.setSpanContext(api_1.ROOT_CONTEXT, {
|
|
105
|
+
traceId,
|
|
106
|
+
spanId,
|
|
107
|
+
traceFlags: 1,
|
|
108
|
+
isRemote: true,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
87
111
|
otelLogger.emit({
|
|
88
112
|
body: msg,
|
|
89
113
|
severityNumber: severity,
|
|
90
114
|
severityText,
|
|
91
115
|
attributes,
|
|
116
|
+
...(logContext && { context: logContext }),
|
|
92
117
|
});
|
|
93
118
|
}
|
|
94
119
|
catch {
|
|
@@ -11,10 +11,11 @@
|
|
|
11
11
|
* Used by kori (Fastify built-in Pino) and cb-proxy (Pino + pino-http).
|
|
12
12
|
*
|
|
13
13
|
* Edge cases covered:
|
|
14
|
-
* - NR not loaded or no active transaction: returns {
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
* - NR not loaded or no active transaction: returns {} so it doesn't
|
|
15
|
+
* overwrite valid trace context injected by pino-http's customProps
|
|
16
|
+
* (Pino serializes mixin fields after customProps — duplicate keys
|
|
17
|
+
* in JSON take the last value).
|
|
18
|
+
* - getTraceContext throws: catch returns {}. Never crashes logging.
|
|
18
19
|
* - Lazy evaluation: the mixin is called at log-write time, not at Pino
|
|
19
20
|
* construction time, so trace context is always current.
|
|
20
21
|
* - Works with pino-pretty in development — fields appear in pretty output.
|
|
@@ -12,10 +12,11 @@
|
|
|
12
12
|
* Used by kori (Fastify built-in Pino) and cb-proxy (Pino + pino-http).
|
|
13
13
|
*
|
|
14
14
|
* Edge cases covered:
|
|
15
|
-
* - NR not loaded or no active transaction: returns {
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* - NR not loaded or no active transaction: returns {} so it doesn't
|
|
16
|
+
* overwrite valid trace context injected by pino-http's customProps
|
|
17
|
+
* (Pino serializes mixin fields after customProps — duplicate keys
|
|
18
|
+
* in JSON take the last value).
|
|
19
|
+
* - getTraceContext throws: catch returns {}. Never crashes logging.
|
|
19
20
|
* - Lazy evaluation: the mixin is called at log-write time, not at Pino
|
|
20
21
|
* construction time, so trace context is always current.
|
|
21
22
|
* - Works with pino-pretty in development — fields appear in pretty output.
|
|
@@ -27,6 +28,9 @@ function createPinoMixin() {
|
|
|
27
28
|
return () => {
|
|
28
29
|
try {
|
|
29
30
|
const ctx = (0, trace_bridge_1.getTraceContext)();
|
|
31
|
+
if (!ctx.traceId) {
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
30
34
|
return {
|
|
31
35
|
traceId: ctx.traceId,
|
|
32
36
|
spanId: ctx.spanId,
|
|
@@ -34,7 +38,7 @@ function createPinoMixin() {
|
|
|
34
38
|
};
|
|
35
39
|
}
|
|
36
40
|
catch {
|
|
37
|
-
return {
|
|
41
|
+
return {};
|
|
38
42
|
}
|
|
39
43
|
};
|
|
40
44
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pre-built OTel metric instruments for queue processing
|
|
3
|
-
* for service-specific custom metrics.
|
|
2
|
+
* Pre-built OTel metric instruments for queue processing.
|
|
4
3
|
*
|
|
5
4
|
* Why this file exists:
|
|
6
5
|
* Provides three auto-recorded instruments that wrapSqsConsumer and
|
|
@@ -9,15 +8,14 @@
|
|
|
9
8
|
* - messageProcessingErrors (counter): how many processing attempts fail
|
|
10
9
|
* - messagesProcessed (counter): total messages processed (success + error)
|
|
11
10
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* These follow OTel semantic conventions for messaging. They are NOT
|
|
12
|
+
* Cliengo-specific — any service with queue consumers gets them for free.
|
|
13
|
+
*
|
|
14
|
+
* For service-specific domain metrics (LLM duration, webhook processing time,
|
|
15
|
+
* automation duration), services use foam.meter directly:
|
|
16
|
+
* const h = foam.meter.createHistogram('llm.duration', { unit: 'ms' });
|
|
15
17
|
*
|
|
16
18
|
* Edge cases covered:
|
|
17
|
-
* - Custom instrument idempotency: calling customHistogram('llm.duration', ...)
|
|
18
|
-
* twice returns the same Histogram instance (cached by name). OTel SDKs
|
|
19
|
-
* already handle duplicate instrument creation, but caching avoids the
|
|
20
|
-
* overhead of repeated createHistogram calls in hot paths.
|
|
21
19
|
* - MeterProvider is non-global — our metrics never conflict with NR's
|
|
22
20
|
* internal metrics or any other OTel MeterProvider in the process.
|
|
23
21
|
* - PeriodicExportingMetricReader (configured in init.ts) exports every 60s
|
|
@@ -25,5 +23,5 @@
|
|
|
25
23
|
* attached and no timer runs.
|
|
26
24
|
*/
|
|
27
25
|
import type { MeterProvider } from '@opentelemetry/sdk-metrics';
|
|
28
|
-
import type {
|
|
29
|
-
export declare function
|
|
26
|
+
import type { FoamMetrics } from '../types';
|
|
27
|
+
export declare function createFoamMetrics(meterProvider: MeterProvider, serviceName: string): FoamMetrics;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* Pre-built OTel metric instruments for queue processing
|
|
4
|
-
* for service-specific custom metrics.
|
|
3
|
+
* Pre-built OTel metric instruments for queue processing.
|
|
5
4
|
*
|
|
6
5
|
* Why this file exists:
|
|
7
6
|
* Provides three auto-recorded instruments that wrapSqsConsumer and
|
|
@@ -10,15 +9,14 @@
|
|
|
10
9
|
* - messageProcessingErrors (counter): how many processing attempts fail
|
|
11
10
|
* - messagesProcessed (counter): total messages processed (success + error)
|
|
12
11
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* These follow OTel semantic conventions for messaging. They are NOT
|
|
13
|
+
* Cliengo-specific — any service with queue consumers gets them for free.
|
|
14
|
+
*
|
|
15
|
+
* For service-specific domain metrics (LLM duration, webhook processing time,
|
|
16
|
+
* automation duration), services use foam.meter directly:
|
|
17
|
+
* const h = foam.meter.createHistogram('llm.duration', { unit: 'ms' });
|
|
16
18
|
*
|
|
17
19
|
* Edge cases covered:
|
|
18
|
-
* - Custom instrument idempotency: calling customHistogram('llm.duration', ...)
|
|
19
|
-
* twice returns the same Histogram instance (cached by name). OTel SDKs
|
|
20
|
-
* already handle duplicate instrument creation, but caching avoids the
|
|
21
|
-
* overhead of repeated createHistogram calls in hot paths.
|
|
22
20
|
* - MeterProvider is non-global — our metrics never conflict with NR's
|
|
23
21
|
* internal metrics or any other OTel MeterProvider in the process.
|
|
24
22
|
* - PeriodicExportingMetricReader (configured in init.ts) exports every 60s
|
|
@@ -26,53 +24,19 @@
|
|
|
26
24
|
* attached and no timer runs.
|
|
27
25
|
*/
|
|
28
26
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
-
exports.
|
|
30
|
-
function
|
|
27
|
+
exports.createFoamMetrics = createFoamMetrics;
|
|
28
|
+
function createFoamMetrics(meterProvider, serviceName) {
|
|
31
29
|
const meter = meterProvider.getMeter(serviceName);
|
|
32
|
-
const messageProcessingDuration = meter.createHistogram('messaging.process.duration', {
|
|
33
|
-
description: 'Time to process a queue message',
|
|
34
|
-
unit: 'ms',
|
|
35
|
-
});
|
|
36
|
-
const messageProcessingErrors = meter.createCounter('messaging.process.errors', {
|
|
37
|
-
description: 'Number of queue message processing errors',
|
|
38
|
-
});
|
|
39
|
-
const messagesProcessed = meter.createCounter('messaging.process.count', {
|
|
40
|
-
description: 'Number of queue messages processed',
|
|
41
|
-
});
|
|
42
|
-
const instrumentCache = new Map();
|
|
43
30
|
return {
|
|
44
|
-
messageProcessingDuration,
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
instrumentCache.set(key, h);
|
|
55
|
-
return h;
|
|
56
|
-
},
|
|
57
|
-
customCounter(name, description, unit) {
|
|
58
|
-
const key = `counter:${name}`;
|
|
59
|
-
const existing = instrumentCache.get(key);
|
|
60
|
-
if (existing) {
|
|
61
|
-
return existing;
|
|
62
|
-
}
|
|
63
|
-
const c = meter.createCounter(name, { description, unit });
|
|
64
|
-
instrumentCache.set(key, c);
|
|
65
|
-
return c;
|
|
66
|
-
},
|
|
67
|
-
customGauge(name, description, unit) {
|
|
68
|
-
const key = `gauge:${name}`;
|
|
69
|
-
const existing = instrumentCache.get(key);
|
|
70
|
-
if (existing) {
|
|
71
|
-
return existing;
|
|
72
|
-
}
|
|
73
|
-
const g = meter.createObservableGauge(name, { description, unit });
|
|
74
|
-
instrumentCache.set(key, g);
|
|
75
|
-
return g;
|
|
76
|
-
},
|
|
31
|
+
messageProcessingDuration: meter.createHistogram('messaging.process.duration', {
|
|
32
|
+
description: 'Time to process a queue message',
|
|
33
|
+
unit: 'ms',
|
|
34
|
+
}),
|
|
35
|
+
messageProcessingErrors: meter.createCounter('messaging.process.errors', {
|
|
36
|
+
description: 'Number of queue message processing errors',
|
|
37
|
+
}),
|
|
38
|
+
messagesProcessed: meter.createCounter('messaging.process.count', {
|
|
39
|
+
description: 'Number of queue messages processed',
|
|
40
|
+
}),
|
|
77
41
|
};
|
|
78
42
|
}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* hsm-backend (conditional NR loading).
|
|
15
15
|
* 2. Module cache: `require.cache[require.resolve('newrelic')]` — works for
|
|
16
16
|
* CJS services (combee, gpt-intentions, cb-proxy) where NR is loaded at
|
|
17
|
-
* the top of the entry file before
|
|
17
|
+
* the top of the entry file before Foam initializes.
|
|
18
18
|
* 3. Fallback: NOOP_NR stub that returns empty trace metadata and calls
|
|
19
19
|
* handler functions directly without creating background transactions.
|
|
20
20
|
*
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* hsm-backend (conditional NR loading).
|
|
16
16
|
* 2. Module cache: `require.cache[require.resolve('newrelic')]` — works for
|
|
17
17
|
* CJS services (combee, gpt-intentions, cb-proxy) where NR is loaded at
|
|
18
|
-
* the top of the entry file before
|
|
18
|
+
* the top of the entry file before Foam initializes.
|
|
19
19
|
* 3. Fallback: NOOP_NR stub that returns empty trace metadata and calls
|
|
20
20
|
* handler functions directly without creating background transactions.
|
|
21
21
|
*
|
|
@@ -8,10 +8,7 @@
|
|
|
8
8
|
* alongside the existing ones. This function spread-merges it in.
|
|
9
9
|
*
|
|
10
10
|
* Edge cases covered:
|
|
11
|
-
* -
|
|
12
|
-
* original attributes are returned unchanged — no traceparent key added.
|
|
13
|
-
* - Empty attrs {}: returns just { traceparent: ... } if NR is active,
|
|
14
|
-
* or {} if not. Never crashes.
|
|
11
|
+
* - Empty attrs {}: returns { traceparent: ... }. Never crashes.
|
|
15
12
|
* - Existing traceparent in attrs: overwritten with the current value
|
|
16
13
|
* (spread puts our key last).
|
|
17
14
|
*/
|
|
@@ -9,10 +9,7 @@
|
|
|
9
9
|
* alongside the existing ones. This function spread-merges it in.
|
|
10
10
|
*
|
|
11
11
|
* Edge cases covered:
|
|
12
|
-
* -
|
|
13
|
-
* original attributes are returned unchanged — no traceparent key added.
|
|
14
|
-
* - Empty attrs {}: returns just { traceparent: ... } if NR is active,
|
|
15
|
-
* or {} if not. Never crashes.
|
|
12
|
+
* - Empty attrs {}: returns { traceparent: ... }. Never crashes.
|
|
16
13
|
* - Existing traceparent in attrs: overwritten with the current value
|
|
17
14
|
* (spread puts our key last).
|
|
18
15
|
*/
|
|
@@ -20,12 +17,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
20
17
|
exports.injectSnsAttributes = injectSnsAttributes;
|
|
21
18
|
const trace_bridge_1 = require("./trace-bridge");
|
|
22
19
|
function injectSnsAttributes(attrs) {
|
|
23
|
-
const tp = (0, trace_bridge_1.buildTraceparent)();
|
|
24
|
-
if (!tp) {
|
|
25
|
-
return attrs;
|
|
26
|
-
}
|
|
27
20
|
return {
|
|
28
21
|
...attrs,
|
|
29
|
-
traceparent: { DataType: 'String', StringValue:
|
|
22
|
+
traceparent: { DataType: 'String', StringValue: (0, trace_bridge_1.buildTraceparent)() },
|
|
30
23
|
};
|
|
31
24
|
}
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
* service's own retry/DLQ logic is unaffected.
|
|
29
29
|
* - NR not loaded: execute() runs without startBackgroundTransaction — OTel
|
|
30
30
|
* span still created, metrics still recorded.
|
|
31
|
-
* - metrics is undefined (inert
|
|
31
|
+
* - metrics is undefined (inert foam instance): all metrics?.xxx calls are no-ops.
|
|
32
32
|
*/
|
|
33
33
|
import { type Tracer } from '@opentelemetry/api';
|
|
34
|
-
import type {
|
|
35
|
-
export declare function wrapSqsConsumer(tracer: Tracer, spanName: string, msg: SqsMessage, fn: () => Promise<void>, metrics?:
|
|
34
|
+
import type { FoamMetrics, SqsMessage } from './types';
|
|
35
|
+
export declare function wrapSqsConsumer(tracer: Tracer, spanName: string, msg: SqsMessage, fn: () => Promise<void>, metrics?: FoamMetrics): Promise<void>;
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
* service's own retry/DLQ logic is unaffected.
|
|
30
30
|
* - NR not loaded: execute() runs without startBackgroundTransaction — OTel
|
|
31
31
|
* span still created, metrics still recorded.
|
|
32
|
-
* - metrics is undefined (inert
|
|
32
|
+
* - metrics is undefined (inert foam instance): all metrics?.xxx calls are no-ops.
|
|
33
33
|
*/
|
|
34
34
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
35
35
|
exports.wrapSqsConsumer = wrapSqsConsumer;
|
|
@@ -1,39 +1,43 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* W3C trace context
|
|
2
|
+
* W3C trace context utilities for New Relic and OpenTelemetry interop.
|
|
3
3
|
*
|
|
4
4
|
* Why this file exists:
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Reads trace context from two sources (NR first, then OTel's own active
|
|
6
|
+
* span) and converts it to/from W3C traceparent format. This means trace
|
|
7
|
+
* propagation works whether NR is installed, partially loaded, or completely
|
|
8
|
+
* removed.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* so the trace context is always current.
|
|
10
|
+
* Two resolution modes:
|
|
11
|
+
* resolveActiveTraceIds() — NR → OTel active span → empty strings.
|
|
12
|
+
* Used by getTraceContext() (log enrichment) and createPinoMixin().
|
|
13
|
+
* Never generates IDs — returns empty when no real source exists, so
|
|
14
|
+
* it doesn't overwrite valid IDs injected by pino-http's customProps.
|
|
15
|
+
*
|
|
16
|
+
* buildTraceparent() — NR → OTel active span → generate fresh IDs.
|
|
17
|
+
* Used by producers (injectSnsAttributes, injectJobData) and the
|
|
18
|
+
* Express/Fastify middleware at request start. Always returns a
|
|
19
|
+
* traceparent so every message/request gets trace context.
|
|
21
20
|
*
|
|
22
21
|
* Edge cases covered:
|
|
23
|
-
* - NR
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* -
|
|
27
|
-
*
|
|
28
|
-
* - Malformed traceparent (wrong number of parts, wrong hex lengths):
|
|
29
|
-
* extractParentContext returns ROOT_CONTEXT — consumer creates a fresh
|
|
30
|
-
* trace instead of crashing.
|
|
31
|
-
* - Flags field always set to 01 (sampled) on buildTraceparent because we
|
|
32
|
-
* want all async boundary crossings traced.
|
|
22
|
+
* - NR's getTraceMetadata() can throw TypeError when the transaction is
|
|
23
|
+
* null (pino-http fires after NR ends the transaction). Guarded with
|
|
24
|
+
* try/catch.
|
|
25
|
+
* - Malformed traceparent: extractParentContext returns ROOT_CONTEXT.
|
|
26
|
+
* - Flags always 01 (sampled) on buildTraceparent.
|
|
33
27
|
*/
|
|
34
28
|
import { type Context } from '@opentelemetry/api';
|
|
35
29
|
import type { TraceContext } from './types';
|
|
36
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Always returns a traceparent — from NR, OTel active span, or freshly
|
|
32
|
+
* generated. Used by producers to inject into SNS attributes / job data,
|
|
33
|
+
* and by the Express/Fastify middleware at request start.
|
|
34
|
+
*/
|
|
35
|
+
export declare function buildTraceparent(): string;
|
|
37
36
|
export declare function extractParentContext(traceparent: string): Context;
|
|
37
|
+
/**
|
|
38
|
+
* Returns trace context from active sources only (NR or OTel active span).
|
|
39
|
+
* Returns empty strings when neither is active — never generates fresh IDs.
|
|
40
|
+
* Used for log enrichment where fake IDs would create false correlations.
|
|
41
|
+
*/
|
|
38
42
|
export declare function getTraceContext(): TraceContext;
|
|
39
43
|
export declare function getActiveContext(): Context;
|
|
@@ -1,52 +1,81 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* W3C trace context
|
|
3
|
+
* W3C trace context utilities for New Relic and OpenTelemetry interop.
|
|
4
4
|
*
|
|
5
5
|
* Why this file exists:
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* Reads trace context from two sources (NR first, then OTel's own active
|
|
7
|
+
* span) and converts it to/from W3C traceparent format. This means trace
|
|
8
|
+
* propagation works whether NR is installed, partially loaded, or completely
|
|
9
|
+
* removed.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* so the trace context is always current.
|
|
11
|
+
* Two resolution modes:
|
|
12
|
+
* resolveActiveTraceIds() — NR → OTel active span → empty strings.
|
|
13
|
+
* Used by getTraceContext() (log enrichment) and createPinoMixin().
|
|
14
|
+
* Never generates IDs — returns empty when no real source exists, so
|
|
15
|
+
* it doesn't overwrite valid IDs injected by pino-http's customProps.
|
|
16
|
+
*
|
|
17
|
+
* buildTraceparent() — NR → OTel active span → generate fresh IDs.
|
|
18
|
+
* Used by producers (injectSnsAttributes, injectJobData) and the
|
|
19
|
+
* Express/Fastify middleware at request start. Always returns a
|
|
20
|
+
* traceparent so every message/request gets trace context.
|
|
22
21
|
*
|
|
23
22
|
* Edge cases covered:
|
|
24
|
-
* - NR
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* -
|
|
28
|
-
*
|
|
29
|
-
* - Malformed traceparent (wrong number of parts, wrong hex lengths):
|
|
30
|
-
* extractParentContext returns ROOT_CONTEXT — consumer creates a fresh
|
|
31
|
-
* trace instead of crashing.
|
|
32
|
-
* - Flags field always set to 01 (sampled) on buildTraceparent because we
|
|
33
|
-
* want all async boundary crossings traced.
|
|
23
|
+
* - NR's getTraceMetadata() can throw TypeError when the transaction is
|
|
24
|
+
* null (pino-http fires after NR ends the transaction). Guarded with
|
|
25
|
+
* try/catch.
|
|
26
|
+
* - Malformed traceparent: extractParentContext returns ROOT_CONTEXT.
|
|
27
|
+
* - Flags always 01 (sampled) on buildTraceparent.
|
|
34
28
|
*/
|
|
35
29
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
30
|
exports.buildTraceparent = buildTraceparent;
|
|
37
31
|
exports.extractParentContext = extractParentContext;
|
|
38
32
|
exports.getTraceContext = getTraceContext;
|
|
39
33
|
exports.getActiveContext = getActiveContext;
|
|
34
|
+
const node_crypto_1 = require("node:crypto");
|
|
40
35
|
const api_1 = require("@opentelemetry/api");
|
|
41
36
|
const nr_1 = require("./nr");
|
|
37
|
+
function generateTraceId() {
|
|
38
|
+
return (0, node_crypto_1.randomBytes)(16).toString('hex');
|
|
39
|
+
}
|
|
40
|
+
function generateSpanId() {
|
|
41
|
+
return (0, node_crypto_1.randomBytes)(8).toString('hex');
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Resolves trace IDs from active sources only (NR or OTel active span).
|
|
45
|
+
* Returns empty strings when neither source has an active trace — never
|
|
46
|
+
* generates fresh IDs.
|
|
47
|
+
*/
|
|
48
|
+
function resolveActiveTraceIds() {
|
|
49
|
+
try {
|
|
50
|
+
const nr = (0, nr_1.getNr)();
|
|
51
|
+
const meta = nr.getTraceMetadata?.() ?? { traceId: '', spanId: '' };
|
|
52
|
+
if (meta.traceId && meta.spanId) {
|
|
53
|
+
return { traceId: meta.traceId, spanId: meta.spanId };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// NR transaction is null or getTraceMetadata threw
|
|
58
|
+
}
|
|
59
|
+
const activeSpan = api_1.trace.getActiveSpan();
|
|
60
|
+
if (activeSpan) {
|
|
61
|
+
const sc = activeSpan.spanContext();
|
|
62
|
+
if (sc.traceId && sc.spanId) {
|
|
63
|
+
return { traceId: sc.traceId, spanId: sc.spanId };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { traceId: '', spanId: '' };
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Always returns a traceparent — from NR, OTel active span, or freshly
|
|
70
|
+
* generated. Used by producers to inject into SNS attributes / job data,
|
|
71
|
+
* and by the Express/Fastify middleware at request start.
|
|
72
|
+
*/
|
|
42
73
|
function buildTraceparent() {
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (!traceId || !spanId) {
|
|
47
|
-
return undefined;
|
|
74
|
+
const { traceId, spanId } = resolveActiveTraceIds();
|
|
75
|
+
if (traceId && spanId) {
|
|
76
|
+
return `00-${traceId}-${spanId}-01`;
|
|
48
77
|
}
|
|
49
|
-
return `00-${
|
|
78
|
+
return `00-${generateTraceId()}-${generateSpanId()}-01`;
|
|
50
79
|
}
|
|
51
80
|
function extractParentContext(traceparent) {
|
|
52
81
|
const parts = traceparent.split('-');
|
|
@@ -66,11 +95,13 @@ function extractParentContext(traceparent) {
|
|
|
66
95
|
};
|
|
67
96
|
return api_1.trace.setSpanContext(api_1.ROOT_CONTEXT, spanContext);
|
|
68
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* Returns trace context from active sources only (NR or OTel active span).
|
|
100
|
+
* Returns empty strings when neither is active — never generates fresh IDs.
|
|
101
|
+
* Used for log enrichment where fake IDs would create false correlations.
|
|
102
|
+
*/
|
|
69
103
|
function getTraceContext() {
|
|
70
|
-
const
|
|
71
|
-
const meta = nr.getTraceMetadata?.() ?? { traceId: '', spanId: '' };
|
|
72
|
-
const traceId = meta.traceId ?? '';
|
|
73
|
-
const spanId = meta.spanId ?? '';
|
|
104
|
+
const { traceId, spanId } = resolveActiveTraceIds();
|
|
74
105
|
const traceparent = traceId && spanId ? `00-${traceId}-${spanId}-01` : '';
|
|
75
106
|
return { traceId, spanId, traceparent };
|
|
76
107
|
}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* All public interfaces and minimal type stubs for @foam/node-cliengo.
|
|
3
3
|
*
|
|
4
4
|
* Why this file exists:
|
|
5
|
-
* Defines every public type the library exposes (
|
|
6
|
-
* TraceContext,
|
|
5
|
+
* Defines every public type the library exposes (FoamInstance, InitOptions,
|
|
6
|
+
* TraceContext, FoamMetrics) plus lightweight stubs for external frameworks
|
|
7
7
|
* (Express, Fastify, Winston, Pino, New Relic). These stubs exist so the
|
|
8
8
|
* library never imports framework types at the top level — services that only
|
|
9
9
|
* use Pino don't need Winston installed, and vice versa.
|
|
@@ -41,22 +41,19 @@ export interface TraceContext {
|
|
|
41
41
|
spanId: string;
|
|
42
42
|
traceparent: string;
|
|
43
43
|
}
|
|
44
|
-
export interface
|
|
44
|
+
export interface FoamMetrics {
|
|
45
45
|
messageProcessingDuration: Histogram;
|
|
46
46
|
messageProcessingErrors: Counter;
|
|
47
47
|
messagesProcessed: Counter;
|
|
48
|
-
customHistogram(name: string, description: string, unit?: string): Histogram;
|
|
49
|
-
customCounter(name: string, description: string, unit?: string): Counter;
|
|
50
|
-
customGauge(name: string, description: string, unit?: string): ObservableGauge;
|
|
51
48
|
}
|
|
52
|
-
export interface
|
|
49
|
+
export interface FoamInstance {
|
|
53
50
|
tracer: Tracer;
|
|
54
51
|
meter: Meter;
|
|
55
52
|
logger: LoggerProvider;
|
|
56
53
|
traceProvider: BasicTracerProvider;
|
|
57
54
|
meterProvider: MeterProvider;
|
|
58
55
|
loggerProvider: LoggerProvider;
|
|
59
|
-
metrics:
|
|
56
|
+
metrics: FoamMetrics;
|
|
60
57
|
getTraceContext(): TraceContext;
|
|
61
58
|
createExpressMiddleware(): ExpressRequestHandler;
|
|
62
59
|
createExpressErrorHandler(): ExpressErrorHandler;
|
|
@@ -64,11 +61,14 @@ export interface OtelBridge {
|
|
|
64
61
|
createWinstonFormat(): WinstonFormat;
|
|
65
62
|
createWinstonTransport(): WinstonTransport;
|
|
66
63
|
createPinoMixin(): () => Record<string, string>;
|
|
64
|
+
createPinoHttpOptions(): {
|
|
65
|
+
customProps: (req: any) => Record<string, string>;
|
|
66
|
+
};
|
|
67
67
|
createPinoDestination(): NodeJS.WritableStream;
|
|
68
|
-
buildTraceparent(): string
|
|
68
|
+
buildTraceparent(): string;
|
|
69
69
|
injectSnsAttributes(attrs: Record<string, SnsMessageAttributeValue>): Record<string, SnsMessageAttributeValue>;
|
|
70
70
|
injectJobData<T extends Record<string, unknown>>(data: T): T & {
|
|
71
|
-
traceparent
|
|
71
|
+
traceparent: string;
|
|
72
72
|
};
|
|
73
73
|
wrapSqsConsumer(tracer: Tracer, spanName: string, msg: SqsMessage, fn: () => Promise<void>): Promise<void>;
|
|
74
74
|
wrapJobConsumer(tracer: Tracer, spanName: string, jobData: Record<string, unknown>, fn: () => Promise<void>): Promise<void>;
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* All public interfaces and minimal type stubs for @foam/node-cliengo.
|
|
4
4
|
*
|
|
5
5
|
* Why this file exists:
|
|
6
|
-
* Defines every public type the library exposes (
|
|
7
|
-
* TraceContext,
|
|
6
|
+
* Defines every public type the library exposes (FoamInstance, InitOptions,
|
|
7
|
+
* TraceContext, FoamMetrics) plus lightweight stubs for external frameworks
|
|
8
8
|
* (Express, Fastify, Winston, Pino, New Relic). These stubs exist so the
|
|
9
9
|
* library never imports framework types at the top level — services that only
|
|
10
10
|
* use Pino don't need Winston installed, and vice versa.
|