@foam-ai/node-cliengo 0.1.0-alpha.1 → 0.1.0-alpha.11

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.
Files changed (30) hide show
  1. package/dist/node-cliengo/src/constants.d.ts +1 -1
  2. package/dist/node-cliengo/src/constants.js +1 -1
  3. package/dist/node-cliengo/src/http/express.d.ts +21 -0
  4. package/dist/node-cliengo/src/http/express.js +63 -4
  5. package/dist/node-cliengo/src/http/fastify.d.ts +10 -0
  6. package/dist/node-cliengo/src/http/fastify.js +48 -3
  7. package/dist/node-cliengo/src/index.d.ts +5 -5
  8. package/dist/node-cliengo/src/index.js +6 -3
  9. package/dist/node-cliengo/src/init.d.ts +14 -11
  10. package/dist/node-cliengo/src/init.js +57 -49
  11. package/dist/node-cliengo/src/job.d.ts +3 -3
  12. package/dist/node-cliengo/src/job.js +1 -5
  13. package/dist/node-cliengo/src/logs/pino-destination.d.ts +7 -2
  14. package/dist/node-cliengo/src/logs/pino-destination.js +34 -9
  15. package/dist/node-cliengo/src/logs/pino-mixin.d.ts +5 -4
  16. package/dist/node-cliengo/src/logs/pino-mixin.js +9 -5
  17. package/dist/node-cliengo/src/logs/winston-transport.js +11 -0
  18. package/dist/node-cliengo/src/metrics/instruments.d.ts +9 -11
  19. package/dist/node-cliengo/src/metrics/instruments.js +19 -55
  20. package/dist/node-cliengo/src/nr.d.ts +1 -1
  21. package/dist/node-cliengo/src/nr.js +1 -1
  22. package/dist/node-cliengo/src/sns.d.ts +1 -4
  23. package/dist/node-cliengo/src/sns.js +2 -9
  24. package/dist/node-cliengo/src/sqs.d.ts +3 -3
  25. package/dist/node-cliengo/src/sqs.js +1 -1
  26. package/dist/node-cliengo/src/trace-bridge.d.ts +31 -27
  27. package/dist/node-cliengo/src/trace-bridge.js +67 -36
  28. package/dist/node-cliengo/src/types.d.ts +10 -10
  29. package/dist/node-cliengo/src/types.js +2 -2
  30. package/package.json +12 -10
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Shared constants for the @foam/node-cliengo bridge.
2
+ * Shared constants for @foam/node-cliengo.
3
3
  *
4
4
  * Why this file exists:
