@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.
@@ -25,6 +25,7 @@
25
25
  * features (trace propagation, log enrichment, NR bridging) still work.
26
26
  * - Token undefined + forceExport: throws (fail-fast — you asked to force
27
27
  * export but provided no credentials).
28
+ * - Endpoint is always the production Foam endpoint — not configurable.
28
29
  * - Auto-shutdown (SIGTERM/SIGINT): flushes all providers with 5s timeout,
29
30
  * then calls process.exit(0). Without the explicit exit, registering
30
31
  * process.on('SIGTERM') replaces Node's default terminate behavior — services
@@ -50,6 +51,8 @@
50
51
  */
51
52
  Object.defineProperty(exports, "__esModule", { value: true });
52
53
  exports.init = init;
54
+ exports.getFoam = getFoam;
55
+ exports.createFastifyLoggerConfig = createFastifyLoggerConfig;
53
56
  const api_1 = require("@opentelemetry/api");
54
57
  const exporter_logs_otlp_http_1 = require("@opentelemetry/exporter-logs-otlp-http");
55
58
  const exporter_metrics_otlp_http_1 = require("@opentelemetry/exporter-metrics-otlp-http");
@@ -73,6 +76,7 @@ const sns_1 = require("./sns");
73
76
  const sqs_1 = require("./sqs");
74
77
  const trace_bridge_1 = require("./trace-bridge");
75
78
  const instances = new Map();
