@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.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Shared constants for the @foam/node-cliengo bridge.
2
+ * Shared constants for @foam/node-cliengo.
3
3
  *
4
4
  * Why this file exists:
5
5
  * Centralizes all hardcoded values (endpoint, limits, redaction keys, health
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  /**
3
- * Shared constants for the @foam/node-cliengo bridge.
3
+ * Shared constants for @foam/node-cliengo.
4
4
  *
5
5
  * Why this file exists:
6
6
  * Centralizes all hardcoded values (endpoint, limits, redaction keys, health
@@ -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 OtelBridge object.
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 bridge instance.
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 { BridgeMetrics, Counter, ExpressErrorHandler, ExpressRequestHandler, FastifyPluginAsync, Histogram, InitOptions, NewRelicAgent, ObservableGauge, OtelBridge, SnsMessageAttributeValue, SqsMessage, TraceContext, WinstonFormat, WinstonLogger, WinstonTransport, } from './types';
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 OtelBridge object.
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 bridge instance.
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 OtelBridge singleton for a service.
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 an OtelBridge object. Services call init() once at
7
- * startup and use the returned bridge for everything else. The bridge is
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 bridge.
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, the bridge is fully
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 bridge (singleton
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 bridge.shutdown() in their own handler.
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 handler flushes via shutdown() — best effort to get the
37
- * fatal log to Foam before the process dies. Never calls process.exit().
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, OtelBridge } from './types';
50
- export declare function init(serviceName: string, token?: string, options?: InitOptions): OtelBridge;
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 OtelBridge singleton for a service.
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 an OtelBridge object. Services call init() once at
8
- * startup and use the returned bridge for everything else. The bridge is
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 bridge.
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, the bridge is fully
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 bridge (singleton
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 bridge.shutdown() in their own handler.
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 handler flushes via shutdown() — best effort to get the
38
- * fatal log to Foam before the process dies. Never calls process.exit().
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 api_logs_1 = require("@opentelemetry/api-logs");
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 bridges = new Map();
75
- function createInertBridge(serviceName) {
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.createBridgeMetrics)(meterProvider, serviceName);
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 = bridges.get(serviceName);
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 bridge`);
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 bridge = createInertBridge(serviceName);
128
- bridges.set(serviceName, bridge);
129
- return bridge;
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.createBridgeMetrics)(meterProvider, serviceName);
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
- const otelLogger = loggerProvider.getLogger(serviceName);
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
- otelLogger.emit({
225
- body: err.message,
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
- void shutdown();
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
- /* best effort */
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
- otelLogger.emit({
245
- body: err.message,
246
- severityNumber: api_logs_1.SeverityNumber.ERROR,
247
- severityText: 'ERROR',
248
- attributes: {
249
- 'exception.type': err.constructor.name,
250
- 'exception.message': err.message,
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 bridge = {
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
- bridges.set(serviceName, bridge);
346
- return bridge;
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 { BridgeMetrics } from './types';
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?: BridgeMetrics): Promise<void>;
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, plus factories
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
- * 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).
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 { BridgeMetrics } from '../types';
29
- export declare function createBridgeMetrics(meterProvider: MeterProvider, serviceName: string): BridgeMetrics;
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, plus factories
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
- * 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).
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.createBridgeMetrics = createBridgeMetrics;
30
- function createBridgeMetrics(meterProvider, serviceName) {
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
- 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
- },
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 the bridge initializes.
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 the bridge initializes.
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 bridge): all metrics?.xxx calls are no-ops.
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 { BridgeMetrics, SqsMessage } from './types';
35
- export declare function wrapSqsConsumer(tracer: Tracer, spanName: string, msg: SqsMessage, fn: () => Promise<void>, metrics?: BridgeMetrics): Promise<void>;
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 bridge): all metrics?.xxx calls are no-ops.
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,5 +1,5 @@
1
1
  /**
2
- * W3C trace context bridge between New Relic and OpenTelemetry.
2
+ * W3C trace context utilities for New Relic and OpenTelemetry interop.
3
3
  *
4
4
  * Why this file exists:
5
5
  * NR owns the active trace (it creates transactions via http monkey-patching),
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  /**
3
- * W3C trace context bridge between New Relic and OpenTelemetry.
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 (OtelBridge, InitOptions,
6
- * TraceContext, BridgeMetrics) plus lightweight stubs for external frameworks
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 BridgeMetrics {
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 OtelBridge {
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: BridgeMetrics;
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 (OtelBridge, InitOptions,
7
- * TraceContext, BridgeMetrics) plus lightweight stubs for external frameworks
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.1",
4
- "description": "Unified observability (traces, logs, metrics) for Cliengo Node.js services, bridging New Relic APM with Foam's OTel collector.",
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
- "winston": "^3.0.0",
24
- "pino": "^8.0.0 || ^9.0.0"
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/sdk-trace-base": "^2.0.0",
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/api-logs": "^0.203.0"
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
- "@types/node": "^20.11.0"
51
+ "vitest": "^4.1.8"
50
52
  },
51
53
  "engines": {
52
54
  "node": ">=18"