@foam-ai/node 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/node/src/capture-exception.d.ts +9 -0
- package/dist/node/src/capture-exception.js +31 -0
- package/dist/node/src/index.d.ts +9 -0
- package/dist/node/src/index.js +12 -0
- package/dist/node/src/init.d.ts +27 -0
- package/dist/node/src/init.js +203 -0
- package/dist/shared/constants.d.ts +1 -0
- package/dist/shared/constants.js +4 -0
- package/dist/shared/util.d.ts +9 -0
- package/dist/shared/util.js +21 -0
- package/package.json +35 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Captures an exception and sends it to Foam.
|
|
3
|
+
*
|
|
4
|
+
* - Records on the active span if one exists (for trace-level visibility)
|
|
5
|
+
* - Always emits an OTEL log record so the error reaches Foam even when
|
|
6
|
+
* there is no active span or auto-instrumentation
|
|
7
|
+
* - Safe to call at any time — silently no-ops if the SDK isn't initialized
|
|
8
|
+
*/
|
|
9
|
+
export declare const captureException: (error: unknown) => void;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.captureException = void 0;
|
|
4
|
+
const api_1 = require("@opentelemetry/api");
|
|
5
|
+
const api_logs_1 = require("@opentelemetry/api-logs");
|
|
6
|
+
const util_1 = require("../../shared/util");
|
|
7
|
+
/**
|
|
8
|
+
* Captures an exception and sends it to Foam.
|
|
9
|
+
*
|
|
10
|
+
* - Records on the active span if one exists (for trace-level visibility)
|
|
11
|
+
* - Always emits an OTEL log record so the error reaches Foam even when
|
|
12
|
+
* there is no active span or auto-instrumentation
|
|
13
|
+
* - Safe to call at any time — silently no-ops if the SDK isn't initialized
|
|
14
|
+
*/
|
|
15
|
+
exports.captureException = (0, util_1.safe)((error) => {
|
|
16
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
17
|
+
const span = api_1.trace.getActiveSpan();
|
|
18
|
+
if (span) {
|
|
19
|
+
span.recordException(err);
|
|
20
|
+
span.setStatus({ code: api_1.SpanStatusCode.ERROR });
|
|
21
|
+
}
|
|
22
|
+
api_logs_1.logs.getLogger('foam').emit({
|
|
23
|
+
severityNumber: api_logs_1.SeverityNumber.ERROR,
|
|
24
|
+
severityText: 'ERROR',
|
|
25
|
+
body: err.message,
|
|
26
|
+
attributes: {
|
|
27
|
+
'exception.type': err.name,
|
|
28
|
+
'exception.stacktrace': err.stack ?? '',
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { captureException } from './capture-exception';
|
|
2
|
+
import { init } from './init';
|
|
3
|
+
export { captureException, init };
|
|
4
|
+
export type { FoamNodeInitOptions } from './init';
|
|
5
|
+
declare const foam: {
|
|
6
|
+
captureException: (error: unknown) => void;
|
|
7
|
+
init: (options: import("./init").FoamNodeInitOptions) => void;
|
|
8
|
+
};
|
|
9
|
+
export default foam;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.init = exports.captureException = void 0;
|
|
4
|
+
const capture_exception_1 = require("./capture-exception");
|
|
5
|
+
Object.defineProperty(exports, "captureException", { enumerable: true, get: function () { return capture_exception_1.captureException; } });
|
|
6
|
+
const init_1 = require("./init");
|
|
7
|
+
Object.defineProperty(exports, "init", { enumerable: true, get: function () { return init_1.init; } });
|
|
8
|
+
const foam = {
|
|
9
|
+
captureException: capture_exception_1.captureException,
|
|
10
|
+
init: init_1.init,
|
|
11
|
+
};
|
|
12
|
+
exports.default = foam;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { InstrumentationBase } from '@opentelemetry/instrumentation';
|
|
2
|
+
export interface FoamNodeInitOptions {
|
|
3
|
+
apiKey: string;
|
|
4
|
+
serviceName: string;
|
|
5
|
+
isProduction: boolean;
|
|
6
|
+
/**
|
|
7
|
+
* OpenTelemetry instrumentations to register (e.g. getNodeAutoInstrumentations()).
|
|
8
|
+
* None are registered by default to keep the SDK webpack/Turbopack-safe.
|
|
9
|
+
* For plain Node.js servers, pass getNodeAutoInstrumentations() to get
|
|
10
|
+
* automatic HTTP, Express, DNS, etc. instrumentation.
|
|
11
|
+
*/
|
|
12
|
+
instrumentations?: InstrumentationBase[];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Initializes the Foam SDK for Node.js.
|
|
16
|
+
*
|
|
17
|
+
* - Production-only: silently no-ops when `isProduction` is false.
|
|
18
|
+
* - Zero latency: all setup is synchronous object construction; actual telemetry
|
|
19
|
+
* export happens asynchronously in background batch intervals.
|
|
20
|
+
* - Crash-safe: every signal is wrapped in its own try/catch so a failure in
|
|
21
|
+
* one (e.g. traces) never prevents the others (logs, metrics) from starting.
|
|
22
|
+
* - Provider-aware: detects each OTEL signal independently. If another SDK
|
|
23
|
+
* (Sentry, @vercel/otel, etc.) already registered a provider, Foam attaches
|
|
24
|
+
* its exporters on top. If no provider exists, Foam creates one. This
|
|
25
|
+
* guarantees all three signals always reach Foam regardless of environment.
|
|
26
|
+
*/
|
|
27
|
+
export declare const init: (options: FoamNodeInitOptions) => void;
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.init = void 0;
|
|
4
|
+
const sdk_trace_node_1 = require("@opentelemetry/sdk-trace-node");
|
|
5
|
+
const sdk_trace_base_1 = require("@opentelemetry/sdk-trace-base");
|
|
6
|
+
const exporter_trace_otlp_http_1 = require("@opentelemetry/exporter-trace-otlp-http");
|
|
7
|
+
const exporter_logs_otlp_http_1 = require("@opentelemetry/exporter-logs-otlp-http");
|
|
8
|
+
const sdk_logs_1 = require("@opentelemetry/sdk-logs");
|
|
9
|
+
const sdk_metrics_1 = require("@opentelemetry/sdk-metrics");
|
|
10
|
+
const exporter_metrics_otlp_http_1 = require("@opentelemetry/exporter-metrics-otlp-http");
|
|
11
|
+
const instrumentation_1 = require("@opentelemetry/instrumentation");
|
|
12
|
+
const api_1 = require("@opentelemetry/api");
|
|
13
|
+
const api_logs_1 = require("@opentelemetry/api-logs");
|
|
14
|
+
const resources_1 = require("@opentelemetry/resources");
|
|
15
|
+
const util_1 = require("util");
|
|
16
|
+
const constants_1 = require("../../shared/constants");
|
|
17
|
+
const util_2 = require("../../shared/util");
|
|
18
|
+
const capture_exception_1 = require("./capture-exception");
|
|
19
|
+
/**
|
|
20
|
+
* Initializes the Foam SDK for Node.js.
|
|
21
|
+
*
|
|
22
|
+
* - Production-only: silently no-ops when `isProduction` is false.
|
|
23
|
+
* - Zero latency: all setup is synchronous object construction; actual telemetry
|
|
24
|
+
* export happens asynchronously in background batch intervals.
|
|
25
|
+
* - Crash-safe: every signal is wrapped in its own try/catch so a failure in
|
|
26
|
+
* one (e.g. traces) never prevents the others (logs, metrics) from starting.
|
|
27
|
+
* - Provider-aware: detects each OTEL signal independently. If another SDK
|
|
28
|
+
* (Sentry, @vercel/otel, etc.) already registered a provider, Foam attaches
|
|
29
|
+
* its exporters on top. If no provider exists, Foam creates one. This
|
|
30
|
+
* guarantees all three signals always reach Foam regardless of environment.
|
|
31
|
+
*/
|
|
32
|
+
exports.init = (0, util_2.safe)((options) => {
|
|
33
|
+
if (!options.isProduction) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const headers = { authorization: `Bearer ${options.apiKey}` };
|
|
37
|
+
const resource = (0, resources_1.resourceFromAttributes)({ 'service.name': options.serviceName });
|
|
38
|
+
// Each signal is independent — a failure in one must not block the others
|
|
39
|
+
safeRun(() => ensureTraces(headers, resource, options.instrumentations));
|
|
40
|
+
safeRun(() => ensureLogs(headers, resource));
|
|
41
|
+
safeRun(() => ensureMetrics(headers, resource));
|
|
42
|
+
safeRun(() => patchConsole());
|
|
43
|
+
safeRun(() => registerProcessErrorHandlers());
|
|
44
|
+
});
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Per-signal setup
|
|
47
|
+
//
|
|
48
|
+
// For each signal the pattern is:
|
|
49
|
+
// 1. Create a batch processor/reader with an HTTP OTLP exporter
|
|
50
|
+
// 2. Check if a real provider is already registered globally
|
|
51
|
+
// 3. If yes → attach the processor to the existing provider
|
|
52
|
+
// 4. If no → create a new provider and register it globally
|
|
53
|
+
//
|
|
54
|
+
// All exporters use HTTP (not gRPC) to stay webpack/Turbopack-compatible.
|
|
55
|
+
// Batch processors buffer spans/logs in memory and flush asynchronously,
|
|
56
|
+
// so init() returns instantly with zero latency impact on the caller.
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
function ensureTraces(headers, resource, instrumentations) {
|
|
59
|
+
const processor = new sdk_trace_base_1.BatchSpanProcessor(new exporter_trace_otlp_http_1.OTLPTraceExporter({ url: `${constants_1.FOAM_OTEL_ENDPOINT}/v1/traces`, headers }));
|
|
60
|
+
const existing = unwrapProvider(api_1.trace.getTracerProvider());
|
|
61
|
+
if (!addProcessor(existing, 'addSpanProcessor', '_activeSpanProcessor', processor)) {
|
|
62
|
+
new sdk_trace_node_1.NodeTracerProvider({ resource, spanProcessors: [processor] }).register();
|
|
63
|
+
}
|
|
64
|
+
if (instrumentations?.length) {
|
|
65
|
+
(0, instrumentation_1.registerInstrumentations)({ instrumentations });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function ensureLogs(headers, resource) {
|
|
69
|
+
const processor = new sdk_logs_1.BatchLogRecordProcessor(new exporter_logs_otlp_http_1.OTLPLogExporter({ url: `${constants_1.FOAM_OTEL_ENDPOINT}/v1/logs`, headers }));
|
|
70
|
+
const existing = unwrapProvider(api_logs_1.logs.getLoggerProvider());
|
|
71
|
+
if (!addProcessor(existing, 'addLogRecordProcessor', '_sharedState', processor)) {
|
|
72
|
+
const provider = new sdk_logs_1.LoggerProvider({ resource, processors: [processor] });
|
|
73
|
+
api_logs_1.logs.setGlobalLoggerProvider(provider);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function ensureMetrics(headers, resource) {
|
|
77
|
+
const reader = new sdk_metrics_1.PeriodicExportingMetricReader({
|
|
78
|
+
exporter: new exporter_metrics_otlp_http_1.OTLPMetricExporter({
|
|
79
|
+
url: `${constants_1.FOAM_OTEL_ENDPOINT}/v1/metrics`,
|
|
80
|
+
headers,
|
|
81
|
+
}),
|
|
82
|
+
});
|
|
83
|
+
const existing = unwrapProvider(api_1.metrics.getMeterProvider());
|
|
84
|
+
if (!addProcessor(existing, 'addMetricReader', '_sharedState', reader)) {
|
|
85
|
+
const provider = new sdk_metrics_1.MeterProvider({ resource, readers: [reader] });
|
|
86
|
+
api_1.metrics.setGlobalMeterProvider(provider);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// Helpers
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
/** Runs a function, swallowing any error so one failed signal can't block others. */
|
|
93
|
+
function safeRun(fn) {
|
|
94
|
+
try {
|
|
95
|
+
fn();
|
|
96
|
+
}
|
|
97
|
+
catch { /* intentionally swallowed */ }
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Unwraps proxy/noop providers to get the real delegate.
|
|
101
|
+
*
|
|
102
|
+
* OTEL wraps every global provider in a Proxy (ProxyTracerProvider,
|
|
103
|
+
* ProxyLoggerProvider, etc.). Before checking for methods like
|
|
104
|
+
* addSpanProcessor we need the real underlying provider.
|
|
105
|
+
*
|
|
106
|
+
* Returns {} for noop providers so callers can safely check for methods
|
|
107
|
+
* with typeof without null guards.
|
|
108
|
+
*/
|
|
109
|
+
function unwrapProvider(provider) {
|
|
110
|
+
const delegate = typeof provider.getDelegate === 'function'
|
|
111
|
+
? provider.getDelegate()
|
|
112
|
+
: provider._delegate ?? provider;
|
|
113
|
+
if (!delegate || delegate.constructor?.name?.startsWith('Noop'))
|
|
114
|
+
return {};
|
|
115
|
+
return delegate;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Attempts to add a processor/reader to an existing provider.
|
|
119
|
+
*
|
|
120
|
+
* Strategy:
|
|
121
|
+
* 1. Try the public v1 method (addSpanProcessor, addLogRecordProcessor, addMetricReader)
|
|
122
|
+
* 2. Fall back to pushing into internal arrays used by SDK v2:
|
|
123
|
+
* - Traces: provider._activeSpanProcessor._spanProcessors
|
|
124
|
+
* - Logs: provider._sharedState.registeredLogRecordProcessors
|
|
125
|
+
* - Metrics: provider._sharedState has no writable reader array, so metrics
|
|
126
|
+
* always creates a new provider if the public method is missing
|
|
127
|
+
*
|
|
128
|
+
* All internal access is guarded by Array.isArray — safe if internals change.
|
|
129
|
+
* Returns true if successfully attached, false if a new provider is needed.
|
|
130
|
+
*/
|
|
131
|
+
function addProcessor(provider, v1Method, v2Field, processor) {
|
|
132
|
+
if (typeof provider[v1Method] === 'function') {
|
|
133
|
+
provider[v1Method](processor);
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
const state = provider[v2Field];
|
|
137
|
+
if (!state)
|
|
138
|
+
return false;
|
|
139
|
+
const internal = state._spanProcessors ??
|
|
140
|
+
state._processors ??
|
|
141
|
+
state.registeredLogRecordProcessors;
|
|
142
|
+
if (Array.isArray(internal)) {
|
|
143
|
+
internal.push(processor);
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// Uncaught error handlers
|
|
150
|
+
//
|
|
151
|
+
// Listens to process-level uncaughtException and unhandledRejection events
|
|
152
|
+
// to capture errors that escape all try/catch blocks. Works for any framework
|
|
153
|
+
// (Express, Fastify, Koa, Next.js, plain Node.js, background jobs, etc.).
|
|
154
|
+
// Idempotent — safe to call multiple times. Never interferes with the app's
|
|
155
|
+
// default crash behavior since we only listen (not override).
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
const PROCESS_HANDLERS_INSTALLED = Symbol.for('foam.process.handlers');
|
|
158
|
+
function registerProcessErrorHandlers() {
|
|
159
|
+
if (globalThis[PROCESS_HANDLERS_INSTALLED])
|
|
160
|
+
return;
|
|
161
|
+
process.on('uncaughtException', (err) => {
|
|
162
|
+
try {
|
|
163
|
+
(0, capture_exception_1.captureException)(err);
|
|
164
|
+
}
|
|
165
|
+
catch { /* never interfere with the crash */ }
|
|
166
|
+
});
|
|
167
|
+
process.on('unhandledRejection', (reason) => {
|
|
168
|
+
try {
|
|
169
|
+
(0, capture_exception_1.captureException)(reason);
|
|
170
|
+
}
|
|
171
|
+
catch { /* never interfere with the crash */ }
|
|
172
|
+
});
|
|
173
|
+
globalThis[PROCESS_HANDLERS_INSTALLED] = true;
|
|
174
|
+
}
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
// Console log capture
|
|
177
|
+
//
|
|
178
|
+
// Wraps console.log/info/error/warn/debug to emit OTel log records.
|
|
179
|
+
// Uses a Symbol to ensure idempotency — safe to call multiple times
|
|
180
|
+
// or if another SDK has already patched console.
|
|
181
|
+
// The original console method is always called first so there's zero
|
|
182
|
+
// impact on the application's logging behavior or latency.
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
const PATCHED = Symbol.for('foam.console.patched');
|
|
185
|
+
function patchConsole() {
|
|
186
|
+
if (console[PATCHED])
|
|
187
|
+
return;
|
|
188
|
+
const logger = api_logs_1.logs.getLogger('console');
|
|
189
|
+
const patch = (method, severityNumber, severityText) => {
|
|
190
|
+
const original = console[method].bind(console);
|
|
191
|
+
console[method] = (...args) => {
|
|
192
|
+
original(...args);
|
|
193
|
+
// logger.emit is non-blocking — it queues into the BatchLogRecordProcessor
|
|
194
|
+
logger.emit({ severityNumber, severityText, body: (0, util_1.format)(...args) });
|
|
195
|
+
};
|
|
196
|
+
};
|
|
197
|
+
patch('log', api_logs_1.SeverityNumber.INFO, 'INFO');
|
|
198
|
+
patch('info', api_logs_1.SeverityNumber.INFO, 'INFO');
|
|
199
|
+
patch('error', api_logs_1.SeverityNumber.ERROR, 'ERROR');
|
|
200
|
+
patch('warn', api_logs_1.SeverityNumber.WARN, 'WARN');
|
|
201
|
+
patch('debug', api_logs_1.SeverityNumber.DEBUG, 'DEBUG');
|
|
202
|
+
console[PATCHED] = true;
|
|
203
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const FOAM_OTEL_ENDPOINT = "https://otel.api.foam.ai";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wraps a function to ensure it never throws by swallowing errors.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* export const init = safe((options: Options) => {
|
|
6
|
+
* // Implementation that might throw
|
|
7
|
+
* });
|
|
8
|
+
*/
|
|
9
|
+
export declare function safe<T extends (...args: any[]) => any>(fn: T): T;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.safe = safe;
|
|
4
|
+
/**
|
|
5
|
+
* Wraps a function to ensure it never throws by swallowing errors.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* export const init = safe((options: Options) => {
|
|
9
|
+
* // Implementation that might throw
|
|
10
|
+
* });
|
|
11
|
+
*/
|
|
12
|
+
function safe(fn) {
|
|
13
|
+
return ((...args) => {
|
|
14
|
+
try {
|
|
15
|
+
return fn(...args);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@foam-ai/node",
|
|
3
|
+
"version": "0.1.0-alpha.1",
|
|
4
|
+
"main": "dist/node/src/index.js",
|
|
5
|
+
"types": "dist/node/src/index.d.ts",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/node/src/index.d.ts",
|
|
9
|
+
"import": "./dist/node/src/index.js",
|
|
10
|
+
"require": "./dist/node/src/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc -p tsconfig.json"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@opentelemetry/api": "^1.9.0",
|
|
21
|
+
"@opentelemetry/api-logs": "^0.214.0",
|
|
22
|
+
"@opentelemetry/exporter-logs-otlp-http": "^0.214.0",
|
|
23
|
+
"@opentelemetry/exporter-metrics-otlp-http": "^0.214.0",
|
|
24
|
+
"@opentelemetry/exporter-trace-otlp-http": "^0.214.0",
|
|
25
|
+
"@opentelemetry/instrumentation": "^0.214.0",
|
|
26
|
+
"@opentelemetry/resources": "^2.6.1",
|
|
27
|
+
"@opentelemetry/sdk-logs": "^0.214.0",
|
|
28
|
+
"@opentelemetry/sdk-metrics": "^2.6.1",
|
|
29
|
+
"@opentelemetry/sdk-trace-base": "^2.6.1",
|
|
30
|
+
"@opentelemetry/sdk-trace-node": "^2.6.1"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"typescript": "^6.0.2"
|
|
34
|
+
}
|
|
35
|
+
}
|