5
5
  * Centralizes all hardcoded values (endpoint, limits, redaction keys, health
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  /**
3
- * Shared constants for the @foam/node-cliengo bridge.
3
+ * Shared constants for @foam/node-cliengo.
4
4
  *
5
5
  * Why this file exists:
6
6
  * Centralizes all hardcoded values (endpoint, limits, redaction keys, health
@@ -36,6 +36,27 @@
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
+ };
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
+ };
39
60
  export declare function createExpressErrorHandler(middlewareRef?: ExpressRequestHandler & {
40
61
  _markErrorHandler?: () => void;
41
62
  }): ExpressErrorHandler;
@@ -36,11 +36,14 @@
36
36
  */
37
37
  Object.defineProperty(exports, "__esModule", { value: true });
38
38
  exports.createExpressMiddleware = createExpressMiddleware;
39
+ exports.getRequestTraceContext = getRequestTraceContext;
40
+ exports.createPinoHttpOptions = createPinoHttpOptions;
39
41
  exports.createExpressErrorHandler = createExpressErrorHandler;
40
42
  const api_1 = require("@opentelemetry/api");
41
43
  const trace_bridge_1 = require("../trace-bridge");
42
44
  const request_context_1 = require("./request-context");
43
45
  const SPAN_KEY = Symbol.for('foam.shadow.span');
46
+ const TRACE_CTX_KEY = Symbol.for('foam.trace.context');
44
47
  /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */
45
48
  function createExpressMiddleware(tracer) {
46
49
  let errorHandlerRegistered = false;
@@ -52,15 +55,35 @@ function createExpressMiddleware(tracer) {
52
55
  const method = req.method ?? 'UNKNOWN';
53
56
  const target = req.originalUrl ?? req.url ?? '/';
54
57
  const { traceId, spanId } = (0, trace_bridge_1.getTraceContext)();
58
+ // Build a parent context so the shadow span shares the same OTel traceId
59
+ // as NR's transaction (or the active OTel span). Without this, startSpan()
60
+ // creates a root span with a separate traceId, breaking end-to-end
61
+ // traceability between HTTP spans, consumer spans, and logs.
62
+ let parentCtx;
63
+ if (traceId && spanId) {
64
+ parentCtx = api_1.trace.setSpanContext(api_1.ROOT_CONTEXT, {
65
+ traceId,
66
+ spanId,
67
+ traceFlags: 1,
68
+ isRemote: true,
69
+ });
70
+ }
55
71
  const span = tracer.startSpan(`${method} ${target}`, {
56
72
  attributes: {
57
73
  'http.method': method,
58
74
  'http.target': target,
59
- ...(traceId ? { 'nr.traceId': traceId } : {}),
60
- ...(spanId ? { 'nr.spanId': spanId } : {}),
61
75
  },
62
- });
76
+ }, parentCtx);
63
77
  req[SPAN_KEY] = span;
78
+ // Read trace context from the span we just created — not from NR.
79
+ // The span's spanContext() always has the correct traceId, whether it
80
+ // inherited from NR or generated a fresh root.
81
+ const sc = span.spanContext();
82
+ req[TRACE_CTX_KEY] = {
83
+ traceId: sc.traceId,
84
+ spanId: sc.spanId,
85
+ traceparent: `00-${sc.traceId}-${sc.spanId}-01`,
86
+ };
64
87
  const startTime = Date.now();
65
88
  const onFinish = () => {
66
89
  try {
@@ -94,11 +117,47 @@ function createExpressMiddleware(tracer) {
94
117
  }
95
118
  };
96
119
  res.on('finish', onFinish);
97
- next();
120
+ // Set the shadow span as the active context for the duration of the
121
+ // request. This makes it discoverable via trace.getActiveSpan() so
122
+ // buildTraceparent() (used by injectJobData/injectSnsAttributes) returns
123
+ // the same traceId — even without NR.
124
+ const activeCtx = api_1.trace.setSpan(parentCtx ?? api_1.ROOT_CONTEXT, span);
125
+ api_1.context.with(activeCtx, () => next());
98
126
  };
99
127
  middleware._markErrorHandler = markErrorHandlerRegistered;
100
128
  return middleware;
101
129
  }
130
+ /**
131
+ * Reads the trace context that was captured on the request at middleware time.
132
+ * Use this in pino-http's `customProps` to get trace IDs even after NR's
133
+ * transaction has ended:
134
+ *
135
+ * pinoHttp({ customProps: (req) => getRequestTraceContext(req) })
136
+ */
137
+ function getRequestTraceContext(req) {
138
+ try {
139
+ const ctx = req[TRACE_CTX_KEY];
140
+ return {
141
+ traceId: ctx?.traceId ?? '',
142
+ spanId: ctx?.spanId ?? '',
143
+ traceparent: ctx?.traceparent ?? '',
144
+ };
145
+ }
146
+ catch {
147
+ return { traceId: '', spanId: '', traceparent: '' };
148
+ }
149
+ }
150
+ /**
151
+ * Returns pino-http options with customProps pre-wired to read the trace
152
+ * context stored on req by createExpressMiddleware(). Zero config for services:
153
+ *
154
+ * app.use(pinoHttp({ logger, ...foam.createPinoHttpOptions() }));
155
+ */
156
+ function createPinoHttpOptions() {
157
+ return {
158
+ customProps: (req) => getRequestTraceContext(req),
159
+ };
160
+ }
102
161
  function createExpressErrorHandler(middlewareRef) {
103
162
  middlewareRef?._markErrorHandler?.();
104
163
  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,32 @@
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
+ return {
45
+ traceId: ctx?.traceId ?? '',
46
+ spanId: ctx?.spanId ?? '',
47
+ traceparent: ctx?.traceparent ?? '',
48
+ };
49
+ }
50
+ catch {
51
+ return { traceId: '', spanId: '', traceparent: '' };
52
+ }
53
+ }
34
54
  /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */
