@foam-ai/node-cliengo 0.1.0-alpha.2 → 0.1.0-alpha.20

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.
@@ -2,38 +2,42 @@
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
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;
@@ -3,50 +3,79 @@
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
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
  }
@@ -27,12 +27,12 @@ import type { MeterProvider } from '@opentelemetry/sdk-metrics';
27
27
  import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base';
28
28
  export interface InitOptions {
29
29
  enabled?: boolean;
30
+ token?: string;
30
31
  newrelic?: NewRelicAgent;
31
32
  winston?: WinstonLogger;
32
33
  enableTraces?: boolean;
33
34
  enableLogs?: boolean;
34
35
  enableMetrics?: boolean;
35
- endpoint?: string;
36
36
  forceExport?: boolean;
37
37
  autoShutdown?: boolean;
38
38
  }
@@ -49,7 +49,6 @@ export interface FoamMetrics {
49
49
  export interface FoamInstance {
50
50
  tracer: Tracer;
51
51
  meter: Meter;
52
- logger: LoggerProvider;
53
52
  traceProvider: BasicTracerProvider;
54
53
  meterProvider: MeterProvider;
55
54
  loggerProvider: LoggerProvider;
@@ -61,14 +60,48 @@ export interface FoamInstance {
61
60
  createWinstonFormat(): WinstonFormat;
62
61
  createWinstonTransport(): WinstonTransport;
63
62
  createPinoMixin(): () => Record<string, string>;
63
+ createPinoHttpOptions(): {
64
+ customProps: (req: any) => Record<string, string>;
65
+ };
64
66
  createPinoDestination(): NodeJS.WritableStream;
65
- buildTraceparent(): string | undefined;
67
+ /**
68
+ * Creates a Pino logger with trace-context mixin and OTLP destination pre-wired.
69
+ * Pass `streams` to replace the default stdout output (e.g., for pino-pretty):
70
+ *
71
+ * foam.createPinoLogger({ streams: [{ stream: pinoPretty() }] })
72
+ *
73
+ * The OTLP destination is always appended automatically.
74
+ */
75
+ createPinoLogger(options?: Record<string, any>): any;
76
+ createPinoHttpConfig(opts?: {
77
+ logger?: any;
78
+ }): Record<string, any>;
79
+ /**
80
+ * Returns a Pino config object for Fastify's `logger` constructor option.
81
+ * Fastify constructs its own Pino instance from this, so request.log gets
82
+ * child loggers with request context automatically.
83
+ *
84
+ * Fastify({ logger: foam.createFastifyLoggerConfig({ level: 'info' }) })
85
+ *
86
+ * Pass `streams` to replace stdout (e.g., pino-pretty in dev):
87
+ *
88
+ * foam.createFastifyLoggerConfig({
89
+ * level: 'debug',
90
+ * streams: [{ stream: pinoPretty() }],
91
+ * })
92
+ */
93
+ createFastifyLoggerConfig(opts?: Record<string, any>): Record<string, any>;
94
+ buildTraceparent(): string;
66
95
  injectSnsAttributes(attrs: Record<string, SnsMessageAttributeValue>): Record<string, SnsMessageAttributeValue>;
67
96
  injectJobData<T extends Record<string, unknown>>(data: T): T & {
68
- traceparent?: string;
97
+ traceparent: string;
69
98
  };
99
+ /** @deprecated Pass (spanName, msg, fn) — the tracer arg is no longer needed. */
70
100
  wrapSqsConsumer(tracer: Tracer, spanName: string, msg: SqsMessage, fn: () => Promise<void>): Promise<void>;
101
+ wrapSqsConsumer(spanName: string, msg: SqsMessage, fn: () => Promise<void>): Promise<void>;
102
+ /** @deprecated Pass (spanName, jobData, fn) — the tracer arg is no longer needed. */
71
103
  wrapJobConsumer(tracer: Tracer, spanName: string, jobData: Record<string, unknown>, fn: () => Promise<void>): Promise<void>;
104
+ wrapJobConsumer(spanName: string, jobData: Record<string, unknown>, fn: () => Promise<void>): Promise<void>;
72
105
  extractParentContext(traceparent: string): Context;
73
106
  shutdown(): Promise<void>;
74
107
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foam-ai/node-cliengo",
3
- "version": "0.1.0-alpha.2",
3
+ "version": "0.1.0-alpha.20",
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",
@@ -20,8 +20,8 @@
20
20
  "peerDependencies": {
21
21
  "@opentelemetry/api": "^1.9.0",
22
22
  "newrelic": ">=12.0.0",
23
- "winston": "^3.0.0",
24
- "pino": "^8.0.0 || ^9.0.0"
23
+ "pino": "^8.0.0 || ^9.0.0",
24
+ "winston": "^3.0.0"
25
25
  },
26
26
  "peerDependenciesMeta": {
27
27
  "newrelic": {
@@ -35,18 +35,20 @@
35
35
  }
36
36
  },
37
37
  "dependencies": {
38
- "@opentelemetry/sdk-trace-base": "^2.0.0",
39
- "@opentelemetry/sdk-logs": "^0.203.0",
40
- "@opentelemetry/sdk-metrics": "^2.0.1",
41
- "@opentelemetry/exporter-trace-otlp-http": "^0.203.0",
38
+ "@opentelemetry/api-logs": "^0.203.0",
42
39
  "@opentelemetry/exporter-logs-otlp-http": "^0.203.0",
43
40
  "@opentelemetry/exporter-metrics-otlp-http": "^0.201.1",
41
+ "@opentelemetry/exporter-trace-otlp-http": "^0.203.0",
44
42
  "@opentelemetry/resources": "^2.0.1",
45
- "@opentelemetry/api-logs": "^0.203.0"
43
+ "@opentelemetry/sdk-logs": "^0.203.0",
44
+ "@opentelemetry/sdk-metrics": "^2.0.1",
45
+ "@opentelemetry/sdk-trace-base": "^2.0.0"
46
46
  },
47
47
  "devDependencies": {
48
+ "@opentelemetry/api": "^1.9.1",
49
+ "@types/node": "^20.11.0",
48
50
  "typescript": "^6.0.2",
49
- "@types/node": "^20.11.0"
51
+ "vitest": "^4.1.8"
50
52
  },
51
53
  "engines": {
52
54
  "node": ">=18"