@fougere/observability 0.3.0-alpha.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Fougere contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,3 @@
1
+ import type { TraceContext } from './port.js';
2
+ export declare const traceContext: TraceContext;
3
+ //# sourceMappingURL=als.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"als.d.ts","sourceRoot":"","sources":["../../src/context/als.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAI9C,eAAO,MAAM,YAAY,EAAE,YAI1B,CAAC"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The default: a real `AsyncLocalStorage`, so a call with no wire above it still finds
3
+ * its parent in the stack it is running inside.
4
+ */
5
+ import { AsyncLocalStorage } from 'node:async_hooks';
6
+ const store = new AsyncLocalStorage();
7
+ export const traceContext = {
8
+ ambient: true,
9
+ current: () => store.getStore(),
10
+ within: (span, fn) => store.run(span, fn),
11
+ };
12
+ //# sourceMappingURL=als.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"als.js","sourceRoot":"","sources":["../../src/context/als.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAIrD,MAAM,KAAK,GAAG,IAAI,iBAAiB,EAAe,CAAC;AAEnD,MAAM,CAAC,MAAM,YAAY,GAAiB;IACxC,OAAO,EAAE,IAAI;IACb,OAAO,EAAE,GAA0B,EAAE,CAAC,KAAK,CAAC,QAAQ,EAAmB;IACvE,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC;CAC1C,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { TraceContext } from './port.js';
2
+ export declare const traceContext: TraceContext;
3
+ //# sourceMappingURL=none.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"none.d.ts","sourceRoot":"","sources":["../../src/context/none.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAE9C,eAAO,MAAM,YAAY,EAAE,YAI1B,CAAC"}
@@ -0,0 +1,6 @@
1
+ export const traceContext = {
2
+ ambient: false,
3
+ current: () => undefined,
4
+ within: (_span, fn) => fn(),
5
+ };
6
+ //# sourceMappingURL=none.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"none.js","sourceRoot":"","sources":["../../src/context/none.ts"],"names":[],"mappings":"AAgBA,MAAM,CAAC,MAAM,YAAY,GAAiB;IACxC,OAAO,EAAE,KAAK;IACd,OAAO,EAAE,GAAyC,EAAE,CAAC,SAAS;IAC9D,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE;CAC5B,CAAC"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Where the step running right now is kept — the one thing a Worker cannot do.
3
+ *
4
+ * A trace has TWO ways to name its parent, and only one of them is ambient: an arriving
5
+ * call carries `traceparent` on the invocation, which every transport moves and which
6
+ * needs nothing from the runtime. This is the other way — the parent of a call that
7
+ * crossed no wire, which is in the stack and nowhere else.
8
+ *
9
+ * `node:async_hooks` does not exist on workerd unless the deployment asks for it, so
10
+ * `#trace-context` resolves to two realizations: the real store by default, and one that
11
+ * answers "no parent here" under the `workerd` condition. What that costs is stated at
12
+ * boot rather than discovered in a trace viewer — see `traceContext.ambient`.
13
+ */
14
+ import type { SpanContext } from '../traceparent.js';
15
+ export interface TraceContext {
16
+ /**
17
+ * Whether a parent can be found without a wire. False means every in-process call
18
+ * starts its own trace, and the boot says so.
19
+ */
20
+ readonly ambient: boolean;
21
+ /** The step running here and now, when one can be known. */
22
+ current<T extends SpanContext>(): T | undefined;
23
+ /** Run `fn` with `span` as the step in scope. */
24
+ within<T extends SpanContext, R>(span: T, fn: () => Promise<R>): Promise<R>;
25
+ }
26
+ //# sourceMappingURL=port.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"port.d.ts","sourceRoot":"","sources":["../../src/context/port.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,4DAA4D;IAC5D,OAAO,CAAC,CAAC,SAAS,WAAW,KAAK,CAAC,GAAG,SAAS,CAAC;IAChD,iDAAiD;IACjD,MAAM,CAAC,CAAC,SAAS,WAAW,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CAC7E"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=port.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"port.js","sourceRoot":"","sources":["../../src/context/port.ts"],"names":[],"mappings":""}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * This package's own ascent and descent, in one value.
3
+ *
4
+ * Four gestures in a fixed order — the middleware, the accumulator, the sinks, the wire op
5
+ * — is a boot sequence, and a host had to know it: `demos/observability` wrote it out and
6
+ * hand-rolled its own `stop()` beside it, which is a `down` under another name. Declared as
7
+ * an extension, the order is stated once here and the release is not the host's to remember.
8
+ *
9
+ * It is not a frond and cannot become one: a frond may move behind `remotes:`, and this
10
+ * reads the process it runs in — moved, it would report the observer instead of the observed.
11
+ */
12
+ import { type Extension } from '@fougere/core';
13
+ export interface ObservabilityOptions {
14
+ /** What a dashboard groups this process by, and the name a log line carries. */
15
+ service?: string;
16
+ /**
17
+ * An OTLP collector's base URL — SigNoz, Jaeger, Tempo all read the same one.
18
+ *
19
+ * Absent, nothing leaves the process and `rpc.topology` still answers: a panel asking
20
+ * the app what shape it is in needs no collector.
21
+ */
22
+ otlp?: string;
23
+ flushMs?: number;
24
+ /** A collector that cannot be reached is the app's business, not its failure. */
25
+ onError?: (error: unknown) => void;
26
+ }
27
+ /**
28
+ * Observe this process — one member of the ascent.
29
+ *
30
+ * `up` wires in the order that matters and `down` withdraws in reverse, which is the whole
31
+ * reason the two halves are one value: `onSpan` and `onLog` already RETURN their withdrawal
32
+ * and nothing was calling it, so a discarded app kept feeding the sinks of the app that
33
+ * replaced it. That is invisible until the ring turns, and then it doubles every metric.
34
+ */
35
+ export declare function observability(options?: ObservabilityOptions): Extension;
36
+ //# sourceMappingURL=extension.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extension.d.ts","sourceRoot":"","sources":["../src/extension.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAA6C,KAAK,SAAS,EAAE,MAAM,eAAe,CAAC;AAQ1F,MAAM,WAAW,oBAAoB;IACnC,gFAAgF;IAChF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iFAAiF;IACjF,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACpC;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,OAAO,GAAE,oBAAyB,GAAG,SAAS,CAsE3E"}
@@ -0,0 +1,91 @@
1
+ /**
2
+ * This package's own ascent and descent, in one value.
3
+ *
4
+ * Four gestures in a fixed order — the middleware, the accumulator, the sinks, the wire op
5
+ * — is a boot sequence, and a host had to know it: `demos/observability` wrote it out and
6
+ * hand-rolled its own `stop()` beside it, which is a `down` under another name. Declared as
7
+ * an extension, the order is stated once here and the release is not the host's to remember.
8
+ *
9
+ * It is not a frond and cannot become one: a frond may move behind `remotes:`, and this
10
+ * reads the process it runs in — moved, it would report the observer instead of the observed.
11
+ */
12
+ import { loggerMiddleware, Logger, onLog } from '@fougere/core';
13
+ import { traceContext } from '#trace-context';
14
+ import { registerFlush } from './index.js';
15
+ import { trace, onSpan } from './index.js';
16
+ import { metrics, serveTopology } from './metrics.js';
17
+ import { otlp } from './otlp.js';
18
+ import { logs } from './logs.js';
19
+ /**
20
+ * Observe this process — one member of the ascent.
21
+ *
22
+ * `up` wires in the order that matters and `down` withdraws in reverse, which is the whole
23
+ * reason the two halves are one value: `onSpan` and `onLog` already RETURN their withdrawal
24
+ * and nothing was calling it, so a discarded app kept feeding the sinks of the app that
25
+ * replaced it. That is invisible until the ring turns, and then it doubles every metric.
26
+ */
27
+ export function observability(options = {}) {
28
+ const service = options.service ?? 'fougere';
29
+ /**
30
+ * Per APP, not per extension. A host declares its extensions once (`configureFougere`),
31
+ * so the same instance goes up on the new app before the old one is released: one shared
32
+ * list meant the old app's `down` withdrew — and erased — the new app's sinks too, after
33
+ * which every counter stayed frozen and nothing said so.
34
+ */
35
+ const undoing = new WeakMap();
36
+ return {
37
+ name: 'observability',
38
+ up(app) {
39
+ const undo = [];
40
+ undoing.set(app, undo);
41
+ // Order matters: `trace()` opens the span that every log line written inside the
42
+ // call will carry. Installed the other way round, the lines leave uncorrelated.
43
+ app.use(trace());
44
+ app.use(loggerMiddleware(new Logger(service)));
45
+ // Said once, here, because the alternative is finding it in a trace viewer three
46
+ // weeks later: without an ambient context a call that crossed NO wire cannot name
47
+ // its parent, so it starts its own trace. What still works is stated too — a
48
+ // warning that only names the loss reads as "tracing is broken", and it is not.
49
+ if (!traceContext.ambient) {
50
+ new Logger(service).warn('no async context: a call that crosses no wire starts its own trace '
51
+ + '(an emission subscriber, a handler reaching another frond in this process). '
52
+ + 'An arriving call and a call to a frond behind `remotes:` are unaffected — both carry '
53
+ + 'traceparent on the invocation. Add "nodejs_als" to compatibility_flags to restore the rest.');
54
+ }
55
+ const measured = metrics(app);
56
+ undo.push(onSpan(measured.sink));
57
+ serveTopology(app, measured);
58
+ if (!options.otlp)
59
+ return;
60
+ const base = options.otlp.replace(/\/$/, '');
61
+ const telemetry = otlp({
62
+ service,
63
+ url: `${base}/v1/traces`,
64
+ metricsUrl: `${base}/v1/metrics`,
65
+ metrics: measured,
66
+ ...(options.flushMs === undefined ? {} : { flushMs: options.flushMs }),
67
+ ...(options.onError ? { onError: options.onError } : {}),
68
+ });
69
+ const written = logs({
70
+ service,
71
+ url: `${base}/v1/logs`,
72
+ ...(options.flushMs === undefined ? {} : { flushMs: options.flushMs }),
73
+ ...(options.onError ? { onError: options.onError } : {}),
74
+ });
75
+ // The timer is not the only way out: an isolate is frozen when it answers, so a
76
+ // host with no time between requests calls `flushTelemetry()` inside `ctx.waitUntil`.
77
+ undo.push(onSpan(telemetry.sink), onLog(written.sink), registerFlush(() => telemetry.flush()), registerFlush(() => written.flush()), () => telemetry.stop(), () => written.stop());
78
+ },
79
+ async down(app) {
80
+ // Reverse, and the exporters' `stop()` is in here: it flushes what is buffered, so a
81
+ // released app publishes what it measured instead of dropping its last window.
82
+ const undo = undoing.get(app);
83
+ if (!undo)
84
+ return;
85
+ undoing.delete(app);
86
+ for (const step of undo.reverse())
87
+ await step();
88
+ },
89
+ };
90
+ }
91
+ //# sourceMappingURL=extension.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extension.js","sourceRoot":"","sources":["../src/extension.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,74 @@
1
+ import type { AppMiddleware } from '@fougere/core';
2
+ import { type SpanContext } from './traceparent.js';
3
+ export { traceparentOf, parseTraceparent } from './traceparent.js';
4
+ export type { SpanContext } from './traceparent.js';
5
+ /** A step that has finished, and what it did. */
6
+ export interface FinishedSpan extends SpanContext {
7
+ parentId: string | undefined;
8
+ /**
9
+ * The frond of the step this one is nested in, when it is a DIFFERENT one — an edge of
10
+ * the call graph, `shop → catalog`.
11
+ *
12
+ * Only the caller can say it: a traceparent carries no frond, so a receiver knows it
13
+ * has a parent and not whose. That is not a gap — the caller already published the edge.
14
+ */
15
+ callerFrond: string | undefined;
16
+ /** Which frond owned the op — the deployment unit, so the first thing a reader groups by. */
17
+ frond: string | undefined;
18
+ entity: string;
19
+ operation: string;
20
+ /**
21
+ * When it started, in epoch milliseconds — an INSTANT, not an offset.
22
+ *
23
+ * `performance.now()` measures the duration below and nothing else: it counts from
24
+ * this process's own start, so two processes' numbers cannot be put on one timeline,
25
+ * which is the only thing a trace is for.
26
+ */
27
+ startedAt: number;
28
+ ms: number;
29
+ /** The FougereError code when it refused, absent when it answered. */
30
+ error: string | undefined;
31
+ }
32
+ export type SpanSink = (span: FinishedSpan) => void;
33
+ /**
34
+ * The step running here and now.
35
+ *
36
+ * It exists for the call the wire cannot describe: a handler reaching a second frond
37
+ * builds a fresh invocation, so the parent is not on that call — it is in the context
38
+ * the first one is still running inside.
39
+ *
40
+ * Behind `#trace-context` because that context is a runtime capability, not ours: a
41
+ * Worker has none unless the deployment asks. `traceContext.ambient` says which one is
42
+ * running, and `trace()` reports it once at install rather than letting a reader
43
+ * discover it in a trace viewer.
44
+ */
45
+ /** The step running right now, if any. */
46
+ export declare function currentSpan(): SpanContext | undefined;
47
+ export declare function activeCalls(): number;
48
+ /**
49
+ * Send what is buffered, now. Safe to call where there is nothing to send.
50
+ *
51
+ * Module-level like `sinks`, and for the same reason: an exporter belongs to the PROCESS,
52
+ * and the handler that wants to flush it holds no reference to the extension that built it.
53
+ */
54
+ export declare function flushTelemetry(): Promise<void>;
55
+ /** Declare an exporter's flush, and answer the way to withdraw it — like `onSpan`. */
56
+ export declare function registerFlush(send: () => Promise<void>): () => void;
57
+ export declare function onSpan(next: SpanSink): () => void;
58
+ /**
59
+ * The middleware. `app.use(trace())` traces every operation; `app.use('post', trace())`
60
+ * traces one entity's.
61
+ *
62
+ * Nothing is opened while no sink is set: observing is a decision, and the cost of one
63
+ * nobody asked for is zero rather than small.
64
+ */
65
+ export declare function trace(): AppMiddleware;
66
+ export { otlp } from './otlp.js';
67
+ export type { OtlpOptions, OtlpExporter } from './otlp.js';
68
+ export { metrics, metricsPayload, serveTopology } from './metrics.js';
69
+ export type { Metrics, MetricsSnapshot, TopologyReport, FrondPlacement, Edge } from './metrics.js';
70
+ export { logs } from './logs.js';
71
+ export type { LogsOptions, LogExporter, CapturedLog } from './logs.js';
72
+ export { observability } from './extension.js';
73
+ export type { ObservabilityOptions } from './extension.js';
74
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EAA8C,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEhG,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACnE,YAAY,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAWpD,iDAAiD;AACjD,MAAM,WAAW,YAAa,SAAQ,WAAW;IAC/C,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B;;;;;;OAMG;IACH,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,6FAA6F;IAC7F,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;;OAMG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB,EAAE,EAAE,MAAM,CAAC;IACX,sEAAsE;IACtE,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;CAC3B;AAED,MAAM,MAAM,QAAQ,GAAG,CAAC,IAAI,EAAE,YAAY,KAAK,IAAI,CAAC;AAEpD;;;;;;;;;;;GAWG;AAEH,0CAA0C;AAC1C,wBAAgB,WAAW,IAAI,WAAW,GAAG,SAAS,CAErD;AAiBD,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAYD;;;;;GAKG;AACH,wBAAsB,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAMpD;AAED,sFAAsF;AACtF,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,MAAM,IAAI,CAMnE;AAED,wBAAgB,MAAM,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,IAAI,CAMjD;AAED;;;;;;GAMG;AACH,wBAAgB,KAAK,IAAI,aAAa,CA4DrC;AAOD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC3D,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AACtE,YAAY,EAAE,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AACnG,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,YAAY,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,172 @@
1
+ /**
2
+ * @fougere/observability — one span per operation, and the trace that survives a wire.
3
+ *
4
+ * Named for the subject and not for today's reading of it: a span carries an op's
5
+ * duration and its verdict, which is the matter of a metric as much as of a trace, and
6
+ * an exporter for either hangs off the same `onSpan`.
7
+ *
8
+ * Optional in the strong sense: core holds no tracing code at all, only a `trace` field
9
+ * on the invocation that it carries and never reads. Everything else is here, behind
10
+ * `app.use(trace())`, and an app that does not install it pays nothing.
11
+ *
12
+ * It is an ordinary app middleware because an operation ALREADY has a lifecycle and it
13
+ * is that one — a second hook system would be a second answer to a settled question.
14
+ * The same middleware runs on both halves of a split: at the door a call arrives at, and
15
+ * at the stand-in it leaves from. That is why the numbers line up across processes, and
16
+ * why the difference between the two spans is what the wire cost.
17
+ */
18
+ import { traceContext } from '#trace-context';
19
+ import { parseTraceparent, traceparentOf, randomHex } from './traceparent.js';
20
+ export { traceparentOf, parseTraceparent } from './traceparent.js';
21
+ /**
22
+ * The step running here and now.
23
+ *
24
+ * It exists for the call the wire cannot describe: a handler reaching a second frond
25
+ * builds a fresh invocation, so the parent is not on that call — it is in the context
26
+ * the first one is still running inside.
27
+ *
28
+ * Behind `#trace-context` because that context is a runtime capability, not ours: a
29
+ * Worker has none unless the deployment asks. `traceContext.ambient` says which one is
30
+ * running, and `trace()` reports it once at install rather than letting a reader
31
+ * discover it in a trace viewer.
32
+ */
33
+ /** The step running right now, if any. */
34
+ export function currentSpan() {
35
+ return traceContext.current();
36
+ }
37
+ /**
38
+ * Who takes the spans this process finishes, consulted at every end like the log level.
39
+ *
40
+ * A list rather than one: a span is the source of a trace AND of a metric, so the two
41
+ * exporters read the same value rather than the middleware producing it twice.
42
+ * Returns the way to withdraw.
43
+ */
44
+ const sinks = [];
45
+ /**
46
+ * Calls running right now — the saturation signal, and the only one a FINISHED span
47
+ * cannot carry. Counted at the same two moments the span is opened and closed.
48
+ */
49
+ let active = 0;
50
+ export function activeCalls() {
51
+ return active;
52
+ }
53
+ /**
54
+ * Every exporter this process installed, so something can make them send NOW.
55
+ *
56
+ * An exporter buffers and flushes on a timer, which assumes a process that lives between
57
+ * two requests. An isolate does not: it is frozen the moment it answers, so the timer
58
+ * never fires and what it held is lost. `ctx.waitUntil(flushTelemetry())` is the whole
59
+ * remedy — the platform's way of saying "this work outlives the response".
60
+ */
61
+ const flushes = [];
62
+ /**
63
+ * Send what is buffered, now. Safe to call where there is nothing to send.
64
+ *
65
+ * Module-level like `sinks`, and for the same reason: an exporter belongs to the PROCESS,
66
+ * and the handler that wants to flush it holds no reference to the extension that built it.
67
+ */
68
+ export async function flushTelemetry() {
69
+ // Every one, and the refusals together — a flush that abandons the rest loses the
70
+ // windows after it, which is the answer `app.deliver` gives for the same shape.
71
+ const failed = await Promise.allSettled(flushes.map((send) => send()));
72
+ const refused = failed.filter((r) => r.status === 'rejected');
73
+ if (refused.length > 0)
74
+ throw new AggregateError(refused.map((r) => r.reason), 'telemetry flush');
75
+ }
76
+ /** Declare an exporter's flush, and answer the way to withdraw it — like `onSpan`. */
77
+ export function registerFlush(send) {
78
+ flushes.push(send);
79
+ return () => {
80
+ const at = flushes.indexOf(send);
81
+ if (at >= 0)
82
+ flushes.splice(at, 1);
83
+ };
84
+ }
85
+ export function onSpan(next) {
86
+ sinks.push(next);
87
+ return () => {
88
+ const at = sinks.indexOf(next);
89
+ if (at >= 0)
90
+ sinks.splice(at, 1);
91
+ };
92
+ }
93
+ /**
94
+ * The middleware. `app.use(trace())` traces every operation; `app.use('post', trace())`
95
+ * traces one entity's.
96
+ *
97
+ * Nothing is opened while no sink is set: observing is a decision, and the cost of one
98
+ * nobody asked for is zero rather than small.
99
+ */
100
+ export function trace() {
101
+ return (ctx, next) => {
102
+ if (sinks.length === 0)
103
+ return next();
104
+ // The wire first, the ambient context second: an arriving call names its parent on
105
+ // the invocation, an outgoing one inherits from the call it is made inside.
106
+ const inherited = parseTraceparent(ctx.invocation?.trace);
107
+ const ambient = traceContext.current();
108
+ const parent = inherited ?? ambient;
109
+ const span = {
110
+ traceId: parent?.traceId ?? randomHex(16),
111
+ spanId: randomHex(8),
112
+ sampled: parent?.sampled ?? true,
113
+ frond: ctx.frond,
114
+ };
115
+ // An edge exists only when the parent is IN this process and belongs to another frond.
116
+ // `inherited` won means the parent is across a wire, and it did not name its frond.
117
+ const callerFrond = !inherited && ambient?.frond && ambient.frond !== ctx.frond ? ambient.frond : undefined;
118
+ // What this call hands to whatever it reaches next. Whether that is a transport or
119
+ // nothing at all is not this middleware's business — it writes the field, the
120
+ // invocation travels, and every transport carries it because every transport
121
+ // carries the invocation.
122
+ if (ctx.invocation)
123
+ ctx.invocation = { ...ctx.invocation, trace: traceparentOf(span) };
124
+ // Both, and they are not the same measurement: the wall clock says WHEN so two
125
+ // processes land on one timeline, the monotonic one says HOW LONG without being
126
+ // moved by an NTP correction mid-call.
127
+ const startedAt = Date.now();
128
+ const start = performance.now();
129
+ active += 1;
130
+ const finish = (error) => {
131
+ active -= 1;
132
+ const done = {
133
+ ...span,
134
+ parentId: parent?.spanId,
135
+ callerFrond,
136
+ frond: ctx.frond,
137
+ entity: ctx.entity,
138
+ operation: ctx.operation,
139
+ startedAt,
140
+ ms: performance.now() - start,
141
+ error,
142
+ };
143
+ // A sink that throws is a broken exporter, never a broken call.
144
+ for (const take of sinks) {
145
+ try {
146
+ take(done);
147
+ }
148
+ catch { /* observing never refuses */ }
149
+ }
150
+ };
151
+ return traceContext.within(span, async () => {
152
+ try {
153
+ const result = await next();
154
+ finish(undefined);
155
+ return result;
156
+ }
157
+ catch (err) {
158
+ finish(codeOf(err));
159
+ throw err;
160
+ }
161
+ });
162
+ };
163
+ }
164
+ function codeOf(err) {
165
+ const code = err?.code;
166
+ return typeof code === 'string' ? code : (err?.name ?? 'error');
167
+ }
168
+ export { otlp } from './otlp.js';
169
+ export { metrics, metricsPayload, serveTopology } from './metrics.js';
170
+ export { logs } from './logs.js';
171
+ export { observability } from './extension.js';
172
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/logs.d.ts ADDED
@@ -0,0 +1,32 @@
1
+ import type { LogRecord } from '@fougere/core';
2
+ /** A record, plus what it could only be told at the moment it was written. */
3
+ export interface CapturedLog extends LogRecord {
4
+ traceId: string | undefined;
5
+ spanId: string | undefined;
6
+ }
7
+ export interface LogExporter {
8
+ /** Hand to `onLog`. */
9
+ sink: (record: LogRecord) => void;
10
+ /** Send what is buffered now. */
11
+ flush(): Promise<void>;
12
+ /** Stop the timer and send what is left. */
13
+ stop(): Promise<void>;
14
+ }
15
+ export interface LogsOptions {
16
+ /** Which service these lines belong to. */
17
+ service: string;
18
+ /** Collector endpoint. Default: the OTLP/HTTP convention on localhost. */
19
+ url?: string;
20
+ /** How often a batch leaves. Default: every second. */
21
+ flushMs?: number;
22
+ /** Told when a batch could not be sent. Default: silence. */
23
+ onError?: (err: unknown) => void;
24
+ /**
25
+ * Drop anything below this level before it leaves the process. Absent means "whatever
26
+ * the logger let through" — `setLogLevel` has already filtered, and a second threshold
27
+ * here would be a second place where the level lives.
28
+ */
29
+ minimum?: 'debug' | 'info' | 'warn' | 'error';
30
+ }
31
+ export declare function logs(options: LogsOptions): LogExporter;
32
+ //# sourceMappingURL=logs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logs.d.ts","sourceRoot":"","sources":["../src/logs.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAU/C,8EAA8E;AAC9E,MAAM,WAAW,WAAY,SAAQ,SAAS;IAC5C,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5B;AAED,MAAM,WAAW,WAAW;IAC1B,uBAAuB;IACvB,IAAI,EAAE,CAAC,MAAM,EAAE,SAAS,KAAK,IAAI,CAAC;IAClC,iCAAiC;IACjC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,4CAA4C;IAC5C,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAED,MAAM,WAAW,WAAW;IAC1B,2CAA2C;IAC3C,OAAO,EAAE,MAAM,CAAC;IAChB,0EAA0E;IAC1E,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,uDAAuD;IACvD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6DAA6D;IAC7D,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;IACjC;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;CAC/C;AAED,wBAAgB,IAAI,CAAC,OAAO,EAAE,WAAW,GAAG,WAAW,CAqCtD"}
package/dist/logs.js ADDED
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Log export — the third signal, and the only one whose value is entirely in its links.
3
+ *
4
+ * A log shipped without a trace id is a log stored somewhere else. What makes it the third
5
+ * pillar is landing on a trace and reading the lines that call produced, so every record
6
+ * leaves with the span it was written inside — which this package knows and the logger
7
+ * deliberately does not.
8
+ *
9
+ * The logger emits a structured record and nothing more (`onLog` in core). Attaching the
10
+ * trace, naming the severity and speaking OTLP all happen here.
11
+ */
12
+ import { currentSpan } from './index.js';
13
+ /** OTLP severity numbers — the scale is 1–24, these are the canonical rungs. */
14
+ const SEVERITY = {
15
+ debug: { number: 5, text: 'DEBUG' },
16
+ info: { number: 9, text: 'INFO' },
17
+ warn: { number: 13, text: 'WARN' },
18
+ error: { number: 17, text: 'ERROR' },
19
+ };
20
+ export function logs(options) {
21
+ const url = options.url ?? 'http://localhost:4318/v1/logs';
22
+ const floor = options.minimum ? SEVERITY[options.minimum].number : 0;
23
+ let buffer = [];
24
+ async function flush() {
25
+ if (buffer.length === 0)
26
+ return;
27
+ const batch = buffer;
28
+ buffer = [];
29
+ try {
30
+ const response = await fetch(url, {
31
+ method: 'POST',
32
+ headers: { 'content-type': 'application/json' },
33
+ body: JSON.stringify(payload(options.service, batch)),
34
+ });
35
+ if (!response.ok)
36
+ options.onError?.(new Error(`${url} answered HTTP ${response.status}`));
37
+ }
38
+ catch (err) {
39
+ options.onError?.(err);
40
+ }
41
+ }
42
+ const every = options.flushMs ?? 1_000;
43
+ // Same rule as the span exporter: a timer at module scope makes a Worker undeployable.
44
+ const timer = every > 0 ? setInterval(() => void flush(), every) : undefined;
45
+ timer?.unref?.();
46
+ return {
47
+ // The span is read HERE, while the line is being written — not at flush time, when
48
+ // the call it belongs to is long over and the context is somebody else's.
49
+ sink: (record) => {
50
+ if (SEVERITY[record.level].number < floor)
51
+ return;
52
+ const span = currentSpan();
53
+ buffer.push({ ...record, traceId: span?.traceId, spanId: span?.spanId });
54
+ },
55
+ flush,
56
+ stop: async () => { if (timer)
57
+ clearInterval(timer); await flush(); },
58
+ };
59
+ }
60
+ /** One batch, as OTLP/JSON spells logs. */
61
+ function payload(service, records) {
62
+ const attr = (key, value) => ({ key, value: { stringValue: value } });
63
+ return {
64
+ resourceLogs: [
65
+ {
66
+ resource: { attributes: [attr('service.name', service)] },
67
+ scopeLogs: [
68
+ {
69
+ scope: { name: '@fougere/observability' },
70
+ logRecords: records.map((record) => {
71
+ const severity = SEVERITY[record.level];
72
+ return {
73
+ timeUnixNano: `${record.at * 1e6}`,
74
+ observedTimeUnixNano: `${record.at * 1e6}`,
75
+ severityNumber: severity.number,
76
+ severityText: severity.text,
77
+ body: { stringValue: bodyOf(record) },
78
+ attributes: [attr('logger.name', record.name)],
79
+ // Absent when the line was written outside any call — a boot line, a
80
+ // shutdown line. Absent is the honest answer, not a zeroed id.
81
+ ...(record.traceId ? { traceId: record.traceId } : {}),
82
+ ...(record.spanId ? { spanId: record.spanId } : {}),
83
+ };
84
+ }),
85
+ },
86
+ ],
87
+ },
88
+ ],
89
+ };
90
+ }
91
+ /**
92
+ * The message with its arguments folded in. A log call takes extras the way `console`
93
+ * does, and a collector stores one body — so they are joined rather than dropped.
94
+ */
95
+ function bodyOf(record) {
96
+ if (record.args.length === 0)
97
+ return record.message;
98
+ return [record.message, ...record.args.map(readable)].join(' ');
99
+ }
100
+ function readable(value) {
101
+ if (typeof value === 'string')
102
+ return value;
103
+ if (value instanceof Error)
104
+ return `${value.name}: ${value.message}`;
105
+ try {
106
+ return JSON.stringify(value) ?? String(value);
107
+ }
108
+ catch {
109
+ return String(value);
110
+ }
111
+ }
112
+ //# sourceMappingURL=logs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logs.js","sourceRoot":"","sources":["../src/logs.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,86 @@
1
+ /**
2
+ * The four signals every service is judged on, derived from the span that already exists.
3
+ *
4
+ * Rate, errors and duration are ONE metric — a histogram of durations, dimensioned by
5
+ * the op and by whether it refused. Its `count` is the rate, its dimension is the error
6
+ * rate, its buckets are the latency: that is how OpenTelemetry spells
7
+ * `http.server.request.duration`, and splitting it into three counters would publish the
8
+ * same numbers three times and let them disagree.
9
+ *
10
+ * The fourth — saturation — cannot come from a finished span, because it is about the
11
+ * ones that have NOT finished. The middleware counts it, `activeCalls()` reads it.
12
+ *
13
+ * Cardinality is bounded by construction: entity × operation × {ok, error}, all of them
14
+ * declared in the code. Nothing carrying an id, a user or a trace ever becomes a
15
+ * dimension — that is the one mistake a metrics layer cannot recover from.
16
+ */
17
+ import type { App, Edge, FrondPlacement } from '@fougere/core';
18
+ export type { Edge, FrondPlacement, TopologyReport } from '@fougere/core';
19
+ import { type SpanSink } from './index.js';
20
+ interface Bucketed {
21
+ frond: string | undefined;
22
+ entity: string;
23
+ operation: string;
24
+ error: string | undefined;
25
+ count: number;
26
+ sum: number;
27
+ /** One more than the bounds: the last holds everything above the highest bound. */
28
+ buckets: number[];
29
+ }
30
+ export interface Metrics {
31
+ /** Hand to `onSpan` — it reads the same span the tracer reads. */
32
+ sink: SpanSink;
33
+ /** What to publish now. */
34
+ snapshot(): MetricsSnapshot;
35
+ }
36
+ export interface MetricsSnapshot {
37
+ /** When this process started counting — cumulative metrics are read against it. */
38
+ since: number;
39
+ series: Bucketed[];
40
+ active: number;
41
+ bounds: number[];
42
+ /** The shape of the system as this process discovered it — declared nowhere. */
43
+ topology: FrondPlacement[];
44
+ /** Who calls whom, as observed here. Bounded by fronds², so it is a safe dimension. */
45
+ edges: Edge[];
46
+ }
47
+ /**
48
+ * `app` is optional and only feeds the topology: what fronds this process found, and
49
+ * which of them run elsewhere. Read from the scan, so it is DISCOVERED — a frond that
50
+ * appears is on the dashboard without anyone declaring it there.
51
+ */
52
+ export declare function metrics(app?: App): Metrics;
53
+ /**
54
+ * Serve the topology on `rpc.topology` — read from inside the process it describes.
55
+ *
56
+ * This is why observability is not a frond: a frond may move behind `remotes:`, and moved,
57
+ * this one would report the observer's process instead of the observed one. The op exists
58
+ * exactly when this package is wired, so an app that installed none refuses it by name.
59
+ */
60
+ export declare function serveTopology(app: App, measured: Metrics): void;
61
+ /** One snapshot, as OTLP/JSON spells metrics. Cumulative, which is what Prometheus reads. */
62
+ export declare function metricsPayload(service: string, snapshot: MetricsSnapshot): {
63
+ resourceMetrics: {
64
+ resource: {
65
+ attributes: {
66
+ key: string;
67
+ value: {
68
+ stringValue: string;
69
+ };
70
+ }[];
71
+ };
72
+ scopeMetrics: {
73
+ scope: {
74
+ name: string;
75
+ };
76
+ /**
77
+ * Only what has points. A metric with an empty `dataPoints` makes a
78
+ * collector reject the WHOLE batch — measured: Prometheus answers 500 and
79
+ * every other metric in the payload is lost with it. A process that calls
80
+ * nobody has no edges, which is ordinary, not a reason to publish nothing.
81
+ */
82
+ metrics: Record<string, any>[];
83
+ }[];
84
+ }[];
85
+ };
86
+ //# sourceMappingURL=metrics.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../src/metrics.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,cAAc,EAAkB,MAAM,eAAe,CAAC;AAE/E,YAAY,EAAE,IAAI,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC1E,OAAO,EAAkC,KAAK,QAAQ,EAAE,MAAM,YAAY,CAAC;AAQ3E,UAAU,QAAQ;IAChB,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,mFAAmF;IACnF,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,OAAO;IACtB,kEAAkE;IAClE,IAAI,EAAE,QAAQ,CAAC;IACf,2BAA2B;IAC3B,QAAQ,IAAI,eAAe,CAAC;CAC7B;AAGD,MAAM,WAAW,eAAe;IAC9B,mFAAmF;IACnF,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,QAAQ,EAAE,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,gFAAgF;IAChF,QAAQ,EAAE,cAAc,EAAE,CAAC;IAC3B,uFAAuF;IACvF,KAAK,EAAE,IAAI,EAAE,CAAC;CACf;AAED;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,GAAG,CAAC,EAAE,GAAG,GAAG,OAAO,CA8C1C;AAmCD;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,OAAO,GAAG,IAAI,CAK/D;AAED,6FAA6F;AAC7F,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,eAAe;;;YAQrD,UAAU;;;oBALkC,WAAW;;;;;;gBAQtD,IAAI;;YACb;;;;;eAKG;;;;EAkGd"}
Binary file
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics.js","sourceRoot":"","sources":["../src/metrics.ts"],"names":[],"mappings":"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"}
package/dist/otlp.d.ts ADDED
@@ -0,0 +1,42 @@
1
+ /**
2
+ * OTLP export — the spans this process finished, in the one shape every collector reads.
3
+ *
4
+ * OTLP has a JSON encoding over plain HTTP, so this needs no protobuf and no dependency:
5
+ * a POST to `/v1/traces`, and Jaeger, Tempo, Datadog or Honeycomb ingest it as-is.
6
+ *
7
+ * It is a sink and nothing more — `onSpan(otlp({...}).sink)` is the whole wiring. What
8
+ * a span IS was decided by the middleware; this file only renames its fields.
9
+ */
10
+ import type { SpanSink } from './index.js';
11
+ import { type Metrics } from './metrics.js';
12
+ export interface OtlpOptions {
13
+ /** Which service these spans belong to — what a dashboard groups by. */
14
+ service: string;
15
+ /** Collector endpoint. Default: the OTLP/HTTP convention on localhost. */
16
+ url?: string;
17
+ /** How often a full batch leaves. Default: every second. */
18
+ flushMs?: number;
19
+ /** Told when a batch could not be sent. Default: silence — a trace must never break a call. */
20
+ onError?: (err: unknown) => void;
21
+ /**
22
+ * Publish these metrics on the same beat.
23
+ *
24
+ * Their endpoint defaults to the traces one with its last segment swapped — the OTLP
25
+ * convention when a single collector takes both. Name `metricsUrl` when they are two:
26
+ * traces and metrics are stored by different engines, and a deployment is free to run
27
+ * one of each rather than a collector in front.
28
+ */
29
+ metrics?: Metrics;
30
+ /** Where metrics go when it is not the same collector as traces. */
31
+ metricsUrl?: string;
32
+ }
33
+ export interface OtlpExporter {
34
+ /** Hand to `onSpan`. */
35
+ sink: SpanSink;
36
+ /** Send what is buffered now — a process about to exit has to call this. */
37
+ flush(): Promise<void>;
38
+ /** Stop the timer and send what is left. */
39
+ stop(): Promise<void>;
40
+ }
41
+ export declare function otlp(options: OtlpOptions): OtlpExporter;
42
+ //# sourceMappingURL=otlp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"otlp.d.ts","sourceRoot":"","sources":["../src/otlp.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAgB,QAAQ,EAAE,MAAM,YAAY,CAAC;AACzD,OAAO,EAAkB,KAAK,OAAO,EAAE,MAAM,cAAc,CAAC;AAE5D,MAAM,WAAW,WAAW;IAC1B,wEAAwE;IACxE,OAAO,EAAE,MAAM,CAAC;IAChB,0EAA0E;IAC1E,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,4DAA4D;IAC5D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+FAA+F;IAC/F,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;IACjC;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,oEAAoE;IACpE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IAC3B,wBAAwB;IACxB,IAAI,EAAE,QAAQ,CAAC;IACf,4EAA4E;IAC5E,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,4CAA4C;IAC5C,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAMD,wBAAgB,IAAI,CAAC,OAAO,EAAE,WAAW,GAAG,YAAY,CA+CvD"}
package/dist/otlp.js ADDED
@@ -0,0 +1,81 @@
1
+ import { metricsPayload } from './metrics.js';
2
+ /** OTLP status codes: 0 unset, 1 ok, 2 error. */
3
+ const OK = 1;
4
+ const ERROR = 2;
5
+ export function otlp(options) {
6
+ const url = options.url ?? 'http://localhost:4318/v1/traces';
7
+ const metricsUrl = options.metricsUrl ?? url.replace(/\/v1\/traces$/, '/v1/metrics');
8
+ let buffer = [];
9
+ async function post(to, body) {
10
+ try {
11
+ const response = await fetch(to, {
12
+ method: 'POST',
13
+ headers: { 'content-type': 'application/json' },
14
+ body: JSON.stringify(body),
15
+ });
16
+ if (!response.ok)
17
+ options.onError?.(new Error(`${to} answered HTTP ${response.status}`));
18
+ }
19
+ catch (err) {
20
+ options.onError?.(err);
21
+ }
22
+ }
23
+ async function flush() {
24
+ const batch = buffer;
25
+ buffer = [];
26
+ // Metrics go on every beat even when no span finished: a gauge that stops being
27
+ // published reads as "gone", not as "idle".
28
+ await Promise.all([
29
+ batch.length > 0 ? post(url, payload(options.service, batch)) : Promise.resolve(),
30
+ options.metrics ? post(metricsUrl, metricsPayload(options.service, options.metrics.snapshot())) : Promise.resolve(),
31
+ ]);
32
+ }
33
+ // `flushMs: 0` means "nobody is on a timer here, I will say when" — and on a Worker it
34
+ // is not a preference, it is the only legal form: Cloudflare REFUSES a deployment whose
35
+ // module scope sets a timeout ("Disallowed operation called within global scope"), and
36
+ // an app built at module scope builds its exporter there. Measured 2026-08-23, the
37
+ // deploy failed with error 10021. The isolate is frozen at the response anyway, so the
38
+ // timer could never have fired; `ctx.waitUntil(flushTelemetry())` is what sends.
39
+ //
40
+ // `unref` so a buffered span never keeps a process alive: exporting is something the
41
+ // process does on its way, never a reason for it to stay.
42
+ const every = options.flushMs ?? 1_000;
43
+ const timer = every > 0 ? setInterval(() => void flush(), every) : undefined;
44
+ timer?.unref?.();
45
+ return {
46
+ sink: (span) => { buffer.push(span); },
47
+ flush,
48
+ stop: async () => { if (timer)
49
+ clearInterval(timer); await flush(); },
50
+ };
51
+ }
52
+ /** One batch, as OTLP/JSON spells it — trace and span ids stay hex, times are nanos. */
53
+ function payload(service, spans) {
54
+ return {
55
+ resourceSpans: [
56
+ {
57
+ resource: { attributes: [{ key: 'service.name', value: { stringValue: service } }] },
58
+ scopeSpans: [
59
+ {
60
+ scope: { name: '@fougere/observability' },
61
+ spans: spans.map((span) => ({
62
+ traceId: span.traceId,
63
+ spanId: span.spanId,
64
+ ...(span.parentId ? { parentSpanId: span.parentId } : {}),
65
+ name: `${span.entity}.${span.operation}`,
66
+ kind: 1,
67
+ startTimeUnixNano: nanos(span.startedAt),
68
+ endTimeUnixNano: nanos(span.startedAt + span.ms),
69
+ status: span.error ? { code: ERROR, message: span.error } : { code: OK },
70
+ })),
71
+ },
72
+ ],
73
+ },
74
+ ],
75
+ };
76
+ }
77
+ /** Epoch milliseconds → the int64 nanoseconds OTLP wants, as a string. */
78
+ function nanos(ms) {
79
+ return `${Math.round(ms * 1e6)}`;
80
+ }
81
+ //# sourceMappingURL=otlp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"otlp.js","sourceRoot":"","sources":["../src/otlp.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * `traceparent` — the one form a trace takes between two processes (W3C Trace Context).
3
+ *
4
+ * A standard rather than ours, so any collector reads it. It rides `InvocationContext.trace`
5
+ * and never a header: a header is HTTP's alone, and the same call over a socket would have
6
+ * arrived untraced.
7
+ *
8
+ * Pure — no runtime, no node builtin. What holds the CURRENT trace is `index.ts`, and the
9
+ * line between the two is the line between a format and the thing that carries it.
10
+ */
11
+ /** A step in a trace, as both halves of a split agree on it. */
12
+ export interface SpanContext {
13
+ /** The whole call, across every process. 32 hex. */
14
+ traceId: string;
15
+ /** This step. 16 hex. */
16
+ spanId: string;
17
+ /** Whether the collector was asked to keep it. */
18
+ sampled: boolean;
19
+ }
20
+ /** The header a caller writes so the other side joins this trace. */
21
+ export declare function traceparentOf(span: SpanContext): string;
22
+ /** The header as a context, or nothing — a malformed one is ignored, never fatal. */
23
+ export declare function parseTraceparent(header: string | undefined): SpanContext | undefined;
24
+ /** A trace id (16) or a span id (8), as the spec sizes them. */
25
+ export declare function randomHex(bytes: number): string;
26
+ //# sourceMappingURL=traceparent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"traceparent.d.ts","sourceRoot":"","sources":["../src/traceparent.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,gEAAgE;AAChE,MAAM,WAAW,WAAW;IAC1B,oDAAoD;IACpD,OAAO,EAAE,MAAM,CAAC;IAChB,yBAAyB;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,kDAAkD;IAClD,OAAO,EAAE,OAAO,CAAC;CAClB;AAMD,qEAAqE;AACrE,wBAAgB,aAAa,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,CAEvD;AAED,qFAAqF;AACrF,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS,CAOpF;AAID,gEAAgE;AAChE,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAM/C"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * `traceparent` — the one form a trace takes between two processes (W3C Trace Context).
3
+ *
4
+ * A standard rather than ours, so any collector reads it. It rides `InvocationContext.trace`
5
+ * and never a header: a header is HTTP's alone, and the same call over a socket would have
6
+ * arrived untraced.
7
+ *
8
+ * Pure — no runtime, no node builtin. What holds the CURRENT trace is `index.ts`, and the
9
+ * line between the two is the line between a format and the thing that carries it.
10
+ */
11
+ const TRACEPARENT = /^00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/;
12
+ const NO_TRACE = '0'.repeat(32);
13
+ const NO_SPAN = '0'.repeat(16);
14
+ /** The header a caller writes so the other side joins this trace. */
15
+ export function traceparentOf(span) {
16
+ return `00-${span.traceId}-${span.spanId}-${span.sampled ? '01' : '00'}`;
17
+ }
18
+ /** The header as a context, or nothing — a malformed one is ignored, never fatal. */
19
+ export function parseTraceparent(header) {
20
+ const found = typeof header === 'string' ? TRACEPARENT.exec(header.trim()) : null;
21
+ if (!found)
22
+ return undefined;
23
+ const [, traceId, spanId, flags] = found;
24
+ if (traceId === NO_TRACE || spanId === NO_SPAN)
25
+ return undefined;
26
+ return { traceId, spanId, sampled: (parseInt(flags, 16) & 1) === 1 };
27
+ }
28
+ const HEX = '0123456789abcdef';
29
+ /** A trace id (16) or a span id (8), as the spec sizes them. */
30
+ export function randomHex(bytes) {
31
+ const buffer = new Uint8Array(bytes);
32
+ crypto.getRandomValues(buffer);
33
+ let out = '';
34
+ for (const byte of buffer)
35
+ out += HEX[byte >> 4] + HEX[byte & 15];
36
+ return out;
37
+ }
38
+ //# sourceMappingURL=traceparent.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"traceparent.js","sourceRoot":"","sources":["../src/traceparent.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAYH,MAAM,WAAW,GAAG,kDAAkD,CAAC;AACvE,MAAM,QAAQ,GAAG,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;AAChC,MAAM,OAAO,GAAG,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;AAE/B,qEAAqE;AACrE,MAAM,UAAU,aAAa,CAAC,IAAiB;IAC7C,OAAO,MAAM,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;AAC3E,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,gBAAgB,CAAC,MAA0B;IACzD,MAAM,KAAK,GAAG,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAClF,IAAI,CAAC,KAAK;QAAE,OAAO,SAAS,CAAC;IAE7B,MAAM,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,GAAG,KAAK,CAAC;IACzC,IAAI,OAAO,KAAK,QAAQ,IAAI,MAAM,KAAK,OAAO;QAAE,OAAO,SAAS,CAAC;IACjE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,QAAQ,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC;AACvE,CAAC;AAED,MAAM,GAAG,GAAG,kBAAkB,CAAC;AAE/B,gEAAgE;AAChE,MAAM,UAAU,SAAS,CAAC,KAAa;IACrC,MAAM,MAAM,GAAG,IAAI,UAAU,CAAC,KAAK,CAAC,CAAC;IACrC,MAAM,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC;IAC/B,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,KAAK,MAAM,IAAI,IAAI,MAAM;QAAE,GAAG,IAAI,GAAG,CAAC,IAAI,IAAI,CAAC,CAAC,GAAG,GAAG,CAAC,IAAI,GAAG,EAAE,CAAC,CAAC;IAClE,OAAO,GAAG,CAAC;AACb,CAAC"}
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@fougere/observability",
3
+ "version": "0.3.0-alpha.0",
4
+ "description": "Optional observability for Fougere: one span per operation, W3C Trace Context across any transport.",
5
+ "keywords": [
6
+ "fougere",
7
+ "typescript",
8
+ "tracing",
9
+ "observability",
10
+ "opentelemetry",
11
+ "trace-context"
12
+ ],
13
+ "license": "MIT",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/chok/fougere.git",
17
+ "directory": "packages/observability"
18
+ },
19
+ "type": "module",
20
+ "main": "dist/index.js",
21
+ "types": "dist/index.d.ts",
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/index.d.ts",
25
+ "import": "./dist/index.js",
26
+ "default": "./dist/index.js"
27
+ }
28
+ },
29
+ "files": [
30
+ "dist"
31
+ ],
32
+ "peerDependencies": {
33
+ "@fougere/core": "^0.3.0-alpha.0"
34
+ },
35
+ "devDependencies": {
36
+ "vitest": "^4.1.0",
37
+ "@fougere/container": "0.3.0-alpha.0",
38
+ "@fougere/schema": "0.3.0-alpha.0",
39
+ "@fougere/transport-http": "0.3.0-alpha.0",
40
+ "@fougere/core": "0.3.0-alpha.0"
41
+ },
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "imports": {
46
+ "#trace-context": {
47
+ "types": "./src/context/als.ts",
48
+ "workerd": "./dist/context/none.js",
49
+ "default": "./dist/context/als.js"
50
+ }
51
+ },
52
+ "scripts": {
53
+ "build": "rm -rf dist && tsc",
54
+ "test": "vitest run",
55
+ "test:watch": "vitest",
56
+ "typecheck": "tsc --noEmit -p tsconfig.test.json"
57
+ }
58
+ }