@foam-ai/node-cliengo 0.1.0-alpha.13 → 0.1.0-alpha.15

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.
@@ -8,8 +8,7 @@
8
8
  * through individual files.
9
9
  *
10
10
  * Edge cases covered:
11
- * - FOAM_OTEL_ENDPOINT is hardcoded (not read from env) to prevent
12
- * misconfiguration; services override via `options.endpoint` only.
11
+ * - FOAM_OTEL_ENDPOINT is hardcoded not configurable via env or options.
13
12
  * - REDACTED_KEYS uses lowercase comparison so "Authorization", "PASSWORD",
14
13
  * "Token" all match. Includes both "creditcard" and "credit_card" variants.
15
14
  * - HEALTH_ROUTES covers the four most common patterns across all 5 Cliengo
@@ -9,8 +9,7 @@
9
9
  * through individual files.
10
10
  *
11
11
  * Edge cases covered:
12
- * - FOAM_OTEL_ENDPOINT is hardcoded (not read from env) to prevent
13
- * misconfiguration; services override via `options.endpoint` only.
12
+ * - FOAM_OTEL_ENDPOINT is hardcoded not configurable via env or options.
14
13
  * - REDACTED_KEYS uses lowercase comparison so "Authorization", "PASSWORD",
15
14
  * "Token" all match. Includes both "creditcard" and "credit_card" variants.
16
15
  * - HEALTH_ROUTES covers the four most common patterns across all 5 Cliengo
@@ -31,6 +30,10 @@ exports.REDACTED_KEYS = new Set([
31
30
  'secret',
32
31
  'creditcard',
33
32
  'credit_card',
33
+ 'api_key',
34
+ 'apikey',
35
+ 'access_token',
36
+ 'accesstoken',
34
37
  ]);
35
38
  exports.HEALTH_ROUTES = new Set(['/health', '/healthz', '/ready', '/status']);
