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

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.
@@ -36,6 +36,18 @@
36
36
  import { type Tracer } from '@opentelemetry/api';
37
37
  import type { ExpressErrorHandler, ExpressRequestHandler } from '../types';
38
38
  export declare function createExpressMiddleware(tracer: Tracer): ExpressRequestHandler;
39
+ /**
40
+ * Reads the trace context that was captured on the request at middleware time.
41
+ * Use this in pino-http's `customProps` to get trace IDs even after NR's
42
+ * transaction has ended:
43
+ *
44
+ * pinoHttp({ customProps: (req) => getRequestTraceContext(req) })
45
+ */
46
+ export declare function getRequestTraceContext(req: any): {
47
+ traceId: string;
48
+ spanId: string;
49
+ traceparent: string;
50
+ };
39
51
  export declare function createExpressErrorHandler(middlewareRef?: ExpressRequestHandler & {
40
52
  _markErrorHandler?: () => void;
41
53
  }): ExpressErrorHandler;
@@ -36,11 +36,13 @@
36
36
  */
37
37
  Object.defineProperty(exports, "__esModule", { value: true });
38
38
  exports.createExpressMiddleware = createExpressMiddleware;
39
+ exports.getRequestTraceContext = getRequestTraceContext;
39
40
  exports.createExpressErrorHandler = createExpressErrorHandler;
40
41
  const api_1 = require("@opentelemetry/api");
41
42
  const trace_bridge_1 = require("../trace-bridge");
42
43
  const request_context_1 = require("./request-context");
43
44
  const SPAN_KEY = Symbol.for('foam.shadow.span');
45
+ const TRACE_CTX_KEY = Symbol.for('foam.trace.context');
44
46
  /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */
45
47
  function createExpressMiddleware(tracer) {
46
48
  let errorHandlerRegistered = false;
@@ -61,6 +63,7 @@ function createExpressMiddleware(tracer) {
61
63
  },
62
64
  });
63
65
  req[SPAN_KEY] = span;
66
+ req[TRACE_CTX_KEY] = { traceId, spanId };
64
67
  const startTime = Date.now();
