@foam-ai/node-cliengo 0.1.0-alpha.7 → 0.1.0-alpha.9

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.
@@ -48,6 +48,15 @@ export declare function getRequestTraceContext(req: any): {
48
48
  spanId: string;
49
49
  traceparent: string;
50
50
  };
51
+ /**
52
+ * Returns pino-http options with customProps pre-wired to read the trace
53
+ * context stored on req by createExpressMiddleware(). Zero config for services:
54
+ *
55
+ * app.use(pinoHttp({ logger, ...foam.createPinoHttpOptions() }));
56
+ */
57
+ export declare function createPinoHttpOptions(): {
58
+ customProps: (req: any) => Record<string, string>;
59
+ };
51
60
  export declare function createExpressErrorHandler(middlewareRef?: ExpressRequestHandler & {
52
61
  _markErrorHandler?: () => void;
53
62
  }): ExpressErrorHandler;
@@ -37,6 +37,7 @@
37
37
  Object.defineProperty(exports, "__esModule", { value: true });
38
38
  exports.createExpressMiddleware = createExpressMiddleware;
39
39
  exports.getRequestTraceContext = getRequestTraceContext;
40
+ exports.createPinoHttpOptions = createPinoHttpOptions;
40
41
  exports.createExpressErrorHandler = createExpressErrorHandler;
41
42
  const api_1 = require("@opentelemetry/api");
42
43
  const trace_bridge_1 = require("../trace-bridge");
@@ -121,6 +122,17 @@ function getRequestTraceContext(req) {
121
122
  return { traceId: '', spanId: '', traceparent: '' };
122
123
  }
123
124
  }