79
+ let _processHandlersRegistered = false;
76
80
  function createInertInstance(serviceName) {
77
81
  const resource = (0, resources_1.resourceFromAttributes)({ 'service.name': serviceName });
78
82
  const traceProvider = new sdk_trace_base_1.BasicTracerProvider({ resource });
@@ -84,7 +88,6 @@ function createInertInstance(serviceName) {
84
88
  return {
85
89
  tracer,
86
90
  meter,
87
- logger: loggerProvider,
88
91
  traceProvider,
89
92
  meterProvider,
90
93
  loggerProvider,
@@ -98,14 +101,24 @@ function createInertInstance(serviceName) {
98
101
  }),
99
102
  createWinstonTransport: () => ({}),
100
103
  createPinoMixin: () => () => ({}),
104
+ createPinoHttpOptions: () => ({ customProps: () => ({}) }),
101
105
  createPinoDestination: () => new node_stream_1.Writable({
102
106
  write: (_c, _e, cb) => cb(),
103
107
  }),
104
- buildTraceparent: () => undefined,
105
- injectSnsAttributes: (attrs) => attrs,
106
- injectJobData: (data) => data,
107
- wrapSqsConsumer: async (_t, _s, _m, fn) => fn(),
108
- wrapJobConsumer: async (_t, _s, _j, fn) => fn(),
108
+ createPinoLogger: () => console,
109
+ createPinoHttpConfig: (opts) => (opts?.logger ? { logger: opts.logger } : {}),
110
+ createFastifyLoggerConfig: (opts) => ({ level: opts?.level ?? 'info' }),
111
+ buildTraceparent: trace_bridge_1.buildTraceparent,
112
+ injectSnsAttributes: sns_1.injectSnsAttributes,
113
+ injectJobData: job_1.injectJobData,
114
+ wrapSqsConsumer: async (...args) => {
115
+ const fn = typeof args[0] === 'string' ? args[2] : args[3];
116
+ return fn();
117
+ },
118
+ wrapJobConsumer: async (...args) => {
119
+ const fn = typeof args[0] === 'string' ? args[2] : args[3];
120
+ return fn();
121
+ },
109
122
  extractParentContext: () => {
110
123
  const { ROOT_CONTEXT } = require('@opentelemetry/api');
111
124
  return ROOT_CONTEXT;
@@ -114,7 +127,7 @@ function createInertInstance(serviceName) {
114
127
  };
115
128
  }
116
129
  /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call */
117
- function init(serviceName, token, options = {}) {
130
+ function init(serviceName, options = {}) {
118
131
  const existing = instances.get(serviceName);
119
132
  if (existing) {
120
133
  // eslint-disable-next-line no-console
@@ -131,20 +144,21 @@ function init(serviceName, token, options = {}) {
131
144
  }
132
145
  // Resolve NR
133
146
  (0, nr_1.resolveNewRelic)(options.newrelic);
147
+ const token = options.token;
134
148
  // Production gate
135
149
  const isProduction = process.env.NODE_ENV === 'production';
136
150
  const shouldExport = isProduction || (options.forceExport === true);
137
151
  // Token validation
138
152
  if (shouldExport && !token) {
139
153
  if (options.forceExport) {
140
- throw new Error('[foam] forceExport is true but no FOAM_OTEL_TOKEN provided — cannot export telemetry');
154
+ throw new Error('[foam] forceExport is true but no token provided — cannot export telemetry');
141
155
  }
142
156
  // eslint-disable-next-line no-console
143
- console.warn('[foam] no FOAM_OTEL_TOKEN — OTLP export disabled');
157
+ console.warn('[foam] no token provided — OTLP export disabled');
144
158
  }
145
159
  const hasToken = Boolean(token);
146
160
  const canExport = shouldExport && hasToken;
147
- const endpoint = options.endpoint ?? constants_1.FOAM_OTEL_ENDPOINT;
161
+ const endpoint = constants_1.FOAM_OTEL_ENDPOINT;
148
162
  const resource = (0, resources_1.resourceFromAttributes)({ 'service.name': serviceName });
149
163
  const authHeaders = token
150
164
  ? { Authorization: `Bearer ${token}` }
@@ -218,46 +232,54 @@ function init(serviceName, token, options = {}) {
218
232
  process.on('SIGTERM', () => void handler());
219
233
  process.on('SIGINT', () => void handler());
220
234
  }
221
- // Process-level exception capture — recorded as span exceptions so they
222
- // land in otel_traces (not otel_logs). Each creates a short-lived span,
223
- // records the exception, ends it, then force-flushes the trace provider.
224
- process.on('uncaughtException', (err) => {
225
- try {
226
- const span = tracer.startSpan('uncaughtException', {
227
- attributes: { 'process.event': 'uncaughtException' },
228
- });
229
- span.recordException({
230
- name: err.constructor.name,
231
- message: err.message,
232
- stack: err.stack,
233
- });
234
- span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: err.message });
235
- span.end();
236
- void traceProvider.forceFlush().then(() => shutdown());
237
- }
238
- catch {
239
- void shutdown();
240
- }
241
- });
242
- process.on('unhandledRejection', (reason) => {
243
- try {
244
- const err = reason instanceof Error ? reason : new Error(String(reason));
245
- const span = tracer.startSpan('unhandledRejection', {
246
- attributes: { 'process.event': 'unhandledRejection' },
247
- });
248
- span.recordException({
249
- name: err.constructor.name,
250
- message: err.message,
251
- stack: err.stack,
252
- });
253
- span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: err.message });
254
- span.end();
255
- void traceProvider.forceFlush();
256
- }
257
- catch {
258
- /* best effort */
259
- }
260
- });
235
+ else {
236
+ process.once('beforeExit', () => {
237
+ if (!_isShutdown) {
238
+ // eslint-disable-next-line no-console
239
+ console.warn('[foam] autoShutdown is false but shutdown() was never called — OTel data may be lost');
240
+ }
241
+ });
242
+ }
243
+ if (!_processHandlersRegistered) {
244
+ _processHandlersRegistered = true;
245
+ process.on('uncaughtException', (err) => {
246
+ try {
247
+ const span = tracer.startSpan('uncaughtException', {
248
+ attributes: { 'process.event': 'uncaughtException' },
249
+ });
250
+ span.recordException({
251
+ name: err.constructor.name,
252
+ message: err.message,
253
+ stack: err.stack,
254
+ });
255
+ span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: err.message });
256
+ span.end();
257
+ void traceProvider.forceFlush().then(() => shutdown());
258
+ }
259
+ catch {
260
+ void shutdown();
261
+ }
262
+ });
263
+ process.on('unhandledRejection', (reason) => {
264
+ try {
265
+ const err = reason instanceof Error ? reason : new Error(String(reason));
266
+ const span = tracer.startSpan('unhandledRejection', {
267
+ attributes: { 'process.event': 'unhandledRejection' },
268
+ });
269
+ span.recordException({
270
+ name: err.constructor.name,
271
+ message: err.message,
272
+ stack: err.stack,
273
+ });
274
+ span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: err.message });
275
+ span.end();
276
+ void traceProvider.forceFlush();
277
+ }
278
+ catch {
279
+ /* best effort */
280
+ }
281
+ });
282
+ }
261
283
  // Winston auto-wiring