35
55
  function createFastifyPlugin(tracer) {
36
56
  return async (fastify) => {
@@ -39,22 +59,47 @@ function createFastifyPlugin(tracer) {
39
59
  const method = request.method ?? 'UNKNOWN';
40
60
  const url = request.url ?? '/';
41
61
  const { traceId, spanId } = (0, trace_bridge_1.getTraceContext)();
62
+ let parentCtx;
63
+ if (traceId && spanId) {
64
+ parentCtx = api_1.trace.setSpanContext(api_1.ROOT_CONTEXT, {
65
+ traceId,
66
+ spanId,
67
+ traceFlags: 1,
68
+ isRemote: true,
69
+ });
70
+ }
42
71
  const span = tracer.startSpan(`${method} ${url}`, {
43
72
  attributes: {
44
73
  'http.method': method,
45
74
  'http.target': url,
46
- ...(traceId ? { 'nr.traceId': traceId } : {}),
47
- ...(spanId ? { 'nr.spanId': spanId } : {}),
48
75
  },
49
- });
76
+ }, parentCtx);
50
77
  request[SPAN_KEY] = span;
51
78
  request[START_KEY] = Date.now();
79
+ const sc = span.spanContext();
80
+ request[TRACE_CTX_KEY] = {
81
+ traceId: sc.traceId,
82
+ spanId: sc.spanId,
83
+ traceparent: `00-${sc.traceId}-${sc.spanId}-01`,
84
+ };
52
85
  }
53
86
  catch {
54
87
  /* never crash */
55
88
  }
56
89
  done();
57
90
  });
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
+ });
58
103
  fastify.addHook('onError', (request, _reply, error, done) => {
59
104
  try {
60
105
  const span = request[SPAN_KEY];
@@ -7,20 +7,20 @@
7
7
  * (buildTraceparent, injectSnsAttributes, etc.), and all public types.
8
8
  *
9
9
  * Most services only need: import { init } from '@foam/node-cliengo'
10
- * Everything else is accessed via the returned OtelBridge object.
10
+ * Everything else is accessed via the returned FoamInstance.
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 bridge instance.
14
+ * access to the foam instance.
15
15
  */
16
16
  export { init } from './init';
17
17
  export { buildTraceparent, extractParentContext, getTraceContext } from './trace-bridge';
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, createPinoHttpOptions } 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';
26
- export type { BridgeMetrics, Counter, ExpressErrorHandler, ExpressRequestHandler, FastifyPluginAsync, Histogram, InitOptions, NewRelicAgent, ObservableGauge, OtelBridge, SnsMessageAttributeValue, SqsMessage, TraceContext, WinstonFormat, WinstonLogger, WinstonTransport, } from './types';
26
+ export type { FoamMetrics, Counter, ExpressErrorHandler, ExpressRequestHandler, FastifyPluginAsync, Histogram, InitOptions, NewRelicAgent, ObservableGauge, FoamInstance, SnsMessageAttributeValue, SqsMessage, TraceContext, WinstonFormat, WinstonLogger, WinstonTransport, } from './types';
@@ -8,14 +8,14 @@
8
8
  * (buildTraceparent, injectSnsAttributes, etc.), and all public types.
9
9
  *
10
10
  * Most services only need: import { init } from '@foam/node-cliengo'
11
- * Everything else is accessed via the returned OtelBridge object.
11
+ * Everything else is accessed via the returned FoamInstance.
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 bridge instance.
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.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");
@@ -32,8 +32,11 @@ 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; } });
36
+ Object.defineProperty(exports, "createPinoHttpOptions", { enumerable: true, get: function () { return express_1.createPinoHttpOptions; } });
35
37
  var fastify_1 = require("./http/fastify");
36
38
  Object.defineProperty(exports, "createFastifyPlugin", { enumerable: true, get: function () { return fastify_1.createFastifyPlugin; } });
39
+ Object.defineProperty(exports, "getFastifyRequestTraceContext", { enumerable: true, get: function () { return fastify_1.getRequestTraceContext; } });
37
40
  var winston_format_1 = require("./logs/winston-format");
38
41
  Object.defineProperty(exports, "createWinstonFormat", { enumerable: true, get: function () { return winston_format_1.createWinstonFormat; } });
39
42
  var pino_mixin_1 = require("./logs/pino-mixin");