125
+ /**
126
+ * Returns pino-http options with customProps pre-wired to read the trace
127
+ * context stored on req by createExpressMiddleware(). Zero config for services:
128
+ *
129
+ * app.use(pinoHttp({ logger, ...foam.createPinoHttpOptions() }));
130
+ */
131
+ function createPinoHttpOptions() {
132
+ return {
133
+ customProps: (req) => getRequestTraceContext(req),
134
+ };
135
+ }
124
136
  function createExpressErrorHandler(middlewareRef) {
125
137
  middlewareRef?._markErrorHandler?.();
126
138
  return (err, req, _res, next) => {
@@ -18,7 +18,7 @@ export { buildTraceparent, extractParentContext, getTraceContext } from './trace
18
18
  export { injectSnsAttributes } from './sns';
19
19
  export { injectJobData, wrapJobConsumer } from './job';
20
20
  export { wrapSqsConsumer } from './sqs';
21
- export { createExpressMiddleware, createExpressErrorHandler, getRequestTraceContext } from './http/express';
21
+ export { createExpressMiddleware, createExpressErrorHandler, getRequestTraceContext, createPinoHttpOptions } from './http/express';
22
22
  export { createFastifyPlugin, getRequestTraceContext as getFastifyRequestTraceContext } from './http/fastify';
23
23
  export { createWinstonFormat } from './logs/winston-format';
24
24
  export { createPinoMixin } from './logs/pino-mixin';
@@ -15,7 +15,7 @@
15
15
  * access to the foam instance.
16
16
  */
17
17
  Object.defineProperty(exports, "__esModule", { value: true });
18
- exports.FOAM_OTEL_ENDPOINT = exports.createPinoMixin = exports.createWinstonFormat = exports.getFastifyRequestTraceContext = exports.createFastifyPlugin = exports.getRequestTraceContext = exports.createExpressErrorHandler = exports.createExpressMiddleware = exports.wrapSqsConsumer = exports.wrapJobConsumer = exports.injectJobData = exports.injectSnsAttributes = exports.getTraceContext = exports.extractParentContext = exports.buildTraceparent = exports.init = void 0;
18
+ exports.FOAM_OTEL_ENDPOINT = exports.createPinoMixin = exports.createWinstonFormat = exports.getFastifyRequestTraceContext = exports.createFastifyPlugin = exports.createPinoHttpOptions = exports.getRequestTraceContext = exports.createExpressErrorHandler = exports.createExpressMiddleware = exports.wrapSqsConsumer = exports.wrapJobConsumer = exports.injectJobData = exports.injectSnsAttributes = exports.getTraceContext = exports.extractParentContext = exports.buildTraceparent = exports.init = void 0;
19
19
  var init_1 = require("./init");
20
20
  Object.defineProperty(exports, "init", { enumerable: true, get: function () { return init_1.init; } });
21
21
  var trace_bridge_1 = require("./trace-bridge");
@@ -33,6 +33,7 @@ var express_1 = require("./http/express");
33
33
  Object.defineProperty(exports, "createExpressMiddleware", { enumerable: true, get: function () { return express_1.createExpressMiddleware; } });
34
34
  Object.defineProperty(exports, "createExpressErrorHandler", { enumerable: true, get: function () { return express_1.createExpressErrorHandler; } });
35
35
  Object.defineProperty(exports, "getRequestTraceContext", { enumerable: true, get: function () { return express_1.getRequestTraceContext; } });
36
+ Object.defineProperty(exports, "createPinoHttpOptions", { enumerable: true, get: function () { return express_1.createPinoHttpOptions; } });
36
37
  var fastify_1 = require("./http/fastify");
37
38
  Object.defineProperty(exports, "createFastifyPlugin", { enumerable: true, get: function () { return fastify_1.createFastifyPlugin; } });
38
39
  Object.defineProperty(exports, "getFastifyRequestTraceContext", { enumerable: true, get: function () { return fastify_1.getRequestTraceContext; } });
@@ -100,6 +100,7 @@ function createInertInstance(serviceName) {
100
100
  }),
101
101
  createWinstonTransport: () => ({}),
102
102
  createPinoMixin: () => () => ({}),
103
+ createPinoHttpOptions: () => ({ customProps: () => ({}) }),
103
104
  createPinoDestination: () => new node_stream_1.Writable({
104
105
  write: (_c, _e, cb) => cb(),
105
106
  }),
@@ -339,6 +340,7 @@ function init(serviceName, token, options = {}) {
339
340
  createWinstonFormat: () => (0, winston_format_1.createWinstonFormat)(),
340
341
  createWinstonTransport: () => (0, winston_transport_1.createWinstonTransport)(loggerProvider, serviceName),
341
342
  createPinoMixin: () => (0, pino_mixin_1.createPinoMixin)(),
343
+ createPinoHttpOptions: () => (0, express_1.createPinoHttpOptions)(),
342
344
  createPinoDestination: () => (0, pino_destination_1.createPinoDestination)(loggerProvider, serviceName),
343
345
  buildTraceparent: trace_bridge_1.buildTraceparent,
344
346
  injectSnsAttributes: sns_1.injectSnsAttributes,
@@ -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
  }
@@ -7,35 +7,37 @@
7
7
  * propagation works whether NR is installed, partially loaded, or completely
8
8
  * removed.
9
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
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.
14
15
  *
15
- * Functions:
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.
19
- * - extractParentContext(): parses a traceparent string into an OTel Context
20
- * with a remote SpanContext. Used by consumers to link their OTel spans
21
- * back to the producer's trace. Returns ROOT_CONTEXT on malformed input.
22
- * - getTraceContext(): returns { traceId, spanId, traceparent } for log
23
- * enrichment. Called lazily at log-write time (not at format creation time)
24
- * so the trace context is always current.
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.
25
20
  *
26
21
  * Edge cases covered:
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.
30
- * - Malformed traceparent (wrong number of parts, wrong hex lengths):
31
- * extractParentContext returns ROOT_CONTEXT consumer creates a fresh
32
- * trace instead of crashing.
33
- * - Flags field always set to 01 (sampled) on buildTraceparent because we
34
- * 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.
35
27
  */
36
28
  import { type Context } from '@opentelemetry/api';
37
29
  import type { TraceContext } from './types';
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
+ */
38
35
  export declare function buildTraceparent(): string;
39
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
+ */
40
42
  export declare function getTraceContext(): TraceContext;
41
43
  export declare function getActiveContext(): Context;
@@ -8,31 +8,23 @@
8
8
  * propagation works whether NR is installed, partially loaded, or completely
9
9
  * removed.
10
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
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.
15
16
  *
16
- * Functions:
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.
20
- * - extractParentContext(): parses a traceparent string into an OTel Context
21
- * with a remote SpanContext. Used by consumers to link their OTel spans
22
- * back to the producer's trace. Returns ROOT_CONTEXT on malformed input.
23
- * - getTraceContext(): returns { traceId, spanId, traceparent } for log
24
- * enrichment. Called lazily at log-write time (not at format creation time)
25
- * so the trace context is always current.
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.
26
21
  *
27
22
  * Edge cases covered:
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.
31
- * - Malformed traceparent (wrong number of parts, wrong hex lengths):
32
- * extractParentContext returns ROOT_CONTEXT consumer creates a fresh
33
- * trace instead of crashing.
34
- * - Flags field always set to 01 (sampled) on buildTraceparent because we
35
- * 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.
36
28
  */
37
29
  Object.defineProperty(exports, "__esModule", { value: true });
38
30
  exports.buildTraceparent = buildTraceparent;
@@ -48,10 +40,12 @@ function generateTraceId() {
48
40
  function generateSpanId() {
49
41
  return (0, node_crypto_1.randomBytes)(8).toString('hex');
50
42
  }
51
- function resolveTraceIds() {
52
- // 1. Try NR (guarded getTraceMetadata() can throw if the underlying
53
- // transaction is null, which happens when pino-http logs fire after
54
- // NR's transaction has ended)
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() {
55
49
  try {
56
50
  const nr = (0, nr_1.getNr)();
57
51
  const meta = nr.getTraceMetadata?.() ?? { traceId: '', spanId: '' };
@@ -60,9 +54,8 @@ function resolveTraceIds() {
60
54
  }
61
55
  }
62
56
  catch {
63
- // NR transaction is null or getTraceMetadata threw — fall through
57
+ // NR transaction is null or getTraceMetadata threw
64
58
  }
65
- // 2. Fall back to OTel's own active span
66
59
  const activeSpan = api_1.trace.getActiveSpan();
67
60
  if (activeSpan) {
68
61
  const sc = activeSpan.spanContext();
@@ -70,12 +63,19 @@ function resolveTraceIds() {
70
63
  return { traceId: sc.traceId, spanId: sc.spanId };
71
64
  }
72
65
  }
73
- // 3. Generate our own ensures trace context is always available
74
- return { traceId: generateTraceId(), spanId: generateSpanId() };
66
+ return { traceId: '', spanId: '' };
75
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
+ */
76
73
  function buildTraceparent() {
77
- const { traceId, spanId } = resolveTraceIds();
78
- return `00-${traceId}-${spanId}-01`;
74
+ const { traceId, spanId } = resolveActiveTraceIds();
75
+ if (traceId && spanId) {
76
+ return `00-${traceId}-${spanId}-01`;
77
+ }
78
+ return `00-${generateTraceId()}-${generateSpanId()}-01`;
79
79
  }
80
80
  function extractParentContext(traceparent) {
81
81
  const parts = traceparent.split('-');
@@ -95,8 +95,13 @@ function extractParentContext(traceparent) {
95
95
  };
96
96
  return api_1.trace.setSpanContext(api_1.ROOT_CONTEXT, spanContext);
97
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
+ */
98
103
  function getTraceContext() {
99
- const { traceId, spanId } = resolveTraceIds();
104
+ const { traceId, spanId } = resolveActiveTraceIds();
100
105
  const traceparent = traceId && spanId ? `00-${traceId}-${spanId}-01` : '';
101
106
  return { traceId, spanId, traceparent };
102
107
  }
@@ -61,6 +61,9 @@ export interface FoamInstance {
61
61
  createWinstonFormat(): WinstonFormat;
62
62
  createWinstonTransport(): WinstonTransport;
63
63
  createPinoMixin(): () => Record<string, string>;
64
+ createPinoHttpOptions(): {
65
+ customProps: (req: any) => Record<string, string>;
66
+ };
64
67
  createPinoDestination(): NodeJS.WritableStream;
65
68
  buildTraceparent(): string;
66
69
  injectSnsAttributes(attrs: Record<string, SnsMessageAttributeValue>): Record<string, SnsMessageAttributeValue>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foam-ai/node-cliengo",
3
- "version": "0.1.0-alpha.7",
3
+ "version": "0.1.0-alpha.9",
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",