262
284
  if (options.winston) {
263
285
  const winstonLogger = options.winston;
@@ -322,7 +344,6 @@ function init(serviceName, token, options = {}) {
322
344
  const foam = {
323
345
  tracer,
324
346
  meter,
325
- logger: loggerProvider,
326
347
  traceProvider,
327
348
  meterProvider,
328
349
  loggerProvider,
@@ -334,12 +355,51 @@ function init(serviceName, token, options = {}) {
334
355
  createWinstonFormat: () => (0, winston_format_1.createWinstonFormat)(),
335
356
  createWinstonTransport: () => (0, winston_transport_1.createWinstonTransport)(loggerProvider, serviceName),
336
357
  createPinoMixin: () => (0, pino_mixin_1.createPinoMixin)(),
358
+ createPinoHttpOptions: () => (0, express_1.createPinoHttpOptions)(),
337
359
  createPinoDestination: () => (0, pino_destination_1.createPinoDestination)(loggerProvider, serviceName),
360
+ createPinoLogger: (opts = {}) => {
361
+ const pino = require('pino');
362
+ const mixin = (0, pino_mixin_1.createPinoMixin)();
363
+ const dest = (0, pino_destination_1.createPinoDestination)(loggerProvider, serviceName);
364
+ const { streams: userStreams, ...pinoOpts } = opts;
365
+ const outputStreams = userStreams ?? [
366
+ { stream: process.stdout },
367
+ ];
368
+ return pino({ ...pinoOpts, mixin }, pino.multistream([...outputStreams, { stream: dest }]));
369
+ },
370
+ createPinoHttpConfig: (opts) => {
371
+ const pinoHttpOpts = (0, express_1.createPinoHttpOptions)();
372
+ return { ...pinoHttpOpts, ...(opts?.logger && { logger: opts.logger }) };
373
+ },
374
+ createFastifyLoggerConfig: (opts = {}) => {
375
+ const pino = require('pino');
376
+ const mixin = (0, pino_mixin_1.createPinoMixin)();
377
+ const dest = (0, pino_destination_1.createPinoDestination)(loggerProvider, serviceName);
378
+ const { streams: userStreams, ...pinoOpts } = opts;
379
+ const outputStreams = userStreams ?? [
380
+ { stream: process.stdout },
381
+ ];
382
+ return {
383
+ ...pinoOpts,
384
+ mixin,
385
+ stream: pino.multistream([...outputStreams, { stream: dest }]),
386
+ };
387
+ },
338
388
  buildTraceparent: trace_bridge_1.buildTraceparent,
339
389
  injectSnsAttributes: sns_1.injectSnsAttributes,
340
390
  injectJobData: job_1.injectJobData,
341
- wrapSqsConsumer: (t, s, m, fn) => (0, sqs_1.wrapSqsConsumer)(t, s, m, fn, metrics),
342
- wrapJobConsumer: (t, s, j, fn) => (0, job_1.wrapJobConsumer)(t, s, j, fn, metrics),
391
+ wrapSqsConsumer: (...args) => {
392
+ if (typeof args[0] === 'string') {
393
+ return (0, sqs_1.wrapSqsConsumer)(tracer, args[0], args[1], args[2], metrics);
394
+ }
395
+ return (0, sqs_1.wrapSqsConsumer)(tracer, args[1], args[2], args[3], metrics);
396
+ },
397
+ wrapJobConsumer: (...args) => {
398
+ if (typeof args[0] === 'string') {
399
+ return (0, job_1.wrapJobConsumer)(tracer, args[0], args[1], args[2], metrics);
400
+ }
401
+ return (0, job_1.wrapJobConsumer)(tracer, args[1], args[2], args[3], metrics);
402
+ },
343
403
  extractParentContext: trace_bridge_1.extractParentContext,
344
404
  shutdown,
345
405
  };
@@ -347,3 +407,40 @@ function init(serviceName, token, options = {}) {
347
407
  return foam;
348
408
  }
349
409
  /* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call */
410
+ /**
411
+ * Returns the Foam instance for the given service name (or the only instance
412
+ * if there's just one). Call this from any module after init() has run —
413
+ * no circular dependency, no holder pattern needed.
414
+ */
415
+ function getFoam(serviceName) {
416
+ if (serviceName) {
417
+ return instances.get(serviceName);
418
+ }
419
+ if (instances.size === 1) {
420
+ return instances.values().next().value;
421
+ }
422
+ return undefined;
423
+ }
424
+ /**
425
+ * Standalone Fastify logger config that handles the getFoam() lookup internally.
426
+ * If Foam is initialized, returns { mixin, stream (multistream with OTLP), ...opts }.
427
+ * If Foam is not initialized (disabled, not yet called), returns opts as-is —
428
+ * Fastify gets a plain Pino logger with no OTLP destination.
429
+ *
430
+ * // No getFoam() check or ternary needed:
431
+ * Fastify({ logger: createFastifyLoggerConfig({ level: 'info' }) })
432
+ *
433
+ * // Dev with pino-pretty:
434
+ * Fastify({ logger: createFastifyLoggerConfig({
435
+ * level: 'debug',
436
+ * streams: [{ stream: pinoPretty() }],
437
+ * })})
438
+ */
439
+ function createFastifyLoggerConfig(opts = {}) {
440
+ const foam = getFoam();
441
+ if (foam) {
442
+ return foam.createFastifyLoggerConfig(opts);
443
+ }
444
+ const { streams: _streams, ...pinoOpts } = opts;
445
+ return { ...pinoOpts };
446
+ }
@@ -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,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
  }
@@ -29,6 +29,7 @@
29
29
  */
30
30
  Object.defineProperty(exports, "__esModule", { value: true });
31
31
  exports.createWinstonTransport = createWinstonTransport;
32
+ const api_1 = require("@opentelemetry/api");
32
33
  const api_logs_1 = require("@opentelemetry/api-logs");
33
34
  const node_stream_1 = require("node:stream");
34
35
  const trace_bridge_1 = require("../trace-bridge");
@@ -69,11 +70,21 @@ function createWinstonTransport(loggerProvider, serviceName) {
69
70
  }
70
71
  }
71
72
  }