@@ -1,40 +1,43 @@
1
1
  /**
2
- * Main entry point — init() creates the OtelBridge singleton for a service.
2
+ * Main entry point — init() creates the Foam singleton for a service.
3
3
  *
4
4
  * Why this file exists:
5
5
  * Orchestrates all three OTel signals (traces, logs, metrics) into a single
6
- * init() call that returns an OtelBridge object. Services call init() once at
7
- * startup and use the returned bridge for everything else. The bridge is
6
+ * init() call that returns a FoamInstance. Services call init() once at
7
+ * startup and use the returned instance for everything else. It is
8
8
  * designed to coexist with New Relic APM without touching NR's globals.
9
9
  *
10
10
  * Two-gate system:
11
- * 1. Kill switch (FOAM_OTEL_ENABLED): if false, returns a fully inert bridge.
11
+ * 1. Kill switch (FOAM_OTEL_ENABLED): if false, returns a fully inert instance.
12
12
  * Every API exists but does nothing — zero overhead, zero network, zero risk.
13
13
  * This is the master emergency shutoff.
14
14
  * 2. Production gate (NODE_ENV): exporters are only attached when NODE_ENV is
15
- * "production" (or forceExport is true). In dev/test, the bridge is fully
15
+ * "production" (or forceExport is true). In dev/test, Foam is fully
16
16
  * functional (trace propagation, log enrichment work) but nothing is sent
17
17
  * to Foam. Developers without FOAM_OTEL_TOKEN get no crashes.
18
18
  *
19
19
  * Edge cases covered:
20
- * - Double init(): logs a warning, returns the existing bridge (singleton
20
+ * - Double init(): logs a warning, returns the existing instance (singleton
21
21
  * per service name). Prevents duplicate providers/processors/handlers.
22
22
  * - Token undefined in dev: silently accepted — no OTLP export anyway.
23
23
  * - Token undefined in prod: logs warning, disables exporters. All other
24
24
  * features (trace propagation, log enrichment, NR bridging) still work.
25
25
  * - Token undefined + forceExport: throws (fail-fast — you asked to force
26
26
  * export but provided no credentials).
27
+ * - Custom endpoint without forceExport: throws. Production always uses the
28
+ * default Foam endpoint; custom endpoints are only for dev/staging.
27
29
  * - Auto-shutdown (SIGTERM/SIGINT): flushes all providers with 5s timeout,
28
30
  * then calls process.exit(0). Without the explicit exit, registering
29
31
  * process.on('SIGTERM') replaces Node's default terminate behavior — services
30
32
  * without their own handler (gpt-intentions, cb-proxy) would hang forever.
31
33
  * - Manual shutdown: services with ordered cleanup (hsm-backend, kori, combee)
32
- * pass autoShutdown: false and call bridge.shutdown() in their own handler.
34
+ * pass autoShutdown: false and call foam.shutdown() in their own handler.
33
35
  * - shutdown() is idempotent — safe to call multiple times.
34
36
  * - Process exception handlers use process.on() (not .once()) so they don't
35
37
  * replace NR's or Winston's existing handlers. All handlers fire independently.
36
- * - uncaughtException handler flushes via shutdown() — best effort to get the
37
- * fatal log to Foam before the process dies. Never calls process.exit().
38
+ * - uncaughtException/unhandledRejection create short-lived spans with
39
+ * recordException() so they appear in otel_traces, then force-flush the
40
+ * trace provider before shutdown. Never calls process.exit().
38
41
  * - Winston auto-wiring: init({ winston: baseLogger }) calls logger.add() for
39
42
  * the OTLP transport and rewrites logger.format to prepend trace enrichment.
40
43
  * Both work post-construction. Avoids circular deps (main.ts imports logger,
@@ -46,5 +49,5 @@
46
49
  * are NEVER registered globally (.register() never called). NR owns the
47
50
  * global registrations.
48
51
  */
49
- import type { InitOptions, OtelBridge } from './types';
50
- export declare function init(serviceName: string, token?: string, options?: InitOptions): OtelBridge;
52
+ import type { InitOptions, FoamInstance } from './types';
53
+ export declare function init(serviceName: string, token?: string, options?: InitOptions): FoamInstance;
@@ -1,41 +1,44 @@
1
1
  "use strict";
