@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.
@@ -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
- * - 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).
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
- const ctx = (0, trace_bridge_1.getTraceContext)();
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 (ctx.traceId) {
72
- attributes['trace.id'] = ctx.traceId;
82
+ if (traceId) {
83
+ attributes['trace.id'] = traceId;
73
84
  }
74
- if (ctx.spanId) {
75
- attributes['span.id'] = ctx.spanId;
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 !== 'level' && key !== 'msg' && key !== 'message' && key !== 'time' && key !== 'pid' && key !== 'hostname') {
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 { traceId: '', spanId: '',
15
- * traceparent: '' }. Consistent schema log parsers always see the fields.
16
- * - getTraceContext throws: catch returns the same empty-string object.
17
- * Never crashes logging.
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 { traceId: '', spanId: '',
16
- * traceparent: '' }. Consistent schema log parsers always see the fields.
17
- * - getTraceContext throws: catch returns the same empty-string object.
18
- * Never crashes logging.
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 { traceId: '', spanId: '', traceparent: '' };
41
+ return {};
38
42
  }
39
43
  };
40
44
  }
@@ -1,6 +1,5 @@
1
1
  /**
2
- * Pre-built OTel metric instruments for queue processing, plus factories
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
- * Also provides customHistogram/customCounter/customGauge factories for
13
- * service-specific domain metrics (LLM duration in gpt-intentions, webhook
14
- * processing time in cb-proxy, automation duration in kori).
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 { BridgeMetrics } from '../types';
29
- export declare function createBridgeMetrics(meterProvider: MeterProvider, serviceName: string): BridgeMetrics;
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, plus factories
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
- * Also provides customHistogram/customCounter/customGauge factories for
14
- * service-specific domain metrics (LLM duration in gpt-intentions, webhook
15
- * processing time in cb-proxy, automation duration in kori).
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.createBridgeMetrics = createBridgeMetrics;
30
- function createBridgeMetrics(meterProvider, serviceName) {
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
- messageProcessingErrors,
46
- messagesProcessed,
47
- customHistogram(name, description, unit) {
48
- const key = `histogram:${name}`;
49
- const existing = instrumentCache.get(key);
50
- if (existing) {
51
- return existing;
52
- }
53
- const h = meter.createHistogram(name, { description, unit });
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 the bridge initializes.
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 the bridge initializes.
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
- * - 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
  }
@@ -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 bridge): all metrics?.xxx calls are no-ops.
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 { BridgeMetrics, SqsMessage } from './types';
35
- export declare function wrapSqsConsumer(tracer: Tracer, spanName: string, msg: SqsMessage, fn: () => Promise<void>, metrics?: BridgeMetrics): Promise<void>;
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 bridge): all metrics?.xxx calls are no-ops.
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 bridge between New Relic and OpenTelemetry.
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
9
  *
10
- * 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.
15
- * - extractParentContext(): parses a traceparent string into an OTel Context
16
- * with a remote SpanContext. Used by consumers to link their OTel spans
17
- * back to the producer's trace. Returns ROOT_CONTEXT on malformed input.
18
- * - getTraceContext(): returns { traceId, spanId, traceparent } for log
19
- * enrichment. Called lazily at log-write time (not at format creation time)
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 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.
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
- export declare function buildTraceparent(): string | undefined;
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 bridge between New Relic and OpenTelemetry.
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
10
  *
11
- * 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.
16
- * - extractParentContext(): parses a traceparent string into an OTel Context
17
- * with a remote SpanContext. Used by consumers to link their OTel spans
18
- * back to the producer's trace. Returns ROOT_CONTEXT on malformed input.
19
- * - getTraceContext(): returns { traceId, spanId, traceparent } for log
20
- * enrichment. Called lazily at log-write time (not at format creation time)
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 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.
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 nr = (0, nr_1.getNr)();
44
- const meta = nr.getTraceMetadata?.() ?? { traceId: '', spanId: '' };
45
- const { traceId, spanId } = meta;
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-${traceId}-${spanId}-01`;
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 nr = (0, nr_1.getNr)();
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 (OtelBridge, InitOptions,
6
- * TraceContext, BridgeMetrics) plus lightweight stubs for external frameworks
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 BridgeMetrics {
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 OtelBridge {
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: BridgeMetrics;
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 | undefined;
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?: string;
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 (OtelBridge, InitOptions,
7
- * TraceContext, BridgeMetrics) plus lightweight stubs for external frameworks
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.