@foam-ai/node-cliengo 0.1.0-alpha.3 → 0.1.0-alpha.6

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.
@@ -103,9 +103,9 @@ function createInertInstance(serviceName) {
103
103
  createPinoDestination: () => new node_stream_1.Writable({
104
104
  write: (_c, _e, cb) => cb(),
105
105
  }),
106
- buildTraceparent: () => undefined,
107
- injectSnsAttributes: (attrs) => attrs,
108
- injectJobData: (data) => data,
106
+ buildTraceparent: trace_bridge_1.buildTraceparent,
107
+ injectSnsAttributes: sns_1.injectSnsAttributes,
108
+ injectJobData: job_1.injectJobData,
109
109
  wrapSqsConsumer: async (_t, _s, _m, fn) => fn(),
110
110
  wrapJobConsumer: async (_t, _s, _j, fn) => fn(),
111
111
  extractParentContext: () => {
@@ -26,6 +26,6 @@
26
26
  import { type Tracer } from '@opentelemetry/api';
27
27
  import type { FoamMetrics } from './types';
28
28
  export declare function injectJobData<T extends Record<string, unknown>>(data: T): T & {
29
- traceparent?: string;
29
+ traceparent: string;
30
30
  };
31
31
  export declare function wrapJobConsumer(tracer: Tracer, spanName: string, jobData: Record<string, unknown>, fn: () => Promise<void>, metrics?: FoamMetrics): Promise<void>;
@@ -31,11 +31,7 @@ const api_1 = require("@opentelemetry/api");
31
31
  const nr_1 = require("./nr");
32
32
  const trace_bridge_1 = require("./trace-bridge");
33
33
  function injectJobData(data) {
34
- const tp = (0, trace_bridge_1.buildTraceparent)();
35
- if (!tp) {
36
- return data;
37
- }
38
- return { ...data, traceparent: tp };
34
+ return { ...data, traceparent: (0, trace_bridge_1.buildTraceparent)() };
39
35
  }
40
36
  async function wrapJobConsumer(tracer, spanName, jobData, fn, metrics) {
41
37
  const nr = (0, nr_1.getNr)();
@@ -21,8 +21,13 @@
21
21
  * 60→FATAL. Unknown numeric levels default to INFO.
22
22
  * - Pino uses `msg` for the message field (not `message` like Winston). We
23
23
  * check both for compatibility with custom Pino configs.
24
- * - Internal Pino fields (pid, hostname, time) are excluded from attributes
25
- * to avoid noise. All other fields are forwarded.
24
+ * - Trace context (traceId, spanId, traceparent) is read from the parsed
25
+ * JSON line, NOT from getTraceContext(). The pino mixin injects these at
26
+ * log-write time (when NR's transaction is still active). The stream write
27
+ * happens asynchronously — by then NR's transaction has ended, so calling
28
+ * getTraceContext() here would return empty strings.
29
+ * - Internal Pino fields (pid, hostname, time) and trace fields (traceId,
30
+ * spanId, traceparent) are excluded from forwarded attributes.
26
31
  * - Callback always called — never blocks the Pino stream pipeline.
27
32
  * - Buffer chunks: converted to string before parsing (handles both Buffer
28
33
  * and string inputs from Node streams).
@@ -22,8 +22,13 @@
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
- * - Internal Pino fields (pid, hostname, time) are excluded from attributes
26
- * to avoid noise. All other fields are forwarded.
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).
@@ -32,7 +37,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
32
37
  exports.createPinoDestination = createPinoDestination;
33
38
  const api_logs_1 = require("@opentelemetry/api-logs");
34
39
  const node_stream_1 = require("node:stream");
35
- const trace_bridge_1 = require("../trace-bridge");
36
40
  const PINO_LEVEL_MAP = {
37
41
  10: api_logs_1.SeverityNumber.TRACE,
38
42
  20: api_logs_1.SeverityNumber.DEBUG,
@@ -66,16 +70,23 @@ function createPinoDestination(loggerProvider, serviceName) {
66
70
  const msg = parsed.msg ?? parsed.message ?? '';
67
71
  const severity = PINO_LEVEL_MAP[level] ?? api_logs_1.SeverityNumber.INFO;
68
72
  const severityText = PINO_LEVEL_TEXT[level] ?? 'INFO';
69
- const ctx = (0, trace_bridge_1.getTraceContext)();
73
+ // Read trace context from the parsed JSON line — the pino mixin already
74
+ // injected traceId/spanId/traceparent at log-write time (when the NR
75
+ // transaction was still active). Calling getTraceContext() here would be
76
+ // too late — the stream write happens asynchronously after NR's
77
+ // transaction has ended.
78
+ const traceId = parsed.traceId ?? '';
79
+ const spanId = parsed.spanId ?? '';
70
80
  const attributes = {};
71
- if (ctx.traceId) {
72
- attributes['trace.id'] = ctx.traceId;
81
+ if (traceId) {
82
+ attributes['trace.id'] = traceId;
73
83
  }
74
- if (ctx.spanId) {
75
- attributes['span.id'] = ctx.spanId;
84
+ if (spanId) {
85
+ attributes['span.id'] = spanId;
76
86
  }
87
+ const SKIP_KEYS = new Set(['level', 'msg', 'message', 'time', 'pid', 'hostname', 'traceId', 'spanId', 'traceparent']);
77
88
  for (const [key, val] of Object.entries(parsed)) {
78
- if (key !== 'level' && key !== 'msg' && key !== 'message' && key !== 'time' && key !== 'pid' && key !== 'hostname') {
89
+ if (!SKIP_KEYS.has(key)) {
79
90
  try {
80
91
  attributes[key] = typeof val === 'string' ? val : JSON.stringify(val);
81
92
  }
@@ -8,10 +8,7 @@
8
8
  * alongside the existing ones. This function spread-merges it in.
9
9
  *
10
10
  * Edge cases covered:
11
- * - No active NR transaction (buildTraceparent returns undefined): the
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
- * - No active NR transaction (buildTraceparent returns undefined): the
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: tp },
22
+ traceparent: { DataType: 'String', StringValue: (0, trace_bridge_1.buildTraceparent)() },
30
23
  };
31
24
  }
@@ -2,16 +2,20 @@
2
2
  * W3C trace context utilities for New Relic and OpenTelemetry interop.
3
3
  *
4
4
  * Why this file exists:
5
- * NR owns the active trace (it creates transactions via http monkey-patching),
6
- * but our OTel spans need to share the same traceId so Foam and NR traces
7
- * correlate. This module reads NR's trace metadata and converts it to/from
8
- * W3C traceparent format (`00-{traceId}-{spanId}-{flags}`).
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
+ *
10
+ * Resolution order (buildTraceparent / getTraceContext):
11
+ * 1. NR's getTraceMetadata() — if NR is loaded and has an active transaction
12
+ * 2. OTel's active span context — if our own tracer has an active span
13
+ * 3. Generate fresh traceId (16 bytes) + spanId (8 bytes) — always has context
9
14
  *
10
15
  * Functions:
11
- * - buildTraceparent(): reads NR's active traceId/spanId, returns a W3C
12
- * traceparent string. Used by producers to inject into SNS attributes or
13
- * BullMQ/Bull job data. Returns undefined when NR is not loaded or no
14
- * active transaction exists.
16
+ * - buildTraceparent(): always returns a W3C traceparent string — from NR,
17
+ * OTel, or freshly generated. Used by producers to inject into SNS
18
+ * attributes or BullMQ/Bull job data.
15
19
  * - extractParentContext(): parses a traceparent string into an OTel Context
16
20
  * with a remote SpanContext. Used by consumers to link their OTel spans
17
21
  * back to the producer's trace. Returns ROOT_CONTEXT on malformed input.
@@ -20,11 +24,9 @@
20
24
  * so the trace context is always current.
21
25
  *
22
26
  * Edge cases covered:
23
- * - NR not loaded: getTraceMetadata() returns undefined we default to
24
- * empty strings, buildTraceparent returns undefined, getTraceContext
25
- * returns empty strings. No crash.
26
- * - NR loaded but no active transaction: traceId/spanId are empty strings.
27
- * buildTraceparent returns undefined. Consumers create a fresh trace.
27
+ * - NR not loaded: falls back to OTel active span, then to generated IDs.
28
+ * - NR loaded but no active transaction: same fallback chain.
29
+ * - NR removed entirely: OTel active span or generated IDs. Always works.
28
30
  * - Malformed traceparent (wrong number of parts, wrong hex lengths):
29
31
  * extractParentContext returns ROOT_CONTEXT — consumer creates a fresh
30
32
  * trace instead of crashing.
@@ -33,7 +35,7 @@
33
35
  */
34
36
  import { type Context } from '@opentelemetry/api';
35
37
  import type { TraceContext } from './types';
36
- export declare function buildTraceparent(): string | undefined;
38
+ export declare function buildTraceparent(): string;
37
39
  export declare function extractParentContext(traceparent: string): Context;
38
40
  export declare function getTraceContext(): TraceContext;
39
41
  export declare function getActiveContext(): Context;
@@ -3,16 +3,20 @@
3
3
  * W3C trace context utilities for New Relic and OpenTelemetry interop.
4
4
  *
5
5
  * Why this file exists:
6
- * NR owns the active trace (it creates transactions via http monkey-patching),
7
- * but our OTel spans need to share the same traceId so Foam and NR traces
8
- * correlate. This module reads NR's trace metadata and converts it to/from
9
- * W3C traceparent format (`00-{traceId}-{spanId}-{flags}`).
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
+ *
11
+ * Resolution order (buildTraceparent / getTraceContext):
12
+ * 1. NR's getTraceMetadata() — if NR is loaded and has an active transaction
13
+ * 2. OTel's active span context — if our own tracer has an active span
14
+ * 3. Generate fresh traceId (16 bytes) + spanId (8 bytes) — always has context
10
15
  *
11
16
  * Functions:
12
- * - buildTraceparent(): reads NR's active traceId/spanId, returns a W3C
13
- * traceparent string. Used by producers to inject into SNS attributes or
14
- * BullMQ/Bull job data. Returns undefined when NR is not loaded or no
15
- * active transaction exists.
17
+ * - buildTraceparent(): always returns a W3C traceparent string — from NR,
18
+ * OTel, or freshly generated. Used by producers to inject into SNS
19
+ * attributes or BullMQ/Bull job data.
16
20
  * - extractParentContext(): parses a traceparent string into an OTel Context
17
21
  * with a remote SpanContext. Used by consumers to link their OTel spans
18
22
  * back to the producer's trace. Returns ROOT_CONTEXT on malformed input.
@@ -21,11 +25,9 @@
21
25
  * so the trace context is always current.
22
26
  *
23
27
  * Edge cases covered:
24
- * - NR not loaded: getTraceMetadata() returns undefined we default to
25
- * empty strings, buildTraceparent returns undefined, getTraceContext
26
- * returns empty strings. No crash.
27
- * - NR loaded but no active transaction: traceId/spanId are empty strings.
28
- * buildTraceparent returns undefined. Consumers create a fresh trace.
28
+ * - NR not loaded: falls back to OTel active span, then to generated IDs.
29
+ * - NR loaded but no active transaction: same fallback chain.
30
+ * - NR removed entirely: OTel active span or generated IDs. Always works.
29
31
  * - Malformed traceparent (wrong number of parts, wrong hex lengths):
30
32
  * extractParentContext returns ROOT_CONTEXT — consumer creates a fresh
31
33
  * trace instead of crashing.
@@ -37,15 +39,35 @@ exports.buildTraceparent = buildTraceparent;
37
39
  exports.extractParentContext = extractParentContext;
38
40
  exports.getTraceContext = getTraceContext;
39
41
  exports.getActiveContext = getActiveContext;
42
+ const node_crypto_1 = require("node:crypto");
40
43
  const api_1 = require("@opentelemetry/api");
41
44
  const nr_1 = require("./nr");
42
- function buildTraceparent() {
45
+ function generateTraceId() {
46
+ return (0, node_crypto_1.randomBytes)(16).toString('hex');
47
+ }
48
+ function generateSpanId() {
49
+ return (0, node_crypto_1.randomBytes)(8).toString('hex');
50
+ }
51
+ function resolveTraceIds() {
52
+ // 1. Try NR
43
53
  const nr = (0, nr_1.getNr)();
44
54
  const meta = nr.getTraceMetadata?.() ?? { traceId: '', spanId: '' };
45
- const { traceId, spanId } = meta;
46
- if (!traceId || !spanId) {
47
- return undefined;
55
+ if (meta.traceId && meta.spanId) {
56
+ return { traceId: meta.traceId, spanId: meta.spanId };
57
+ }
58
+ // 2. Fall back to OTel's own active span
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
+ }
48
65
  }
66
+ // 3. Generate our own — ensures trace context is always available
67
+ return { traceId: generateTraceId(), spanId: generateSpanId() };
68
+ }
69
+ function buildTraceparent() {
70
+ const { traceId, spanId } = resolveTraceIds();
49
71
  return `00-${traceId}-${spanId}-01`;
50
72
  }
51
73
  function extractParentContext(traceparent) {
@@ -67,10 +89,7 @@ function extractParentContext(traceparent) {
67
89
  return api_1.trace.setSpanContext(api_1.ROOT_CONTEXT, spanContext);
68
90
  }
69
91
  function getTraceContext() {
70
- const nr = (0, nr_1.getNr)();
71
- const meta = nr.getTraceMetadata?.() ?? { traceId: '', spanId: '' };
72
- const traceId = meta.traceId ?? '';
73
- const spanId = meta.spanId ?? '';
92
+ const { traceId, spanId } = resolveTraceIds();
74
93
  const traceparent = traceId && spanId ? `00-${traceId}-${spanId}-01` : '';
75
94
  return { traceId, spanId, traceparent };
76
95
  }
@@ -62,10 +62,10 @@ export interface FoamInstance {
62
62
  createWinstonTransport(): WinstonTransport;
63
63
  createPinoMixin(): () => Record<string, string>;
64
64
  createPinoDestination(): NodeJS.WritableStream;
65
- buildTraceparent(): string | undefined;
65
+ buildTraceparent(): string;
66
66
  injectSnsAttributes(attrs: Record<string, SnsMessageAttributeValue>): Record<string, SnsMessageAttributeValue>;
67
67
  injectJobData<T extends Record<string, unknown>>(data: T): T & {
68
- traceparent?: string;
68
+ traceparent: string;
69
69
  };
70
70
  wrapSqsConsumer(tracer: Tracer, spanName: string, msg: SqsMessage, fn: () => Promise<void>): Promise<void>;
71
71
  wrapJobConsumer(tracer: Tracer, spanName: string, jobData: Record<string, unknown>, fn: () => Promise<void>): Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foam-ai/node-cliengo",
3
- "version": "0.1.0-alpha.3",
3
+ "version": "0.1.0-alpha.6",
4
4
  "description": "Unified observability (traces, logs, metrics) for Cliengo Node.js services, connecting New Relic APM with Foam's OTel collector.",
5
5
  "main": "dist/node-cliengo/src/index.js",
6
6
  "types": "dist/node-cliengo/src/index.d.ts",