2
2
  /**
3
- * Main entry point — init() creates the OtelBridge singleton for a service.
3
+ * Main entry point — init() creates the Foam singleton for a service.
4
4
  *
5
5
  * Why this file exists:
6
6
  * Orchestrates all three OTel signals (traces, logs, metrics) into a single
7
- * init() call that returns an OtelBridge object. Services call init() once at
8
- * startup and use the returned bridge for everything else. The bridge is
7
+ * init() call that returns a FoamInstance. Services call init() once at
8
+ * startup and use the returned instance for everything else. It is
9
9
  * designed to coexist with New Relic APM without touching NR's globals.
10
10
  *
11
11
  * Two-gate system:
12
- * 1. Kill switch (FOAM_OTEL_ENABLED): if false, returns a fully inert bridge.
12
+ * 1. Kill switch (FOAM_OTEL_ENABLED): if false, returns a fully inert instance.
13
13
  * Every API exists but does nothing — zero overhead, zero network, zero risk.
14
14
  * This is the master emergency shutoff.
15
15
  * 2. Production gate (NODE_ENV): exporters are only attached when NODE_ENV is
16
- * "production" (or forceExport is true). In dev/test, the bridge is fully
16
+ * "production" (or forceExport is true). In dev/test, Foam is fully
17
17
  * functional (trace propagation, log enrichment work) but nothing is sent
18
18
  * to Foam. Developers without FOAM_OTEL_TOKEN get no crashes.
19
19
  *
20
20
  * Edge cases covered:
21
- * - Double init(): logs a warning, returns the existing bridge (singleton
21
+ * - Double init(): logs a warning, returns the existing instance (singleton
22
22
  * per service name). Prevents duplicate providers/processors/handlers.
23
23
  * - Token undefined in dev: silently accepted — no OTLP export anyway.
24
24
  * - Token undefined in prod: logs warning, disables exporters. All other
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
+ * - Custom endpoint without forceExport: throws. Production always uses the
29
+ * default Foam endpoint; custom endpoints are only for dev/staging.
28
30
  * - Auto-shutdown (SIGTERM/SIGINT): flushes all providers with 5s timeout,
29
31
  * then calls process.exit(0). Without the explicit exit, registering
30
32
  * process.on('SIGTERM') replaces Node's default terminate behavior — services
31
33
  * without their own handler (gpt-intentions, cb-proxy) would hang forever.
32
34
  * - Manual shutdown: services with ordered cleanup (hsm-backend, kori, combee)
33
- * pass autoShutdown: false and call bridge.shutdown() in their own handler.
35
+ * pass autoShutdown: false and call foam.shutdown() in their own handler.
34
36
  * - shutdown() is idempotent — safe to call multiple times.
35
37
  * - Process exception handlers use process.on() (not .once()) so they don't
36
38
  * replace NR's or Winston's existing handlers. All handlers fire independently.
37
- * - uncaughtException handler flushes via shutdown() — best effort to get the
38
- * fatal log to Foam before the process dies. Never calls process.exit().
39
+ * - uncaughtException/unhandledRejection create short-lived spans with
40
+ * recordException() so they appear in otel_traces, then force-flush the
41
+ * trace provider before shutdown. Never calls process.exit().
39
42
  * - Winston auto-wiring: init({ winston: baseLogger }) calls logger.add() for
40
43
  * the OTLP transport and rewrites logger.format to prepend trace enrichment.
41
44
  * Both work post-construction. Avoids circular deps (main.ts imports logger,
@@ -49,7 +52,7 @@
49
52
  */
50
53
  Object.defineProperty(exports, "__esModule", { value: true });
51
54
  exports.init = init;
52
- const api_logs_1 = require("@opentelemetry/api-logs");
55
+ const api_1 = require("@opentelemetry/api");
53
56
  const exporter_logs_otlp_http_1 = require("@opentelemetry/exporter-logs-otlp-http");
54
57
  const exporter_metrics_otlp_http_1 = require("@opentelemetry/exporter-metrics-otlp-http");
55
58
  const exporter_trace_otlp_http_1 = require("@opentelemetry/exporter-trace-otlp-http");
@@ -71,15 +74,15 @@ const nr_1 = require("./nr");
71
74
  const sns_1 = require("./sns");
72
75
  const sqs_1 = require("./sqs");
73
76
  const trace_bridge_1 = require("./trace-bridge");
