@foam-ai/node 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/README.md +123 -0
- package/dist/node/src/index.d.ts +5 -1
- package/dist/node/src/index.js +8 -1
- package/dist/node/src/init.js +17 -2
- package/dist/node/src/metrics.d.ts +28 -0
- package/dist/node/src/metrics.js +69 -0
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# @foam-ai/node
|
|
2
|
+
|
|
3
|
+
Foam observability SDK for Node.js. Sends traces, logs, and metrics to Foam with zero configuration overhead.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @foam-ai/node
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
Call `init()` as early as possible in your application entry point:
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import foam from '@foam-ai/node';
|
|
17
|
+
|
|
18
|
+
foam.init({
|
|
19
|
+
apiKey: process.env.FOAM_API_KEY,
|
|
20
|
+
serviceName: 'my-service',
|
|
21
|
+
isProduction: process.env.NODE_ENV === 'production',
|
|
22
|
+
});
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
That's it. In production, Foam will automatically collect:
|
|
26
|
+
|
|
27
|
+
- **Traces** — distributed traces via OpenTelemetry
|
|
28
|
+
- **Logs** — console.log/info/error/warn/debug output
|
|
29
|
+
- **Metrics** — runtime and custom metrics
|
|
30
|
+
- **Uncaught errors** — process-level `uncaughtException` and `unhandledRejection`
|
|
31
|
+
|
|
32
|
+
In non-production environments, `init()` is a silent no-op.
|
|
33
|
+
|
|
34
|
+
## Capturing errors
|
|
35
|
+
|
|
36
|
+
Unhandled exceptions and rejections are captured automatically. For caught errors you want to report, use `captureException`:
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import { captureException } from '@foam-ai/node';
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
await chargeCustomer(order);
|
|
43
|
+
} catch (err) {
|
|
44
|
+
captureException(err);
|
|
45
|
+
return res.status(500).json({ error: 'Payment failed' });
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`captureException` works with or without an active trace span. It always emits an OpenTelemetry log record so errors reach Foam regardless of instrumentation setup.
|
|
50
|
+
|
|
51
|
+
## Auto-instrumentation (optional)
|
|
52
|
+
|
|
53
|
+
By default, no auto-instrumentations are registered. This keeps the SDK webpack/Turbopack-safe with no bundler configuration needed.
|
|
54
|
+
|
|
55
|
+
For plain Node.js servers (Express, Fastify, etc.), you can opt into automatic HTTP, DNS, and framework instrumentation:
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
import foam from '@foam-ai/node';
|
|
59
|
+
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
|
|
60
|
+
|
|
61
|
+
foam.init({
|
|
62
|
+
apiKey: process.env.FOAM_API_KEY,
|
|
63
|
+
serviceName: 'my-api',
|
|
64
|
+
isProduction: process.env.NODE_ENV === 'production',
|
|
65
|
+
instrumentations: getNodeAutoInstrumentations(),
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npm install @opentelemetry/auto-instrumentations-node
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Next.js
|
|
74
|
+
|
|
75
|
+
In a Next.js app, create an `instrumentation.ts` file at the project root:
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
export async function register() {
|
|
79
|
+
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
|
80
|
+
const foam = await import('@foam-ai/node');
|
|
81
|
+
foam.init({
|
|
82
|
+
apiKey: process.env.FOAM_API_KEY!,
|
|
83
|
+
serviceName: 'my-nextjs-app',
|
|
84
|
+
isProduction: process.env.NODE_ENV === 'production',
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
No webpack externals or bundler config needed.
|
|
91
|
+
|
|
92
|
+
## Coexistence with other SDKs
|
|
93
|
+
|
|
94
|
+
Foam detects each OpenTelemetry signal independently. If another SDK (Sentry, @vercel/otel, Datadog, etc.) has already registered a TracerProvider, LoggerProvider, or MeterProvider, Foam attaches its exporters on top rather than replacing them. Both SDKs receive data.
|
|
95
|
+
|
|
96
|
+
If no providers exist, Foam creates its own. This means Foam works in any combination:
|
|
97
|
+
|
|
98
|
+
- Foam alone
|
|
99
|
+
- Foam + Sentry
|
|
100
|
+
- Foam + @vercel/otel
|
|
101
|
+
- Foam + any OpenTelemetry-compatible SDK
|
|
102
|
+
|
|
103
|
+
## API
|
|
104
|
+
|
|
105
|
+
### `init(options)`
|
|
106
|
+
|
|
107
|
+
| Option | Type | Required | Description |
|
|
108
|
+
|---|---|---|---|
|
|
109
|
+
| `apiKey` | `string` | Yes | Your Foam API key |
|
|
110
|
+
| `serviceName` | `string` | Yes | Name of your service (appears in Foam dashboard) |
|
|
111
|
+
| `isProduction` | `boolean` | Yes | Set to `true` to enable telemetry. `false` = no-op. |
|
|
112
|
+
| `instrumentations` | `InstrumentationBase[]` | No | OpenTelemetry instrumentations to register |
|
|
113
|
+
|
|
114
|
+
### `captureException(error)`
|
|
115
|
+
|
|
116
|
+
Captures a caught error and sends it to Foam. Works with or without an active span.
|
|
117
|
+
|
|
118
|
+
## Design
|
|
119
|
+
|
|
120
|
+
- **HTTP-only exporters** — no gRPC, no Node.js built-in dependencies that break bundlers
|
|
121
|
+
- **Zero latency** — all setup is synchronous; exporting happens in background batch intervals
|
|
122
|
+
- **Crash-safe** — each signal (traces, logs, metrics, console, process handlers) is independently wrapped in try/catch
|
|
123
|
+
- **Idempotent** — safe to call `init()` multiple times; console patching and process handlers use Symbols to prevent double-registration
|
package/dist/node/src/index.d.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { captureException } from './capture-exception';
|
|
2
2
|
import { init } from './init';
|
|
3
|
-
|
|
3
|
+
import { incrementCounter, recordHistogram, recordGauge } from './metrics';
|
|
4
|
+
export { captureException, init, incrementCounter, recordHistogram, recordGauge };
|
|
4
5
|
export type { FoamNodeInitOptions } from './init';
|
|
5
6
|
declare const foam: {
|
|
6
7
|
captureException: (error: unknown) => void;
|
|
7
8
|
init: (options: import("./init").FoamNodeInitOptions) => void;
|
|
9
|
+
incrementCounter: (name: string, value?: number, attributes?: import("@opentelemetry/api").Attributes) => void;
|
|
10
|
+
recordHistogram: (name: string, value: number, attributes?: import("@opentelemetry/api").Attributes) => void;
|
|
11
|
+
recordGauge: (name: string, value: number, attributes?: import("@opentelemetry/api").Attributes) => void;
|
|
8
12
|
};
|
|
9
13
|
export default foam;
|
package/dist/node/src/index.js
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.init = exports.captureException = void 0;
|
|
3
|
+
exports.recordGauge = exports.recordHistogram = exports.incrementCounter = exports.init = exports.captureException = void 0;
|
|
4
4
|
const capture_exception_1 = require("./capture-exception");
|
|
5
5
|
Object.defineProperty(exports, "captureException", { enumerable: true, get: function () { return capture_exception_1.captureException; } });
|
|
6
6
|
const init_1 = require("./init");
|
|
7
7
|
Object.defineProperty(exports, "init", { enumerable: true, get: function () { return init_1.init; } });
|
|
8
|
+
const metrics_1 = require("./metrics");
|
|
9
|
+
Object.defineProperty(exports, "incrementCounter", { enumerable: true, get: function () { return metrics_1.incrementCounter; } });
|
|
10
|
+
Object.defineProperty(exports, "recordHistogram", { enumerable: true, get: function () { return metrics_1.recordHistogram; } });
|
|
11
|
+
Object.defineProperty(exports, "recordGauge", { enumerable: true, get: function () { return metrics_1.recordGauge; } });
|
|
8
12
|
const foam = {
|
|
9
13
|
captureException: capture_exception_1.captureException,
|
|
10
14
|
init: init_1.init,
|
|
15
|
+
incrementCounter: metrics_1.incrementCounter,
|
|
16
|
+
recordHistogram: metrics_1.recordHistogram,
|
|
17
|
+
recordGauge: metrics_1.recordGauge,
|
|
11
18
|
};
|
|
12
19
|
exports.default = foam;
|
package/dist/node/src/init.js
CHANGED
|
@@ -190,8 +190,23 @@ function patchConsole() {
|
|
|
190
190
|
const original = console[method].bind(console);
|
|
191
191
|
console[method] = (...args) => {
|
|
192
192
|
original(...args);
|
|
193
|
-
|
|
194
|
-
|
|
193
|
+
let ctx;
|
|
194
|
+
let traceAttributes;
|
|
195
|
+
try {
|
|
196
|
+
ctx = api_1.context.active();
|
|
197
|
+
const spanContext = api_1.trace.getSpanContext(ctx);
|
|
198
|
+
if (spanContext && (0, api_1.isSpanContextValid)(spanContext)) {
|
|
199
|
+
traceAttributes = { 'trace.id': spanContext.traceId, 'span.id': spanContext.spanId };
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
catch { /* never break the caller's console.log */ }
|
|
203
|
+
logger.emit({
|
|
204
|
+
context: ctx,
|
|
205
|
+
severityNumber,
|
|
206
|
+
severityText,
|
|
207
|
+
body: (0, util_1.format)(...args),
|
|
208
|
+
attributes: traceAttributes,
|
|
209
|
+
});
|
|
195
210
|
};
|
|
196
211
|
};
|
|
197
212
|
patch('log', api_logs_1.SeverityNumber.INFO, 'INFO');
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type Attributes } from '@opentelemetry/api';
|
|
2
|
+
/**
|
|
3
|
+
* Increments a counter metric by the given value (default 1).
|
|
4
|
+
*
|
|
5
|
+
* Counters are monotonically increasing — use for things like request counts,
|
|
6
|
+
* items processed, or errors encountered.
|
|
7
|
+
*
|
|
8
|
+
* Safe to call before `init()` — silently no-ops via the OTel noop meter.
|
|
9
|
+
*/
|
|
10
|
+
export declare const incrementCounter: (name: string, value?: number, attributes?: Attributes) => void;
|
|
11
|
+
/**
|
|
12
|
+
* Records a value on a histogram metric.
|
|
13
|
+
*
|
|
14
|
+
* Histograms capture distributions — use for latencies, request sizes,
|
|
15
|
+
* or any measurement where percentiles and averages matter.
|
|
16
|
+
*
|
|
17
|
+
* Safe to call before `init()` — silently no-ops via the OTel noop meter.
|
|
18
|
+
*/
|
|
19
|
+
export declare const recordHistogram: (name: string, value: number, attributes?: Attributes) => void;
|
|
20
|
+
/**
|
|
21
|
+
* Records a point-in-time gauge value.
|
|
22
|
+
*
|
|
23
|
+
* Gauges represent a current measurement that can go up or down — use for
|
|
24
|
+
* things like queue depth, active connections, or memory usage.
|
|
25
|
+
*
|
|
26
|
+
* Safe to call before `init()` — silently no-ops via the OTel noop meter.
|
|
27
|
+
*/
|
|
28
|
+
export declare const recordGauge: (name: string, value: number, attributes?: Attributes) => void;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.recordGauge = exports.recordHistogram = exports.incrementCounter = void 0;
|
|
4
|
+
const api_1 = require("@opentelemetry/api");
|
|
5
|
+
const util_1 = require("../../shared/util");
|
|
6
|
+
const METER_NAME = 'foam';
|
|
7
|
+
const counters = new Map();
|
|
8
|
+
const histograms = new Map();
|
|
9
|
+
const gauges = new Map();
|
|
10
|
+
function getMeter() {
|
|
11
|
+
return api_1.metrics.getMeter(METER_NAME);
|
|
12
|
+
}
|
|
13
|
+
function getOrCreateCounter(name) {
|
|
14
|
+
let counter = counters.get(name);
|
|
15
|
+
if (!counter) {
|
|
16
|
+
counter = getMeter().createCounter(name);
|
|
17
|
+
counters.set(name, counter);
|
|
18
|
+
}
|
|
19
|
+
return counter;
|
|
20
|
+
}
|
|
21
|
+
function getOrCreateHistogram(name) {
|
|
22
|
+
let histogram = histograms.get(name);
|
|
23
|
+
if (!histogram) {
|
|
24
|
+
histogram = getMeter().createHistogram(name);
|
|
25
|
+
histograms.set(name, histogram);
|
|
26
|
+
}
|
|
27
|
+
return histogram;
|
|
28
|
+
}
|
|
29
|
+
function getOrCreateGauge(name) {
|
|
30
|
+
let gauge = gauges.get(name);
|
|
31
|
+
if (!gauge) {
|
|
32
|
+
gauge = getMeter().createGauge(name);
|
|
33
|
+
gauges.set(name, gauge);
|
|
34
|
+
}
|
|
35
|
+
return gauge;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Increments a counter metric by the given value (default 1).
|
|
39
|
+
*
|
|
40
|
+
* Counters are monotonically increasing — use for things like request counts,
|
|
41
|
+
* items processed, or errors encountered.
|
|
42
|
+
*
|
|
43
|
+
* Safe to call before `init()` — silently no-ops via the OTel noop meter.
|
|
44
|
+
*/
|
|
45
|
+
exports.incrementCounter = (0, util_1.safe)((name, value, attributes) => {
|
|
46
|
+
getOrCreateCounter(name).add(value ?? 1, attributes);
|
|
47
|
+
});
|
|
48
|
+
/**
|
|
49
|
+
* Records a value on a histogram metric.
|
|
50
|
+
*
|
|
51
|
+
* Histograms capture distributions — use for latencies, request sizes,
|
|
52
|
+
* or any measurement where percentiles and averages matter.
|
|
53
|
+
*
|
|
54
|
+
* Safe to call before `init()` — silently no-ops via the OTel noop meter.
|
|
55
|
+
*/
|
|
56
|
+
exports.recordHistogram = (0, util_1.safe)((name, value, attributes) => {
|
|
57
|
+
getOrCreateHistogram(name).record(value, attributes);
|
|
58
|
+
});
|
|
59
|
+
/**
|
|
60
|
+
* Records a point-in-time gauge value.
|
|
61
|
+
*
|
|
62
|
+
* Gauges represent a current measurement that can go up or down — use for
|
|
63
|
+
* things like queue depth, active connections, or memory usage.
|
|
64
|
+
*
|
|
65
|
+
* Safe to call before `init()` — silently no-ops via the OTel noop meter.
|
|
66
|
+
*/
|
|
67
|
+
exports.recordGauge = (0, util_1.safe)((name, value, attributes) => {
|
|
68
|
+
getOrCreateGauge(name).record(value, attributes);
|
|
69
|
+
});
|