@foam-ai/node-cliengo 0.1.0-alpha.14 → 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.
- package/dist/node-cliengo/src/constants.d.ts +1 -2
- package/dist/node-cliengo/src/constants.js +5 -2
- package/dist/node-cliengo/src/http/express.js +6 -1
- package/dist/node-cliengo/src/http/fastify.d.ts +5 -4
- package/dist/node-cliengo/src/http/fastify.js +23 -22
- package/dist/node-cliengo/src/index.d.ts +5 -2
- package/dist/node-cliengo/src/index.js +6 -3
- package/dist/node-cliengo/src/init.js +61 -56
- package/dist/node-cliengo/src/nr.d.ts +17 -0
- package/dist/node-cliengo/src/nr.js +29 -0
- package/dist/node-cliengo/src/types.d.ts +13 -4
- package/package.json +1 -1
|
@@ -8,8 +8,7 @@
|
|
|
8
8
|
* through individual files.
|
|
9
9
|
*
|
|
10
10
|
* Edge cases covered:
|
|
11
|
-
* - FOAM_OTEL_ENDPOINT is hardcoded
|
|
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
|
|
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 (
|
|
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.
|
|
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
|
-
* -
|
|
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.
|
|
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
|
-
* -
|
|
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
|
-
|
|
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
|
|
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 (
|
|
131
|
-
|
|
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,7 +11,9 @@
|
|
|
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
18
|
export { init, getFoam } from './init';
|
|
17
19
|
export { buildTraceparent, extractParentContext, getTraceContext } from './trace-bridge';
|
|
@@ -19,8 +21,9 @@ 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 {
|
|
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,10 +12,12 @@
|
|
|
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.
|
|
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; } });
|
|
21
23
|
Object.defineProperty(exports, "getFoam", { enumerable: true, get: function () { return init_1.getFoam; } });
|
|
@@ -36,11 +38,12 @@ Object.defineProperty(exports, "createExpressErrorHandler", { enumerable: true,
|
|
|
36
38
|
Object.defineProperty(exports, "getRequestTraceContext", { enumerable: true, get: function () { return express_1.getRequestTraceContext; } });
|
|
37
39
|
Object.defineProperty(exports, "createPinoHttpOptions", { enumerable: true, get: function () { return express_1.createPinoHttpOptions; } });
|
|
38
40
|
var fastify_1 = require("./http/fastify");
|
|
39
|
-
Object.defineProperty(exports, "createFastifyPlugin", { enumerable: true, get: function () { return fastify_1.createFastifyPlugin; } });
|
|
40
41
|
Object.defineProperty(exports, "getFastifyRequestTraceContext", { enumerable: true, get: function () { return fastify_1.getRequestTraceContext; } });
|
|
41
42
|
var winston_format_1 = require("./logs/winston-format");
|
|
42
43
|
Object.defineProperty(exports, "createWinstonFormat", { enumerable: true, get: function () { return winston_format_1.createWinstonFormat; } });
|
|
43
44
|
var pino_mixin_1 = require("./logs/pino-mixin");
|
|
44
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; } });
|
|
45
48
|
var constants_1 = require("./constants");
|
|
46
49
|
Object.defineProperty(exports, "FOAM_OTEL_ENDPOINT", { enumerable: true, get: function () { return constants_1.FOAM_OTEL_ENDPOINT; } });
|
|
@@ -75,6 +75,7 @@ const sns_1 = require("./sns");
|
|
|
75
75
|
const sqs_1 = require("./sqs");
|
|
76
76
|
const trace_bridge_1 = require("./trace-bridge");
|
|
77
77
|
const instances = new Map();
|
|
78
|
+
let _processHandlersRegistered = false;
|
|
78
79
|
function createInertInstance(serviceName) {
|
|
79
80
|
const resource = (0, resources_1.resourceFromAttributes)({ 'service.name': serviceName });
|
|
80
81
|
const traceProvider = new sdk_trace_base_1.BasicTracerProvider({ resource });
|
|
@@ -86,7 +87,6 @@ function createInertInstance(serviceName) {
|
|
|
86
87
|
return {
|
|
87
88
|
tracer,
|
|
88
89
|
meter,
|
|
89
|
-
logger: loggerProvider,
|
|
90
90
|
traceProvider,
|
|
91
91
|
meterProvider,
|
|
92
92
|
loggerProvider,
|
|
@@ -104,16 +104,13 @@ function createInertInstance(serviceName) {
|
|
|
104
104
|
createPinoDestination: () => new node_stream_1.Writable({
|
|
105
105
|
write: (_c, _e, cb) => cb(),
|
|
106
106
|
}),
|
|
107
|
-
createPinoLogger: (
|
|
108
|
-
|
|
109
|
-
return pino({ ...opts });
|
|
110
|
-
},
|
|
111
|
-
createPinoHttpConfig: () => ({}),
|
|
107
|
+
createPinoLogger: () => console,
|
|
108
|
+
createPinoHttpConfig: (opts) => (opts?.logger ? { logger: opts.logger } : {}),
|
|
112
109
|
buildTraceparent: trace_bridge_1.buildTraceparent,
|
|
113
110
|
injectSnsAttributes: sns_1.injectSnsAttributes,
|
|
114
111
|
injectJobData: job_1.injectJobData,
|
|
115
|
-
wrapSqsConsumer: async (
|
|
116
|
-
wrapJobConsumer: async (
|
|
112
|
+
wrapSqsConsumer: async (_s, _m, fn) => fn(),
|
|
113
|
+
wrapJobConsumer: async (_s, _j, fn) => fn(),
|
|
117
114
|
extractParentContext: () => {
|
|
118
115
|
const { ROOT_CONTEXT } = require('@opentelemetry/api');
|
|
119
116
|
return ROOT_CONTEXT;
|
|
@@ -227,46 +224,54 @@ function init(serviceName, options = {}) {
|
|
|
227
224
|
process.on('SIGTERM', () => void handler());
|
|
228
225
|
process.on('SIGINT', () => void handler());
|
|
229
226
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
+
}
|
|
270
275
|
// Winston auto-wiring
|
|
271
276
|
if (options.winston) {
|
|
272
277
|
const winstonLogger = options.winston;
|
|
@@ -331,7 +336,6 @@ function init(serviceName, options = {}) {
|
|
|
331
336
|
const foam = {
|
|
332
337
|
tracer,
|
|
333
338
|
meter,
|
|
334
|
-
logger: loggerProvider,
|
|
335
339
|
traceProvider,
|
|
336
340
|
meterProvider,
|
|
337
341
|
loggerProvider,
|
|
@@ -349,20 +353,21 @@ function init(serviceName, options = {}) {
|
|
|
349
353
|
const pino = require('pino');
|
|
350
354
|
const mixin = (0, pino_mixin_1.createPinoMixin)();
|
|
351
355
|
const dest = (0, pino_destination_1.createPinoDestination)(loggerProvider, serviceName);
|
|
352
|
-
|
|
356
|
+
const { streams: userStreams, ...pinoOpts } = opts;
|
|
357
|
+
const outputStreams = userStreams ?? [
|
|
353
358
|
{ stream: process.stdout },
|
|
354
|
-
|
|
355
|
-
]));
|
|
359
|
+
];
|
|
360
|
+
return pino({ ...pinoOpts, mixin }, pino.multistream([...outputStreams, { stream: dest }]));
|
|
356
361
|
},
|
|
357
|
-
createPinoHttpConfig: () => {
|
|
362
|
+
createPinoHttpConfig: (opts) => {
|
|
358
363
|
const pinoHttpOpts = (0, express_1.createPinoHttpOptions)();
|
|
359
|
-
return { ...pinoHttpOpts };
|
|
364
|
+
return { ...pinoHttpOpts, ...(opts?.logger && { logger: opts.logger }) };
|
|
360
365
|
},
|
|
361
366
|
buildTraceparent: trace_bridge_1.buildTraceparent,
|
|
362
367
|
injectSnsAttributes: sns_1.injectSnsAttributes,
|
|
363
368
|
injectJobData: job_1.injectJobData,
|
|
364
|
-
wrapSqsConsumer: (
|
|
365
|
-
wrapJobConsumer: (
|
|
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),
|
|
366
371
|
extractParentContext: trace_bridge_1.extractParentContext,
|
|
367
372
|
shutdown,
|
|
368
373
|
};
|
|
@@ -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(
|
|
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(
|
|
76
|
-
wrapJobConsumer(
|
|
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.
|
|
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",
|