74
- const bridges = new Map();
75
- function createInertBridge(serviceName) {
77
+ const instances = new Map();
78
+ function createInertInstance(serviceName) {
76
79
  const resource = (0, resources_1.resourceFromAttributes)({ 'service.name': serviceName });
77
80
  const traceProvider = new sdk_trace_base_1.BasicTracerProvider({ resource });
78
81
  const loggerProvider = new sdk_logs_1.LoggerProvider({ resource });
79
82
  const meterProvider = new sdk_metrics_1.MeterProvider({ resource });
80
83
  const tracer = traceProvider.getTracer(serviceName);
81
84
  const meter = meterProvider.getMeter(serviceName);
82
- const metrics = (0, instruments_1.createBridgeMetrics)(meterProvider, serviceName);
85
+ const metrics = (0, instruments_1.createFoamMetrics)(meterProvider, serviceName);
83
86
  return {
84
87
  tracer,
85
88
  meter,
@@ -97,12 +100,13 @@ function createInertBridge(serviceName) {
97
100
  }),
98
101
  createWinstonTransport: () => ({}),
99
102
  createPinoMixin: () => () => ({}),
103
+ createPinoHttpOptions: () => ({ customProps: () => ({}) }),
100
104
  createPinoDestination: () => new node_stream_1.Writable({
101
105
  write: (_c, _e, cb) => cb(),
102
106
  }),
103
- buildTraceparent: () => undefined,
104
- injectSnsAttributes: (attrs) => attrs,
105
- injectJobData: (data) => data,
107
+ buildTraceparent: trace_bridge_1.buildTraceparent,
108
+ injectSnsAttributes: sns_1.injectSnsAttributes,
109
+ injectJobData: job_1.injectJobData,
106
110
  wrapSqsConsumer: async (_t, _s, _m, fn) => fn(),
107
111
  wrapJobConsumer: async (_t, _s, _j, fn) => fn(),
108
112
  extractParentContext: () => {
@@ -114,19 +118,19 @@ function createInertBridge(serviceName) {
114
118
  }
115
119
  /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call */
116
120
  function init(serviceName, token, options = {}) {
117
- const existing = bridges.get(serviceName);
121
+ const existing = instances.get(serviceName);
118
122
  if (existing) {
119
123
  // eslint-disable-next-line no-console
120
- console.warn(`[foam] init() already called for "${serviceName}" — returning existing bridge`);
124
+ console.warn(`[foam] init() already called for "${serviceName}" — returning existing instance`);
121
125
  return existing;
122
126
  }
123
127
  // Kill switch
124
128
  const isEnabled = (options.enabled ??
125
129
  ((process.env.FOAM_OTEL_ENABLED ?? 'true').toLowerCase() === 'true')) === true;
126
130
  if (!isEnabled) {
127
- const bridge = createInertBridge(serviceName);
128
- bridges.set(serviceName, bridge);
129
- return bridge;
131
+ const foam = createInertInstance(serviceName);
132
+ instances.set(serviceName, foam);
133
+ return foam;
130
134
  }
131
135
  // Resolve NR
132
136
  (0, nr_1.resolveNewRelic)(options.newrelic);
@@ -143,6 +147,9 @@ function init(serviceName, token, options = {}) {
143
147
  }
144
148
  const hasToken = Boolean(token);
145
149
  const canExport = shouldExport && hasToken;
150
+ if (options.endpoint && !options.forceExport) {
151
+ throw new Error('[foam] custom endpoint requires forceExport: true — production always uses the default Foam endpoint');
152
+ }
146
153
  const endpoint = options.endpoint ?? constants_1.FOAM_OTEL_ENDPOINT;
147
154
  const resource = (0, resources_1.resourceFromAttributes)({ 'service.name': serviceName });
148
155
  const authHeaders = token
@@ -193,7 +200,7 @@ function init(serviceName, token, options = {}) {
193
200
  });
194
201
  const tracer = traceProvider.getTracer(serviceName);
195
202
  const meter = meterProvider.getMeter(serviceName);
196
- const metrics = (0, instruments_1.createBridgeMetrics)(meterProvider, serviceName);
203
+ const metrics = (0, instruments_1.createFoamMetrics)(meterProvider, serviceName);
197
204
  let _isShutdown = false;
198
205
  const shutdown = async () => {
199
206
  if (_isShutdown) {
@@ -217,41 +224,41 @@ function init(serviceName, token, options = {}) {
217
224
  process.on('SIGTERM', () => void handler());
218
225
  process.on('SIGINT', () => void handler());
219
226
  }
220
- // Process-level exception capture
221
- const otelLogger = loggerProvider.getLogger(serviceName);
227
+ // Process-level exception capture — recorded as span exceptions so they
228
+ // land in otel_traces (not otel_logs). Each creates a short-lived span,
229
+ // records the exception, ends it, then force-flushes the trace provider.
222
230
  process.on('uncaughtException', (err) => {
223
231
  try {
224
- otelLogger.emit({
225
- body: err.message,
226
- severityNumber: api_logs_1.SeverityNumber.FATAL,
227
- severityText: 'FATAL',
228
- attributes: {
229
- 'exception.type': err.constructor.name,
230
- 'exception.message': err.message,
231
- 'exception.stacktrace': err.stack ?? '',
232
- 'process.event': 'uncaughtException',
233
- },
232
+ const span = tracer.startSpan('uncaughtException', {
233
+ attributes: { 'process.event': 'uncaughtException' },
234
234
  });
235
- void shutdown();
235
+ span.recordException({
236
+ name: err.constructor.name,
237
+ message: err.message,
238
+ stack: err.stack,
239
+ });
240
+ span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: err.message });
241
+ span.end();
242
+ void traceProvider.forceFlush().then(() => shutdown());
236
243
  }
237
244
  catch {
238
- /* best effort */
245
+ void shutdown();
239
246
  }
240
247
  });
241
248
  process.on('unhandledRejection', (reason) => {
242
249
  try {
243
250
  const err = reason instanceof Error ? reason : new Error(String(reason));
244
- otelLogger.emit({
245
- body: err.message,
246
- severityNumber: api_logs_1.SeverityNumber.ERROR,
247
- severityText: 'ERROR',
248
- attributes: {
249
- 'exception.type': err.constructor.name,
250
- 'exception.message': err.message,
251
- 'exception.stacktrace': err.stack ?? '',
252
- 'process.event': 'unhandledRejection',
253
- },
251
+ const span = tracer.startSpan('unhandledRejection', {
252
+ attributes: { 'process.event': 'unhandledRejection' },
253
+ });
254
+ span.recordException({
255
+ name: err.constructor.name,
256
+ message: err.message,
257
+ stack: err.stack,
254
258
  });
259
+ span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: err.message });
260
+ span.end();
261
+ void traceProvider.forceFlush();
255
262
  }
256
263
  catch {
257
264
  /* best effort */
@@ -318,7 +325,7 @@ function init(serviceName, token, options = {}) {
318
325
  }
319
326
  }
320
327
  const expressMiddleware = (0, express_1.createExpressMiddleware)(tracer);
321
- const bridge = {
328
+ const foam = {
322
329
  tracer,
323
330
  meter,
324
331
  logger: loggerProvider,
@@ -333,6 +340,7 @@ function init(serviceName, token, options = {}) {
333
340
  createWinstonFormat: () => (0, winston_format_1.createWinstonFormat)(),
334
341
  createWinstonTransport: () => (0, winston_transport_1.createWinstonTransport)(loggerProvider, serviceName),
335
342
  createPinoMixin: () => (0, pino_mixin_1.createPinoMixin)(),
343
+ createPinoHttpOptions: () => (0, express_1.createPinoHttpOptions)(),
336
344
  createPinoDestination: () => (0, pino_destination_1.createPinoDestination)(loggerProvider, serviceName),
337
345
  buildTraceparent: trace_bridge_1.buildTraceparent,
338
346
  injectSnsAttributes: sns_1.injectSnsAttributes,
@@ -342,7 +350,7 @@ function init(serviceName, token, options = {}) {
342
350
  extractParentContext: trace_bridge_1.extractParentContext,
343
351
  shutdown,
344
352
  };
345
- bridges.set(serviceName, bridge);
346
- return bridge;
353
+ instances.set(serviceName, foam);
354
+ return foam;
347
355
  }
348
356
  /* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call */
@@ -24,8 +24,8 @@
24
24
  * consistent exception recording.
25
25
  */
26
26
  import { type Tracer } from '@opentelemetry/api';
27
- import type { BridgeMetrics } from './types';
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
- export declare function wrapJobConsumer(tracer: Tracer, spanName: string, jobData: Record<string, unknown>, fn: () => Promise<void>, metrics?: BridgeMetrics): Promise<void>;
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).