36
39
  exports.REQUEST_CONTEXT_FIELDS = [
@@ -44,6 +44,7 @@ const trace_bridge_1 = require("../trace-bridge");
44
44
  const request_context_1 = require("./request-context");
45
45
  const SPAN_KEY = Symbol.for('foam.shadow.span');
46
46
  const TRACE_CTX_KEY = Symbol.for('foam.trace.context');
47
+ const ERROR_RECORDED_KEY = Symbol.for('foam.shadow.errorRecorded');
47
48
  /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */
48
49
  function createExpressMiddleware(tracer) {
49
50
  let errorHandlerRegistered = false;
@@ -94,7 +95,10 @@ function createExpressMiddleware(tracer) {
94
95
  span.setAttribute('http.route', route);
95
96
  span.setAttribute('http.response_content_length', String(contentLength));
96
97
  span.setAttribute('http.duration_ms', Date.now() - startTime);
97
- if (statusCode >= 500) {
98
+ if (req[ERROR_RECORDED_KEY]) {
99
+ // Error handler already set status — don't overwrite
100
+ }
101
+ else if (statusCode >= 500) {
98
102
  span.setStatus({ code: api_1.SpanStatusCode.ERROR });
99
103
  if (!errorHandlerRegistered && !warnedMissingErrorHandler) {
100
104
  warnedMissingErrorHandler = true;
@@ -171,6 +175,7 @@ function createExpressErrorHandler(middlewareRef) {
171
175
  stack: error.stack,
172
176
  });
173
177
  span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: error.message });
178
+ req[ERROR_RECORDED_KEY] = true;
174
179
  }
175
180
  }
176
181
  catch {
@@ -5,7 +5,8 @@
5
5
  * Why this file exists:
6
6
  * Same rationale as the Express middleware (see express.ts). Fastify's hook
7
7
  * system is different — we use three hooks instead of middleware + error handler:
8
- * - onRequest: creates the shadow span, reads NR's traceId
8
+ * - onRequest: creates the shadow span, reads NR's traceId, sets the span as
9
+ * the active OTel context so route handlers inherit it via AsyncLocalStorage
9
10
  * - onError: records exception details (fires BEFORE errorHandler, so we see
10
11
  * every error including ones mapped to non-500 responses like AppError.badRequest)
11
12
  * - onResponse: records status/route/duration, captures request context, ends span
@@ -15,13 +16,13 @@
15
16
  * Edge cases covered:
16
17
  * - Fastify's onError fires for ALL errors including 4xx AppError variants.
17
18
  * We record the exception on the span regardless — Foam sees the full error
18
- * even for 400s. The span status is only set to ERROR by onError (not
19
- * overwritten to OK by onResponse for <500 codes, since onError already set it).
19
+ * even for 400s. Once onError sets ERROR status, onResponse never overwrites it.
20
20
  * - request.routeOptions?.url may not exist in older Fastify versions — falls
21
21
  * back to request.url.
22
22
  * - Health routes skipped for request.context capture (same as Express).
23
23
  * - All hooks wrapped in try/catch — never crashes the request pipeline.
24
- * - done() always called even if our code throws — Fastify hook contract.
24
+ * - onError/onResponse use async hooks (Fastify 5 forward-compat).
25
+ * onRequest uses callback style intentionally for context.with() propagation.
25
26
  */
26
27
  import { type Tracer } from '@opentelemetry/api';
27
28
  import type { FastifyPluginAsync } from '../types';
@@ -6,7 +6,8 @@
6
6
  * Why this file exists:
7
7
  * Same rationale as the Express middleware (see express.ts). Fastify's hook
8
8
  * system is different — we use three hooks instead of middleware + error handler:
9
- * - onRequest: creates the shadow span, reads NR's traceId
9
+ * - onRequest: creates the shadow span, reads NR's traceId, sets the span as
10
+ * the active OTel context so route handlers inherit it via AsyncLocalStorage
10
11
  * - onError: records exception details (fires BEFORE errorHandler, so we see
11
12
  * every error including ones mapped to non-500 responses like AppError.badRequest)
12
13
  * - onResponse: records status/route/duration, captures request context, ends span
@@ -16,13 +17,13 @@
16
17
  * Edge cases covered:
17
18
  * - Fastify's onError fires for ALL errors including 4xx AppError variants.
18
19
  * We record the exception on the span regardless — Foam sees the full error
19
- * even for 400s. The span status is only set to ERROR by onError (not
20
- * overwritten to OK by onResponse for <500 codes, since onError already set it).
20
+ * even for 400s. Once onError sets ERROR status, onResponse never overwrites it.
21
21
  * - request.routeOptions?.url may not exist in older Fastify versions — falls
22
22
  * back to request.url.
23
23
  * - Health routes skipped for request.context capture (same as Express).
24
24
  * - All hooks wrapped in try/catch — never crashes the request pipeline.
25
- * - done() always called even if our code throws — Fastify hook contract.
25
+ * - onError/onResponse use async hooks (Fastify 5 forward-compat).
26
+ * onRequest uses callback style intentionally for context.with() propagation.
26
27
  */
27
28
  Object.defineProperty(exports, "__esModule", { value: true });
28
29
  exports.getRequestTraceContext = getRequestTraceContext;
@@ -33,6 +34,7 @@ const request_context_1 = require("./request-context");
33
34
  const SPAN_KEY = Symbol.for('foam.shadow.span');
34
35
  const START_KEY = Symbol.for('foam.shadow.start');
35
36
  const TRACE_CTX_KEY = Symbol.for('foam.trace.context');
37
+ const ERROR_RECORDED_KEY = Symbol.for('foam.shadow.errorRecorded');
36
38
  /**
37
39
  * Reads the trace context that was captured on the request at hook time.
38
40
  * Use this in Fastify's logger mixin or serializers to get trace IDs even
@@ -54,6 +56,10 @@ function getRequestTraceContext(request) {
54
56
  /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */
55
57
  function createFastifyPlugin(tracer) {
56
58
  return async (fastify) => {
59
+ // onRequest uses callback style intentionally — otelContext.with(ctx, done)
60
+ // propagates the shadow span as the active OTel context via AsyncLocalStorage
61
+ // to all downstream hooks and the route handler. Async hooks would lose the
62
+ // context after the awaited promise resolves.
57
63
  fastify.addHook('onRequest', (request, _reply, done) => {
58
64
  try {
59
65
  const method = request.method ?? 'UNKNOWN';
@@ -82,25 +88,16 @@ function createFastifyPlugin(tracer) {
82
88
  spanId: sc.spanId,
83
89
  traceparent: `00-${sc.traceId}-${sc.spanId}-01`,
84
90
  };
91
+ const activeCtx = api_1.trace.setSpan(parentCtx ?? api_1.ROOT_CONTEXT, span);
92
+ api_1.context.with(activeCtx, () => done());
93
+ return;
85
94
  }
86
95
  catch {
87
96
  /* never crash */
88
97
  }
89
98
  done();
90
99
  });
91
- // Set the shadow span as the active context for route handlers so
92
- // buildTraceparent() returns the same traceId — even without NR.
93
- fastify.addHook('preHandler', (request, _reply, done) => {
94
- const span = request[SPAN_KEY];
95
- if (span) {
96
- const activeCtx = api_1.trace.setSpan(api_1.ROOT_CONTEXT, span);
97
- api_1.context.with(activeCtx, () => done());
98
- }
99
- else {
100
- done();
101
- }
102
- });
103
- fastify.addHook('onError', (request, _reply, error, done) => {
100
+ fastify.addHook('onError', async (request, _reply, error) => {
104
101
  try {
105
102
  const span = request[SPAN_KEY];
106
103
  if (span) {
@@ -110,14 +107,14 @@ function createFastifyPlugin(tracer) {
110
107
  stack: error.stack,
111
108
  });
112
109
  span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: error.message });
110
+ request[ERROR_RECORDED_KEY] = true;
113
111
  }
114
112
  }
115
113
  catch {
116
114
  /* never crash */
117
115
  }
118
- done();
119
116
  });
120
- fastify.addHook('onResponse', (request, reply, done) => {
117
+ fastify.addHook('onResponse', async (request, reply) => {
121
118
  try {
122
119
  const span = request[SPAN_KEY];
123
120
  if (span) {
@@ -127,8 +124,13 @@ function createFastifyPlugin(tracer) {
127
124
  span.setAttribute('http.status_code', statusCode);
128
125
  span.setAttribute('http.route', route);
129
126
  span.setAttribute('http.duration_ms', Date.now() - start);
130
- if (statusCode < 500) {
131
- span.setStatus({ code: api_1.SpanStatusCode.OK });
127
+ if (!request[ERROR_RECORDED_KEY]) {
128
+ if (statusCode >= 500) {
129
+ span.setStatus({ code: api_1.SpanStatusCode.ERROR });
130
+ }
131
+ else {
132
+ span.setStatus({ code: api_1.SpanStatusCode.OK });
133
+ }
132
134
  }
133
135
  if (!(0, request_context_1.isHealthRoute)(route) && !(0, request_context_1.isHealthRoute)(request.url ?? '/')) {
134
136
  (0, request_context_1.captureRequestContext)(span, request);
@@ -139,7 +141,6 @@ function createFastifyPlugin(tracer) {
139
141
  catch {
140
142
  /* never crash */
141
143
  }
142
- done();
143
144
  });
144
145
  };
145
146
  }
@@ -11,16 +11,19 @@
11
11
  *
12
12
  * The individual function exports exist for tree-shaking and for cases where
13
13
  * a service needs to call e.g. injectJobData() from a file that doesn't have
14
- * access to the foam instance.
14
+ * access to the foam instance. createFastifyPlugin is NOT exported standalone
15
+ * because the raw function takes a tracer argument — use foam.createFastifyPlugin()
16
+ * instead.
15
17
  */
16
- export { init } from './init';
18
+ export { init, getFoam } from './init';
17
19
  export { buildTraceparent, extractParentContext, getTraceContext } from './trace-bridge';
18
20
  export { injectSnsAttributes } from './sns';
19
21
  export { injectJobData, wrapJobConsumer } from './job';
20
22
  export { wrapSqsConsumer } from './sqs';
21
23
  export { createExpressMiddleware, createExpressErrorHandler, getRequestTraceContext, createPinoHttpOptions } from './http/express';
22
- export { createFastifyPlugin, getRequestTraceContext as getFastifyRequestTraceContext } from './http/fastify';
24
+ export { getRequestTraceContext as getFastifyRequestTraceContext } from './http/fastify';
23
25
  export { createWinstonFormat } from './logs/winston-format';
24
26
  export { createPinoMixin } from './logs/pino-mixin';
27
+ export { loadNewRelic } from './nr';
25
28
  export { FOAM_OTEL_ENDPOINT } from './constants';
26
29
  export type { FoamMetrics, Counter, ExpressErrorHandler, ExpressRequestHandler, FastifyPluginAsync, Histogram, InitOptions, NewRelicAgent, ObservableGauge, FoamInstance, SnsMessageAttributeValue, SqsMessage, TraceContext, WinstonFormat, WinstonLogger, WinstonTransport, } from './types';
@@ -12,12 +12,15 @@
12
12
  *
13
13
  * The individual function exports exist for tree-shaking and for cases where
14
14
  * a service needs to call e.g. injectJobData() from a file that doesn't have
15
- * access to the foam instance.
15
+ * access to the foam instance. createFastifyPlugin is NOT exported standalone
16
+ * because the raw function takes a tracer argument — use foam.createFastifyPlugin()
17
+ * instead.
16
18
  */
17
19
  Object.defineProperty(exports, "__esModule", { value: true });
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;
20
+ exports.FOAM_OTEL_ENDPOINT = exports.loadNewRelic = exports.createPinoMixin = exports.createWinstonFormat = exports.getFastifyRequestTraceContext = exports.createPinoHttpOptions = exports.getRequestTraceContext = exports.createExpressErrorHandler = exports.createExpressMiddleware = exports.wrapSqsConsumer = exports.wrapJobConsumer = exports.injectJobData = exports.injectSnsAttributes = exports.getTraceContext = exports.extractParentContext = exports.buildTraceparent = exports.getFoam = exports.init = void 0;
19
21
  var init_1 = require("./init");
20
22
  Object.defineProperty(exports, "init", { enumerable: true, get: function () { return init_1.init; } });
23
+ Object.defineProperty(exports, "getFoam", { enumerable: true, get: function () { return init_1.getFoam; } });
21
24
  var trace_bridge_1 = require("./trace-bridge");
22
25
  Object.defineProperty(exports, "buildTraceparent", { enumerable: true, get: function () { return trace_bridge_1.buildTraceparent; } });
23
26
  Object.defineProperty(exports, "extractParentContext", { enumerable: true, get: function () { return trace_bridge_1.extractParentContext; } });
@@ -35,11 +38,12 @@ Object.defineProperty(exports, "createExpressErrorHandler", { enumerable: true,
35
38
  Object.defineProperty(exports, "getRequestTraceContext", { enumerable: true, get: function () { return express_1.getRequestTraceContext; } });
36
39
  Object.defineProperty(exports, "createPinoHttpOptions", { enumerable: true, get: function () { return express_1.createPinoHttpOptions; } });
37
40
  var fastify_1 = require("./http/fastify");
38
- Object.defineProperty(exports, "createFastifyPlugin", { enumerable: true, get: function () { return fastify_1.createFastifyPlugin; } });
39
41
  Object.defineProperty(exports, "getFastifyRequestTraceContext", { enumerable: true, get: function () { return fastify_1.getRequestTraceContext; } });
40
42
  var winston_format_1 = require("./logs/winston-format");
41
43
  Object.defineProperty(exports, "createWinstonFormat", { enumerable: true, get: function () { return winston_format_1.createWinstonFormat; } });
42
44
  var pino_mixin_1 = require("./logs/pino-mixin");
43
45
  Object.defineProperty(exports, "createPinoMixin", { enumerable: true, get: function () { return pino_mixin_1.createPinoMixin; } });
46
+ var nr_1 = require("./nr");
47
+ Object.defineProperty(exports, "loadNewRelic", { enumerable: true, get: function () { return nr_1.loadNewRelic; } });
44
48
  var constants_1 = require("./constants");
45
49
  Object.defineProperty(exports, "FOAM_OTEL_ENDPOINT", { enumerable: true, get: function () { return constants_1.FOAM_OTEL_ENDPOINT; } });
@@ -50,3 +50,9 @@
50
50
  */
51
51
  import type { InitOptions, FoamInstance } from './types';
52
52
  export declare function init(serviceName: string, options?: InitOptions): FoamInstance;
53
+ /**
54
+ * Returns the Foam instance for the given service name (or the only instance
55
+ * if there's just one). Call this from any module after init() has run —
56
+ * no circular dependency, no holder pattern needed.
57
+ */
58
+ export declare function getFoam(serviceName?: string): FoamInstance | undefined;
@@ -51,6 +51,7 @@
51
51
  */
52
52
  Object.defineProperty(exports, "__esModule", { value: true });
53
53
  exports.init = init;
54
+ exports.getFoam = getFoam;
54
55
  const api_1 = require("@opentelemetry/api");
55
56
  const exporter_logs_otlp_http_1 = require("@opentelemetry/exporter-logs-otlp-http");
56
57
  const exporter_metrics_otlp_http_1 = require("@opentelemetry/exporter-metrics-otlp-http");
@@ -74,6 +75,7 @@ const sns_1 = require("./sns");
74
75
  const sqs_1 = require("./sqs");
75
76
  const trace_bridge_1 = require("./trace-bridge");
76
77
  const instances = new Map();
78
+ let _processHandlersRegistered = false;
77
79
  function createInertInstance(serviceName) {
78
80
  const resource = (0, resources_1.resourceFromAttributes)({ 'service.name': serviceName });
79
81
  const traceProvider = new sdk_trace_base_1.BasicTracerProvider({ resource });
@@ -85,7 +87,6 @@ function createInertInstance(serviceName) {
85
87
  return {
86
88
  tracer,
87
89
  meter,
88
- logger: loggerProvider,
89
90
  traceProvider,
90
91
  meterProvider,
91
92
  loggerProvider,
@@ -103,16 +104,13 @@ function createInertInstance(serviceName) {
103
104
  createPinoDestination: () => new node_stream_1.Writable({
104
105
  write: (_c, _e, cb) => cb(),
105
106
  }),
106
- createPinoLogger: (opts = {}) => {
107
- const pino = require('pino');
108
- return pino({ ...opts });
109
- },
110
- createPinoHttpConfig: () => ({}),
107
+ createPinoLogger: () => console,
108
+ createPinoHttpConfig: (opts) => (opts?.logger ? { logger: opts.logger } : {}),
111
109
  buildTraceparent: trace_bridge_1.buildTraceparent,
112
110
  injectSnsAttributes: sns_1.injectSnsAttributes,
113
111
  injectJobData: job_1.injectJobData,
114
- wrapSqsConsumer: async (_t, _s, _m, fn) => fn(),
115
- wrapJobConsumer: async (_t, _s, _j, fn) => fn(),
112
+ wrapSqsConsumer: async (_s, _m, fn) => fn(),
113
+ wrapJobConsumer: async (_s, _j, fn) => fn(),
116
114
  extractParentContext: () => {
117
115
  const { ROOT_CONTEXT } = require('@opentelemetry/api');
118
116
  return ROOT_CONTEXT;
@@ -226,46 +224,54 @@ function init(serviceName, options = {}) {
226
224
  process.on('SIGTERM', () => void handler());
227
225
  process.on('SIGINT', () => void handler());
228
226
  }
229
- // Process-level exception capture — recorded as span exceptions so they
230
- // land in otel_traces (not otel_logs). Each creates a short-lived span,
231
- // records the exception, ends it, then force-flushes the trace provider.
232
- process.on('uncaughtException', (err) => {
233
- try {
234
- const span = tracer.startSpan('uncaughtException', {
235
- attributes: { 'process.event': 'uncaughtException' },
236
- });
237
- span.recordException({
238
- name: err.constructor.name,
239
- message: err.message,
240
- stack: err.stack,
241
- });
242
- span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: err.message });
243
- span.end();
244
- void traceProvider.forceFlush().then(() => shutdown());
245
- }
246
- catch {
247
- void shutdown();
248
- }
249
- });
250
- process.on('unhandledRejection', (reason) => {
251
- try {
252
- const err = reason instanceof Error ? reason : new Error(String(reason));
253
- const span = tracer.startSpan('unhandledRejection', {
254
- attributes: { 'process.event': 'unhandledRejection' },
255
- });
256
- span.recordException({
257
- name: err.constructor.name,
258
- message: err.message,
259
- stack: err.stack,
260
- });
261
- span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: err.message });
262
- span.end();
263
- void traceProvider.forceFlush();
264
- }
265
- catch {
266
- /* best effort */
267
- }
268
- });
227
+ else {
228
+ process.once('beforeExit', () => {
229
+ if (!_isShutdown) {
230
+ // eslint-disable-next-line no-console
231
+ console.warn('[foam] autoShutdown is false but shutdown() was never called — OTel data may be lost');
232
+ }
233
+ });
234
+ }
235
+ if (!_processHandlersRegistered) {
236
+ _processHandlersRegistered = true;
237
+ process.on('uncaughtException', (err) => {
238
+ try {
239
+ const span = tracer.startSpan('uncaughtException', {
240
+ attributes: { 'process.event': 'uncaughtException' },
241
+ });
242
+ span.recordException({
243
+ name: err.constructor.name,
244
+ message: err.message,
245
+ stack: err.stack,
246
+ });
247
+ span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: err.message });
248
+ span.end();
249
+ void traceProvider.forceFlush().then(() => shutdown());
250
+ }
251
+ catch {
252
+ void shutdown();
253
+ }
254
+ });
255
+ process.on('unhandledRejection', (reason) => {
256
+ try {
257
+ const err = reason instanceof Error ? reason : new Error(String(reason));
258
+ const span = tracer.startSpan('unhandledRejection', {
259
+ attributes: { 'process.event': 'unhandledRejection' },
260
+ });
261
+ span.recordException({
262
+ name: err.constructor.name,
263
+ message: err.message,
264
+ stack: err.stack,
265
+ });
266
+ span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: err.message });
267
+ span.end();
268
+ void traceProvider.forceFlush();
269
+ }
270
+ catch {
271
+ /* best effort */
272
+ }
273
+ });
274
+ }
269
275
  // Winston auto-wiring
270
276
  if (options.winston) {
271
277
  const winstonLogger = options.winston;
@@ -330,7 +336,6 @@ function init(serviceName, options = {}) {
330
336
  const foam = {
331
337
  tracer,
332
338
  meter,
333
- logger: loggerProvider,
334
339
  traceProvider,
335
340
  meterProvider,
336
341
  loggerProvider,
@@ -348,20 +353,21 @@ function init(serviceName, options = {}) {
348
353
  const pino = require('pino');
349
354
  const mixin = (0, pino_mixin_1.createPinoMixin)();
350
355
  const dest = (0, pino_destination_1.createPinoDestination)(loggerProvider, serviceName);
351
- return pino({ ...opts, mixin }, pino.multistream([
356
+ const { streams: userStreams, ...pinoOpts } = opts;
357
+ const outputStreams = userStreams ?? [
352
358
  { stream: process.stdout },
353
- { stream: dest },
354
- ]));
359
+ ];
360
+ return pino({ ...pinoOpts, mixin }, pino.multistream([...outputStreams, { stream: dest }]));
355
361
  },
356
- createPinoHttpConfig: () => {
362
+ createPinoHttpConfig: (opts) => {
357
363
  const pinoHttpOpts = (0, express_1.createPinoHttpOptions)();
358
- return { ...pinoHttpOpts };
364
+ return { ...pinoHttpOpts, ...(opts?.logger && { logger: opts.logger }) };
359
365
  },
360
366
  buildTraceparent: trace_bridge_1.buildTraceparent,
361
367
  injectSnsAttributes: sns_1.injectSnsAttributes,
362
368
  injectJobData: job_1.injectJobData,
363
- wrapSqsConsumer: (t, s, m, fn) => (0, sqs_1.wrapSqsConsumer)(t, s, m, fn, metrics),
364
- wrapJobConsumer: (t, s, j, fn) => (0, job_1.wrapJobConsumer)(t, s, j, fn, metrics),
369
+ wrapSqsConsumer: (s, m, fn) => (0, sqs_1.wrapSqsConsumer)(tracer, s, m, fn, metrics),
370
+ wrapJobConsumer: (s, j, fn) => (0, job_1.wrapJobConsumer)(tracer, s, j, fn, metrics),
365
371
  extractParentContext: trace_bridge_1.extractParentContext,
366
372
  shutdown,
367
373
  };
@@ -369,3 +375,17 @@ function init(serviceName, options = {}) {
369
375
  return foam;
370
376
  }
371
377
  /* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call */
378
+ /**
379
+ * Returns the Foam instance for the given service name (or the only instance
380
+ * if there's just one). Call this from any module after init() has run —
381
+ * no circular dependency, no holder pattern needed.
382
+ */
383
+ function getFoam(serviceName) {
384
+ if (serviceName) {
385
+ return instances.get(serviceName);
386
+ }
387
+ if (instances.size === 1) {
388
+ return instances.values().next().value;
389
+ }
390
+ return undefined;
391
+ }
@@ -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
+ }
@@ -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;
@@ -65,15 +64,25 @@ export interface FoamInstance {
65
64
  customProps: (req: any) => Record<string, string>;
66
65
  };
67
66
  createPinoDestination(): NodeJS.WritableStream;
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
+ */
68
75
  createPinoLogger(options?: Record<string, any>): any;
69
- createPinoHttpConfig(): Record<string, any>;
76
+ createPinoHttpConfig(opts?: {
77
+ logger?: any;
78
+ }): Record<string, any>;
70
79
  buildTraceparent(): string;
71
80
  injectSnsAttributes(attrs: Record<string, SnsMessageAttributeValue>): Record<string, SnsMessageAttributeValue>;
72
81
  injectJobData<T extends Record<string, unknown>>(data: T): T & {
73
82
  traceparent: string;
74
83
  };
75
- wrapSqsConsumer(tracer: Tracer, spanName: string, msg: SqsMessage, fn: () => Promise<void>): Promise<void>;
76
- wrapJobConsumer(tracer: Tracer, spanName: string, jobData: Record<string, unknown>, fn: () => Promise<void>): Promise<void>;
84
+ wrapSqsConsumer(spanName: string, msg: SqsMessage, fn: () => Promise<void>): Promise<void>;
85
+ wrapJobConsumer(spanName: string, jobData: Record<string, unknown>, fn: () => Promise<void>): Promise<void>;
77
86
  extractParentContext(traceparent: string): Context;
78
87
  shutdown(): Promise<void>;
79
88
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foam-ai/node-cliengo",
3
- "version": "0.1.0-alpha.13",
3
+ "version": "0.1.0-alpha.15",
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",