@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.
Files changed (39) hide show
  1. package/dist/node-cliengo/src/constants.d.ts +26 -0
  2. package/dist/node-cliengo/src/constants.js +49 -0
  3. package/dist/node-cliengo/src/http/express.d.ts +41 -0
  4. package/dist/node-cliengo/src/http/express.js +123 -0
  5. package/dist/node-cliengo/src/http/fastify.d.ts +28 -0
  6. package/dist/node-cliengo/src/http/fastify.js +101 -0
  7. package/dist/node-cliengo/src/http/request-context.d.ts +32 -0
  8. package/dist/node-cliengo/src/http/request-context.js +114 -0
  9. package/dist/node-cliengo/src/index.d.ts +26 -0
  10. package/dist/node-cliengo/src/index.js +42 -0
  11. package/dist/node-cliengo/src/init.d.ts +50 -0
  12. package/dist/node-cliengo/src/init.js +348 -0
  13. package/dist/node-cliengo/src/job.d.ts +31 -0
  14. package/dist/node-cliengo/src/job.js +86 -0
  15. package/dist/node-cliengo/src/logs/pino-destination.d.ts +31 -0
  16. package/dist/node-cliengo/src/logs/pino-destination.js +100 -0
  17. package/dist/node-cliengo/src/logs/pino-mixin.d.ts +22 -0
  18. package/dist/node-cliengo/src/logs/pino-mixin.js +40 -0
  19. package/dist/node-cliengo/src/logs/winston-format.d.ts +28 -0
  20. package/dist/node-cliengo/src/logs/winston-format.js +48 -0
  21. package/dist/node-cliengo/src/logs/winston-transport.d.ts +31 -0
  22. package/dist/node-cliengo/src/logs/winston-transport.js +93 -0
  23. package/dist/node-cliengo/src/metrics/instruments.d.ts +29 -0
  24. package/dist/node-cliengo/src/metrics/instruments.js +78 -0
  25. package/dist/node-cliengo/src/nr.d.ts +35 -0
  26. package/dist/node-cliengo/src/nr.js +71 -0
  27. package/dist/node-cliengo/src/sns.d.ts +19 -0
  28. package/dist/node-cliengo/src/sns.js +31 -0
  29. package/dist/node-cliengo/src/sqs.d.ts +35 -0
  30. package/dist/node-cliengo/src/sqs.js +110 -0
  31. package/dist/node-cliengo/src/trace-bridge.d.ts +39 -0
  32. package/dist/node-cliengo/src/trace-bridge.js +79 -0
  33. package/dist/node-cliengo/src/types.d.ts +115 -0
  34. package/dist/node-cliengo/src/types.js +26 -0
  35. package/dist/shared/constants.d.ts +1 -0
  36. package/dist/shared/constants.js +4 -0
  37. package/dist/shared/util.d.ts +9 -0
  38. package/dist/shared/util.js +21 -0
  39. package/package.json +54 -0
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Shared constants for the @foam/node-cliengo bridge.
3
+ *
4
+ * Why this file exists:
5
+ * Centralizes all hardcoded values (endpoint, limits, redaction keys, health
6
+ * routes, request fields) so they are defined once and shared across every
7
+ * module. Changing a constant here propagates everywhere without hunting
8
+ * through individual files.
9
+ *
10
+ * Edge cases covered:
11
+ * - FOAM_OTEL_ENDPOINT is hardcoded (not read from env) to prevent
12
+ * misconfiguration; services override via `options.endpoint` only.
13
+ * - REDACTED_KEYS uses lowercase comparison so "Authorization", "PASSWORD",
14
+ * "Token" all match. Includes both "creditcard" and "credit_card" variants.
15
+ * - HEALTH_ROUTES covers the four most common patterns across all 5 Cliengo
16
+ * services so health-check traffic doesn't bloat span events.
17
+ * - REQUEST_CONTEXT_FIELDS lists every property the five services may put on
18
+ * req (Express or Fastify): auth, user, jwtPayload, session, cookies, etc.
19
+ * Missing properties are silently skipped at capture time.
20
+ */
21
+ export declare const FOAM_OTEL_ENDPOINT = "https://otel.api.foam.ai";
22
+ export declare const SHUTDOWN_TIMEOUT_MS = 5000;
23
+ export declare const BODY_TRUNCATION_LIMIT = 4096;
24
+ export declare const REDACTED_KEYS: Set<string>;
25
+ export declare const HEALTH_ROUTES: Set<string>;
26
+ export declare const REQUEST_CONTEXT_FIELDS: readonly ["params", "query", "body", "headers", "auth", "user", "jwtPayload", "session", "cookies", "ip", "hostname", "protocol"];
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ /**
3
+ * Shared constants for the @foam/node-cliengo bridge.
4
+ *
5
+ * Why this file exists:
6
+ * Centralizes all hardcoded values (endpoint, limits, redaction keys, health
7
+ * routes, request fields) so they are defined once and shared across every
8
+ * module. Changing a constant here propagates everywhere without hunting
9
+ * through individual files.
10
+ *
11
+ * Edge cases covered:
12
+ * - FOAM_OTEL_ENDPOINT is hardcoded (not read from env) to prevent
13
+ * misconfiguration; services override via `options.endpoint` only.
14
+ * - REDACTED_KEYS uses lowercase comparison so "Authorization", "PASSWORD",
15
+ * "Token" all match. Includes both "creditcard" and "credit_card" variants.
16
+ * - HEALTH_ROUTES covers the four most common patterns across all 5 Cliengo
17
+ * services so health-check traffic doesn't bloat span events.
18
+ * - REQUEST_CONTEXT_FIELDS lists every property the five services may put on
19
+ * req (Express or Fastify): auth, user, jwtPayload, session, cookies, etc.
20
+ * Missing properties are silently skipped at capture time.
21
+ */
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.REQUEST_CONTEXT_FIELDS = exports.HEALTH_ROUTES = exports.REDACTED_KEYS = exports.BODY_TRUNCATION_LIMIT = exports.SHUTDOWN_TIMEOUT_MS = exports.FOAM_OTEL_ENDPOINT = void 0;
24
+ exports.FOAM_OTEL_ENDPOINT = 'https://otel.api.foam.ai';
25
+ exports.SHUTDOWN_TIMEOUT_MS = 5000;
26
+ exports.BODY_TRUNCATION_LIMIT = 4096;
27
+ exports.REDACTED_KEYS = new Set([
28
+ 'password',
29
+ 'token',
30
+ 'authorization',
31
+ 'secret',
32
+ 'creditcard',
33
+ 'credit_card',
34
+ ]);
35
+ exports.HEALTH_ROUTES = new Set(['/health', '/healthz', '/ready', '/status']);
36
+ exports.REQUEST_CONTEXT_FIELDS = [
37
+ 'params',
38
+ 'query',
39
+ 'body',
40
+ 'headers',
41
+ 'auth',
42
+ 'user',
43
+ 'jwtPayload',
44
+ 'session',
45
+ 'cookies',
46
+ 'ip',
47
+ 'hostname',
48
+ 'protocol',
49
+ ];
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Express shadow HTTP middleware — creates one OTel span per request on our
3
+ * non-global provider, sharing NR's traceId for correlation.
4
+ *
5
+ * Why this file exists:
6
+ * NR auto-instruments HTTP via monkey-patching, but those are NR segments —
7
+ * not OTel spans. We can't inject into NR's provider (addSpanProcessor was
8
+ * removed in OTel SDK v2). Instead, this middleware creates a "shadow" span
9
+ * that mirrors the request lifecycle. Foam sees every HTTP request; NR is
10
+ * completely untouched.
11
+ *
12
+ * Two functions required:
13
+ * - createExpressMiddleware(): registered BEFORE routes. Creates the span on
14
+ * request start, records status/route/duration on `res.on('finish')`, and
15
+ * captures request context as a span event.
16
+ * - createExpressErrorHandler(): registered AFTER routes. Records exception
17
+ * details (type, message, stacktrace) on the shadow span. Must have the
18
+ * 4-argument Express error handler signature (err, req, res, next).
19
+ *
20
+ * Edge cases covered:
21
+ * - Error handler not registered: if a 500 response finishes without the
22
+ * error handler, we log a one-time warning so devs know they're missing
23
+ * exception details. Shadow span still shows status 500 — just no
24
+ * exception.type/message/stacktrace.
25
+ * - NR not loaded: getTraceContext returns empty strings. Span still created
26
+ * but without nr.traceId/nr.spanId attributes.
27
+ * - Health routes (/health, /healthz, /ready, /status): no request.context
28
+ * span event attached — avoids bloating spans with empty health check data.
29
+ * - req.route is undefined for 404s or middleware-only errors: falls back to
30
+ * req.originalUrl for the http.route attribute.
31
+ * - Errors thrown as non-Error objects (e.g. strings): wrapped in new Error()
32
+ * for consistent recording.
33
+ * - All callbacks wrapped in try/catch — a crash in span logic never takes
34
+ * down the request.
35
+ */
36
+ import { type Tracer } from '@opentelemetry/api';
37
+ import type { ExpressErrorHandler, ExpressRequestHandler } from '../types';
38
+ export declare function createExpressMiddleware(tracer: Tracer): ExpressRequestHandler;
39
+ export declare function createExpressErrorHandler(middlewareRef?: ExpressRequestHandler & {
40
+ _markErrorHandler?: () => void;
41
+ }): ExpressErrorHandler;
@@ -0,0 +1,123 @@
1
+ "use strict";
2
+ /**
3
+ * Express shadow HTTP middleware — creates one OTel span per request on our
4
+ * non-global provider, sharing NR's traceId for correlation.
5
+ *
6
+ * Why this file exists:
7
+ * NR auto-instruments HTTP via monkey-patching, but those are NR segments —
8
+ * not OTel spans. We can't inject into NR's provider (addSpanProcessor was
9
+ * removed in OTel SDK v2). Instead, this middleware creates a "shadow" span
10
+ * that mirrors the request lifecycle. Foam sees every HTTP request; NR is
11
+ * completely untouched.
12
+ *
13
+ * Two functions required:
14
+ * - createExpressMiddleware(): registered BEFORE routes. Creates the span on
15
+ * request start, records status/route/duration on `res.on('finish')`, and
16
+ * captures request context as a span event.
17
+ * - createExpressErrorHandler(): registered AFTER routes. Records exception
18
+ * details (type, message, stacktrace) on the shadow span. Must have the
19
+ * 4-argument Express error handler signature (err, req, res, next).
20
+ *
21
+ * Edge cases covered:
22
+ * - Error handler not registered: if a 500 response finishes without the
23
+ * error handler, we log a one-time warning so devs know they're missing
24
+ * exception details. Shadow span still shows status 500 — just no
25
+ * exception.type/message/stacktrace.
26
+ * - NR not loaded: getTraceContext returns empty strings. Span still created
27
+ * but without nr.traceId/nr.spanId attributes.
28
+ * - Health routes (/health, /healthz, /ready, /status): no request.context
29
+ * span event attached — avoids bloating spans with empty health check data.
30
+ * - req.route is undefined for 404s or middleware-only errors: falls back to
31
+ * req.originalUrl for the http.route attribute.
32
+ * - Errors thrown as non-Error objects (e.g. strings): wrapped in new Error()
33
+ * for consistent recording.
34
+ * - All callbacks wrapped in try/catch — a crash in span logic never takes
35
+ * down the request.
36
+ */
37
+ Object.defineProperty(exports, "__esModule", { value: true });
38
+ exports.createExpressMiddleware = createExpressMiddleware;
39
+ exports.createExpressErrorHandler = createExpressErrorHandler;
40
+ const api_1 = require("@opentelemetry/api");
41
+ const trace_bridge_1 = require("../trace-bridge");
42
+ const request_context_1 = require("./request-context");
43
+ const SPAN_KEY = Symbol.for('foam.shadow.span');
44
+ /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */
45
+ function createExpressMiddleware(tracer) {
46
+ let errorHandlerRegistered = false;
47
+ let warnedMissingErrorHandler = false;
48
+ const markErrorHandlerRegistered = () => {
49
+ errorHandlerRegistered = true;
50
+ };
51
+ const middleware = (req, res, next) => {
52
+ const method = req.method ?? 'UNKNOWN';
53
+ const target = req.originalUrl ?? req.url ?? '/';
54
+ const { traceId, spanId } = (0, trace_bridge_1.getTraceContext)();
55
+ const span = tracer.startSpan(`${method} ${target}`, {
56
+ attributes: {
57
+ 'http.method': method,
58
+ 'http.target': target,
59
+ ...(traceId ? { 'nr.traceId': traceId } : {}),
60
+ ...(spanId ? { 'nr.spanId': spanId } : {}),
61
+ },
62
+ });
63
+ req[SPAN_KEY] = span;
64
+ const startTime = Date.now();
65
+ const onFinish = () => {
66
+ try {
67
+ const statusCode = res.statusCode ?? 0;
68
+ const route = req.route?.path ?? target;
69
+ const contentLength = res.getHeader?.('content-length') ?? '';
70
+ span.setAttribute('http.status_code', statusCode);
71
+ span.setAttribute('http.route', route);
72
+ span.setAttribute('http.response_content_length', String(contentLength));
73
+ span.setAttribute('http.duration_ms', Date.now() - startTime);
74
+ if (statusCode >= 500) {
75
+ span.setStatus({ code: api_1.SpanStatusCode.ERROR });
76
+ if (!errorHandlerRegistered && !warnedMissingErrorHandler) {
77
+ warnedMissingErrorHandler = true;
78
+ // eslint-disable-next-line no-console
79
+ console.warn('[foam] createExpressErrorHandler() not registered — exception details will be missing from shadow spans');
80
+ }
81
+ }
82
+ else {
83
+ span.setStatus({ code: api_1.SpanStatusCode.OK });
84
+ }
85
+ if (!(0, request_context_1.isHealthRoute)(route) && !(0, request_context_1.isHealthRoute)(target)) {
86
+ (0, request_context_1.captureRequestContext)(span, req);
87
+ }
88
+ }
89
+ catch {
90
+ /* never crash on finish */
91
+ }
92
+ finally {
93
+ span.end();
94
+ }
95
+ };
96
+ res.on('finish', onFinish);
97
+ next();
98
+ };
99
+ middleware._markErrorHandler = markErrorHandlerRegistered;
100
+ return middleware;
101
+ }
102
+ function createExpressErrorHandler(middlewareRef) {
103
+ middlewareRef?._markErrorHandler?.();
104
+ return (err, req, _res, next) => {
105
+ try {
106
+ const span = req[SPAN_KEY];
107
+ if (span) {
108
+ const error = err instanceof Error ? err : new Error(String(err));
109
+ span.recordException({
110
+ name: error.constructor.name,
111
+ message: error.message,
112
+ stack: error.stack,
113
+ });
114
+ span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: error.message });
115
+ }
116
+ }
117
+ catch {
118
+ /* never crash error handling */
119
+ }
120
+ next(err);
121
+ };
122
+ }
123
+ /* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Fastify shadow HTTP plugin — creates one OTel span per request on our
3
+ * non-global provider, sharing NR's traceId for correlation.
4
+ *
5
+ * Why this file exists:
6
+ * Same rationale as the Express middleware (see express.ts). Fastify's hook
7
+ * system is different — we use three hooks instead of middleware + error handler:
8
+ * - onRequest: creates the shadow span, reads NR's traceId
9
+ * - onError: records exception details (fires BEFORE errorHandler, so we see
10
+ * every error including ones mapped to non-500 responses like AppError.badRequest)
11
+ * - onResponse: records status/route/duration, captures request context, ends span
12
+ *
13
+ * Used by kori (~230 Fastify routes).
14
+ *
15
+ * Edge cases covered:
16
+ * - Fastify's onError fires for ALL errors including 4xx AppError variants.
17
+ * We record the exception on the span regardless — Foam sees the full error
18
+ * even for 400s. The span status is only set to ERROR by onError (not
19
+ * overwritten to OK by onResponse for <500 codes, since onError already set it).
20
+ * - request.routeOptions?.url may not exist in older Fastify versions — falls
21
+ * back to request.url.
22
+ * - Health routes skipped for request.context capture (same as Express).
23
+ * - All hooks wrapped in try/catch — never crashes the request pipeline.
24
+ * - done() always called even if our code throws — Fastify hook contract.
25
+ */
26
+ import { type Tracer } from '@opentelemetry/api';
27
+ import type { FastifyPluginAsync } from '../types';
28
+ export declare function createFastifyPlugin(tracer: Tracer): FastifyPluginAsync;
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ /**
3
+ * Fastify shadow HTTP plugin — creates one OTel span per request on our
4
+ * non-global provider, sharing NR's traceId for correlation.
5
+ *
6
+ * Why this file exists:
7
+ * Same rationale as the Express middleware (see express.ts). Fastify's hook
8
+ * system is different — we use three hooks instead of middleware + error handler:
9
+ * - onRequest: creates the shadow span, reads NR's traceId
10
+ * - onError: records exception details (fires BEFORE errorHandler, so we see
11
+ * every error including ones mapped to non-500 responses like AppError.badRequest)
12
+ * - onResponse: records status/route/duration, captures request context, ends span
13
+ *
14
+ * Used by kori (~230 Fastify routes).
15
+ *
16
+ * Edge cases covered:
17
+ * - Fastify's onError fires for ALL errors including 4xx AppError variants.
18
+ * We record the exception on the span regardless — Foam sees the full error
19
+ * even for 400s. The span status is only set to ERROR by onError (not
20
+ * overwritten to OK by onResponse for <500 codes, since onError already set it).
21
+ * - request.routeOptions?.url may not exist in older Fastify versions — falls
22
+ * back to request.url.
23
+ * - Health routes skipped for request.context capture (same as Express).
24
+ * - All hooks wrapped in try/catch — never crashes the request pipeline.
25
+ * - done() always called even if our code throws — Fastify hook contract.
26
+ */
27
+ Object.defineProperty(exports, "__esModule", { value: true });
28
+ exports.createFastifyPlugin = createFastifyPlugin;
29
+ const api_1 = require("@opentelemetry/api");
30
+ const trace_bridge_1 = require("../trace-bridge");
31
+ const request_context_1 = require("./request-context");
32
+ const SPAN_KEY = Symbol.for('foam.shadow.span');
33
+ const START_KEY = Symbol.for('foam.shadow.start');
34
+ /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */
35
+ function createFastifyPlugin(tracer) {
36
+ return async (fastify) => {
37
+ fastify.addHook('onRequest', (request, _reply, done) => {
38
+ try {
39
+ const method = request.method ?? 'UNKNOWN';
40
+ const url = request.url ?? '/';
41
+ const { traceId, spanId } = (0, trace_bridge_1.getTraceContext)();
42
+ const span = tracer.startSpan(`${method} ${url}`, {
43
+ attributes: {
44
+ 'http.method': method,
45
+ 'http.target': url,
46
+ ...(traceId ? { 'nr.traceId': traceId } : {}),
47
+ ...(spanId ? { 'nr.spanId': spanId } : {}),
48
+ },
49
+ });
50
+ request[SPAN_KEY] = span;
51
+ request[START_KEY] = Date.now();
52
+ }
53
+ catch {
54
+ /* never crash */
55
+ }
56
+ done();
57
+ });
58
+ fastify.addHook('onError', (request, _reply, error, done) => {
59
+ try {
60
+ const span = request[SPAN_KEY];
61
+ if (span) {
62
+ span.recordException({
63
+ name: error.constructor.name,
64
+ message: error.message,
65
+ stack: error.stack,
66
+ });
67
+ span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: error.message });
68
+ }
69
+ }
70
+ catch {
71
+ /* never crash */
72
+ }
73
+ done();
74
+ });
75
+ fastify.addHook('onResponse', (request, reply, done) => {
76
+ try {
77
+ const span = request[SPAN_KEY];
78
+ if (span) {
79
+ const statusCode = reply.statusCode ?? 0;
80
+ const route = request.routeOptions?.url ?? request.url ?? '/';
81
+ const start = request[START_KEY] ?? Date.now();
82
+ span.setAttribute('http.status_code', statusCode);
83
+ span.setAttribute('http.route', route);
84
+ span.setAttribute('http.duration_ms', Date.now() - start);
85
+ if (statusCode < 500) {
86
+ span.setStatus({ code: api_1.SpanStatusCode.OK });
87
+ }
88
+ if (!(0, request_context_1.isHealthRoute)(route) && !(0, request_context_1.isHealthRoute)(request.url ?? '/')) {
89
+ (0, request_context_1.captureRequestContext)(span, request);
90
+ }
91
+ span.end();
92
+ }
93
+ }
94
+ catch {
95
+ /* never crash */
96
+ }
97
+ done();
98
+ });
99
+ };
100
+ }
101
+ /* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Request context capture — always-on, fail-safe serialization of HTTP request
3
+ * inputs into a single OTel span event called "request.context".
4
+ *
5
+ * Why this file exists:
6
+ * When a crash occurs, the exception alone (type + message + stack) often
7
+ * isn't enough to reproduce it — you need the inputs: route params, query,
8
+ * body, auth token, etc. This module generically captures whatever is on the
9
+ * request object and attaches it as a span event (not span attributes — those
10
+ * would blow up cardinality indexes).
11
+ *
12
+ * Why always-on:
13
+ * If opt-in, nobody enables it. The safety controls make it safe on every request.
14
+ *
15
+ * Why generic field capture:
16
+ * Different services put auth in different places (req.auth, req.user,
17
+ * req.jwtPayload). Rather than maintaining a per-service mapping, we loop
18
+ * over REQUEST_CONTEXT_FIELDS and serialize whatever is present.
19
+ *
20
+ * Safety controls:
21
+ * - Body truncation: 4KB limit prevents span bloat from file uploads, bulk
22
+ * CSV, or base64 media.
23
+ * - Field redaction: password, token, authorization, secret, creditCard are
24
+ * replaced with "[REDACTED]". Deep-redacts nested objects. Case-insensitive.
25
+ * - Health route skip: /health, /healthz, /ready, /status produce no event.
26
+ * - Fail-safe: every property access AND serialization is in its own try/catch.
27
+ * A throwing getter, circular object, or proxy trap on one field can never
28
+ * prevent other fields from being captured, and can never crash the request.
29
+ */
30
+ import type { Span } from '@opentelemetry/api';
31
+ export declare function isHealthRoute(path: string): boolean;
32
+ export declare function captureRequestContext(span: Span, req: any): void;
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ /**
3
+ * Request context capture — always-on, fail-safe serialization of HTTP request
4
+ * inputs into a single OTel span event called "request.context".
5
+ *
6
+ * Why this file exists:
7
+ * When a crash occurs, the exception alone (type + message + stack) often
8
+ * isn't enough to reproduce it — you need the inputs: route params, query,
9
+ * body, auth token, etc. This module generically captures whatever is on the
10
+ * request object and attaches it as a span event (not span attributes — those
11
+ * would blow up cardinality indexes).
12
+ *
13
+ * Why always-on:
14
+ * If opt-in, nobody enables it. The safety controls make it safe on every request.
15
+ *
16
+ * Why generic field capture:
17
+ * Different services put auth in different places (req.auth, req.user,
18
+ * req.jwtPayload). Rather than maintaining a per-service mapping, we loop
19
+ * over REQUEST_CONTEXT_FIELDS and serialize whatever is present.
20
+ *
21
+ * Safety controls:
22
+ * - Body truncation: 4KB limit prevents span bloat from file uploads, bulk
23
+ * CSV, or base64 media.
24
+ * - Field redaction: password, token, authorization, secret, creditCard are
25
+ * replaced with "[REDACTED]". Deep-redacts nested objects. Case-insensitive.
26
+ * - Health route skip: /health, /healthz, /ready, /status produce no event.
27
+ * - Fail-safe: every property access AND serialization is in its own try/catch.
28
+ * A throwing getter, circular object, or proxy trap on one field can never
29
+ * prevent other fields from being captured, and can never crash the request.
30
+ */
31
+ Object.defineProperty(exports, "__esModule", { value: true });
32
+ exports.isHealthRoute = isHealthRoute;
33
+ exports.captureRequestContext = captureRequestContext;
34
+ const constants_1 = require("../constants");
35
+ function truncate(value, limit) {
36
+ if (value.length <= limit) {
37
+ return value;
38
+ }
39
+ return value.slice(0, limit) + '...[truncated]';
40
+ }
41
+ /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */
42
+ function redactValue(obj) {
43
+ if (typeof obj === 'string') {
44
+ return obj;
45
+ }
46
+ if (Array.isArray(obj)) {
47
+ return obj.map((item) => redactValue(item));
48
+ }
49
+ if (obj !== null && typeof obj === 'object') {
50
+ const result = {};
51
+ for (const [key, val] of Object.entries(obj)) {
52
+ if (constants_1.REDACTED_KEYS.has(key.toLowerCase())) {
53
+ result[key] = '[REDACTED]';
54
+ }
55
+ else {
56
+ result[key] = redactValue(val);
57
+ }
58
+ }
59
+ return result;
60
+ }
61
+ return obj;
62
+ }
63
+ function redact(raw) {
64
+ try {
65
+ const parsed = JSON.parse(raw);
66
+ return JSON.stringify(redactValue(parsed));
67
+ }
68
+ catch {
69
+ return raw;
70
+ }
71
+ }
72
+ /* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */
73
+ function isHealthRoute(path) {
74
+ return constants_1.HEALTH_ROUTES.has(path);
75
+ }
76
+ /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */
77
+ function captureRequestContext(span, req) {
78
+ try {
79
+ const attrs = {};
80
+ const fields = {};
81
+ for (const key of constants_1.REQUEST_CONTEXT_FIELDS) {
82
+ try {
83
+ const val = req[key];
84
+ if (val !== undefined && val !== null) {
85
+ fields[key] = val;
86
+ }
87
+ }
88
+ catch {
89
+ /* skip inaccessible properties */
90
+ }
91
+ }
92
+ for (const [key, val] of Object.entries(fields)) {
93
+ try {
94
+ const raw = typeof val === 'string' ? val : JSON.stringify(val);
95
+ if (key === 'body') {
96
+ attrs[key] = truncate(redact(raw), constants_1.BODY_TRUNCATION_LIMIT);
97
+ }
98
+ else {
99
+ attrs[key] = redact(typeof val === 'object' ? raw : String(val));
100
+ }
101
+ }
102
+ catch {
103
+ /* skip unserializable values */
104
+ }
105
+ }
106
+ if (Object.keys(attrs).length > 0) {
107
+ span.addEvent('request.context', attrs);
108
+ }
109
+ }
110
+ catch {
111
+ /* never crash the request */
112
+ }
113
+ }
114
+ /* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment */
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Barrel re-export for @foam/node-cliengo.
3
+ *
4
+ * Why this file exists:
5
+ * Single entry point so consumers import from '@foam/node-cliengo' directly.
6
+ * Re-exports init() (the main API), individual helpers for advanced usage
7
+ * (buildTraceparent, injectSnsAttributes, etc.), and all public types.
8
+ *
9
+ * Most services only need: import { init } from '@foam/node-cliengo'
10
+ * Everything else is accessed via the returned OtelBridge object.
11
+ *
12
+ * The individual function exports exist for tree-shaking and for cases where
13
+ * a service needs to call e.g. injectJobData() from a file that doesn't have
14
+ * access to the bridge instance.
15
+ */
16
+ export { init } from './init';
17
+ export { buildTraceparent, extractParentContext, getTraceContext } from './trace-bridge';
18
+ export { injectSnsAttributes } from './sns';
19
+ export { injectJobData, wrapJobConsumer } from './job';
20
+ export { wrapSqsConsumer } from './sqs';
21
+ export { createExpressMiddleware, createExpressErrorHandler } from './http/express';
22
+ export { createFastifyPlugin } from './http/fastify';
23
+ export { createWinstonFormat } from './logs/winston-format';
24
+ export { createPinoMixin } from './logs/pino-mixin';
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';
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ /**
3
+ * Barrel re-export for @foam/node-cliengo.
4
+ *
5
+ * Why this file exists:
6
+ * Single entry point so consumers import from '@foam/node-cliengo' directly.
7
+ * Re-exports init() (the main API), individual helpers for advanced usage
8
+ * (buildTraceparent, injectSnsAttributes, etc.), and all public types.
9
+ *
10
+ * Most services only need: import { init } from '@foam/node-cliengo'
11
+ * Everything else is accessed via the returned OtelBridge object.
12
+ *
13
+ * The individual function exports exist for tree-shaking and for cases where
14
+ * a service needs to call e.g. injectJobData() from a file that doesn't have
15
+ * access to the bridge instance.
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
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;
19
+ var init_1 = require("./init");
20
+ Object.defineProperty(exports, "init", { enumerable: true, get: function () { return init_1.init; } });
21
+ var trace_bridge_1 = require("./trace-bridge");
22
+ Object.defineProperty(exports, "buildTraceparent", { enumerable: true, get: function () { return trace_bridge_1.buildTraceparent; } });
23
+ Object.defineProperty(exports, "extractParentContext", { enumerable: true, get: function () { return trace_bridge_1.extractParentContext; } });
24
+ Object.defineProperty(exports, "getTraceContext", { enumerable: true, get: function () { return trace_bridge_1.getTraceContext; } });
25
+ var sns_1 = require("./sns");
26
+ Object.defineProperty(exports, "injectSnsAttributes", { enumerable: true, get: function () { return sns_1.injectSnsAttributes; } });
27
+ var job_1 = require("./job");
28
+ Object.defineProperty(exports, "injectJobData", { enumerable: true, get: function () { return job_1.injectJobData; } });
29
+ Object.defineProperty(exports, "wrapJobConsumer", { enumerable: true, get: function () { return job_1.wrapJobConsumer; } });
30
+ var sqs_1 = require("./sqs");
31
+ Object.defineProperty(exports, "wrapSqsConsumer", { enumerable: true, get: function () { return sqs_1.wrapSqsConsumer; } });
32
+ var express_1 = require("./http/express");
33
+ Object.defineProperty(exports, "createExpressMiddleware", { enumerable: true, get: function () { return express_1.createExpressMiddleware; } });
34
+ Object.defineProperty(exports, "createExpressErrorHandler", { enumerable: true, get: function () { return express_1.createExpressErrorHandler; } });
35
+ var fastify_1 = require("./http/fastify");
36
+ Object.defineProperty(exports, "createFastifyPlugin", { enumerable: true, get: function () { return fastify_1.createFastifyPlugin; } });
37
+ var winston_format_1 = require("./logs/winston-format");
38
+ Object.defineProperty(exports, "createWinstonFormat", { enumerable: true, get: function () { return winston_format_1.createWinstonFormat; } });
39
+ var pino_mixin_1 = require("./logs/pino-mixin");
40
+ Object.defineProperty(exports, "createPinoMixin", { enumerable: true, get: function () { return pino_mixin_1.createPinoMixin; } });
41
+ var constants_1 = require("./constants");
42
+ Object.defineProperty(exports, "FOAM_OTEL_ENDPOINT", { enumerable: true, get: function () { return constants_1.FOAM_OTEL_ENDPOINT; } });
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Main entry point — init() creates the OtelBridge singleton for a service.
3
+ *
4
+ * Why this file exists:
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
8
+ * designed to coexist with New Relic APM without touching NR's globals.
9
+ *
10
+ * Two-gate system:
11
+ * 1. Kill switch (FOAM_OTEL_ENABLED): if false, returns a fully inert bridge.
12
+ * Every API exists but does nothing — zero overhead, zero network, zero risk.
13
+ * This is the master emergency shutoff.
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
16
+ * functional (trace propagation, log enrichment work) but nothing is sent
17
+ * to Foam. Developers without FOAM_OTEL_TOKEN get no crashes.
18
+ *
19
+ * Edge cases covered:
20
+ * - Double init(): logs a warning, returns the existing bridge (singleton
21
+ * per service name). Prevents duplicate providers/processors/handlers.
22
+ * - Token undefined in dev: silently accepted — no OTLP export anyway.
23
+ * - Token undefined in prod: logs warning, disables exporters. All other
24
+ * features (trace propagation, log enrichment, NR bridging) still work.
25
+ * - Token undefined + forceExport: throws (fail-fast — you asked to force
26
+ * export but provided no credentials).
27
+ * - Auto-shutdown (SIGTERM/SIGINT): flushes all providers with 5s timeout,
28
+ * then calls process.exit(0). Without the explicit exit, registering
29
+ * process.on('SIGTERM') replaces Node's default terminate behavior — services
30
+ * without their own handler (gpt-intentions, cb-proxy) would hang forever.
31
+ * - Manual shutdown: services with ordered cleanup (hsm-backend, kori, combee)
32
+ * pass autoShutdown: false and call bridge.shutdown() in their own handler.
33
+ * - shutdown() is idempotent — safe to call multiple times.
34
+ * - Process exception handlers use process.on() (not .once()) so they don't
35
+ * 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
+ * - Winston auto-wiring: init({ winston: baseLogger }) calls logger.add() for
39
+ * the OTLP transport and rewrites logger.format to prepend trace enrichment.
40
+ * Both work post-construction. Avoids circular deps (main.ts imports logger,
41
+ * not the other way around).
42
+ * - First-export health check: monkey-patches the BatchSpanProcessor's exporter
43
+ * to log success/failure on the first OTLP export. Non-critical — wrapped in
44
+ * try/catch, uses internal OTel SDK APIs that may change.
45
+ * - Non-global providers: BasicTracerProvider, LoggerProvider, MeterProvider
46
+ * are NEVER registered globally (.register() never called). NR owns the
47
+ * global registrations.
48
+ */
49
+ import type { InitOptions, OtelBridge } from './types';
50
+ export declare function init(serviceName: string, token?: string, options?: InitOptions): OtelBridge;