@foam-ai/node-cliengo 0.1.0-alpha.1 → 0.1.0-alpha.10
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.
- package/dist/node-cliengo/src/constants.d.ts +1 -1
- package/dist/node-cliengo/src/constants.js +1 -1
- package/dist/node-cliengo/src/http/express.d.ts +21 -0
- package/dist/node-cliengo/src/http/express.js +57 -3
- package/dist/node-cliengo/src/http/fastify.d.ts +10 -0
- package/dist/node-cliengo/src/http/fastify.js +36 -3
- package/dist/node-cliengo/src/index.d.ts +5 -5
- package/dist/node-cliengo/src/index.js +6 -3
- package/dist/node-cliengo/src/init.d.ts +14 -11
- package/dist/node-cliengo/src/init.js +57 -49
- package/dist/node-cliengo/src/job.d.ts +3 -3
- package/dist/node-cliengo/src/job.js +1 -5
- package/dist/node-cliengo/src/logs/pino-destination.d.ts +7 -2
- package/dist/node-cliengo/src/logs/pino-destination.js +34 -9
- package/dist/node-cliengo/src/logs/pino-mixin.d.ts +5 -4
- package/dist/node-cliengo/src/logs/pino-mixin.js +9 -5
- package/dist/node-cliengo/src/metrics/instruments.d.ts +9 -11
- package/dist/node-cliengo/src/metrics/instruments.js +19 -55
- package/dist/node-cliengo/src/nr.d.ts +1 -1
- package/dist/node-cliengo/src/nr.js +1 -1
- package/dist/node-cliengo/src/sns.d.ts +1 -4
- package/dist/node-cliengo/src/sns.js +2 -9
- package/dist/node-cliengo/src/sqs.d.ts +3 -3
- package/dist/node-cliengo/src/sqs.js +1 -1
- package/dist/node-cliengo/src/trace-bridge.d.ts +31 -27
- package/dist/node-cliengo/src/trace-bridge.js +67 -36
- package/dist/node-cliengo/src/types.d.ts +10 -10
- package/dist/node-cliengo/src/types.js +2 -2
- package/package.json +12 -10
|
@@ -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 {
|
|
@@ -99,6 +122,37 @@ function createExpressMiddleware(tracer) {
|
|
|
99
122
|
middleware._markErrorHandler = markErrorHandlerRegistered;
|
|
100
123
|
return middleware;
|
|
101
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* Reads the trace context that was captured on the request at middleware time.
|
|
127
|
+
* Use this in pino-http's `customProps` to get trace IDs even after NR's
|
|
128
|
+
* transaction has ended:
|
|
129
|
+
*
|
|
130
|
+
* pinoHttp({ customProps: (req) => getRequestTraceContext(req) })
|
|
131
|
+
*/
|
|
132
|
+
function getRequestTraceContext(req) {
|
|
133
|
+
try {
|
|
134
|
+
const ctx = req[TRACE_CTX_KEY];
|
|
135
|
+
return {
|
|
136
|
+
traceId: ctx?.traceId ?? '',
|
|
137
|
+
spanId: ctx?.spanId ?? '',
|
|
138
|
+
traceparent: ctx?.traceparent ?? '',
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return { traceId: '', spanId: '', traceparent: '' };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Returns pino-http options with customProps pre-wired to read the trace
|
|
147
|
+
* context stored on req by createExpressMiddleware(). Zero config for services:
|
|
148
|
+
*
|
|
149
|
+
* app.use(pinoHttp({ logger, ...foam.createPinoHttpOptions() }));
|
|
150
|
+
*/
|
|
151
|
+
function createPinoHttpOptions() {
|
|
152
|
+
return {
|
|
153
|
+
customProps: (req) => getRequestTraceContext(req),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
102
156
|
function createExpressErrorHandler(middlewareRef) {
|
|
103
157
|
middlewareRef?._markErrorHandler?.();
|
|
104
158
|
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,16 +59,29 @@ 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 */
|
|
@@ -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
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
7
|
-
* startup and use the returned
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
37
|
-
*
|
|
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,
|
|
50
|
-
export declare function init(serviceName: string, token?: string, options?: InitOptions):
|
|
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
|
|
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
|
|
8
|
-
* startup and use the returned
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
38
|
-
*
|
|
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
|
|
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
|
|
75
|
-
function
|
|
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.
|
|
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:
|
|
104
|
-
injectSnsAttributes:
|
|
105
|
-
injectJobData:
|
|
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 =
|
|
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
|
|
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
|
|
128
|
-
|
|
129
|
-
return
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
225
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
|
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
|
-
|
|
346
|
-
return
|
|
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 {
|
|
27
|
+
import type { FoamMetrics } from './types';
|
|
28
28
|
export declare function injectJobData<T extends Record<string, unknown>>(data: T): T & {
|
|
29
|
-
traceparent
|
|
29
|
+
traceparent: string;
|
|
30
30
|
};
|
|
31
|
-
export declare function wrapJobConsumer(tracer: Tracer, spanName: string, jobData: Record<string, unknown>, fn: () => Promise<void>, metrics?:
|
|
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
|
-
|
|
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
|
-
* -
|
|
25
|
-
*
|
|
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).
|