@foam-ai/node-cliengo 0.1.0-alpha.1 → 0.1.0-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/node-cliengo/src/constants.d.ts +1 -1
- package/dist/node-cliengo/src/constants.js +1 -1
- package/dist/node-cliengo/src/index.d.ts +3 -3
- package/dist/node-cliengo/src/index.js +2 -2
- package/dist/node-cliengo/src/init.d.ts +14 -11
- package/dist/node-cliengo/src/init.js +52 -46
- package/dist/node-cliengo/src/job.d.ts +2 -2
- package/dist/node-cliengo/src/metrics/instruments.d.ts +9 -11
- package/dist/node-cliengo/src/metrics/instruments.js +19 -55
- package/dist/node-cliengo/src/nr.d.ts +1 -1
- package/dist/node-cliengo/src/nr.js +1 -1
- package/dist/node-cliengo/src/sqs.d.ts +3 -3
- package/dist/node-cliengo/src/sqs.js +1 -1
- package/dist/node-cliengo/src/trace-bridge.d.ts +1 -1
- package/dist/node-cliengo/src/trace-bridge.js +1 -1
- package/dist/node-cliengo/src/types.d.ts +5 -8
- package/dist/node-cliengo/src/types.js +2 -2
- package/package.json +12 -10
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
* (buildTraceparent, injectSnsAttributes, etc.), and all public types.
|
|
8
8
|
*
|
|
9
9
|
* Most services only need: import { init } from '@foam/node-cliengo'
|
|
10
|
-
* Everything else is accessed via the returned
|
|
10
|
+
* Everything else is accessed via the returned FoamInstance.
|
|
11
11
|
*
|
|
12
12
|
* The individual function exports exist for tree-shaking and for cases where
|
|
13
13
|
* a service needs to call e.g. injectJobData() from a file that doesn't have
|
|
14
|
-
* access to the
|
|
14
|
+
* access to the foam instance.
|
|
15
15
|
*/
|
|
16
16
|
export { init } from './init';
|
|
17
17
|
export { buildTraceparent, extractParentContext, getTraceContext } from './trace-bridge';
|
|
@@ -23,4 +23,4 @@ export { createFastifyPlugin } from './http/fastify';
|
|
|
23
23
|
export { createWinstonFormat } from './logs/winston-format';
|
|
24
24
|
export { createPinoMixin } from './logs/pino-mixin';
|
|
25
25
|
export { FOAM_OTEL_ENDPOINT } from './constants';
|
|
26
|
-
export type {
|
|
26
|
+
export type { FoamMetrics, Counter, ExpressErrorHandler, ExpressRequestHandler, FastifyPluginAsync, Histogram, InitOptions, NewRelicAgent, ObservableGauge, FoamInstance, SnsMessageAttributeValue, SqsMessage, TraceContext, WinstonFormat, WinstonLogger, WinstonTransport, } from './types';
|
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
* (buildTraceparent, injectSnsAttributes, etc.), and all public types.
|
|
9
9
|
*
|
|
10
10
|
* Most services only need: import { init } from '@foam/node-cliengo'
|
|
11
|
-
* Everything else is accessed via the returned
|
|
11
|
+
* Everything else is accessed via the returned FoamInstance.
|
|
12
12
|
*
|
|
13
13
|
* The individual function exports exist for tree-shaking and for cases where
|
|
14
14
|
* a service needs to call e.g. injectJobData() from a file that doesn't have
|
|
15
|
-
* access to the
|
|
15
|
+
* access to the foam instance.
|
|
16
16
|
*/
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
18
|
exports.FOAM_OTEL_ENDPOINT = exports.createPinoMixin = exports.createWinstonFormat = exports.createFastifyPlugin = exports.createExpressErrorHandler = exports.createExpressMiddleware = exports.wrapSqsConsumer = exports.wrapJobConsumer = exports.injectJobData = exports.injectSnsAttributes = exports.getTraceContext = exports.extractParentContext = exports.buildTraceparent = exports.init = void 0;
|
|
@@ -1,40 +1,43 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Main entry point — init() creates the
|
|
2
|
+
* Main entry point — init() creates the Foam singleton for a service.
|
|
3
3
|
*
|
|
4
4
|
* Why this file exists:
|
|
5
5
|
* Orchestrates all three OTel signals (traces, logs, metrics) into a single
|
|
6
|
-
* init() call that returns
|
|
7
|
-
* startup and use the returned
|
|
6
|
+
* init() call that returns a FoamInstance. Services call init() once at
|
|
7
|
+
* startup and use the returned instance for everything else. It is
|
|
8
8
|
* designed to coexist with New Relic APM without touching NR's globals.
|
|
9
9
|
*
|
|
10
10
|
* Two-gate system:
|
|
11
|
-
* 1. Kill switch (FOAM_OTEL_ENABLED): if false, returns a fully inert
|
|
11
|
+
* 1. Kill switch (FOAM_OTEL_ENABLED): if false, returns a fully inert instance.
|
|
12
12
|
* Every API exists but does nothing — zero overhead, zero network, zero risk.
|
|
13
13
|
* This is the master emergency shutoff.
|
|
14
14
|
* 2. Production gate (NODE_ENV): exporters are only attached when NODE_ENV is
|
|
15
|
-
* "production" (or forceExport is true). In dev/test,
|
|
15
|
+
* "production" (or forceExport is true). In dev/test, Foam is fully
|
|
16
16
|
* functional (trace propagation, log enrichment work) but nothing is sent
|
|
17
17
|
* to Foam. Developers without FOAM_OTEL_TOKEN get no crashes.
|
|
18
18
|
*
|
|
19
19
|
* Edge cases covered:
|
|
20
|
-
* - Double init(): logs a warning, returns the existing
|
|
20
|
+
* - Double init(): logs a warning, returns the existing instance (singleton
|
|
21
21
|
* per service name). Prevents duplicate providers/processors/handlers.
|
|
22
22
|
* - Token undefined in dev: silently accepted — no OTLP export anyway.
|
|
23
23
|
* - Token undefined in prod: logs warning, disables exporters. All other
|
|
24
24
|
* features (trace propagation, log enrichment, NR bridging) still work.
|
|
25
25
|
* - Token undefined + forceExport: throws (fail-fast — you asked to force
|
|
26
26
|
* export but provided no credentials).
|
|
27
|
+
* - Custom endpoint without forceExport: throws. Production always uses the
|
|
28
|
+
* default Foam endpoint; custom endpoints are only for dev/staging.
|
|
27
29
|
* - Auto-shutdown (SIGTERM/SIGINT): flushes all providers with 5s timeout,
|
|
28
30
|
* then calls process.exit(0). Without the explicit exit, registering
|
|
29
31
|
* process.on('SIGTERM') replaces Node's default terminate behavior — services
|
|
30
32
|
* without their own handler (gpt-intentions, cb-proxy) would hang forever.
|
|
31
33
|
* - Manual shutdown: services with ordered cleanup (hsm-backend, kori, combee)
|
|
32
|
-
* pass autoShutdown: false and call
|
|
34
|
+
* pass autoShutdown: false and call foam.shutdown() in their own handler.
|
|
33
35
|
* - shutdown() is idempotent — safe to call multiple times.
|
|
34
36
|
* - Process exception handlers use process.on() (not .once()) so they don't
|
|
35
37
|
* replace NR's or Winston's existing handlers. All handlers fire independently.
|
|
36
|
-
* - uncaughtException
|
|
37
|
-
*
|
|
38
|
+
* - uncaughtException/unhandledRejection create short-lived spans with
|
|
39
|
+
* recordException() so they appear in otel_traces, then force-flush the
|
|
40
|
+
* trace provider before shutdown. Never calls process.exit().
|
|
38
41
|
* - Winston auto-wiring: init({ winston: baseLogger }) calls logger.add() for
|
|
39
42
|
* the OTLP transport and rewrites logger.format to prepend trace enrichment.
|
|
40
43
|
* Both work post-construction. Avoids circular deps (main.ts imports logger,
|
|
@@ -46,5 +49,5 @@
|
|
|
46
49
|
* are NEVER registered globally (.register() never called). NR owns the
|
|
47
50
|
* global registrations.
|
|
48
51
|
*/
|
|
49
|
-
import type { InitOptions,
|
|
50
|
-
export declare function init(serviceName: string, token?: string, options?: InitOptions):
|
|
52
|
+
import type { InitOptions, FoamInstance } from './types';
|
|
53
|
+
export declare function init(serviceName: string, token?: string, options?: InitOptions): FoamInstance;
|
|
@@ -1,41 +1,44 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* Main entry point — init() creates the
|
|
3
|
+
* Main entry point — init() creates the Foam singleton for a service.
|
|
4
4
|
*
|
|
5
5
|
* Why this file exists:
|
|
6
6
|
* Orchestrates all three OTel signals (traces, logs, metrics) into a single
|
|
7
|
-
* init() call that returns
|
|
8
|
-
* startup and use the returned
|
|
7
|
+
* init() call that returns a FoamInstance. Services call init() once at
|
|
8
|
+
* startup and use the returned instance for everything else. It is
|
|
9
9
|
* designed to coexist with New Relic APM without touching NR's globals.
|
|
10
10
|
*
|
|
11
11
|
* Two-gate system:
|
|
12
|
-
* 1. Kill switch (FOAM_OTEL_ENABLED): if false, returns a fully inert
|
|
12
|
+
* 1. Kill switch (FOAM_OTEL_ENABLED): if false, returns a fully inert instance.
|
|
13
13
|
* Every API exists but does nothing — zero overhead, zero network, zero risk.
|
|
14
14
|
* This is the master emergency shutoff.
|
|
15
15
|
* 2. Production gate (NODE_ENV): exporters are only attached when NODE_ENV is
|
|
16
|
-
* "production" (or forceExport is true). In dev/test,
|
|
16
|
+
* "production" (or forceExport is true). In dev/test, Foam is fully
|
|
17
17
|
* functional (trace propagation, log enrichment work) but nothing is sent
|
|
18
18
|
* to Foam. Developers without FOAM_OTEL_TOKEN get no crashes.
|
|
19
19
|
*
|
|
20
20
|
* Edge cases covered:
|
|
21
|
-
* - Double init(): logs a warning, returns the existing
|
|
21
|
+
* - Double init(): logs a warning, returns the existing instance (singleton
|
|
22
22
|
* per service name). Prevents duplicate providers/processors/handlers.
|
|
23
23
|
* - Token undefined in dev: silently accepted — no OTLP export anyway.
|
|
24
24
|
* - Token undefined in prod: logs warning, disables exporters. All other
|
|
25
25
|
* features (trace propagation, log enrichment, NR bridging) still work.
|
|
26
26
|
* - Token undefined + forceExport: throws (fail-fast — you asked to force
|
|
27
27
|
* export but provided no credentials).
|
|
28
|
+
* - Custom endpoint without forceExport: throws. Production always uses the
|
|
29
|
+
* default Foam endpoint; custom endpoints are only for dev/staging.
|
|
28
30
|
* - Auto-shutdown (SIGTERM/SIGINT): flushes all providers with 5s timeout,
|
|
29
31
|
* then calls process.exit(0). Without the explicit exit, registering
|
|
30
32
|
* process.on('SIGTERM') replaces Node's default terminate behavior — services
|
|
31
33
|
* without their own handler (gpt-intentions, cb-proxy) would hang forever.
|
|
32
34
|
* - Manual shutdown: services with ordered cleanup (hsm-backend, kori, combee)
|
|
33
|
-
* pass autoShutdown: false and call
|
|
35
|
+
* pass autoShutdown: false and call foam.shutdown() in their own handler.
|
|
34
36
|
* - shutdown() is idempotent — safe to call multiple times.
|
|
35
37
|
* - Process exception handlers use process.on() (not .once()) so they don't
|
|
36
38
|
* replace NR's or Winston's existing handlers. All handlers fire independently.
|
|
37
|
-
* - uncaughtException
|
|
38
|
-
*
|
|
39
|
+
* - uncaughtException/unhandledRejection create short-lived spans with
|
|
40
|
+
* recordException() so they appear in otel_traces, then force-flush the
|
|
41
|
+
* trace provider before shutdown. Never calls process.exit().
|
|
39
42
|
* - Winston auto-wiring: init({ winston: baseLogger }) calls logger.add() for
|
|
40
43
|
* the OTLP transport and rewrites logger.format to prepend trace enrichment.
|
|
41
44
|
* Both work post-construction. Avoids circular deps (main.ts imports logger,
|
|
@@ -49,7 +52,7 @@
|
|
|
49
52
|
*/
|
|
50
53
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
51
54
|
exports.init = init;
|
|
52
|
-
const
|
|
55
|
+
const api_1 = require("@opentelemetry/api");
|
|
53
56
|
const exporter_logs_otlp_http_1 = require("@opentelemetry/exporter-logs-otlp-http");
|
|
54
57
|
const exporter_metrics_otlp_http_1 = require("@opentelemetry/exporter-metrics-otlp-http");
|
|
55
58
|
const exporter_trace_otlp_http_1 = require("@opentelemetry/exporter-trace-otlp-http");
|
|
@@ -71,15 +74,15 @@ const nr_1 = require("./nr");
|
|
|
71
74
|
const sns_1 = require("./sns");
|
|
72
75
|
const sqs_1 = require("./sqs");
|
|
73
76
|
const trace_bridge_1 = require("./trace-bridge");
|
|
74
|
-
const
|
|
75
|
-
function
|
|
77
|
+
const instances = new Map();
|
|
78
|
+
function createInertInstance(serviceName) {
|
|
76
79
|
const resource = (0, resources_1.resourceFromAttributes)({ 'service.name': serviceName });
|
|
77
80
|
const traceProvider = new sdk_trace_base_1.BasicTracerProvider({ resource });
|
|
78
81
|
const loggerProvider = new sdk_logs_1.LoggerProvider({ resource });
|
|
79
82
|
const meterProvider = new sdk_metrics_1.MeterProvider({ resource });
|
|
80
83
|
const tracer = traceProvider.getTracer(serviceName);
|
|
81
84
|
const meter = meterProvider.getMeter(serviceName);
|
|
82
|
-
const metrics = (0, instruments_1.
|
|
85
|
+
const metrics = (0, instruments_1.createFoamMetrics)(meterProvider, serviceName);
|
|
83
86
|
return {
|
|
84
87
|
tracer,
|
|
85
88
|
meter,
|
|
@@ -114,19 +117,19 @@ function createInertBridge(serviceName) {
|
|
|
114
117
|
}
|
|
115
118
|
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call */
|
|
116
119
|
function init(serviceName, token, options = {}) {
|
|
117
|
-
const existing =
|
|
120
|
+
const existing = instances.get(serviceName);
|
|
118
121
|
if (existing) {
|
|
119
122
|
// eslint-disable-next-line no-console
|
|
120
|
-
console.warn(`[foam] init() already called for "${serviceName}" — returning existing
|
|
123
|
+
console.warn(`[foam] init() already called for "${serviceName}" — returning existing instance`);
|
|
121
124
|
return existing;
|
|
122
125
|
}
|
|
123
126
|
// Kill switch
|
|
124
127
|
const isEnabled = (options.enabled ??
|
|
125
128
|
((process.env.FOAM_OTEL_ENABLED ?? 'true').toLowerCase() === 'true')) === true;
|
|
126
129
|
if (!isEnabled) {
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
return
|
|
130
|
+
const foam = createInertInstance(serviceName);
|
|
131
|
+
instances.set(serviceName, foam);
|
|
132
|
+
return foam;
|
|
130
133
|
}
|
|
131
134
|
// Resolve NR
|
|
132
135
|
(0, nr_1.resolveNewRelic)(options.newrelic);
|
|
@@ -143,6 +146,9 @@ function init(serviceName, token, options = {}) {
|
|
|
143
146
|
}
|
|
144
147
|
const hasToken = Boolean(token);
|
|
145
148
|
const canExport = shouldExport && hasToken;
|
|
149
|
+
if (options.endpoint && !options.forceExport) {
|
|
150
|
+
throw new Error('[foam] custom endpoint requires forceExport: true — production always uses the default Foam endpoint');
|
|
151
|
+
}
|
|
146
152
|
const endpoint = options.endpoint ?? constants_1.FOAM_OTEL_ENDPOINT;
|
|
147
153
|
const resource = (0, resources_1.resourceFromAttributes)({ 'service.name': serviceName });
|
|
148
154
|
const authHeaders = token
|
|
@@ -193,7 +199,7 @@ function init(serviceName, token, options = {}) {
|
|
|
193
199
|
});
|
|
194
200
|
const tracer = traceProvider.getTracer(serviceName);
|
|
195
201
|
const meter = meterProvider.getMeter(serviceName);
|
|
196
|
-
const metrics = (0, instruments_1.
|
|
202
|
+
const metrics = (0, instruments_1.createFoamMetrics)(meterProvider, serviceName);
|
|
197
203
|
let _isShutdown = false;
|
|
198
204
|
const shutdown = async () => {
|
|
199
205
|
if (_isShutdown) {
|
|
@@ -217,41 +223,41 @@ function init(serviceName, token, options = {}) {
|
|
|
217
223
|
process.on('SIGTERM', () => void handler());
|
|
218
224
|
process.on('SIGINT', () => void handler());
|
|
219
225
|
}
|
|
220
|
-
// Process-level exception capture
|
|
221
|
-
|
|
226
|
+
// Process-level exception capture — recorded as span exceptions so they
|
|
227
|
+
// land in otel_traces (not otel_logs). Each creates a short-lived span,
|
|
228
|
+
// records the exception, ends it, then force-flushes the trace provider.
|
|
222
229
|
process.on('uncaughtException', (err) => {
|
|
223
230
|
try {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
severityNumber: api_logs_1.SeverityNumber.FATAL,
|
|
227
|
-
severityText: 'FATAL',
|
|
228
|
-
attributes: {
|
|
229
|
-
'exception.type': err.constructor.name,
|
|
230
|
-
'exception.message': err.message,
|
|
231
|
-
'exception.stacktrace': err.stack ?? '',
|
|
232
|
-
'process.event': 'uncaughtException',
|
|
233
|
-
},
|
|
231
|
+
const span = tracer.startSpan('uncaughtException', {
|
|
232
|
+
attributes: { 'process.event': 'uncaughtException' },
|
|
234
233
|
});
|
|
235
|
-
|
|
234
|
+
span.recordException({
|
|
235
|
+
name: err.constructor.name,
|
|
236
|
+
message: err.message,
|
|
237
|
+
stack: err.stack,
|
|
238
|
+
});
|
|
239
|
+
span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: err.message });
|
|
240
|
+
span.end();
|
|
241
|
+
void traceProvider.forceFlush().then(() => shutdown());
|
|
236
242
|
}
|
|
237
243
|
catch {
|
|
238
|
-
|
|
244
|
+
void shutdown();
|
|
239
245
|
}
|
|
240
246
|
});
|
|
241
247
|
process.on('unhandledRejection', (reason) => {
|
|
242
248
|
try {
|
|
243
249
|
const err = reason instanceof Error ? reason : new Error(String(reason));
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
'exception.stacktrace': err.stack ?? '',
|
|
252
|
-
'process.event': 'unhandledRejection',
|
|
253
|
-
},
|
|
250
|
+
const span = tracer.startSpan('unhandledRejection', {
|
|
251
|
+
attributes: { 'process.event': 'unhandledRejection' },
|
|
252
|
+
});
|
|
253
|
+
span.recordException({
|
|
254
|
+
name: err.constructor.name,
|
|
255
|
+
message: err.message,
|
|
256
|
+
stack: err.stack,
|
|
254
257
|
});
|
|
258
|
+
span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: err.message });
|
|
259
|
+
span.end();
|
|
260
|
+
void traceProvider.forceFlush();
|
|
255
261
|
}
|
|
256
262
|
catch {
|
|
257
263
|
/* best effort */
|
|
@@ -318,7 +324,7 @@ function init(serviceName, token, options = {}) {
|
|
|
318
324
|
}
|
|
319
325
|
}
|
|
320
326
|
const expressMiddleware = (0, express_1.createExpressMiddleware)(tracer);
|
|
321
|
-
const
|
|
327
|
+
const foam = {
|
|
322
328
|
tracer,
|
|
323
329
|
meter,
|
|
324
330
|
logger: loggerProvider,
|
|
@@ -342,7 +348,7 @@ function init(serviceName, token, options = {}) {
|
|
|
342
348
|
extractParentContext: trace_bridge_1.extractParentContext,
|
|
343
349
|
shutdown,
|
|
344
350
|
};
|
|
345
|
-
|
|
346
|
-
return
|
|
351
|
+
instances.set(serviceName, foam);
|
|
352
|
+
return foam;
|
|
347
353
|
}
|
|
348
354
|
/* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call */
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
* consistent exception recording.
|
|
25
25
|
*/
|
|
26
26
|
import { type Tracer } from '@opentelemetry/api';
|
|
27
|
-
import type {
|
|
27
|
+
import type { FoamMetrics } from './types';
|
|
28
28
|
export declare function injectJobData<T extends Record<string, unknown>>(data: T): T & {
|
|
29
29
|
traceparent?: string;
|
|
30
30
|
};
|
|
31
|
-
export declare function wrapJobConsumer(tracer: Tracer, spanName: string, jobData: Record<string, unknown>, fn: () => Promise<void>, metrics?:
|
|
31
|
+
export declare function wrapJobConsumer(tracer: Tracer, spanName: string, jobData: Record<string, unknown>, fn: () => Promise<void>, metrics?: FoamMetrics): Promise<void>;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pre-built OTel metric instruments for queue processing
|
|
3
|
-
* for service-specific custom metrics.
|
|
2
|
+
* Pre-built OTel metric instruments for queue processing.
|
|
4
3
|
*
|
|
5
4
|
* Why this file exists:
|
|
6
5
|
* Provides three auto-recorded instruments that wrapSqsConsumer and
|
|
@@ -9,15 +8,14 @@
|
|
|
9
8
|
* - messageProcessingErrors (counter): how many processing attempts fail
|
|
10
9
|
* - messagesProcessed (counter): total messages processed (success + error)
|
|
11
10
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* These follow OTel semantic conventions for messaging. They are NOT
|
|
12
|
+
* Cliengo-specific — any service with queue consumers gets them for free.
|
|
13
|
+
*
|
|
14
|
+
* For service-specific domain metrics (LLM duration, webhook processing time,
|
|
15
|
+
* automation duration), services use foam.meter directly:
|
|
16
|
+
* const h = foam.meter.createHistogram('llm.duration', { unit: 'ms' });
|
|
15
17
|
*
|
|
16
18
|
* 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
19
|
* - MeterProvider is non-global — our metrics never conflict with NR's
|
|
22
20
|
* internal metrics or any other OTel MeterProvider in the process.
|
|
23
21
|
* - PeriodicExportingMetricReader (configured in init.ts) exports every 60s
|
|
@@ -25,5 +23,5 @@
|
|
|
25
23
|
* attached and no timer runs.
|
|
26
24
|
*/
|
|
27
25
|
import type { MeterProvider } from '@opentelemetry/sdk-metrics';
|
|
28
|
-
import type {
|
|
29
|
-
export declare function
|
|
26
|
+
import type { FoamMetrics } from '../types';
|
|
27
|
+
export declare function createFoamMetrics(meterProvider: MeterProvider, serviceName: string): FoamMetrics;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* Pre-built OTel metric instruments for queue processing
|
|
4
|
-
* for service-specific custom metrics.
|
|
3
|
+
* Pre-built OTel metric instruments for queue processing.
|
|
5
4
|
*
|
|
6
5
|
* Why this file exists:
|
|
7
6
|
* Provides three auto-recorded instruments that wrapSqsConsumer and
|
|
@@ -10,15 +9,14 @@
|
|
|
10
9
|
* - messageProcessingErrors (counter): how many processing attempts fail
|
|
11
10
|
* - messagesProcessed (counter): total messages processed (success + error)
|
|
12
11
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* These follow OTel semantic conventions for messaging. They are NOT
|
|
13
|
+
* Cliengo-specific — any service with queue consumers gets them for free.
|
|
14
|
+
*
|
|
15
|
+
* For service-specific domain metrics (LLM duration, webhook processing time,
|
|
16
|
+
* automation duration), services use foam.meter directly:
|
|
17
|
+
* const h = foam.meter.createHistogram('llm.duration', { unit: 'ms' });
|
|
16
18
|
*
|
|
17
19
|
* 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
20
|
* - MeterProvider is non-global — our metrics never conflict with NR's
|
|
23
21
|
* internal metrics or any other OTel MeterProvider in the process.
|
|
24
22
|
* - PeriodicExportingMetricReader (configured in init.ts) exports every 60s
|
|
@@ -26,53 +24,19 @@
|
|
|
26
24
|
* attached and no timer runs.
|
|
27
25
|
*/
|
|
28
26
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
-
exports.
|
|
30
|
-
function
|
|
27
|
+
exports.createFoamMetrics = createFoamMetrics;
|
|
28
|
+
function createFoamMetrics(meterProvider, serviceName) {
|
|
31
29
|
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
30
|
return {
|
|
44
|
-
messageProcessingDuration,
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
},
|
|
31
|
+
messageProcessingDuration: meter.createHistogram('messaging.process.duration', {
|
|
32
|
+
description: 'Time to process a queue message',
|
|
33
|
+
unit: 'ms',
|
|
34
|
+
}),
|
|
35
|
+
messageProcessingErrors: meter.createCounter('messaging.process.errors', {
|
|
36
|
+
description: 'Number of queue message processing errors',
|
|
37
|
+
}),
|
|
38
|
+
messagesProcessed: meter.createCounter('messaging.process.count', {
|
|
39
|
+
description: 'Number of queue messages processed',
|
|
40
|
+
}),
|
|
77
41
|
};
|
|
78
42
|
}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* hsm-backend (conditional NR loading).
|
|
15
15
|
* 2. Module cache: `require.cache[require.resolve('newrelic')]` — works for
|
|
16
16
|
* CJS services (combee, gpt-intentions, cb-proxy) where NR is loaded at
|
|
17
|
-
* the top of the entry file before
|
|
17
|
+
* the top of the entry file before Foam initializes.
|
|
18
18
|
* 3. Fallback: NOOP_NR stub that returns empty trace metadata and calls
|
|
19
19
|
* handler functions directly without creating background transactions.
|
|
20
20
|
*
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* hsm-backend (conditional NR loading).
|
|
16
16
|
* 2. Module cache: `require.cache[require.resolve('newrelic')]` — works for
|
|
17
17
|
* CJS services (combee, gpt-intentions, cb-proxy) where NR is loaded at
|
|
18
|
-
* the top of the entry file before
|
|
18
|
+
* the top of the entry file before Foam initializes.
|
|
19
19
|
* 3. Fallback: NOOP_NR stub that returns empty trace metadata and calls
|
|
20
20
|
* handler functions directly without creating background transactions.
|
|
21
21
|
*
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
* service's own retry/DLQ logic is unaffected.
|
|
29
29
|
* - NR not loaded: execute() runs without startBackgroundTransaction — OTel
|
|
30
30
|
* span still created, metrics still recorded.
|
|
31
|
-
* - metrics is undefined (inert
|
|
31
|
+
* - metrics is undefined (inert foam instance): all metrics?.xxx calls are no-ops.
|
|
32
32
|
*/
|
|
33
33
|
import { type Tracer } from '@opentelemetry/api';
|
|
34
|
-
import type {
|
|
35
|
-
export declare function wrapSqsConsumer(tracer: Tracer, spanName: string, msg: SqsMessage, fn: () => Promise<void>, metrics?:
|
|
34
|
+
import type { FoamMetrics, SqsMessage } from './types';
|
|
35
|
+
export declare function wrapSqsConsumer(tracer: Tracer, spanName: string, msg: SqsMessage, fn: () => Promise<void>, metrics?: FoamMetrics): Promise<void>;
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
* service's own retry/DLQ logic is unaffected.
|
|
30
30
|
* - NR not loaded: execute() runs without startBackgroundTransaction — OTel
|
|
31
31
|
* span still created, metrics still recorded.
|
|
32
|
-
* - metrics is undefined (inert
|
|
32
|
+
* - metrics is undefined (inert foam instance): all metrics?.xxx calls are no-ops.
|
|
33
33
|
*/
|
|
34
34
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
35
35
|
exports.wrapSqsConsumer = wrapSqsConsumer;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* W3C trace context
|
|
3
|
+
* W3C trace context utilities for New Relic and OpenTelemetry interop.
|
|
4
4
|
*
|
|
5
5
|
* Why this file exists:
|
|
6
6
|
* NR owns the active trace (it creates transactions via http monkey-patching),
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* All public interfaces and minimal type stubs for @foam/node-cliengo.
|
|
3
3
|
*
|
|
4
4
|
* Why this file exists:
|
|
5
|
-
* Defines every public type the library exposes (
|
|
6
|
-
* TraceContext,
|
|
5
|
+
* Defines every public type the library exposes (FoamInstance, InitOptions,
|
|
6
|
+
* TraceContext, FoamMetrics) plus lightweight stubs for external frameworks
|
|
7
7
|
* (Express, Fastify, Winston, Pino, New Relic). These stubs exist so the
|
|
8
8
|
* library never imports framework types at the top level — services that only
|
|
9
9
|
* use Pino don't need Winston installed, and vice versa.
|
|
@@ -41,22 +41,19 @@ export interface TraceContext {
|
|
|
41
41
|
spanId: string;
|
|
42
42
|
traceparent: string;
|
|
43
43
|
}
|
|
44
|
-
export interface
|
|
44
|
+
export interface FoamMetrics {
|
|
45
45
|
messageProcessingDuration: Histogram;
|
|
46
46
|
messageProcessingErrors: Counter;
|
|
47
47
|
messagesProcessed: Counter;
|
|
48
|
-
customHistogram(name: string, description: string, unit?: string): Histogram;
|
|
49
|
-
customCounter(name: string, description: string, unit?: string): Counter;
|
|
50
|
-
customGauge(name: string, description: string, unit?: string): ObservableGauge;
|
|
51
48
|
}
|
|
52
|
-
export interface
|
|
49
|
+
export interface FoamInstance {
|
|
53
50
|
tracer: Tracer;
|
|
54
51
|
meter: Meter;
|
|
55
52
|
logger: LoggerProvider;
|
|
56
53
|
traceProvider: BasicTracerProvider;
|
|
57
54
|
meterProvider: MeterProvider;
|
|
58
55
|
loggerProvider: LoggerProvider;
|
|
59
|
-
metrics:
|
|
56
|
+
metrics: FoamMetrics;
|
|
60
57
|
getTraceContext(): TraceContext;
|
|
61
58
|
createExpressMiddleware(): ExpressRequestHandler;
|
|
62
59
|
createExpressErrorHandler(): ExpressErrorHandler;
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* All public interfaces and minimal type stubs for @foam/node-cliengo.
|
|
4
4
|
*
|
|
5
5
|
* Why this file exists:
|
|
6
|
-
* Defines every public type the library exposes (
|
|
7
|
-
* TraceContext,
|
|
6
|
+
* Defines every public type the library exposes (FoamInstance, InitOptions,
|
|
7
|
+
* TraceContext, FoamMetrics) plus lightweight stubs for external frameworks
|
|
8
8
|
* (Express, Fastify, Winston, Pino, New Relic). These stubs exist so the
|
|
9
9
|
* library never imports framework types at the top level — services that only
|
|
10
10
|
* use Pino don't need Winston installed, and vice versa.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@foam-ai/node-cliengo",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
4
|
-
"description": "Unified observability (traces, logs, metrics) for Cliengo Node.js services,
|
|
3
|
+
"version": "0.1.0-alpha.3",
|
|
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",
|
|
7
7
|
"exports": {
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
"peerDependencies": {
|
|
21
21
|
"@opentelemetry/api": "^1.9.0",
|
|
22
22
|
"newrelic": ">=12.0.0",
|
|
23
|
-
"
|
|
24
|
-
"
|
|
23
|
+
"pino": "^8.0.0 || ^9.0.0",
|
|
24
|
+
"winston": "^3.0.0"
|
|
25
25
|
},
|
|
26
26
|
"peerDependenciesMeta": {
|
|
27
27
|
"newrelic": {
|
|
@@ -35,18 +35,20 @@
|
|
|
35
35
|
}
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@opentelemetry/
|
|
39
|
-
"@opentelemetry/sdk-logs": "^0.203.0",
|
|
40
|
-
"@opentelemetry/sdk-metrics": "^2.0.1",
|
|
41
|
-
"@opentelemetry/exporter-trace-otlp-http": "^0.203.0",
|
|
38
|
+
"@opentelemetry/api-logs": "^0.203.0",
|
|
42
39
|
"@opentelemetry/exporter-logs-otlp-http": "^0.203.0",
|
|
43
40
|
"@opentelemetry/exporter-metrics-otlp-http": "^0.201.1",
|
|
41
|
+
"@opentelemetry/exporter-trace-otlp-http": "^0.203.0",
|
|
44
42
|
"@opentelemetry/resources": "^2.0.1",
|
|
45
|
-
"@opentelemetry/
|
|
43
|
+
"@opentelemetry/sdk-logs": "^0.203.0",
|
|
44
|
+
"@opentelemetry/sdk-metrics": "^2.0.1",
|
|
45
|
+
"@opentelemetry/sdk-trace-base": "^2.0.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
+
"@opentelemetry/api": "^1.9.1",
|
|
49
|
+
"@types/node": "^20.11.0",
|
|
48
50
|
"typescript": "^6.0.2",
|
|
49
|
-
"
|
|
51
|
+
"vitest": "^4.1.8"
|
|
50
52
|
},
|
|
51
53
|
"engines": {
|
|
52
54
|
"node": ">=18"
|