65
68
  const onFinish = () => {
66
69
  try {
@@ -99,6 +102,25 @@ function createExpressMiddleware(tracer) {
99
102
  middleware._markErrorHandler = markErrorHandlerRegistered;
100
103
  return middleware;
101
104
  }
105
+ /**
106
+ * Reads the trace context that was captured on the request at middleware time.
107
+ * Use this in pino-http's `customProps` to get trace IDs even after NR's
108
+ * transaction has ended:
109
+ *
110
+ * pinoHttp({ customProps: (req) => getRequestTraceContext(req) })
111
+ */
112
+ function getRequestTraceContext(req) {
113
+ try {
114
+ const ctx = req[TRACE_CTX_KEY];
115
+ const traceId = ctx?.traceId ?? '';
116
+ const spanId = ctx?.spanId ?? '';
117
+ const traceparent = traceId && spanId ? `00-${traceId}-${spanId}-01` : '';
118
+ return { traceId, spanId, traceparent };
119
+ }
120
+ catch {
121
+ return { traceId: '', spanId: '', traceparent: '' };
122
+ }
123
+ }
102
124
  function createExpressErrorHandler(middlewareRef) {
103
125
  middlewareRef?._markErrorHandler?.();
104
126
  return (err, req, _res, next) => {
@@ -25,4 +25,14 @@
25
25
  */
26
26
  import { type Tracer } from '@opentelemetry/api';
27
27
  import type { FastifyPluginAsync } from '../types';
28
+ /**
29
+ * Reads the trace context that was captured on the request at hook time.
30
+ * Use this in Fastify's logger mixin or serializers to get trace IDs even
31
+ * after NR's transaction has ended.
32
+ */
33
+ export declare function getRequestTraceContext(request: any): {
34
+ traceId: string;
35
+ spanId: string;
36
+ traceparent: string;
37
+ };
28
38
  export declare function createFastifyPlugin(tracer: Tracer): FastifyPluginAsync;
@@ -25,12 +25,31 @@
25
25
  * - done() always called even if our code throws — Fastify hook contract.
26
26
  */
27
27
  Object.defineProperty(exports, "__esModule", { value: true });
28
+ exports.getRequestTraceContext = getRequestTraceContext;
28
29
  exports.createFastifyPlugin = createFastifyPlugin;
29
30
  const api_1 = require("@opentelemetry/api");
30
31
  const trace_bridge_1 = require("../trace-bridge");
31
32
  const request_context_1 = require("./request-context");
32
33
  const SPAN_KEY = Symbol.for('foam.shadow.span');
33
34
  const START_KEY = Symbol.for('foam.shadow.start');
35
+ const TRACE_CTX_KEY = Symbol.for('foam.trace.context');
36
+ /**
37
+ * Reads the trace context that was captured on the request at hook time.
38
+ * Use this in Fastify's logger mixin or serializers to get trace IDs even
39
+ * after NR's transaction has ended.
40
+ */
41
+ function getRequestTraceContext(request) {
42
+ try {
43
+ const ctx = request[TRACE_CTX_KEY];
44
+ const traceId = ctx?.traceId ?? '';
45
+ const spanId = ctx?.spanId ?? '';
46
+ const traceparent = traceId && spanId ? `00-${traceId}-${spanId}-01` : '';
47
+ return { traceId, spanId, traceparent };
48
+ }
49
+ catch {
50
+ return { traceId: '', spanId: '', traceparent: '' };
51
+ }
52
+ }
34
53
  /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */
35
54
  function createFastifyPlugin(tracer) {
36
55
  return async (fastify) => {
@@ -49,6 +68,7 @@ function createFastifyPlugin(tracer) {
49
68
  });
50
69
  request[SPAN_KEY] = span;
51
70
  request[START_KEY] = Date.now();
71
+ request[TRACE_CTX_KEY] = { traceId, spanId };
52
72
  }
53
73
  catch {
54
74
  /* never crash */
@@ -18,8 +18,8 @@ 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 } from './http/express';
22
- export { createFastifyPlugin } from './http/fastify';
21
+ export { createExpressMiddleware, createExpressErrorHandler, getRequestTraceContext } from './http/express';
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';
25
25
  export { FOAM_OTEL_ENDPOINT } from './constants';
@@ -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.createFastifyPlugin = 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.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");
@@ -32,8 +32,10 @@ Object.defineProperty(exports, "wrapSqsConsumer", { enumerable: true, get: funct
32
32
  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
+ Object.defineProperty(exports, "getRequestTraceContext", { enumerable: true, get: function () { return express_1.getRequestTraceContext; } });
35
36
  var fastify_1 = require("./http/fastify");
36
37
  Object.defineProperty(exports, "createFastifyPlugin", { enumerable: true, get: function () { return fastify_1.createFastifyPlugin; } });
38
+ Object.defineProperty(exports, "getFastifyRequestTraceContext", { enumerable: true, get: function () { return fastify_1.getRequestTraceContext; } });
37
39
  var winston_format_1 = require("./logs/winston-format");
38
40
  Object.defineProperty(exports, "createWinstonFormat", { enumerable: true, get: function () { return winston_format_1.createWinstonFormat; } });
39
41
  var pino_mixin_1 = require("./logs/pino-mixin");
@@ -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,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 {
@@ -49,11 +49,18 @@ function generateSpanId() {
49
49
  return (0, node_crypto_1.randomBytes)(8).toString('hex');
50
50
  }
51
51
  function resolveTraceIds() {
52
- // 1. Try NR
53
- const nr = (0, nr_1.getNr)();
54
- const meta = nr.getTraceMetadata?.() ?? { traceId: '', spanId: '' };
55
- if (meta.traceId && meta.spanId) {
56
- return { traceId: meta.traceId, spanId: meta.spanId };
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)
55
+ try {
56
+ const nr = (0, nr_1.getNr)();
57
+ const meta = nr.getTraceMetadata?.() ?? { traceId: '', spanId: '' };
58
+ if (meta.traceId && meta.spanId) {
59
+ return { traceId: meta.traceId, spanId: meta.spanId };
60
+ }
61
+ }
62
+ catch {
63
+ // NR transaction is null or getTraceMetadata threw — fall through
57
64
  }
58
65
  // 2. Fall back to OTel's own active span
59
66
  const activeSpan = api_1.trace.getActiveSpan();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foam-ai/node-cliengo",
3
- "version": "0.1.0-alpha.5",
3
+ "version": "0.1.0-alpha.7",
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",