@foam-ai/node-cliengo 0.1.0-alpha.1
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 +26 -0
- package/dist/node-cliengo/src/constants.js +49 -0
- package/dist/node-cliengo/src/http/express.d.ts +41 -0
- package/dist/node-cliengo/src/http/express.js +123 -0
- package/dist/node-cliengo/src/http/fastify.d.ts +28 -0
- package/dist/node-cliengo/src/http/fastify.js +101 -0
- package/dist/node-cliengo/src/http/request-context.d.ts +32 -0
- package/dist/node-cliengo/src/http/request-context.js +114 -0
- package/dist/node-cliengo/src/index.d.ts +26 -0
- package/dist/node-cliengo/src/index.js +42 -0
- package/dist/node-cliengo/src/init.d.ts +50 -0
- package/dist/node-cliengo/src/init.js +348 -0
- package/dist/node-cliengo/src/job.d.ts +31 -0
- package/dist/node-cliengo/src/job.js +86 -0
- package/dist/node-cliengo/src/logs/pino-destination.d.ts +31 -0
- package/dist/node-cliengo/src/logs/pino-destination.js +100 -0
- package/dist/node-cliengo/src/logs/pino-mixin.d.ts +22 -0
- package/dist/node-cliengo/src/logs/pino-mixin.js +40 -0
- package/dist/node-cliengo/src/logs/winston-format.d.ts +28 -0
- package/dist/node-cliengo/src/logs/winston-format.js +48 -0
- package/dist/node-cliengo/src/logs/winston-transport.d.ts +31 -0
- package/dist/node-cliengo/src/logs/winston-transport.js +93 -0
- package/dist/node-cliengo/src/metrics/instruments.d.ts +29 -0
- package/dist/node-cliengo/src/metrics/instruments.js +78 -0
- package/dist/node-cliengo/src/nr.d.ts +35 -0
- package/dist/node-cliengo/src/nr.js +71 -0
- package/dist/node-cliengo/src/sns.d.ts +19 -0
- package/dist/node-cliengo/src/sns.js +31 -0
- package/dist/node-cliengo/src/sqs.d.ts +35 -0
- package/dist/node-cliengo/src/sqs.js +110 -0
- package/dist/node-cliengo/src/trace-bridge.d.ts +39 -0
- package/dist/node-cliengo/src/trace-bridge.js +79 -0
- package/dist/node-cliengo/src/types.d.ts +115 -0
- package/dist/node-cliengo/src/types.js +26 -0
- package/dist/shared/constants.d.ts +1 -0
- package/dist/shared/constants.js +4 -0
- package/dist/shared/util.d.ts +9 -0
- package/dist/shared/util.js +21 -0
- package/package.json +54 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Winston format for trace context enrichment (metadata injection only).
|
|
3
|
+
*
|
|
4
|
+
* Why this file exists:
|
|
5
|
+
* NR correlates logs with traces when log lines contain `trace.id` / `span.id`.
|
|
6
|
+
* NR gets logs via stdout forwarding, so the trace context must be in the log
|
|
7
|
+
* output. This format injects { traceId, spanId, traceparent } into every
|
|
8
|
+
* Winston log entry's metadata. It does NOT send logs anywhere — that's the
|
|
9
|
+
* transport's job. It does NOT change the output format (JSON vs text is still
|
|
10
|
+
* the service's choice).
|
|
11
|
+
*
|
|
12
|
+
* Used by combee, gpt-intentions, and hsm-backend (all Winston).
|
|
13
|
+
*
|
|
14
|
+
* Edge cases covered:
|
|
15
|
+
* - NR not loaded: getTraceContext returns empty strings. Fields are still
|
|
16
|
+
* added (as empty strings) — consistent schema for log parsers.
|
|
17
|
+
* - No active NR transaction: same as above.
|
|
18
|
+
* - Combined with format.json(): traceId/spanId appear as top-level JSON keys.
|
|
19
|
+
* - Combined with format.printf(): fields available on info.traceId etc.
|
|
20
|
+
* - Placed first in format.combine() chain so all subsequent formatters have
|
|
21
|
+
* access to trace context.
|
|
22
|
+
* - getTraceContext is lazy (called at log-write time, not at format creation
|
|
23
|
+
* time) so the trace context is always current for the active transaction.
|
|
24
|
+
* - If getTraceContext throws for any reason, the log entry passes through
|
|
25
|
+
* unmodified — never crashes logging.
|
|
26
|
+
*/
|
|
27
|
+
import type { WinstonFormat } from '../types';
|
|
28
|
+
export declare function createWinstonFormat(): WinstonFormat;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Winston format for trace context enrichment (metadata injection only).
|
|
4
|
+
*
|
|
5
|
+
* Why this file exists:
|
|
6
|
+
* NR correlates logs with traces when log lines contain `trace.id` / `span.id`.
|
|
7
|
+
* NR gets logs via stdout forwarding, so the trace context must be in the log
|
|
8
|
+
* output. This format injects { traceId, spanId, traceparent } into every
|
|
9
|
+
* Winston log entry's metadata. It does NOT send logs anywhere — that's the
|
|
10
|
+
* transport's job. It does NOT change the output format (JSON vs text is still
|
|
11
|
+
* the service's choice).
|
|
12
|
+
*
|
|
13
|
+
* Used by combee, gpt-intentions, and hsm-backend (all Winston).
|
|
14
|
+
*
|
|
15
|
+
* Edge cases covered:
|
|
16
|
+
* - NR not loaded: getTraceContext returns empty strings. Fields are still
|
|
17
|
+
* added (as empty strings) — consistent schema for log parsers.
|
|
18
|
+
* - No active NR transaction: same as above.
|
|
19
|
+
* - Combined with format.json(): traceId/spanId appear as top-level JSON keys.
|
|
20
|
+
* - Combined with format.printf(): fields available on info.traceId etc.
|
|
21
|
+
* - Placed first in format.combine() chain so all subsequent formatters have
|
|
22
|
+
* access to trace context.
|
|
23
|
+
* - getTraceContext is lazy (called at log-write time, not at format creation
|
|
24
|
+
* time) so the trace context is always current for the active transaction.
|
|
25
|
+
* - If getTraceContext throws for any reason, the log entry passes through
|
|
26
|
+
* unmodified — never crashes logging.
|
|
27
|
+
*/
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.createWinstonFormat = createWinstonFormat;
|
|
30
|
+
const trace_bridge_1 = require("../trace-bridge");
|
|
31
|
+
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */
|
|
32
|
+
function createWinstonFormat() {
|
|
33
|
+
return {
|
|
34
|
+
transform(info) {
|
|
35
|
+
try {
|
|
36
|
+
const ctx = (0, trace_bridge_1.getTraceContext)();
|
|
37
|
+
info.traceId = ctx.traceId;
|
|
38
|
+
info.spanId = ctx.spanId;
|
|
39
|
+
info.traceparent = ctx.traceparent;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
/* never crash logging */
|
|
43
|
+
}
|
|
44
|
+
return info;
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Winston transport that delivers every log entry to Foam via the OTLP logs
|
|
3
|
+
* pipeline (non-global LoggerProvider → BatchLogRecordProcessor → OTLPLogExporter).
|
|
4
|
+
*
|
|
5
|
+
* Why this file exists:
|
|
6
|
+
* NR gets logs via stdout forwarding (unchanged). Foam needs a separate
|
|
7
|
+
* delivery path. This transport converts each Winston log entry into an OTel
|
|
8
|
+
* log record with severity mapping and trace context, then emits it through
|
|
9
|
+
* our non-global LoggerProvider. No double-logging to the same destination —
|
|
10
|
+
* NR gets stdout, Foam gets OTLP.
|
|
11
|
+
*
|
|
12
|
+
* Used by combee, gpt-intentions, and hsm-backend.
|
|
13
|
+
*
|
|
14
|
+
* Edge cases covered:
|
|
15
|
+
* - handleExceptions: false, handleRejections: false — critical. combee
|
|
16
|
+
* creates 3-5 Winston loggers, each with existing transports that already
|
|
17
|
+
* listen for uncaught events. Adding more would trigger Node's
|
|
18
|
+
* MaxListenersExceededWarning. Process exceptions are captured separately
|
|
19
|
+
* by init()'s own handlers.
|
|
20
|
+
* - objectMode: true — Winston transports receive structured info objects,
|
|
21
|
+
* not string chunks.
|
|
22
|
+
* - Severity mapping: error→ERROR, warn→WARN, info/http→INFO, verbose/debug→DEBUG,
|
|
23
|
+
* silly→TRACE. Unknown levels default to INFO.
|
|
24
|
+
* - Unserializable attribute values (circular refs, symbols): silently skipped
|
|
25
|
+
* per-key — other attributes still flow.
|
|
26
|
+
* - Winston's internal Symbol(level) key is skipped in attribute iteration.
|
|
27
|
+
* - Transport callback always called — never blocks the Winston pipeline.
|
|
28
|
+
*/
|
|
29
|
+
import type { LoggerProvider } from '@opentelemetry/sdk-logs';
|
|
30
|
+
import type { WinstonTransport } from '../types';
|
|
31
|
+
export declare function createWinstonTransport(loggerProvider: LoggerProvider, serviceName: string): WinstonTransport;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Winston transport that delivers every log entry to Foam via the OTLP logs
|
|
4
|
+
* pipeline (non-global LoggerProvider → BatchLogRecordProcessor → OTLPLogExporter).
|
|
5
|
+
*
|
|
6
|
+
* Why this file exists:
|
|
7
|
+
* NR gets logs via stdout forwarding (unchanged). Foam needs a separate
|
|
8
|
+
* delivery path. This transport converts each Winston log entry into an OTel
|
|
9
|
+
* log record with severity mapping and trace context, then emits it through
|
|
10
|
+
* our non-global LoggerProvider. No double-logging to the same destination —
|
|
11
|
+
* NR gets stdout, Foam gets OTLP.
|
|
12
|
+
*
|
|
13
|
+
* Used by combee, gpt-intentions, and hsm-backend.
|
|
14
|
+
*
|
|
15
|
+
* Edge cases covered:
|
|
16
|
+
* - handleExceptions: false, handleRejections: false — critical. combee
|
|
17
|
+
* creates 3-5 Winston loggers, each with existing transports that already
|
|
18
|
+
* listen for uncaught events. Adding more would trigger Node's
|
|
19
|
+
* MaxListenersExceededWarning. Process exceptions are captured separately
|
|
20
|
+
* by init()'s own handlers.
|
|
21
|
+
* - objectMode: true — Winston transports receive structured info objects,
|
|
22
|
+
* not string chunks.
|
|
23
|
+
* - Severity mapping: error→ERROR, warn→WARN, info/http→INFO, verbose/debug→DEBUG,
|
|
24
|
+
* silly→TRACE. Unknown levels default to INFO.
|
|
25
|
+
* - Unserializable attribute values (circular refs, symbols): silently skipped
|
|
26
|
+
* per-key — other attributes still flow.
|
|
27
|
+
* - Winston's internal Symbol(level) key is skipped in attribute iteration.
|
|
28
|
+
* - Transport callback always called — never blocks the Winston pipeline.
|
|
29
|
+
*/
|
|
30
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
31
|
+
exports.createWinstonTransport = createWinstonTransport;
|
|
32
|
+
const api_logs_1 = require("@opentelemetry/api-logs");
|
|
33
|
+
const node_stream_1 = require("node:stream");
|
|
34
|
+
const trace_bridge_1 = require("../trace-bridge");
|
|
35
|
+
const SEVERITY_MAP = {
|
|
36
|
+
error: api_logs_1.SeverityNumber.ERROR,
|
|
37
|
+
warn: api_logs_1.SeverityNumber.WARN,
|
|
38
|
+
info: api_logs_1.SeverityNumber.INFO,
|
|
39
|
+
http: api_logs_1.SeverityNumber.INFO,
|
|
40
|
+
verbose: api_logs_1.SeverityNumber.DEBUG,
|
|
41
|
+
debug: api_logs_1.SeverityNumber.DEBUG,
|
|
42
|
+
silly: api_logs_1.SeverityNumber.TRACE,
|
|
43
|
+
};
|
|
44
|
+
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */
|
|
45
|
+
function createWinstonTransport(loggerProvider, serviceName) {
|
|
46
|
+
const otelLogger = loggerProvider.getLogger(serviceName);
|
|
47
|
+
const stream = new node_stream_1.Writable({
|
|
48
|
+
objectMode: true,
|
|
49
|
+
write(info, _encoding, callback) {
|
|
50
|
+
try {
|
|
51
|
+
const level = info.level ?? 'info';
|
|
52
|
+
const message = info.message ?? '';
|
|
53
|
+
const severity = SEVERITY_MAP[level] ?? api_logs_1.SeverityNumber.INFO;
|
|
54
|
+
const ctx = (0, trace_bridge_1.getTraceContext)();
|
|
55
|
+
const attributes = {};
|
|
56
|
+
if (ctx.traceId) {
|
|
57
|
+
attributes['trace.id'] = ctx.traceId;
|
|
58
|
+
}
|
|
59
|
+
if (ctx.spanId) {
|
|
60
|
+
attributes['span.id'] = ctx.spanId;
|
|
61
|
+
}
|
|
62
|
+
for (const [key, val] of Object.entries(info)) {
|
|
63
|
+
if (key !== 'level' && key !== 'message' && key !== 'timestamp' && key !== 'Symbol(level)') {
|
|
64
|
+
try {
|
|
65
|
+
attributes[key] = typeof val === 'string' ? val : JSON.stringify(val);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
/* skip unserializable */
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
otelLogger.emit({
|
|
73
|
+
body: message,
|
|
74
|
+
severityNumber: severity,
|
|
75
|
+
severityText: level.toUpperCase(),
|
|
76
|
+
attributes,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
/* never crash logging */
|
|
81
|
+
}
|
|
82
|
+
callback();
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
// Winston transport contract: the stream with transport metadata
|
|
86
|
+
const transport = stream;
|
|
87
|
+
transport.name = 'foam-otel';
|
|
88
|
+
transport.level = undefined; // Accept all levels
|
|
89
|
+
transport.handleExceptions = false;
|
|
90
|
+
transport.handleRejections = false;
|
|
91
|
+
return transport;
|
|
92
|
+
}
|
|
93
|
+
/* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-built OTel metric instruments for queue processing, plus factories
|
|
3
|
+
* for service-specific custom metrics.
|
|
4
|
+
*
|
|
5
|
+
* Why this file exists:
|
|
6
|
+
* Provides three auto-recorded instruments that wrapSqsConsumer and
|
|
7
|
+
* wrapJobConsumer populate automatically:
|
|
8
|
+
* - messageProcessingDuration (histogram, ms): how long each message takes
|
|
9
|
+
* - messageProcessingErrors (counter): how many processing attempts fail
|
|
10
|
+
* - messagesProcessed (counter): total messages processed (success + error)
|
|
11
|
+
*
|
|
12
|
+
* Also provides customHistogram/customCounter/customGauge factories for
|
|
13
|
+
* service-specific domain metrics (LLM duration in gpt-intentions, webhook
|
|
14
|
+
* processing time in cb-proxy, automation duration in kori).
|
|
15
|
+
*
|
|
16
|
+
* Edge cases covered:
|
|
17
|
+
* - Custom instrument idempotency: calling customHistogram('llm.duration', ...)
|
|
18
|
+
* twice returns the same Histogram instance (cached by name). OTel SDKs
|
|
19
|
+
* already handle duplicate instrument creation, but caching avoids the
|
|
20
|
+
* overhead of repeated createHistogram calls in hot paths.
|
|
21
|
+
* - MeterProvider is non-global — our metrics never conflict with NR's
|
|
22
|
+
* internal metrics or any other OTel MeterProvider in the process.
|
|
23
|
+
* - PeriodicExportingMetricReader (configured in init.ts) exports every 60s
|
|
24
|
+
* by default. If no exporter is configured (non-production), no reader is
|
|
25
|
+
* attached and no timer runs.
|
|
26
|
+
*/
|
|
27
|
+
import type { MeterProvider } from '@opentelemetry/sdk-metrics';
|
|
28
|
+
import type { BridgeMetrics } from '../types';
|
|
29
|
+
export declare function createBridgeMetrics(meterProvider: MeterProvider, serviceName: string): BridgeMetrics;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Pre-built OTel metric instruments for queue processing, plus factories
|
|
4
|
+
* for service-specific custom metrics.
|
|
5
|
+
*
|
|
6
|
+
* Why this file exists:
|
|
7
|
+
* Provides three auto-recorded instruments that wrapSqsConsumer and
|
|
8
|
+
* wrapJobConsumer populate automatically:
|
|
9
|
+
* - messageProcessingDuration (histogram, ms): how long each message takes
|
|
10
|
+
* - messageProcessingErrors (counter): how many processing attempts fail
|
|
11
|
+
* - messagesProcessed (counter): total messages processed (success + error)
|
|
12
|
+
*
|
|
13
|
+
* Also provides customHistogram/customCounter/customGauge factories for
|
|
14
|
+
* service-specific domain metrics (LLM duration in gpt-intentions, webhook
|
|
15
|
+
* processing time in cb-proxy, automation duration in kori).
|
|
16
|
+
*
|
|
17
|
+
* Edge cases covered:
|
|
18
|
+
* - Custom instrument idempotency: calling customHistogram('llm.duration', ...)
|
|
19
|
+
* twice returns the same Histogram instance (cached by name). OTel SDKs
|
|
20
|
+
* already handle duplicate instrument creation, but caching avoids the
|
|
21
|
+
* overhead of repeated createHistogram calls in hot paths.
|
|
22
|
+
* - MeterProvider is non-global — our metrics never conflict with NR's
|
|
23
|
+
* internal metrics or any other OTel MeterProvider in the process.
|
|
24
|
+
* - PeriodicExportingMetricReader (configured in init.ts) exports every 60s
|
|
25
|
+
* by default. If no exporter is configured (non-production), no reader is
|
|
26
|
+
* attached and no timer runs.
|
|
27
|
+
*/
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.createBridgeMetrics = createBridgeMetrics;
|
|
30
|
+
function createBridgeMetrics(meterProvider, serviceName) {
|
|
31
|
+
const meter = meterProvider.getMeter(serviceName);
|
|
32
|
+
const messageProcessingDuration = meter.createHistogram('messaging.process.duration', {
|
|
33
|
+
description: 'Time to process a queue message',
|
|
34
|
+
unit: 'ms',
|
|
35
|
+
});
|
|
36
|
+
const messageProcessingErrors = meter.createCounter('messaging.process.errors', {
|
|
37
|
+
description: 'Number of queue message processing errors',
|
|
38
|
+
});
|
|
39
|
+
const messagesProcessed = meter.createCounter('messaging.process.count', {
|
|
40
|
+
description: 'Number of queue messages processed',
|
|
41
|
+
});
|
|
42
|
+
const instrumentCache = new Map();
|
|
43
|
+
return {
|
|
44
|
+
messageProcessingDuration,
|
|
45
|
+
messageProcessingErrors,
|
|
46
|
+
messagesProcessed,
|
|
47
|
+
customHistogram(name, description, unit) {
|
|
48
|
+
const key = `histogram:${name}`;
|
|
49
|
+
const existing = instrumentCache.get(key);
|
|
50
|
+
if (existing) {
|
|
51
|
+
return existing;
|
|
52
|
+
}
|
|
53
|
+
const h = meter.createHistogram(name, { description, unit });
|
|
54
|
+
instrumentCache.set(key, h);
|
|
55
|
+
return h;
|
|
56
|
+
},
|
|
57
|
+
customCounter(name, description, unit) {
|
|
58
|
+
const key = `counter:${name}`;
|
|
59
|
+
const existing = instrumentCache.get(key);
|
|
60
|
+
if (existing) {
|
|
61
|
+
return existing;
|
|
62
|
+
}
|
|
63
|
+
const c = meter.createCounter(name, { description, unit });
|
|
64
|
+
instrumentCache.set(key, c);
|
|
65
|
+
return c;
|
|
66
|
+
},
|
|
67
|
+
customGauge(name, description, unit) {
|
|
68
|
+
const key = `gauge:${name}`;
|
|
69
|
+
const existing = instrumentCache.get(key);
|
|
70
|
+
if (existing) {
|
|
71
|
+
return existing;
|
|
72
|
+
}
|
|
73
|
+
const g = meter.createObservableGauge(name, { description, unit });
|
|
74
|
+
instrumentCache.set(key, g);
|
|
75
|
+
return g;
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* New Relic agent detection and lazy resolution.
|
|
3
|
+
*
|
|
4
|
+
* Why this file exists:
|
|
5
|
+
* The library must NEVER call `require('newrelic')` directly — doing so would
|
|
6
|
+
* start the NR agent in services that explicitly disable it (hsm-backend uses
|
|
7
|
+
* a conditional `NEW_RELIC_LOG_ENABLED` check). Instead, we detect NR via
|
|
8
|
+
* `require.cache` — if the service already loaded NR, we reuse that instance.
|
|
9
|
+
* If not, all NR calls silently no-op via the NOOP_NR stub.
|
|
10
|
+
*
|
|
11
|
+
* Resolution strategy:
|
|
12
|
+
* 1. Explicit: `init('svc', token, { newrelic: nrInstance })` — used by
|
|
13
|
+
* kori (ESM + pnpm, where require.cache detection is unreliable) and
|
|
14
|
+
* hsm-backend (conditional NR loading).
|
|
15
|
+
* 2. Module cache: `require.cache[require.resolve('newrelic')]` — works for
|
|
16
|
+
* CJS services (combee, gpt-intentions, cb-proxy) where NR is loaded at
|
|
17
|
+
* the top of the entry file before the bridge initializes.
|
|
18
|
+
* 3. Fallback: NOOP_NR stub that returns empty trace metadata and calls
|
|
19
|
+
* handler functions directly without creating background transactions.
|
|
20
|
+
*
|
|
21
|
+
* Edge cases covered:
|
|
22
|
+
* - `require.resolve('newrelic')` throws if newrelic is not installed at all.
|
|
23
|
+
* Caught silently — returns NOOP_NR.
|
|
24
|
+
* - NR's stub API (agent disabled via config) returns `{ traceId: '', spanId: '' }`
|
|
25
|
+
* from `getTraceMetadata()` — our code treats empty strings as "no trace,"
|
|
26
|
+
* so buildTraceparent() returns undefined. No crash.
|
|
27
|
+
* - `startBackgroundTransaction` in the stub just calls the handler directly
|
|
28
|
+
* and returns its result, preserving async/await flow.
|
|
29
|
+
* - Once resolved, the NR instance is cached so subsequent calls to `getNr()`
|
|
30
|
+
* don't re-probe the module cache.
|
|
31
|
+
*/
|
|
32
|
+
import type { NewRelicAgent } from './types';
|
|
33
|
+
export declare function resolveNewRelic(explicit?: NewRelicAgent): NewRelicAgent;
|
|
34
|
+
export declare function getNr(): NewRelicAgent;
|
|
35
|
+
export declare function isNrLoaded(): boolean;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* New Relic agent detection and lazy resolution.
|
|
4
|
+
*
|
|
5
|
+
* Why this file exists:
|
|
6
|
+
* The library must NEVER call `require('newrelic')` directly — doing so would
|
|
7
|
+
* start the NR agent in services that explicitly disable it (hsm-backend uses
|
|
8
|
+
* a conditional `NEW_RELIC_LOG_ENABLED` check). Instead, we detect NR via
|
|
9
|
+
* `require.cache` — if the service already loaded NR, we reuse that instance.
|
|
10
|
+
* If not, all NR calls silently no-op via the NOOP_NR stub.
|
|
11
|
+
*
|
|
12
|
+
* Resolution strategy:
|
|
13
|
+
* 1. Explicit: `init('svc', token, { newrelic: nrInstance })` — used by
|
|
14
|
+
* kori (ESM + pnpm, where require.cache detection is unreliable) and
|
|
15
|
+
* hsm-backend (conditional NR loading).
|
|
16
|
+
* 2. Module cache: `require.cache[require.resolve('newrelic')]` — works for
|
|
17
|
+
* CJS services (combee, gpt-intentions, cb-proxy) where NR is loaded at
|
|
18
|
+
* the top of the entry file before the bridge initializes.
|
|
19
|
+
* 3. Fallback: NOOP_NR stub that returns empty trace metadata and calls
|
|
20
|
+
* handler functions directly without creating background transactions.
|
|
21
|
+
*
|
|
22
|
+
* Edge cases covered:
|
|
23
|
+
* - `require.resolve('newrelic')` throws if newrelic is not installed at all.
|
|
24
|
+
* Caught silently — returns NOOP_NR.
|
|
25
|
+
* - NR's stub API (agent disabled via config) returns `{ traceId: '', spanId: '' }`
|
|
26
|
+
* from `getTraceMetadata()` — our code treats empty strings as "no trace,"
|
|
27
|
+
* so buildTraceparent() returns undefined. No crash.
|
|
28
|
+
* - `startBackgroundTransaction` in the stub just calls the handler directly
|
|
29
|
+
* and returns its result, preserving async/await flow.
|
|
30
|
+
* - Once resolved, the NR instance is cached so subsequent calls to `getNr()`
|
|
31
|
+
* don't re-probe the module cache.
|
|
32
|
+
*/
|
|
33
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
34
|
+
exports.resolveNewRelic = resolveNewRelic;
|
|
35
|
+
exports.getNr = getNr;
|
|
36
|
+
exports.isNrLoaded = isNrLoaded;
|
|
37
|
+
const NOOP_NR = {
|
|
38
|
+
getTransaction: () => null,
|
|
39
|
+
startBackgroundTransaction: (_name, _group, handler) => typeof handler === 'function' ? handler() : undefined,
|
|
40
|
+
getTraceMetadata: () => ({ traceId: '', spanId: '' }),
|
|
41
|
+
acceptDistributedTraceHeaders: () => undefined,
|
|
42
|
+
};
|
|
43
|
+
let cachedNr = null;
|
|
44
|
+
function resolveNewRelic(explicit) {
|
|
45
|
+
if (explicit) {
|
|
46
|
+
cachedNr = explicit;
|
|
47
|
+
return explicit;
|
|
48
|
+
}
|
|
49
|
+
if (cachedNr) {
|
|
50
|
+
return cachedNr;
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
/* eslint-disable @typescript-eslint/no-require-imports */
|
|
54
|
+
const resolved = require.resolve('newrelic');
|
|
55
|
+
if (require.cache[resolved]) {
|
|
56
|
+
cachedNr = require('newrelic');
|
|
57
|
+
return cachedNr;
|
|
58
|
+
}
|
|
59
|
+
/* eslint-enable @typescript-eslint/no-require-imports */
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// NR not available
|
|
63
|
+
}
|
|
64
|
+
return NOOP_NR;
|
|
65
|
+
}
|
|
66
|
+
function getNr() {
|
|
67
|
+
return cachedNr ?? NOOP_NR;
|
|
68
|
+
}
|
|
69
|
+
function isNrLoaded() {
|
|
70
|
+
return cachedNr !== null && cachedNr !== NOOP_NR;
|
|
71
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SNS message attribute injection for trace propagation.
|
|
3
|
+
*
|
|
4
|
+
* Why this file exists:
|
|
5
|
+
* combee publishes to the `cliengo-events` SNS topic with MessageAttributes
|
|
6
|
+
* (triggerType, category, companyId, channel). To propagate trace context
|
|
7
|
+
* across the SNS → SQS boundary, we need to add a `traceparent` attribute
|
|
8
|
+
* alongside the existing ones. This function spread-merges it in.
|
|
9
|
+
*
|
|
10
|
+
* Edge cases covered:
|
|
11
|
+
* - No active NR transaction (buildTraceparent returns undefined): the
|
|
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.
|
|
15
|
+
* - Existing traceparent in attrs: overwritten with the current value
|
|
16
|
+
* (spread puts our key last).
|
|
17
|
+
*/
|
|
18
|
+
import type { SnsMessageAttributeValue } from './types';
|
|
19
|
+
export declare function injectSnsAttributes(attrs: Record<string, SnsMessageAttributeValue>): Record<string, SnsMessageAttributeValue>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* SNS message attribute injection for trace propagation.
|
|
4
|
+
*
|
|
5
|
+
* Why this file exists:
|
|
6
|
+
* combee publishes to the `cliengo-events` SNS topic with MessageAttributes
|
|
7
|
+
* (triggerType, category, companyId, channel). To propagate trace context
|
|
8
|
+
* across the SNS → SQS boundary, we need to add a `traceparent` attribute
|
|
9
|
+
* alongside the existing ones. This function spread-merges it in.
|
|
10
|
+
*
|
|
11
|
+
* Edge cases covered:
|
|
12
|
+
* - No active NR transaction (buildTraceparent returns undefined): the
|
|
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.
|
|
16
|
+
* - Existing traceparent in attrs: overwritten with the current value
|
|
17
|
+
* (spread puts our key last).
|
|
18
|
+
*/
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.injectSnsAttributes = injectSnsAttributes;
|
|
21
|
+
const trace_bridge_1 = require("./trace-bridge");
|
|
22
|
+
function injectSnsAttributes(attrs) {
|
|
23
|
+
const tp = (0, trace_bridge_1.buildTraceparent)();
|
|
24
|
+
if (!tp) {
|
|
25
|
+
return attrs;
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
...attrs,
|
|
29
|
+
traceparent: { DataType: 'String', StringValue: tp },
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQS consumer wrapper — creates OTel spans and NR background transactions
|
|
3
|
+
* for every message processed from an SQS queue.
|
|
4
|
+
*
|
|
5
|
+
* Why this file exists:
|
|
6
|
+
* SQS consumers (combee whatsapp-status, kori automation-events, hsm-backend
|
|
7
|
+
* wa-meta) poll queues in a loop with no NR transaction context. This wrapper
|
|
8
|
+
* creates both an NR background transaction (so NR sees the work) and an OTel
|
|
9
|
+
* span on our non-global provider (so Foam sees the work). It also records
|
|
10
|
+
* metrics (duration, error count, processed count) automatically.
|
|
11
|
+
*
|
|
12
|
+
* Dual delivery mode extraction:
|
|
13
|
+
* SNS→SQS subscriptions have two modes that put traceparent in different places:
|
|
14
|
+
* - RawMessageDelivery=true (kori): traceparent is in the SQS-level
|
|
15
|
+
* `msg.MessageAttributes.traceparent.StringValue`
|
|
16
|
+
* - RawMessageDelivery=false (combee, hsm-backend): traceparent is inside the
|
|
17
|
+
* SNS envelope in `JSON.parse(msg.Body).MessageAttributes.traceparent.Value`
|
|
18
|
+
* (note: `Value` not `StringValue` — different key format in SNS envelopes)
|
|
19
|
+
* extractTraceparentFromMessage tries both locations.
|
|
20
|
+
*
|
|
21
|
+
* Edge cases covered:
|
|
22
|
+
* - No traceparent anywhere: consumer creates a fresh trace (new traceId).
|
|
23
|
+
* This is expected in Phase 1 when producers haven't been updated yet.
|
|
24
|
+
* - Body is not valid JSON (non-SNS message): JSON.parse catch silently
|
|
25
|
+
* falls through to return empty string.
|
|
26
|
+
* - fn() throws: exception is recorded on the span (exception.type, message,
|
|
27
|
+
* stacktrace), error metrics incremented, then error is re-thrown so the
|
|
28
|
+
* service's own retry/DLQ logic is unaffected.
|
|
29
|
+
* - NR not loaded: execute() runs without startBackgroundTransaction — OTel
|
|
30
|
+
* span still created, metrics still recorded.
|
|
31
|
+
* - metrics is undefined (inert bridge): all metrics?.xxx calls are no-ops.
|
|
32
|
+
*/
|
|
33
|
+
import { type Tracer } from '@opentelemetry/api';
|
|
34
|
+
import type { BridgeMetrics, SqsMessage } from './types';
|
|
35
|
+
export declare function wrapSqsConsumer(tracer: Tracer, spanName: string, msg: SqsMessage, fn: () => Promise<void>, metrics?: BridgeMetrics): Promise<void>;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* SQS consumer wrapper — creates OTel spans and NR background transactions
|
|
4
|
+
* for every message processed from an SQS queue.
|
|
5
|
+
*
|
|
6
|
+
* Why this file exists:
|
|
7
|
+
* SQS consumers (combee whatsapp-status, kori automation-events, hsm-backend
|
|
8
|
+
* wa-meta) poll queues in a loop with no NR transaction context. This wrapper
|
|
9
|
+
* creates both an NR background transaction (so NR sees the work) and an OTel
|
|
10
|
+
* span on our non-global provider (so Foam sees the work). It also records
|
|
11
|
+
* metrics (duration, error count, processed count) automatically.
|
|
12
|
+
*
|
|
13
|
+
* Dual delivery mode extraction:
|
|
14
|
+
* SNS→SQS subscriptions have two modes that put traceparent in different places:
|
|
15
|
+
* - RawMessageDelivery=true (kori): traceparent is in the SQS-level
|
|
16
|
+
* `msg.MessageAttributes.traceparent.StringValue`
|
|
17
|
+
* - RawMessageDelivery=false (combee, hsm-backend): traceparent is inside the
|
|
18
|
+
* SNS envelope in `JSON.parse(msg.Body).MessageAttributes.traceparent.Value`
|
|
19
|
+
* (note: `Value` not `StringValue` — different key format in SNS envelopes)
|
|
20
|
+
* extractTraceparentFromMessage tries both locations.
|
|
21
|
+
*
|
|
22
|
+
* Edge cases covered:
|
|
23
|
+
* - No traceparent anywhere: consumer creates a fresh trace (new traceId).
|
|
24
|
+
* This is expected in Phase 1 when producers haven't been updated yet.
|
|
25
|
+
* - Body is not valid JSON (non-SNS message): JSON.parse catch silently
|
|
26
|
+
* falls through to return empty string.
|
|
27
|
+
* - fn() throws: exception is recorded on the span (exception.type, message,
|
|
28
|
+
* stacktrace), error metrics incremented, then error is re-thrown so the
|
|
29
|
+
* service's own retry/DLQ logic is unaffected.
|
|
30
|
+
* - NR not loaded: execute() runs without startBackgroundTransaction — OTel
|
|
31
|
+
* span still created, metrics still recorded.
|
|
32
|
+
* - metrics is undefined (inert bridge): all metrics?.xxx calls are no-ops.
|
|
33
|
+
*/
|
|
34
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
35
|
+
exports.wrapSqsConsumer = wrapSqsConsumer;
|
|
36
|
+
const api_1 = require("@opentelemetry/api");
|
|
37
|
+
const nr_1 = require("./nr");
|
|
38
|
+
const trace_bridge_1 = require("./trace-bridge");
|
|
39
|
+
function extractTraceparentFromMessage(msg) {
|
|
40
|
+
// Raw delivery — traceparent in SQS-level MessageAttributes
|
|
41
|
+
const raw = msg.MessageAttributes?.traceparent?.StringValue;
|
|
42
|
+
if (raw) {
|
|
43
|
+
return raw;
|
|
44
|
+
}
|
|
45
|
+
// Non-raw (SNS-wrapped) — traceparent inside the SNS envelope in Body
|
|
46
|
+
if (msg.Body) {
|
|
47
|
+
try {
|
|
48
|
+
const body = JSON.parse(msg.Body);
|
|
49
|
+
const envelope = body.MessageAttributes?.traceparent?.Value;
|
|
50
|
+
if (envelope) {
|
|
51
|
+
return envelope;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Body is not JSON or doesn't have SNS envelope — no traceparent
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return '';
|
|
59
|
+
}
|
|
60
|
+
async function wrapSqsConsumer(tracer, spanName, msg, fn, metrics) {
|
|
61
|
+
const nr = (0, nr_1.getNr)();
|
|
62
|
+
const traceparent = extractTraceparentFromMessage(msg);
|
|
63
|
+
const parentCtx = traceparent ? (0, trace_bridge_1.extractParentContext)(traceparent) : undefined;
|
|
64
|
+
const headers = {};
|
|
65
|
+
if (traceparent) {
|
|
66
|
+
headers.traceparent = traceparent;
|
|
67
|
+
}
|
|
68
|
+
const execute = async () => {
|
|
69
|
+
nr.acceptDistributedTraceHeaders?.('Queue', headers);
|
|
70
|
+
const span = tracer.startSpan(spanName, {
|
|
71
|
+
attributes: {
|
|
72
|
+
'messaging.system': 'aws.sqs',
|
|
73
|
+
'messaging.operation': 'process',
|
|
74
|
+
'messaging.message.id': msg.MessageId ?? '',
|
|
75
|
+
},
|
|
76
|
+
}, parentCtx);
|
|
77
|
+
const start = Date.now();
|
|
78
|
+
try {
|
|
79
|
+
await fn();
|
|
80
|
+
span.setStatus({ code: api_1.SpanStatusCode.OK });
|
|
81
|
+
metrics?.messagesProcessed.add(1, { queue: spanName, status: 'success' });
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
85
|
+
span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: error.message });
|
|
86
|
+
span.recordException({
|
|
87
|
+
name: error.constructor.name,
|
|
88
|
+
message: error.message,
|
|
89
|
+
stack: error.stack,
|
|
90
|
+
});
|
|
91
|
+
metrics?.messageProcessingErrors.add(1, {
|
|
92
|
+
queue: spanName,
|
|
93
|
+
'error.type': error.constructor.name,
|
|
94
|
+
});
|
|
95
|
+
metrics?.messagesProcessed.add(1, { queue: spanName, status: 'error' });
|
|
96
|
+
throw err;
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
const duration = Date.now() - start;
|
|
100
|
+
metrics?.messageProcessingDuration.record(duration, { queue: spanName });
|
|
101
|
+
span.end();
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
if (nr.startBackgroundTransaction) {
|
|
105
|
+
await nr.startBackgroundTransaction(spanName, 'QueueConsumer', execute);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
await execute();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* W3C trace context bridge between New Relic and OpenTelemetry.
|
|
3
|
+
*
|
|
4
|
+
* Why this file exists:
|
|
5
|
+
* NR owns the active trace (it creates transactions via http monkey-patching),
|
|
6
|
+
* but our OTel spans need to share the same traceId so Foam and NR traces
|
|
7
|
+
* correlate. This module reads NR's trace metadata and converts it to/from
|
|
8
|
+
* W3C traceparent format (`00-{traceId}-{spanId}-{flags}`).
|
|
9
|
+
*
|
|
10
|
+
* Functions:
|
|
11
|
+
* - buildTraceparent(): reads NR's active traceId/spanId, returns a W3C
|
|
12
|
+
* traceparent string. Used by producers to inject into SNS attributes or
|
|
13
|
+
* BullMQ/Bull job data. Returns undefined when NR is not loaded or no
|
|
14
|
+
* active transaction exists.
|
|
15
|
+
* - extractParentContext(): parses a traceparent string into an OTel Context
|
|
16
|
+
* with a remote SpanContext. Used by consumers to link their OTel spans
|
|
17
|
+
* back to the producer's trace. Returns ROOT_CONTEXT on malformed input.
|
|
18
|
+
* - getTraceContext(): returns { traceId, spanId, traceparent } for log
|
|
19
|
+
* enrichment. Called lazily at log-write time (not at format creation time)
|
|
20
|
+
* so the trace context is always current.
|
|
21
|
+
*
|
|
22
|
+
* Edge cases covered:
|
|
23
|
+
* - NR not loaded: getTraceMetadata() returns undefined — we default to
|
|
24
|
+
* empty strings, buildTraceparent returns undefined, getTraceContext
|
|
25
|
+
* returns empty strings. No crash.
|
|
26
|
+
* - NR loaded but no active transaction: traceId/spanId are empty strings.
|
|
27
|
+
* buildTraceparent returns undefined. Consumers create a fresh trace.
|
|
28
|
+
* - Malformed traceparent (wrong number of parts, wrong hex lengths):
|
|
29
|
+
* extractParentContext returns ROOT_CONTEXT — consumer creates a fresh
|
|
30
|
+
* trace instead of crashing.
|
|
31
|
+
* - Flags field always set to 01 (sampled) on buildTraceparent because we
|
|
32
|
+
* want all async boundary crossings traced.
|
|
33
|
+
*/
|
|
34
|
+
import { type Context } from '@opentelemetry/api';
|
|
35
|
+
import type { TraceContext } from './types';
|
|
36
|
+
export declare function buildTraceparent(): string | undefined;
|
|
37
|
+
export declare function extractParentContext(traceparent: string): Context;
|
|
38
|
+
export declare function getTraceContext(): TraceContext;
|
|
39
|
+
export declare function getActiveContext(): Context;
|