73
+ let logContext;
74
+ if (ctx.traceId && ctx.spanId) {
75
+ logContext = api_1.trace.setSpanContext(api_1.ROOT_CONTEXT, {
76
+ traceId: ctx.traceId,
77
+ spanId: ctx.spanId,
78
+ traceFlags: 1,
79
+ isRemote: true,
80
+ });
81
+ }
72
82
  otelLogger.emit({
73
83
  body: message,
74
84
  severityNumber: severity,
75
85
  severityText: level.toUpperCase(),
76
86
  attributes,
87
+ ...(logContext && { context: logContext }),
77
88
  });
78
89
  }
79
90
  catch {
@@ -33,3 +33,20 @@ import type { NewRelicAgent } from './types';
33
33
  export declare function resolveNewRelic(explicit?: NewRelicAgent): NewRelicAgent;
34
34
  export declare function getNr(): NewRelicAgent;
35
35
  export declare function isNrLoaded(): boolean;
36
+ /**
37
+ * Loads the New Relic agent via CJS require — safe to call from ESM services.
38
+ * Eliminates the createRequire boilerplate:
39
+ *
40
+ * // Before (ESM service):
41
+ * import { createRequire } from 'node:module';
42
+ * const require = createRequire(import.meta.url);
43
+ * let nr; try { nr = require('newrelic'); } catch {}
44
+ * init('kori', { newrelic: nr });
45
+ *
46
+ * // After:
47
+ * import { init, loadNewRelic } from '@foam-ai/node-cliengo';
48
+ * init('kori', { newrelic: loadNewRelic() });
49
+ *
50
+ * Returns undefined if the newrelic package is not installed.
51
+ */
52
+ export declare function loadNewRelic(): NewRelicAgent | undefined;
@@ -34,6 +34,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
34
34
  exports.resolveNewRelic = resolveNewRelic;
35
35
  exports.getNr = getNr;
36
36
  exports.isNrLoaded = isNrLoaded;
37
+ exports.loadNewRelic = loadNewRelic;
37
38
  const NOOP_NR = {
38
39
  getTransaction: () => null,
39
40
  startBackgroundTransaction: (_name, _group, handler) => typeof handler === 'function' ? handler() : undefined,
@@ -69,3 +70,31 @@ function getNr() {
69
70
  function isNrLoaded() {
70
71
  return cachedNr !== null && cachedNr !== NOOP_NR;
71
72
  }
73
+ /**
74
+ * Loads the New Relic agent via CJS require — safe to call from ESM services.
75
+ * Eliminates the createRequire boilerplate:
76
+ *
77
+ * // Before (ESM service):
78
+ * import { createRequire } from 'node:module';
79
+ * const require = createRequire(import.meta.url);
80
+ * let nr; try { nr = require('newrelic'); } catch {}
81
+ * init('kori', { newrelic: nr });
82
+ *
83
+ * // After:
84
+ * import { init, loadNewRelic } from '@foam-ai/node-cliengo';
85
+ * init('kori', { newrelic: loadNewRelic() });
86
+ *
87
+ * Returns undefined if the newrelic package is not installed.
88
+ */
89
+ function loadNewRelic() {
90
+ try {
91
+ /* eslint-disable @typescript-eslint/no-require-imports */
92
+ const nr = require('newrelic');
93
+ /* eslint-enable @typescript-eslint/no-require-imports */
94
+ cachedNr = nr;
95
+ return nr;
96
+ }
97
+ catch {
98
+ return undefined;
99
+ }
100
+ }
@@ -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
  }