@foam-ai/node-cliengo 0.1.0-alpha.2 → 0.1.0-alpha.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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.d.ts +21 -0
- package/dist/node-cliengo/src/http/express.js +69 -5
- package/dist/node-cliengo/src/http/fastify.d.ts +18 -4
- package/dist/node-cliengo/src/http/fastify.js +69 -14
- package/dist/node-cliengo/src/index.d.ts +7 -4
- package/dist/node-cliengo/src/index.js +11 -3
- package/dist/node-cliengo/src/init.d.ts +24 -1
- package/dist/node-cliengo/src/init.js +150 -53
- package/dist/node-cliengo/src/job.d.ts +1 -1
- 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/logs/winston-transport.js +11 -0
- package/dist/node-cliengo/src/nr.d.ts +17 -0
- package/dist/node-cliengo/src/nr.js +29 -0
- package/dist/node-cliengo/src/sns.d.ts +1 -4
- package/dist/node-cliengo/src/sns.js +2 -9
- package/dist/node-cliengo/src/trace-bridge.d.ts +30 -26
- package/dist/node-cliengo/src/trace-bridge.js +66 -35
- package/dist/node-cliengo/src/types.d.ts +37 -4
- package/package.json +11 -9
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
* features (trace propagation, log enrichment, NR bridging) still work.
|
|
26
26
|
* - Token undefined + forceExport: throws (fail-fast — you asked to force
|
|
27
27
|
* export but provided no credentials).
|
|
28
|
+
* - Endpoint is always the production Foam endpoint — not configurable.
|
|
28
29
|
* - Auto-shutdown (SIGTERM/SIGINT): flushes all providers with 5s timeout,
|
|
29
30
|
* then calls process.exit(0). Without the explicit exit, registering
|
|
30
31
|
* process.on('SIGTERM') replaces Node's default terminate behavior — services
|
|
@@ -50,6 +51,8 @@
|
|
|
50
51
|
*/
|
|
51
52
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
52
53
|
exports.init = init;
|
|
54
|
+
exports.getFoam = getFoam;
|
|
55
|
+
exports.createFastifyLoggerConfig = createFastifyLoggerConfig;
|
|
53
56
|
const api_1 = require("@opentelemetry/api");
|
|
54
57
|
const exporter_logs_otlp_http_1 = require("@opentelemetry/exporter-logs-otlp-http");
|
|
55
58
|
const exporter_metrics_otlp_http_1 = require("@opentelemetry/exporter-metrics-otlp-http");
|
|
@@ -73,6 +76,7 @@ const sns_1 = require("./sns");
|
|
|
73
76
|
const sqs_1 = require("./sqs");
|
|
74
77
|
const trace_bridge_1 = require("./trace-bridge");
|
|
75
78
|
const instances = new Map();
|
|
79
|
+
let _processHandlersRegistered = false;
|
|
76
80
|
function createInertInstance(serviceName) {
|
|
77
81
|
const resource = (0, resources_1.resourceFromAttributes)({ 'service.name': serviceName });
|
|
78
82
|
const traceProvider = new sdk_trace_base_1.BasicTracerProvider({ resource });
|
|
@@ -84,7 +88,6 @@ function createInertInstance(serviceName) {
|
|
|
84
88
|
return {
|
|
85
89
|
tracer,
|
|
86
90
|
meter,
|
|
87
|
-
logger: loggerProvider,
|
|
88
91
|
traceProvider,
|
|
89
92
|
meterProvider,
|
|
90
93
|
loggerProvider,
|
|
@@ -98,14 +101,24 @@ function createInertInstance(serviceName) {
|
|
|
98
101
|
}),
|
|
99
102
|
createWinstonTransport: () => ({}),
|
|
100
103
|
createPinoMixin: () => () => ({}),
|
|
104
|
+
createPinoHttpOptions: () => ({ customProps: () => ({}) }),
|
|
101
105
|
createPinoDestination: () => new node_stream_1.Writable({
|
|
102
106
|
write: (_c, _e, cb) => cb(),
|
|
103
107
|
}),
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
createPinoLogger: () => console,
|
|
109
|
+
createPinoHttpConfig: (opts) => (opts?.logger ? { logger: opts.logger } : {}),
|
|
110
|
+
createFastifyLoggerConfig: (opts) => ({ level: opts?.level ?? 'info' }),
|
|
111
|
+
buildTraceparent: trace_bridge_1.buildTraceparent,
|
|
112
|
+
injectSnsAttributes: sns_1.injectSnsAttributes,
|
|
113
|
+
injectJobData: job_1.injectJobData,
|
|
114
|
+
wrapSqsConsumer: async (...args) => {
|
|
115
|
+
const fn = typeof args[0] === 'string' ? args[2] : args[3];
|
|
116
|
+
return fn();
|
|
117
|
+
},
|
|
118
|
+
wrapJobConsumer: async (...args) => {
|
|
119
|
+
const fn = typeof args[0] === 'string' ? args[2] : args[3];
|
|
120
|
+
return fn();
|
|
121
|
+
},
|
|
109
122
|
extractParentContext: () => {
|
|
110
123
|
const { ROOT_CONTEXT } = require('@opentelemetry/api');
|
|
111
124
|
return ROOT_CONTEXT;
|
|
@@ -114,7 +127,7 @@ function createInertInstance(serviceName) {
|
|
|
114
127
|
};
|
|
115
128
|
}
|
|
116
129
|
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call */
|
|
117
|
-
function init(serviceName,
|
|
130
|
+
function init(serviceName, options = {}) {
|
|
118
131
|
const existing = instances.get(serviceName);
|
|
119
132
|
if (existing) {
|
|
120
133
|
// eslint-disable-next-line no-console
|
|
@@ -131,20 +144,21 @@ function init(serviceName, token, options = {}) {
|
|
|
131
144
|
}
|
|
132
145
|
// Resolve NR
|
|
133
146
|
(0, nr_1.resolveNewRelic)(options.newrelic);
|
|
147
|
+
const token = options.token;
|
|
134
148
|
// Production gate
|
|
135
149
|
const isProduction = process.env.NODE_ENV === 'production';
|
|
136
150
|
const shouldExport = isProduction || (options.forceExport === true);
|
|
137
151
|
// Token validation
|
|
138
152
|
if (shouldExport && !token) {
|
|
139
153
|
if (options.forceExport) {
|
|
140
|
-
throw new Error('[foam] forceExport is true but no
|
|
154
|
+
throw new Error('[foam] forceExport is true but no token provided — cannot export telemetry');
|
|
141
155
|
}
|
|
142
156
|
// eslint-disable-next-line no-console
|
|
143
|
-
console.warn('[foam] no
|
|
157
|
+
console.warn('[foam] no token provided — OTLP export disabled');
|
|
144
158
|
}
|
|
145
159
|
const hasToken = Boolean(token);
|
|
146
160
|
const canExport = shouldExport && hasToken;
|
|
147
|
-
const endpoint =
|
|
161
|
+
const endpoint = constants_1.FOAM_OTEL_ENDPOINT;
|
|
148
162
|
const resource = (0, resources_1.resourceFromAttributes)({ 'service.name': serviceName });
|
|
149
163
|
const authHeaders = token
|
|
150
164
|
? { Authorization: `Bearer ${token}` }
|
|
@@ -218,46 +232,54 @@ function init(serviceName, token, options = {}) {
|
|
|
218
232
|
process.on('SIGTERM', () => void handler());
|
|
219
233
|
process.on('SIGINT', () => void handler());
|
|
220
234
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
235
|
+
else {
|
|
236
|
+
process.once('beforeExit', () => {
|
|
237
|
+
if (!_isShutdown) {
|
|
238
|
+
// eslint-disable-next-line no-console
|
|
239
|
+
console.warn('[foam] autoShutdown is false but shutdown() was never called — OTel data may be lost');
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
if (!_processHandlersRegistered) {
|
|
244
|
+
_processHandlersRegistered = true;
|
|
245
|
+
process.on('uncaughtException', (err) => {
|
|
246
|
+
try {
|
|
247
|
+
const span = tracer.startSpan('uncaughtException', {
|
|
248
|
+
attributes: { 'process.event': 'uncaughtException' },
|
|
249
|
+
});
|
|
250
|
+
span.recordException({
|
|
251
|
+
name: err.constructor.name,
|
|
252
|
+
message: err.message,
|
|
253
|
+
stack: err.stack,
|
|
254
|
+
});
|
|
255
|
+
span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: err.message });
|
|
256
|
+
span.end();
|
|
257
|
+
void traceProvider.forceFlush().then(() => shutdown());
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
void shutdown();
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
process.on('unhandledRejection', (reason) => {
|
|
264
|
+
try {
|
|
265
|
+
const err = reason instanceof Error ? reason : new Error(String(reason));
|
|
266
|
+
const span = tracer.startSpan('unhandledRejection', {
|
|
267
|
+
attributes: { 'process.event': 'unhandledRejection' },
|
|
268
|
+
});
|
|
269
|
+
span.recordException({
|
|
270
|
+
name: err.constructor.name,
|
|
271
|
+
message: err.message,
|
|
272
|
+
stack: err.stack,
|
|
273
|
+
});
|
|
274
|
+
span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: err.message });
|
|
275
|
+
span.end();
|
|
276
|
+
void traceProvider.forceFlush();
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
/* best effort */
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
}
|
|
261
283
|
// Winston auto-wiring
|
|
262
284
|
if (options.winston) {
|
|
263
285
|
const winstonLogger = options.winston;
|
|
@@ -322,7 +344,6 @@ function init(serviceName, token, options = {}) {
|
|
|
322
344
|
const foam = {
|
|
323
345
|
tracer,
|
|
324
346
|
meter,
|
|
325
|
-
logger: loggerProvider,
|
|
326
347
|
traceProvider,
|
|
327
348
|
meterProvider,
|
|
328
349
|
loggerProvider,
|
|
@@ -334,12 +355,51 @@ function init(serviceName, token, options = {}) {
|
|
|
334
355
|
createWinstonFormat: () => (0, winston_format_1.createWinstonFormat)(),
|
|
335
356
|
createWinstonTransport: () => (0, winston_transport_1.createWinstonTransport)(loggerProvider, serviceName),
|
|
336
357
|
createPinoMixin: () => (0, pino_mixin_1.createPinoMixin)(),
|
|
358
|
+
createPinoHttpOptions: () => (0, express_1.createPinoHttpOptions)(),
|
|
337
359
|
createPinoDestination: () => (0, pino_destination_1.createPinoDestination)(loggerProvider, serviceName),
|
|
360
|
+
createPinoLogger: (opts = {}) => {
|
|
361
|
+
const pino = require('pino');
|
|
362
|
+
const mixin = (0, pino_mixin_1.createPinoMixin)();
|
|
363
|
+
const dest = (0, pino_destination_1.createPinoDestination)(loggerProvider, serviceName);
|
|
364
|
+
const { streams: userStreams, ...pinoOpts } = opts;
|
|
365
|
+
const outputStreams = userStreams ?? [
|
|
366
|
+
{ stream: process.stdout },
|
|
367
|
+
];
|
|
368
|
+
return pino({ ...pinoOpts, mixin }, pino.multistream([...outputStreams, { stream: dest }]));
|
|
369
|
+
},
|
|
370
|
+
createPinoHttpConfig: (opts) => {
|
|
371
|
+
const pinoHttpOpts = (0, express_1.createPinoHttpOptions)();
|
|
372
|
+
return { ...pinoHttpOpts, ...(opts?.logger && { logger: opts.logger }) };
|
|
373
|
+
},
|
|
374
|
+
createFastifyLoggerConfig: (opts = {}) => {
|
|
375
|
+
const pino = require('pino');
|
|
376
|
+
const mixin = (0, pino_mixin_1.createPinoMixin)();
|
|
377
|
+
const dest = (0, pino_destination_1.createPinoDestination)(loggerProvider, serviceName);
|
|
378
|
+
const { streams: userStreams, ...pinoOpts } = opts;
|
|
379
|
+
const outputStreams = userStreams ?? [
|
|
380
|
+
{ stream: process.stdout },
|
|
381
|
+
];
|
|
382
|
+
return {
|
|
383
|
+
...pinoOpts,
|
|
384
|
+
mixin,
|
|
385
|
+
stream: pino.multistream([...outputStreams, { stream: dest }]),
|
|
386
|
+
};
|
|
387
|
+
},
|
|
338
388
|
buildTraceparent: trace_bridge_1.buildTraceparent,
|
|
339
389
|
injectSnsAttributes: sns_1.injectSnsAttributes,
|
|
340
390
|
injectJobData: job_1.injectJobData,
|
|
341
|
-
wrapSqsConsumer: (
|
|
342
|
-
|
|
391
|
+
wrapSqsConsumer: (...args) => {
|
|
392
|
+
if (typeof args[0] === 'string') {
|
|
393
|
+
return (0, sqs_1.wrapSqsConsumer)(tracer, args[0], args[1], args[2], metrics);
|
|
394
|
+
}
|
|
395
|
+
return (0, sqs_1.wrapSqsConsumer)(tracer, args[1], args[2], args[3], metrics);
|
|
396
|
+
},
|
|
397
|
+
wrapJobConsumer: (...args) => {
|
|
398
|
+
if (typeof args[0] === 'string') {
|
|
399
|
+
return (0, job_1.wrapJobConsumer)(tracer, args[0], args[1], args[2], metrics);
|
|
400
|
+
}
|
|
401
|
+
return (0, job_1.wrapJobConsumer)(tracer, args[1], args[2], args[3], metrics);
|
|
402
|
+
},
|
|
343
403
|
extractParentContext: trace_bridge_1.extractParentContext,
|
|
344
404
|
shutdown,
|
|
345
405
|
};
|
|
@@ -347,3 +407,40 @@ function init(serviceName, token, options = {}) {
|
|
|
347
407
|
return foam;
|
|
348
408
|
}
|
|
349
409
|
/* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call */
|
|
410
|
+
/**
|
|
411
|
+
* Returns the Foam instance for the given service name (or the only instance
|
|
412
|
+
* if there's just one). Call this from any module after init() has run —
|
|
413
|
+
* no circular dependency, no holder pattern needed.
|
|
414
|
+
*/
|
|
415
|
+
function getFoam(serviceName) {
|
|
416
|
+
if (serviceName) {
|
|
417
|
+
return instances.get(serviceName);
|
|
418
|
+
}
|
|
419
|
+
if (instances.size === 1) {
|
|
420
|
+
return instances.values().next().value;
|
|
421
|
+
}
|
|
422
|
+
return undefined;
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Standalone Fastify logger config that handles the getFoam() lookup internally.
|
|
426
|
+
* If Foam is initialized, returns { mixin, stream (multistream with OTLP), ...opts }.
|
|
427
|
+
* If Foam is not initialized (disabled, not yet called), returns opts as-is —
|
|
428
|
+
* Fastify gets a plain Pino logger with no OTLP destination.
|
|
429
|
+
*
|
|
430
|
+
* // No getFoam() check or ternary needed:
|
|
431
|
+
* Fastify({ logger: createFastifyLoggerConfig({ level: 'info' }) })
|
|
432
|
+
*
|
|
433
|
+
* // Dev with pino-pretty:
|
|
434
|
+
* Fastify({ logger: createFastifyLoggerConfig({
|
|
435
|
+
* level: 'debug',
|
|
436
|
+
* streams: [{ stream: pinoPretty() }],
|
|
437
|
+
* })})
|
|
438
|
+
*/
|
|
439
|
+
function createFastifyLoggerConfig(opts = {}) {
|
|
440
|
+
const foam = getFoam();
|
|
441
|
+
if (foam) {
|
|
442
|
+
return foam.createFastifyLoggerConfig(opts);
|
|
443
|
+
}
|
|
444
|
+
const { streams: _streams, ...pinoOpts } = opts;
|
|
445
|
+
return { ...pinoOpts };
|
|
446
|
+
}
|
|
@@ -26,6 +26,6 @@
|
|
|
26
26
|
import { type Tracer } from '@opentelemetry/api';
|
|
27
27
|
import type { FoamMetrics } from './types';
|
|
28
28
|
export declare function injectJobData<T extends Record<string, unknown>>(data: T): T & {
|
|
29
|
-
traceparent
|
|
29
|
+
traceparent: string;
|
|
30
30
|
};
|
|
31
31
|
export declare function wrapJobConsumer(tracer: Tracer, spanName: string, jobData: Record<string, unknown>, fn: () => Promise<void>, metrics?: FoamMetrics): Promise<void>;
|
|
@@ -31,11 +31,7 @@ const api_1 = require("@opentelemetry/api");
|
|
|
31
31
|
const nr_1 = require("./nr");
|
|
32
32
|
const trace_bridge_1 = require("./trace-bridge");
|
|
33
33
|
function injectJobData(data) {
|
|
34
|
-
|
|
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).
|
|
@@ -22,17 +22,22 @@
|
|
|
22
22
|
* 60→FATAL. Unknown numeric levels default to INFO.
|
|
23
23
|
* - Pino uses `msg` for the message field (not `message` like Winston). We
|
|
24
24
|
* check both for compatibility with custom Pino configs.
|
|
25
|
-
* -
|
|
26
|
-
*
|
|
25
|
+
* - Trace context (traceId, spanId, traceparent) is read from the parsed
|
|
26
|
+
* JSON line, NOT from getTraceContext(). The pino mixin injects these at
|
|
27
|
+
* log-write time (when NR's transaction is still active). The stream write
|
|
28
|
+
* happens asynchronously — by then NR's transaction has ended, so calling
|
|
29
|
+
* getTraceContext() here would return empty strings.
|
|
30
|
+
* - Internal Pino fields (pid, hostname, time) and trace fields (traceId,
|
|
31
|
+
* spanId, traceparent) are excluded from forwarded attributes.
|
|
27
32
|
* - Callback always called — never blocks the Pino stream pipeline.
|
|
28
33
|
* - Buffer chunks: converted to string before parsing (handles both Buffer
|
|
29
34
|
* and string inputs from Node streams).
|
|
30
35
|
*/
|
|
31
36
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
37
|
exports.createPinoDestination = createPinoDestination;
|
|
38
|
+
const api_1 = require("@opentelemetry/api");
|
|
33
39
|
const api_logs_1 = require("@opentelemetry/api-logs");
|
|
34
40
|
const node_stream_1 = require("node:stream");
|
|
35
|
-
const trace_bridge_1 = require("../trace-bridge");
|
|
36
41
|
const PINO_LEVEL_MAP = {
|
|
37
42
|
10: api_logs_1.SeverityNumber.TRACE,
|
|
38
43
|
20: api_logs_1.SeverityNumber.DEBUG,
|
|
@@ -66,16 +71,23 @@ function createPinoDestination(loggerProvider, serviceName) {
|
|
|
66
71
|
const msg = parsed.msg ?? parsed.message ?? '';
|
|
67
72
|
const severity = PINO_LEVEL_MAP[level] ?? api_logs_1.SeverityNumber.INFO;
|
|
68
73
|
const severityText = PINO_LEVEL_TEXT[level] ?? 'INFO';
|
|
69
|
-
|
|
74
|
+
// Read trace context from the parsed JSON line — the pino mixin already
|
|
75
|
+
// injected traceId/spanId/traceparent at log-write time (when the NR
|
|
76
|
+
// transaction was still active). Calling getTraceContext() here would be
|
|
77
|
+
// too late — the stream write happens asynchronously after NR's
|
|
78
|
+
// transaction has ended.
|
|
79
|
+
const traceId = parsed.traceId ?? '';
|
|
80
|
+
const spanId = parsed.spanId ?? '';
|
|
70
81
|
const attributes = {};
|
|
71
|
-
if (
|
|
72
|
-
attributes['trace.id'] =
|
|
82
|
+
if (traceId) {
|
|
83
|
+
attributes['trace.id'] = traceId;
|
|
73
84
|
}
|
|
74
|
-
if (
|
|
75
|
-
attributes['span.id'] =
|
|
85
|
+
if (spanId) {
|
|
86
|
+
attributes['span.id'] = spanId;
|
|
76
87
|
}
|
|
88
|
+
const SKIP_KEYS = new Set(['level', 'msg', 'message', 'time', 'pid', 'hostname', 'traceId', 'spanId', 'traceparent']);
|
|
77
89
|
for (const [key, val] of Object.entries(parsed)) {
|
|
78
|
-
if (key
|
|
90
|
+
if (!SKIP_KEYS.has(key)) {
|
|
79
91
|
try {
|
|
80
92
|
attributes[key] = typeof val === 'string' ? val : JSON.stringify(val);
|
|
81
93
|
}
|
|
@@ -84,11 +96,24 @@ function createPinoDestination(loggerProvider, serviceName) {
|
|
|
84
96
|
}
|
|
85
97
|
}
|
|
86
98
|
}
|
|
99
|
+
// Build an OTel Context carrying the SpanContext so the SDK stamps the
|
|
100
|
+
// native traceId/spanId fields on the exported OTLP LogRecord. Without
|
|
101
|
+
// this, backends can't correlate logs to traces.
|
|
102
|
+
let logContext;
|
|
103
|
+
if (traceId && spanId) {
|
|
104
|
+
logContext = api_1.trace.setSpanContext(api_1.ROOT_CONTEXT, {
|
|
105
|
+
traceId,
|
|
106
|
+
spanId,
|
|
107
|
+
traceFlags: 1,
|
|
108
|
+
isRemote: true,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
87
111
|
otelLogger.emit({
|
|
88
112
|
body: msg,
|
|
89
113
|
severityNumber: severity,
|
|
90
114
|
severityText,
|
|
91
115
|
attributes,
|
|
116
|
+
...(logContext && { context: logContext }),
|
|
92
117
|
});
|
|
93
118
|
}
|
|
94
119
|
catch {
|
|
@@ -11,10 +11,11 @@
|
|
|
11
11
|
* Used by kori (Fastify built-in Pino) and cb-proxy (Pino + pino-http).
|
|
12
12
|
*
|
|
13
13
|
* Edge cases covered:
|
|
14
|
-
* - NR not loaded or no active transaction: returns {
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
* - NR not loaded or no active transaction: returns {} so it doesn't
|
|
15
|
+
* overwrite valid trace context injected by pino-http's customProps
|
|
16
|
+
* (Pino serializes mixin fields after customProps — duplicate keys
|
|
17
|
+
* in JSON take the last value).
|
|
18
|
+
* - getTraceContext throws: catch returns {}. Never crashes logging.
|
|
18
19
|
* - Lazy evaluation: the mixin is called at log-write time, not at Pino
|
|
19
20
|
* construction time, so trace context is always current.
|
|
20
21
|
* - Works with pino-pretty in development — fields appear in pretty output.
|
|
@@ -12,10 +12,11 @@
|
|
|
12
12
|
* Used by kori (Fastify built-in Pino) and cb-proxy (Pino + pino-http).
|
|
13
13
|
*
|
|
14
14
|
* Edge cases covered:
|
|
15
|
-
* - NR not loaded or no active transaction: returns {
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* - NR not loaded or no active transaction: returns {} so it doesn't
|
|
16
|
+
* overwrite valid trace context injected by pino-http's customProps
|
|
17
|
+
* (Pino serializes mixin fields after customProps — duplicate keys
|
|
18
|
+
* in JSON take the last value).
|
|
19
|
+
* - getTraceContext throws: catch returns {}. Never crashes logging.
|
|
19
20
|
* - Lazy evaluation: the mixin is called at log-write time, not at Pino
|
|
20
21
|
* construction time, so trace context is always current.
|
|
21
22
|
* - Works with pino-pretty in development — fields appear in pretty output.
|
|
@@ -27,6 +28,9 @@ function createPinoMixin() {
|
|
|
27
28
|
return () => {
|
|
28
29
|
try {
|
|
29
30
|
const ctx = (0, trace_bridge_1.getTraceContext)();
|
|
31
|
+
if (!ctx.traceId) {
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
30
34
|
return {
|
|
31
35
|
traceId: ctx.traceId,
|
|
32
36
|
spanId: ctx.spanId,
|
|
@@ -34,7 +38,7 @@ function createPinoMixin() {
|
|
|
34
38
|
};
|
|
35
39
|
}
|
|
36
40
|
catch {
|
|
37
|
-
return {
|
|
41
|
+
return {};
|
|
38
42
|
}
|
|
39
43
|
};
|
|
40
44
|
}
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
*/
|
|
30
30
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
31
31
|
exports.createWinstonTransport = createWinstonTransport;
|
|
32
|
+
const api_1 = require("@opentelemetry/api");
|
|
32
33
|
const api_logs_1 = require("@opentelemetry/api-logs");
|
|
33
34
|
const node_stream_1 = require("node:stream");
|
|
34
35
|
const trace_bridge_1 = require("../trace-bridge");
|
|
@@ -69,11 +70,21 @@ function createWinstonTransport(loggerProvider, serviceName) {
|
|
|
69
70
|
}
|
|
70
71
|
}
|
|
71
72
|
}
|
|
73
|
+
let logContext;
|
|
74
|
+
if (ctx.traceId && ctx.spanId) {
|
|
75
|
+
logContext = api_1.trace.setSpanContext(api_1.ROOT_CONTEXT, {
|
|
76
|
+
traceId: ctx.traceId,
|
|
77
|
+
spanId: ctx.spanId,
|
|
78
|
+
traceFlags: 1,
|
|
79
|
+
isRemote: true,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
72
82
|
otelLogger.emit({
|
|
73
83
|
body: message,
|
|
74
84
|
severityNumber: severity,
|
|
75
85
|
severityText: level.toUpperCase(),
|
|
76
86
|
attributes,
|
|
87
|
+
...(logContext && { context: logContext }),
|
|
77
88
|
});
|
|
78
89
|
}
|
|
79
90
|
catch {
|
|
@@ -33,3 +33,20 @@ import type { NewRelicAgent } from './types';
|
|
|
33
33
|
export declare function resolveNewRelic(explicit?: NewRelicAgent): NewRelicAgent;
|
|
34
34
|
export declare function getNr(): NewRelicAgent;
|
|
35
35
|
export declare function isNrLoaded(): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Loads the New Relic agent via CJS require — safe to call from ESM services.
|
|
38
|
+
* Eliminates the createRequire boilerplate:
|
|
39
|
+
*
|
|
40
|
+
* // Before (ESM service):
|
|
41
|
+
* import { createRequire } from 'node:module';
|
|
42
|
+
* const require = createRequire(import.meta.url);
|
|
43
|
+
* let nr; try { nr = require('newrelic'); } catch {}
|
|
44
|
+
* init('kori', { newrelic: nr });
|
|
45
|
+
*
|
|
46
|
+
* // After:
|
|
47
|
+
* import { init, loadNewRelic } from '@foam-ai/node-cliengo';
|
|
48
|
+
* init('kori', { newrelic: loadNewRelic() });
|
|
49
|
+
*
|
|
50
|
+
* Returns undefined if the newrelic package is not installed.
|
|
51
|
+
*/
|
|
52
|
+
export declare function loadNewRelic(): NewRelicAgent | undefined;
|
|
@@ -34,6 +34,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
34
34
|
exports.resolveNewRelic = resolveNewRelic;
|
|
35
35
|
exports.getNr = getNr;
|
|
36
36
|
exports.isNrLoaded = isNrLoaded;
|
|
37
|
+
exports.loadNewRelic = loadNewRelic;
|
|
37
38
|
const NOOP_NR = {
|
|
38
39
|
getTransaction: () => null,
|
|
39
40
|
startBackgroundTransaction: (_name, _group, handler) => typeof handler === 'function' ? handler() : undefined,
|
|
@@ -69,3 +70,31 @@ function getNr() {
|
|
|
69
70
|
function isNrLoaded() {
|
|
70
71
|
return cachedNr !== null && cachedNr !== NOOP_NR;
|
|
71
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Loads the New Relic agent via CJS require — safe to call from ESM services.
|
|
75
|
+
* Eliminates the createRequire boilerplate:
|
|
76
|
+
*
|
|
77
|
+
* // Before (ESM service):
|
|
78
|
+
* import { createRequire } from 'node:module';
|
|
79
|
+
* const require = createRequire(import.meta.url);
|
|
80
|
+
* let nr; try { nr = require('newrelic'); } catch {}
|
|
81
|
+
* init('kori', { newrelic: nr });
|
|
82
|
+
*
|
|
83
|
+
* // After:
|
|
84
|
+
* import { init, loadNewRelic } from '@foam-ai/node-cliengo';
|
|
85
|
+
* init('kori', { newrelic: loadNewRelic() });
|
|
86
|
+
*
|
|
87
|
+
* Returns undefined if the newrelic package is not installed.
|
|
88
|
+
*/
|
|
89
|
+
function loadNewRelic() {
|
|
90
|
+
try {
|
|
91
|
+
/* eslint-disable @typescript-eslint/no-require-imports */
|
|
92
|
+
const nr = require('newrelic');
|
|
93
|
+
/* eslint-enable @typescript-eslint/no-require-imports */
|
|
94
|
+
cachedNr = nr;
|
|
95
|
+
return nr;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -8,10 +8,7 @@
|
|
|
8
8
|
* alongside the existing ones. This function spread-merges it in.
|
|
9
9
|
*
|
|
10
10
|
* Edge cases covered:
|
|
11
|
-
* -
|
|
12
|
-
* original attributes are returned unchanged — no traceparent key added.
|
|
13
|
-
* - Empty attrs {}: returns just { traceparent: ... } if NR is active,
|
|
14
|
-
* or {} if not. Never crashes.
|
|
11
|
+
* - Empty attrs {}: returns { traceparent: ... }. Never crashes.
|
|
15
12
|
* - Existing traceparent in attrs: overwritten with the current value
|
|
16
13
|
* (spread puts our key last).
|
|
17
14
|
*/
|
|
@@ -9,10 +9,7 @@
|
|
|
9
9
|
* alongside the existing ones. This function spread-merges it in.
|
|
10
10
|
*
|
|
11
11
|
* Edge cases covered:
|
|
12
|
-
* -
|
|
13
|
-
* original attributes are returned unchanged — no traceparent key added.
|
|
14
|
-
* - Empty attrs {}: returns just { traceparent: ... } if NR is active,
|
|
15
|
-
* or {} if not. Never crashes.
|
|
12
|
+
* - Empty attrs {}: returns { traceparent: ... }. Never crashes.
|
|
16
13
|
* - Existing traceparent in attrs: overwritten with the current value
|
|
17
14
|
* (spread puts our key last).
|
|
18
15
|
*/
|
|
@@ -20,12 +17,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
20
17
|
exports.injectSnsAttributes = injectSnsAttributes;
|
|
21
18
|
const trace_bridge_1 = require("./trace-bridge");
|
|
22
19
|
function injectSnsAttributes(attrs) {
|
|
23
|
-
const tp = (0, trace_bridge_1.buildTraceparent)();
|
|
24
|
-
if (!tp) {
|
|
25
|
-
return attrs;
|
|
26
|
-
}
|
|
27
20
|
return {
|
|
28
21
|
...attrs,
|
|
29
|
-
traceparent: { DataType: 'String', StringValue:
|
|
22
|
+
traceparent: { DataType: 'String', StringValue: (0, trace_bridge_1.buildTraceparent)() },
|
|
30
23
|
};
|
|
31
24
|
}
|