@nifrajs/otel 1.3.1 → 1.4.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/README.md +14 -5
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/lifecycle.d.ts +64 -0
- package/dist/lifecycle.d.ts.map +1 -0
- package/dist/lifecycle.js +121 -0
- package/dist/lifecycle.js.map +1 -0
- package/dist/span.d.ts +10 -2
- package/dist/span.d.ts.map +1 -1
- package/dist/span.js +30 -1
- package/dist/span.js.map +1 -1
- package/dist/tracing.d.ts +5 -9
- package/dist/tracing.d.ts.map +1 -1
- package/dist/tracing.js +39 -44
- package/dist/tracing.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +13 -0
- package/src/lifecycle.ts +209 -0
- package/src/span.ts +35 -2
- package/src/tracing.ts +46 -61
package/README.md
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Distributed tracing for nifra. The `tracing()` plugin continues (or starts) a **W3C trace** per
|
|
4
4
|
request, opens an **OpenTelemetry-semantic-convention** span, and exposes `c.trace` so you can
|
|
5
|
-
forward the trace to downstream services.
|
|
6
|
-
|
|
5
|
+
forward the trace to downstream services. One fail-open lifecycle owns parentage, identity, timing,
|
|
6
|
+
errors, final status, and exactly-once completion. Pluggable adapters project that observation into
|
|
7
|
+
the OpenTelemetry SDK, DevTools, private backends, or structured logs. No SDK bundled; edge-safe.
|
|
7
8
|
|
|
8
9
|
```ts
|
|
9
10
|
import { tracing, traceHeaders, consoleSpanExporter } from "@nifrajs/otel"
|
|
@@ -26,20 +27,25 @@ const app = server()
|
|
|
26
27
|
(`error` for 5xx, `ok` otherwise).
|
|
27
28
|
- **Exposes `c.trace`** (`{ traceId, spanId, parentSpanId?, sampled, traceparent }`) — spread
|
|
28
29
|
`traceHeaders(c.trace)` into any downstream `fetch`/`ctx.api` call to continue the trace.
|
|
30
|
+
- **Exposes `c.observation`** — integrations can start correctly-parented child observations or
|
|
31
|
+
attach an adapter without rebuilding request lifecycle state.
|
|
29
32
|
- `responseHeader: true` also sets `traceparent` on the response (browser/client correlation).
|
|
30
33
|
|
|
31
|
-
##
|
|
34
|
+
## Adapters
|
|
32
35
|
|
|
33
|
-
Implement `
|
|
36
|
+
Implement `ObservationAdapter` to send spans wherever you collect them. `SpanExporter` remains as a
|
|
37
|
+
backwards-compatible type alias:
|
|
34
38
|
|
|
35
39
|
```ts
|
|
36
|
-
interface
|
|
40
|
+
interface ObservationAdapter {
|
|
37
41
|
onStart?(span: NifraSpan): void
|
|
38
42
|
onEnd(span: NifraSpan): void
|
|
39
43
|
}
|
|
40
44
|
```
|
|
41
45
|
|
|
42
46
|
- `consoleSpanExporter()` — logs each completed span as one structured line (dev / starting point).
|
|
47
|
+
- `tracing({ adapters: [devtoolsAdapter, privateAdapter] })` — fan out the same lifecycle; adapter
|
|
48
|
+
failures are isolated and never alter the response.
|
|
43
49
|
- **OpenTelemetry SDK bridge** — a ~10-line adapter maps `NifraSpan` onto a real OTel `Span` from a
|
|
44
50
|
`Tracer` (the attribute names already follow OTel conventions, so they pass straight through). Your
|
|
45
51
|
app depends on `@opentelemetry/*`; `@nifrajs/otel` does not.
|
|
@@ -50,6 +56,9 @@ Use `traceparent` and the built-in semantic attributes in every request span, th
|
|
|
50
56
|
your own collector through an exporter. Keep the package edge-safe by installing the OpenTelemetry
|
|
51
57
|
SDK only in apps that need that exporter.
|
|
52
58
|
|
|
59
|
+
For non-HTTP work, `createObservationLifecycle()` exposes the same state machine directly. Prefer
|
|
60
|
+
it over hand-rolling traceparent parsing, clocks, error status, or completion guards.
|
|
61
|
+
|
|
53
62
|
## For AI agents
|
|
54
63
|
|
|
55
64
|
Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
|
package/dist/index.d.ts
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* import { tracing, traceHeaders, consoleSpanExporter } from "@nifrajs/otel"
|
|
9
9
|
* app.use(tracing({ exporter: consoleSpanExporter(), serviceName: "orders-api" }))
|
|
10
10
|
*/
|
|
11
|
-
export { type
|
|
11
|
+
export { type ActiveObservation, createObservationLifecycle, type EndObservation, type ObservationClock, type ObservationContext, type ObservationLifecycle, type ObservationLifecycleOptions, type ObservationParent, type StartObservation, } from "./lifecycle.js";
|
|
12
|
+
export { type AttributeValue, combineObservationAdapters, consoleSpanExporter, type NifraSpan, type ObservationAdapter, type SpanExporter, type SpanStatus, } from "./span.js";
|
|
12
13
|
export { formatTraceparent, generateSpanId, generateTraceId, type ParsedTraceparent, parseTraceparent, } from "./traceparent.js";
|
|
13
14
|
export { type TraceContext, type TracingOptions, traceHeaders, tracing } from "./tracing.js";
|
|
14
15
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EACL,KAAK,cAAc,EACnB,mBAAmB,EACnB,KAAK,SAAS,EACd,KAAK,YAAY,EACjB,KAAK,UAAU,GAChB,MAAM,WAAW,CAAA;AAClB,OAAO,EACL,iBAAiB,EACjB,cAAc,EACd,eAAe,EACf,KAAK,iBAAiB,EACtB,gBAAgB,GACjB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,cAAc,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EACL,KAAK,iBAAiB,EACtB,0BAA0B,EAC1B,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EACzB,KAAK,2BAA2B,EAChC,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,GACtB,MAAM,gBAAgB,CAAA;AACvB,OAAO,EACL,KAAK,cAAc,EACnB,0BAA0B,EAC1B,mBAAmB,EACnB,KAAK,SAAS,EACd,KAAK,kBAAkB,EACvB,KAAK,YAAY,EACjB,KAAK,UAAU,GAChB,MAAM,WAAW,CAAA;AAClB,OAAO,EACL,iBAAiB,EACjB,cAAc,EACd,eAAe,EACf,KAAK,iBAAiB,EACtB,gBAAgB,GACjB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,cAAc,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* import { tracing, traceHeaders, consoleSpanExporter } from "@nifrajs/otel"
|
|
9
9
|
* app.use(tracing({ exporter: consoleSpanExporter(), serviceName: "orders-api" }))
|
|
10
10
|
*/
|
|
11
|
-
export {
|
|
11
|
+
export { createObservationLifecycle, } from "./lifecycle.js";
|
|
12
|
+
export { combineObservationAdapters, consoleSpanExporter, } from "./span.js";
|
|
12
13
|
export { formatTraceparent, generateSpanId, generateTraceId, parseTraceparent, } from "./traceparent.js";
|
|
13
14
|
export { traceHeaders, tracing } from "./tracing.js";
|
|
14
15
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAEL,mBAAmB,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAEL,0BAA0B,GAQ3B,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAEL,0BAA0B,EAC1B,mBAAmB,GAKpB,MAAM,WAAW,CAAA;AAClB,OAAO,EACL,iBAAiB,EACjB,cAAc,EACd,eAAe,EAEf,gBAAgB,GACjB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAA0C,YAAY,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The deep request-observation lifecycle. This module alone owns trace parent selection, identity,
|
|
3
|
+
* timing, error recording, final status classification, and exactly-once completion. Integrations
|
|
4
|
+
* should adapt completed spans; they should not reimplement this state machine.
|
|
5
|
+
*/
|
|
6
|
+
import type { AttributeValue, NifraSpan, ObservationAdapter, SpanStatus } from "./span.js";
|
|
7
|
+
export interface ObservationContext {
|
|
8
|
+
readonly traceId: string;
|
|
9
|
+
readonly spanId: string;
|
|
10
|
+
readonly parentSpanId?: string;
|
|
11
|
+
readonly sampled: boolean;
|
|
12
|
+
/** This span's outbound W3C trace context. */
|
|
13
|
+
readonly traceparent: string;
|
|
14
|
+
}
|
|
15
|
+
export interface ObservationParent {
|
|
16
|
+
readonly traceId: string;
|
|
17
|
+
readonly spanId: string;
|
|
18
|
+
readonly sampled: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface StartObservation {
|
|
21
|
+
readonly name: string;
|
|
22
|
+
/** Explicit parent. `null` forces a root span; `undefined` falls back to `traceparent`. */
|
|
23
|
+
readonly parent?: ObservationParent | null;
|
|
24
|
+
/** Inbound W3C header used when `parent` is undefined. */
|
|
25
|
+
readonly traceparent?: string | null;
|
|
26
|
+
readonly attributes?: Readonly<Record<string, AttributeValue>>;
|
|
27
|
+
}
|
|
28
|
+
export interface EndObservation {
|
|
29
|
+
readonly statusCode?: number;
|
|
30
|
+
/** Explicit status wins over status-code and recorded-error classification. */
|
|
31
|
+
readonly status?: Exclude<SpanStatus, "unset">;
|
|
32
|
+
readonly attributes?: Readonly<Record<string, AttributeValue>>;
|
|
33
|
+
}
|
|
34
|
+
export interface ActiveObservation {
|
|
35
|
+
readonly span: NifraSpan;
|
|
36
|
+
readonly context: ObservationContext;
|
|
37
|
+
/** Adds a sink to this in-flight span. Re-adding the same adapter is idempotent. */
|
|
38
|
+
addAdapter(adapter: ObservationAdapter): void;
|
|
39
|
+
/** Starts a child that inherits this span's trace identity and sampling decision. */
|
|
40
|
+
startChild(input: Omit<StartObservation, "parent" | "traceparent">, additionalAdapters?: readonly ObservationAdapter[]): ActiveObservation;
|
|
41
|
+
/** Records failure evidence without deciding the final status until the response is known. */
|
|
42
|
+
recordError(error: unknown): void;
|
|
43
|
+
/** Ends and exports once. Repeated calls return the same completed span without notifying again. */
|
|
44
|
+
end(input?: EndObservation): NifraSpan;
|
|
45
|
+
}
|
|
46
|
+
export interface ObservationLifecycle {
|
|
47
|
+
start(input: StartObservation, additionalAdapters?: readonly ObservationAdapter[]): ActiveObservation;
|
|
48
|
+
}
|
|
49
|
+
export interface ObservationClock {
|
|
50
|
+
/** Epoch milliseconds for interoperable start/end timestamps. */
|
|
51
|
+
wallTime(): number;
|
|
52
|
+
/** Monotonic milliseconds for durations. */
|
|
53
|
+
monotonicTime(): number;
|
|
54
|
+
}
|
|
55
|
+
export interface ObservationLifecycleOptions {
|
|
56
|
+
readonly adapters?: readonly ObservationAdapter[];
|
|
57
|
+
/** Injectable seams are primarily useful for deterministic tests and constrained runtimes. */
|
|
58
|
+
readonly clock?: ObservationClock;
|
|
59
|
+
readonly generateTraceId?: () => string;
|
|
60
|
+
readonly generateSpanId?: () => string;
|
|
61
|
+
}
|
|
62
|
+
/** Creates an independent lifecycle factory. Adapters are always called fail-open. */
|
|
63
|
+
export declare function createObservationLifecycle(options?: ObservationLifecycleOptions): ObservationLifecycle;
|
|
64
|
+
//# sourceMappingURL=lifecycle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../src/lifecycle.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,kBAAkB,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AAQ1F,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAA;IAC9B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,8CAA8C;IAC9C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;CAC7B;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;CAC1B;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,2FAA2F;IAC3F,QAAQ,CAAC,MAAM,CAAC,EAAE,iBAAiB,GAAG,IAAI,CAAA;IAC1C,0DAA0D;IAC1D,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACpC,QAAQ,CAAC,UAAU,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,CAAA;CAC/D;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;IAC5B,+EAA+E;IAC/E,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,EAAE,OAAO,CAAC,CAAA;IAC9C,QAAQ,CAAC,UAAU,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,CAAA;CAC/D;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;IACxB,QAAQ,CAAC,OAAO,EAAE,kBAAkB,CAAA;IACpC,oFAAoF;IACpF,UAAU,CAAC,OAAO,EAAE,kBAAkB,GAAG,IAAI,CAAA;IAC7C,qFAAqF;IACrF,UAAU,CACR,KAAK,EAAE,IAAI,CAAC,gBAAgB,EAAE,QAAQ,GAAG,aAAa,CAAC,EACvD,kBAAkB,CAAC,EAAE,SAAS,kBAAkB,EAAE,GACjD,iBAAiB,CAAA;IACpB,8FAA8F;IAC9F,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAAA;IACjC,oGAAoG;IACpG,GAAG,CAAC,KAAK,CAAC,EAAE,cAAc,GAAG,SAAS,CAAA;CACvC;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,CACH,KAAK,EAAE,gBAAgB,EACvB,kBAAkB,CAAC,EAAE,SAAS,kBAAkB,EAAE,GACjD,iBAAiB,CAAA;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,iEAAiE;IACjE,QAAQ,IAAI,MAAM,CAAA;IAClB,4CAA4C;IAC5C,aAAa,IAAI,MAAM,CAAA;CACxB;AAED,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,kBAAkB,EAAE,CAAA;IACjD,8FAA8F;IAC9F,QAAQ,CAAC,KAAK,CAAC,EAAE,gBAAgB,CAAA;IACjC,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,MAAM,CAAA;IACvC,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,MAAM,CAAA;CACvC;AAUD,sFAAsF;AACtF,wBAAgB,0BAA0B,CACxC,OAAO,GAAE,2BAAgC,GACxC,oBAAoB,CAkHtB"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The deep request-observation lifecycle. This module alone owns trace parent selection, identity,
|
|
3
|
+
* timing, error recording, final status classification, and exactly-once completion. Integrations
|
|
4
|
+
* should adapt completed spans; they should not reimplement this state machine.
|
|
5
|
+
*/
|
|
6
|
+
import { formatTraceparent, generateSpanId, generateTraceId, parseTraceparent, } from "./traceparent.js";
|
|
7
|
+
const defaultClock = {
|
|
8
|
+
wallTime: () => Date.now(),
|
|
9
|
+
monotonicTime: () => (typeof performance === "undefined" ? Date.now() : performance.now()),
|
|
10
|
+
};
|
|
11
|
+
const errorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
12
|
+
/** Creates an independent lifecycle factory. Adapters are always called fail-open. */
|
|
13
|
+
export function createObservationLifecycle(options = {}) {
|
|
14
|
+
const adapters = options.adapters ?? [];
|
|
15
|
+
const clock = options.clock ?? defaultClock;
|
|
16
|
+
const newTraceId = options.generateTraceId ?? generateTraceId;
|
|
17
|
+
const newSpanId = options.generateSpanId ?? generateSpanId;
|
|
18
|
+
const notifyStart = (span, spanAdapters) => {
|
|
19
|
+
for (const adapter of spanAdapters) {
|
|
20
|
+
try {
|
|
21
|
+
adapter.onStart?.(span);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// Observation must never change request behavior.
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const notifyEnd = (span, spanAdapters) => {
|
|
29
|
+
for (const adapter of spanAdapters) {
|
|
30
|
+
try {
|
|
31
|
+
adapter.onEnd(span);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Observation must never change request behavior.
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
const start = (input, additionalAdapters = []) => {
|
|
39
|
+
const spanAdapters = new Set([...adapters, ...additionalAdapters]);
|
|
40
|
+
const parent = input.parent === undefined ? parseTraceparent(input.traceparent ?? null) : input.parent;
|
|
41
|
+
const traceId = parent?.traceId ?? newTraceId();
|
|
42
|
+
const spanId = newSpanId();
|
|
43
|
+
const sampled = parent?.sampled ?? true;
|
|
44
|
+
const startTime = clock.wallTime();
|
|
45
|
+
const monotonicStart = clock.monotonicTime();
|
|
46
|
+
const span = {
|
|
47
|
+
traceId,
|
|
48
|
+
spanId,
|
|
49
|
+
...(parent === null ? {} : { parentSpanId: parent.spanId }),
|
|
50
|
+
sampled,
|
|
51
|
+
name: input.name,
|
|
52
|
+
startTime,
|
|
53
|
+
status: "unset",
|
|
54
|
+
attributes: { ...input.attributes },
|
|
55
|
+
};
|
|
56
|
+
const context = {
|
|
57
|
+
traceId,
|
|
58
|
+
spanId,
|
|
59
|
+
...(parent === null ? {} : { parentSpanId: parent.spanId }),
|
|
60
|
+
sampled,
|
|
61
|
+
traceparent: formatTraceparent(traceId, spanId, sampled),
|
|
62
|
+
};
|
|
63
|
+
let recordedError = false;
|
|
64
|
+
let ended = false;
|
|
65
|
+
const active = {
|
|
66
|
+
span,
|
|
67
|
+
context,
|
|
68
|
+
addAdapter(adapter) {
|
|
69
|
+
if (ended || spanAdapters.has(adapter))
|
|
70
|
+
return;
|
|
71
|
+
spanAdapters.add(adapter);
|
|
72
|
+
try {
|
|
73
|
+
adapter.onStart?.(span);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// Dynamic adapters have the same fail-open guarantee as initial adapters.
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
startChild(child, childAdapters) {
|
|
80
|
+
return start({
|
|
81
|
+
...child,
|
|
82
|
+
parent: context,
|
|
83
|
+
}, childAdapters);
|
|
84
|
+
},
|
|
85
|
+
recordError(error) {
|
|
86
|
+
if (ended)
|
|
87
|
+
return;
|
|
88
|
+
recordedError = true;
|
|
89
|
+
span.attributes["error.message"] = errorMessage(error);
|
|
90
|
+
},
|
|
91
|
+
end(endInput = {}) {
|
|
92
|
+
if (ended)
|
|
93
|
+
return span;
|
|
94
|
+
ended = true;
|
|
95
|
+
Object.assign(span.attributes, endInput.attributes);
|
|
96
|
+
span.endTime = clock.wallTime();
|
|
97
|
+
span.durationMs = Math.max(0, clock.monotonicTime() - monotonicStart);
|
|
98
|
+
span.status =
|
|
99
|
+
endInput.status ??
|
|
100
|
+
(endInput.statusCode === undefined
|
|
101
|
+
? recordedError
|
|
102
|
+
? "error"
|
|
103
|
+
: "ok"
|
|
104
|
+
: endInput.statusCode >= 500
|
|
105
|
+
? "error"
|
|
106
|
+
: "ok");
|
|
107
|
+
// Publish an immutable completed span: a misbehaving adapter can no longer mutate the shared
|
|
108
|
+
// attributes bag and corrupt what other adapters (or the caller) observe. Adapters are fail-open,
|
|
109
|
+
// so a stray write throws into their own try/catch rather than changing request behavior.
|
|
110
|
+
Object.freeze(span.attributes);
|
|
111
|
+
Object.freeze(span);
|
|
112
|
+
notifyEnd(span, [...spanAdapters]);
|
|
113
|
+
return span;
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
notifyStart(span, [...spanAdapters]);
|
|
117
|
+
return active;
|
|
118
|
+
};
|
|
119
|
+
return { start };
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=lifecycle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lifecycle.js","sourceRoot":"","sources":["../src/lifecycle.ts"],"names":[],"mappings":"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"}
|
package/dist/span.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The span model + exporter seam. Attribute names follow OpenTelemetry HTTP semantic conventions
|
|
3
3
|
* (`http.request.method`, `url.path`, `http.response.status_code`, …) so a span maps cleanly onto an
|
|
4
|
-
* OTel `Span` when bridged — but nothing here depends on the OTel SDK. You supply
|
|
4
|
+
* OTel `Span` when bridged — but nothing here depends on the OTel SDK. You supply an
|
|
5
|
+
* {@link ObservationAdapter}
|
|
5
6
|
* (a ~10-line adapter to `@opentelemetry/api`, or the bundled {@link consoleSpanExporter}).
|
|
6
7
|
*/
|
|
7
8
|
export type SpanStatus = "unset" | "ok" | "error";
|
|
@@ -33,10 +34,17 @@ export interface NifraSpan {
|
|
|
33
34
|
* real `Span` from a `Tracer`), ship to a collector, or just log. `onStart` is optional (most
|
|
34
35
|
* backends only need the completed span).
|
|
35
36
|
*/
|
|
36
|
-
export interface
|
|
37
|
+
export interface ObservationAdapter {
|
|
37
38
|
onStart?(span: NifraSpan): void;
|
|
38
39
|
onEnd(span: NifraSpan): void;
|
|
39
40
|
}
|
|
41
|
+
/** Backwards-compatible name for an observation sink. */
|
|
42
|
+
export type SpanExporter = ObservationAdapter;
|
|
43
|
+
/**
|
|
44
|
+
* Fan out lifecycle notifications to several adapters. Each adapter is isolated: an exception in
|
|
45
|
+
* one sink cannot prevent the remaining sinks from observing the span.
|
|
46
|
+
*/
|
|
47
|
+
export declare function combineObservationAdapters(adapters: readonly ObservationAdapter[]): ObservationAdapter;
|
|
40
48
|
/** A no-frills exporter that logs each completed span as one structured line. Useful in dev or as a
|
|
41
49
|
* starting point before wiring a real backend. */
|
|
42
50
|
export declare function consoleSpanExporter(log?: (line: string) => void): SpanExporter;
|
package/dist/span.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"span.d.ts","sourceRoot":"","sources":["../src/span.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"span.d.ts","sourceRoot":"","sources":["../src/span.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,IAAI,GAAG,OAAO,CAAA;AACjD,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAA;AAEtD,8DAA8D;AAC9D,MAAM,WAAW,SAAS;IACxB,2EAA2E;IAC3E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,8BAA8B;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,0EAA0E;IAC1E,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAA;IAC9B,mDAAmD;IACnD,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,uCAAuC;IACvC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,mCAAmC;IACnC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,qDAAqD;IACrD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,kCAAkC;IAClC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,UAAU,CAAA;IAClB,2CAA2C;IAC3C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAA;CACpD;AAED;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,OAAO,CAAC,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI,CAAA;IAC/B,KAAK,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI,CAAA;CAC7B;AAED,yDAAyD;AACzD,MAAM,MAAM,YAAY,GAAG,kBAAkB,CAAA;AAE7C;;;GAGG;AACH,wBAAgB,0BAA0B,CACxC,QAAQ,EAAE,SAAS,kBAAkB,EAAE,GACtC,kBAAkB,CAqBpB;AAED;kDACkD;AAClD,wBAAgB,mBAAmB,CACjC,GAAG,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAEtB,GACA,YAAY,CAgBd"}
|
package/dist/span.js
CHANGED
|
@@ -1,9 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The span model + exporter seam. Attribute names follow OpenTelemetry HTTP semantic conventions
|
|
3
3
|
* (`http.request.method`, `url.path`, `http.response.status_code`, …) so a span maps cleanly onto an
|
|
4
|
-
* OTel `Span` when bridged — but nothing here depends on the OTel SDK. You supply
|
|
4
|
+
* OTel `Span` when bridged — but nothing here depends on the OTel SDK. You supply an
|
|
5
|
+
* {@link ObservationAdapter}
|
|
5
6
|
* (a ~10-line adapter to `@opentelemetry/api`, or the bundled {@link consoleSpanExporter}).
|
|
6
7
|
*/
|
|
8
|
+
/**
|
|
9
|
+
* Fan out lifecycle notifications to several adapters. Each adapter is isolated: an exception in
|
|
10
|
+
* one sink cannot prevent the remaining sinks from observing the span.
|
|
11
|
+
*/
|
|
12
|
+
export function combineObservationAdapters(adapters) {
|
|
13
|
+
return {
|
|
14
|
+
onStart(span) {
|
|
15
|
+
for (const adapter of adapters) {
|
|
16
|
+
try {
|
|
17
|
+
adapter.onStart?.(span);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// Observation is fail-open by contract.
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
onEnd(span) {
|
|
25
|
+
for (const adapter of adapters) {
|
|
26
|
+
try {
|
|
27
|
+
adapter.onEnd(span);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// Observation is fail-open by contract.
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
7
36
|
/** A no-frills exporter that logs each completed span as one structured line. Useful in dev or as a
|
|
8
37
|
* starting point before wiring a real backend. */
|
|
9
38
|
export function consoleSpanExporter(log = (l) => {
|
package/dist/span.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"span.js","sourceRoot":"","sources":["../src/span.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"span.js","sourceRoot":"","sources":["../src/span.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAyCH;;;GAGG;AACH,MAAM,UAAU,0BAA0B,CACxC,QAAuC;IAEvC,OAAO;QACL,OAAO,CAAC,IAAI;YACV,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;gBAC/B,IAAI,CAAC;oBACH,OAAO,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,CAAA;gBACzB,CAAC;gBAAC,MAAM,CAAC;oBACP,wCAAwC;gBAC1C,CAAC;YACH,CAAC;QACH,CAAC;QACD,KAAK,CAAC,IAAI;YACR,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;gBAC/B,IAAI,CAAC;oBACH,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;gBACrB,CAAC;gBAAC,MAAM,CAAC;oBACP,wCAAwC;gBAC1C,CAAC;YACH,CAAC;QACH,CAAC;KACF,CAAA;AACH,CAAC;AAED;kDACkD;AAClD,MAAM,UAAU,mBAAmB,CACjC,MAA8B,CAAC,CAAC,EAAE,EAAE;IAClC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;AAChB,CAAC;IAED,OAAO;QACL,KAAK,CAAC,IAAI;YACR,GAAG,CACD,IAAI,CAAC,SAAS,CAAC;gBACb,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,OAAO,EAAE,IAAI,CAAC,OAAO;gBACrB,MAAM,EAAE,IAAI,CAAC,MAAM;gBACnB,YAAY,EAAE,IAAI,CAAC,YAAY;gBAC/B,UAAU,EAAE,IAAI,CAAC,UAAU;gBAC3B,MAAM,EAAE,IAAI,CAAC,MAAM;gBACnB,GAAG,IAAI,CAAC,UAAU;aACnB,CAAC,CACH,CAAA;QACH,CAAC;KACF,CAAA;AACH,CAAC"}
|
package/dist/tracing.d.ts
CHANGED
|
@@ -4,19 +4,15 @@
|
|
|
4
4
|
* (traceparent) and attribute names (HTTP semantic conventions); the span itself is exported through
|
|
5
5
|
* your {@link SpanExporter} (bridge to the OTel SDK, or log via `consoleSpanExporter`).
|
|
6
6
|
*/
|
|
7
|
-
import { type
|
|
7
|
+
import { type ObservationContext } from "./lifecycle.js";
|
|
8
|
+
import { type ObservationAdapter, type SpanExporter } from "./span.js";
|
|
8
9
|
/** The trace context exposed on the handler `c.trace` (typed, threaded via `derive`). */
|
|
9
|
-
export
|
|
10
|
-
readonly traceId: string;
|
|
11
|
-
readonly spanId: string;
|
|
12
|
-
readonly parentSpanId?: string;
|
|
13
|
-
readonly sampled: boolean;
|
|
14
|
-
/** This request's outbound `traceparent` — forward it on downstream calls to continue the trace. */
|
|
15
|
-
readonly traceparent: string;
|
|
16
|
-
}
|
|
10
|
+
export type TraceContext = ObservationContext;
|
|
17
11
|
export interface TracingOptions {
|
|
18
12
|
/** Where spans are sent. Default: {@link consoleSpanExporter}. */
|
|
19
13
|
readonly exporter?: SpanExporter;
|
|
14
|
+
/** Additional observation adapters (DevTools, a private redacting backend, metrics, …). */
|
|
15
|
+
readonly adapters?: readonly ObservationAdapter[];
|
|
20
16
|
/** Sets the `service.name` attribute on every span. */
|
|
21
17
|
readonly serviceName?: string;
|
|
22
18
|
/**
|
package/dist/tracing.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tracing.d.ts","sourceRoot":"","sources":["../src/tracing.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,
|
|
1
|
+
{"version":3,"file":"tracing.d.ts","sourceRoot":"","sources":["../src/tracing.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAGL,KAAK,kBAAkB,EACxB,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAuB,KAAK,kBAAkB,EAAE,KAAK,YAAY,EAAE,MAAM,WAAW,CAAA;AAE3F,yFAAyF;AACzF,MAAM,MAAM,YAAY,GAAG,kBAAkB,CAAA;AAE7C,MAAM,WAAW,cAAc;IAC7B,kEAAkE;IAClE,QAAQ,CAAC,QAAQ,CAAC,EAAE,YAAY,CAAA;IAChC,2FAA2F;IAC3F,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,kBAAkB,EAAE,CAAA;IACjD,uDAAuD;IACvD,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B;;;OAGG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,CAAA;CAClC;AAED;uDACuD;AACvD,wBAAgB,YAAY,CAAC,KAAK,EAAE,YAAY,GAAG;IAAE,WAAW,EAAE,MAAM,CAAA;CAAE,CAEzE;AAUD;;;;;;;;GAQG;AACH,wBAAgB,OAAO,CAAC,OAAO,GAAE,cAAmB,oHAuDnD"}
|
package/dist/tracing.js
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* your {@link SpanExporter} (bridge to the OTel SDK, or log via `consoleSpanExporter`).
|
|
6
6
|
*/
|
|
7
7
|
import { definePlugin } from "@nifrajs/core";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { createObservationLifecycle, } from "./lifecycle.js";
|
|
9
|
+
import { consoleSpanExporter } from "./span.js";
|
|
10
10
|
/** Spread into an outgoing `fetch`/`ctx.api` call's headers to continue the trace downstream:
|
|
11
11
|
* `fetch(url, { headers: traceHeaders(c.trace) })`. */
|
|
12
12
|
export function traceHeaders(trace) {
|
|
@@ -30,61 +30,56 @@ const pathOf = (url) => {
|
|
|
30
30
|
* ```
|
|
31
31
|
*/
|
|
32
32
|
export function tracing(options = {}) {
|
|
33
|
-
const
|
|
33
|
+
const adapters = [
|
|
34
|
+
...(options.exporter === undefined && options.adapters === undefined
|
|
35
|
+
? [consoleSpanExporter()]
|
|
36
|
+
: options.exporter === undefined
|
|
37
|
+
? []
|
|
38
|
+
: [options.exporter]),
|
|
39
|
+
...(options.adapters ?? []),
|
|
40
|
+
];
|
|
41
|
+
const lifecycle = createObservationLifecycle({ adapters });
|
|
34
42
|
const serviceName = options.serviceName;
|
|
35
|
-
//
|
|
36
|
-
// timing). A WeakMap so an abandoned request can't leak a span.
|
|
43
|
+
// A WeakMap so an abandoned request cannot leak its active observation.
|
|
37
44
|
const inFlight = new WeakMap();
|
|
38
45
|
return definePlugin("tracing", (app) => app
|
|
39
46
|
.derive((c) => {
|
|
40
|
-
const parent = parseTraceparent(c.req.headers.get("traceparent"));
|
|
41
|
-
const traceId = parent?.traceId ?? generateTraceId();
|
|
42
|
-
const spanId = generateSpanId();
|
|
43
|
-
const sampled = parent?.sampled ?? true;
|
|
44
47
|
const path = pathOf(c.req.url);
|
|
45
|
-
const
|
|
46
|
-
"http.request.method": c.req.method,
|
|
47
|
-
"url.path": path,
|
|
48
|
-
};
|
|
49
|
-
if (serviceName !== undefined)
|
|
50
|
-
attributes["service.name"] = serviceName;
|
|
51
|
-
const span = {
|
|
52
|
-
traceId,
|
|
53
|
-
spanId,
|
|
54
|
-
...(parent !== null ? { parentSpanId: parent.spanId } : {}),
|
|
55
|
-
sampled,
|
|
48
|
+
const observation = lifecycle.start({
|
|
56
49
|
name: `${c.req.method} ${path}`,
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
50
|
+
traceparent: c.req.headers.get("traceparent"),
|
|
51
|
+
attributes: {
|
|
52
|
+
"http.request.method": c.req.method,
|
|
53
|
+
"url.path": path,
|
|
54
|
+
...(serviceName === undefined ? {} : { "service.name": serviceName }),
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
inFlight.set(c.req, observation);
|
|
64
58
|
if (options.responseHeader)
|
|
65
|
-
c.set.headers.traceparent = traceparent;
|
|
66
|
-
|
|
67
|
-
traceId,
|
|
68
|
-
spanId,
|
|
69
|
-
...(parent !== null ? { parentSpanId: parent.spanId } : {}),
|
|
70
|
-
sampled,
|
|
71
|
-
traceparent,
|
|
72
|
-
};
|
|
73
|
-
return { trace };
|
|
59
|
+
c.set.headers.traceparent = observation.context.traceparent;
|
|
60
|
+
return { trace: observation.context, observation };
|
|
74
61
|
})
|
|
75
62
|
.use({
|
|
76
63
|
name: "tracing-end",
|
|
64
|
+
onError: (error, context) => {
|
|
65
|
+
inFlight.get(context.request)?.recordError(error);
|
|
66
|
+
return undefined;
|
|
67
|
+
},
|
|
77
68
|
onResponse: (res, req) => {
|
|
78
|
-
const
|
|
79
|
-
if (
|
|
69
|
+
const observation = inFlight.get(req);
|
|
70
|
+
if (observation === undefined)
|
|
80
71
|
return res;
|
|
81
72
|
inFlight.delete(req);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
73
|
+
const bodySize = Number(res.headers.get("content-length") ?? "0") || 0;
|
|
74
|
+
const isrStatus = res.headers.get("x-nifra-isr");
|
|
75
|
+
observation.end({
|
|
76
|
+
statusCode: res.status,
|
|
77
|
+
attributes: {
|
|
78
|
+
"http.response.status_code": res.status,
|
|
79
|
+
"http.response.body.size": bodySize,
|
|
80
|
+
...(isrStatus === null ? {} : { "nifra.isr.status": isrStatus }),
|
|
81
|
+
},
|
|
82
|
+
});
|
|
88
83
|
return res;
|
|
89
84
|
},
|
|
90
85
|
}));
|
package/dist/tracing.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tracing.js","sourceRoot":"","sources":["../src/tracing.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAC5C,OAAO,EAEL,
|
|
1
|
+
{"version":3,"file":"tracing.js","sourceRoot":"","sources":["../src/tracing.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nifrajs/otel",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Distributed tracing for nifra — W3C traceparent/tracestate propagation + OpenTelemetry-semantic-convention spans via a pluggable exporter. Bridge to the OpenTelemetry SDK or log spans directly; no SDK bundled. Edge-safe.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bun",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"access": "public"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
|
-
"@nifrajs/core": "^1.
|
|
42
|
+
"@nifrajs/core": "^1.4.0"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@nifrajs/core": "workspace:*"
|
package/src/index.ts
CHANGED
|
@@ -9,10 +9,23 @@
|
|
|
9
9
|
* app.use(tracing({ exporter: consoleSpanExporter(), serviceName: "orders-api" }))
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
export {
|
|
13
|
+
type ActiveObservation,
|
|
14
|
+
createObservationLifecycle,
|
|
15
|
+
type EndObservation,
|
|
16
|
+
type ObservationClock,
|
|
17
|
+
type ObservationContext,
|
|
18
|
+
type ObservationLifecycle,
|
|
19
|
+
type ObservationLifecycleOptions,
|
|
20
|
+
type ObservationParent,
|
|
21
|
+
type StartObservation,
|
|
22
|
+
} from "./lifecycle.ts"
|
|
12
23
|
export {
|
|
13
24
|
type AttributeValue,
|
|
25
|
+
combineObservationAdapters,
|
|
14
26
|
consoleSpanExporter,
|
|
15
27
|
type NifraSpan,
|
|
28
|
+
type ObservationAdapter,
|
|
16
29
|
type SpanExporter,
|
|
17
30
|
type SpanStatus,
|
|
18
31
|
} from "./span.ts"
|
package/src/lifecycle.ts
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The deep request-observation lifecycle. This module alone owns trace parent selection, identity,
|
|
3
|
+
* timing, error recording, final status classification, and exactly-once completion. Integrations
|
|
4
|
+
* should adapt completed spans; they should not reimplement this state machine.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { AttributeValue, NifraSpan, ObservationAdapter, SpanStatus } from "./span.ts"
|
|
8
|
+
import {
|
|
9
|
+
formatTraceparent,
|
|
10
|
+
generateSpanId,
|
|
11
|
+
generateTraceId,
|
|
12
|
+
parseTraceparent,
|
|
13
|
+
} from "./traceparent.ts"
|
|
14
|
+
|
|
15
|
+
export interface ObservationContext {
|
|
16
|
+
readonly traceId: string
|
|
17
|
+
readonly spanId: string
|
|
18
|
+
readonly parentSpanId?: string
|
|
19
|
+
readonly sampled: boolean
|
|
20
|
+
/** This span's outbound W3C trace context. */
|
|
21
|
+
readonly traceparent: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ObservationParent {
|
|
25
|
+
readonly traceId: string
|
|
26
|
+
readonly spanId: string
|
|
27
|
+
readonly sampled: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface StartObservation {
|
|
31
|
+
readonly name: string
|
|
32
|
+
/** Explicit parent. `null` forces a root span; `undefined` falls back to `traceparent`. */
|
|
33
|
+
readonly parent?: ObservationParent | null
|
|
34
|
+
/** Inbound W3C header used when `parent` is undefined. */
|
|
35
|
+
readonly traceparent?: string | null
|
|
36
|
+
readonly attributes?: Readonly<Record<string, AttributeValue>>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface EndObservation {
|
|
40
|
+
readonly statusCode?: number
|
|
41
|
+
/** Explicit status wins over status-code and recorded-error classification. */
|
|
42
|
+
readonly status?: Exclude<SpanStatus, "unset">
|
|
43
|
+
readonly attributes?: Readonly<Record<string, AttributeValue>>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ActiveObservation {
|
|
47
|
+
readonly span: NifraSpan
|
|
48
|
+
readonly context: ObservationContext
|
|
49
|
+
/** Adds a sink to this in-flight span. Re-adding the same adapter is idempotent. */
|
|
50
|
+
addAdapter(adapter: ObservationAdapter): void
|
|
51
|
+
/** Starts a child that inherits this span's trace identity and sampling decision. */
|
|
52
|
+
startChild(
|
|
53
|
+
input: Omit<StartObservation, "parent" | "traceparent">,
|
|
54
|
+
additionalAdapters?: readonly ObservationAdapter[],
|
|
55
|
+
): ActiveObservation
|
|
56
|
+
/** Records failure evidence without deciding the final status until the response is known. */
|
|
57
|
+
recordError(error: unknown): void
|
|
58
|
+
/** Ends and exports once. Repeated calls return the same completed span without notifying again. */
|
|
59
|
+
end(input?: EndObservation): NifraSpan
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface ObservationLifecycle {
|
|
63
|
+
start(
|
|
64
|
+
input: StartObservation,
|
|
65
|
+
additionalAdapters?: readonly ObservationAdapter[],
|
|
66
|
+
): ActiveObservation
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface ObservationClock {
|
|
70
|
+
/** Epoch milliseconds for interoperable start/end timestamps. */
|
|
71
|
+
wallTime(): number
|
|
72
|
+
/** Monotonic milliseconds for durations. */
|
|
73
|
+
monotonicTime(): number
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface ObservationLifecycleOptions {
|
|
77
|
+
readonly adapters?: readonly ObservationAdapter[]
|
|
78
|
+
/** Injectable seams are primarily useful for deterministic tests and constrained runtimes. */
|
|
79
|
+
readonly clock?: ObservationClock
|
|
80
|
+
readonly generateTraceId?: () => string
|
|
81
|
+
readonly generateSpanId?: () => string
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const defaultClock: ObservationClock = {
|
|
85
|
+
wallTime: () => Date.now(),
|
|
86
|
+
monotonicTime: () => (typeof performance === "undefined" ? Date.now() : performance.now()),
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const errorMessage = (error: unknown): string =>
|
|
90
|
+
error instanceof Error ? error.message : String(error)
|
|
91
|
+
|
|
92
|
+
/** Creates an independent lifecycle factory. Adapters are always called fail-open. */
|
|
93
|
+
export function createObservationLifecycle(
|
|
94
|
+
options: ObservationLifecycleOptions = {},
|
|
95
|
+
): ObservationLifecycle {
|
|
96
|
+
const adapters = options.adapters ?? []
|
|
97
|
+
const clock = options.clock ?? defaultClock
|
|
98
|
+
const newTraceId = options.generateTraceId ?? generateTraceId
|
|
99
|
+
const newSpanId = options.generateSpanId ?? generateSpanId
|
|
100
|
+
|
|
101
|
+
const notifyStart = (span: NifraSpan, spanAdapters: readonly ObservationAdapter[]): void => {
|
|
102
|
+
for (const adapter of spanAdapters) {
|
|
103
|
+
try {
|
|
104
|
+
adapter.onStart?.(span)
|
|
105
|
+
} catch {
|
|
106
|
+
// Observation must never change request behavior.
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const notifyEnd = (span: NifraSpan, spanAdapters: readonly ObservationAdapter[]): void => {
|
|
112
|
+
for (const adapter of spanAdapters) {
|
|
113
|
+
try {
|
|
114
|
+
adapter.onEnd(span)
|
|
115
|
+
} catch {
|
|
116
|
+
// Observation must never change request behavior.
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const start = (
|
|
122
|
+
input: StartObservation,
|
|
123
|
+
additionalAdapters: readonly ObservationAdapter[] = [],
|
|
124
|
+
): ActiveObservation => {
|
|
125
|
+
const spanAdapters = new Set([...adapters, ...additionalAdapters])
|
|
126
|
+
const parent =
|
|
127
|
+
input.parent === undefined ? parseTraceparent(input.traceparent ?? null) : input.parent
|
|
128
|
+
const traceId = parent?.traceId ?? newTraceId()
|
|
129
|
+
const spanId = newSpanId()
|
|
130
|
+
const sampled = parent?.sampled ?? true
|
|
131
|
+
const startTime = clock.wallTime()
|
|
132
|
+
const monotonicStart = clock.monotonicTime()
|
|
133
|
+
const span: NifraSpan = {
|
|
134
|
+
traceId,
|
|
135
|
+
spanId,
|
|
136
|
+
...(parent === null ? {} : { parentSpanId: parent.spanId }),
|
|
137
|
+
sampled,
|
|
138
|
+
name: input.name,
|
|
139
|
+
startTime,
|
|
140
|
+
status: "unset",
|
|
141
|
+
attributes: { ...input.attributes },
|
|
142
|
+
}
|
|
143
|
+
const context: ObservationContext = {
|
|
144
|
+
traceId,
|
|
145
|
+
spanId,
|
|
146
|
+
...(parent === null ? {} : { parentSpanId: parent.spanId }),
|
|
147
|
+
sampled,
|
|
148
|
+
traceparent: formatTraceparent(traceId, spanId, sampled),
|
|
149
|
+
}
|
|
150
|
+
let recordedError = false
|
|
151
|
+
let ended = false
|
|
152
|
+
|
|
153
|
+
const active: ActiveObservation = {
|
|
154
|
+
span,
|
|
155
|
+
context,
|
|
156
|
+
addAdapter(adapter) {
|
|
157
|
+
if (ended || spanAdapters.has(adapter)) return
|
|
158
|
+
spanAdapters.add(adapter)
|
|
159
|
+
try {
|
|
160
|
+
adapter.onStart?.(span)
|
|
161
|
+
} catch {
|
|
162
|
+
// Dynamic adapters have the same fail-open guarantee as initial adapters.
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
startChild(child, childAdapters) {
|
|
166
|
+
return start(
|
|
167
|
+
{
|
|
168
|
+
...child,
|
|
169
|
+
parent: context,
|
|
170
|
+
},
|
|
171
|
+
childAdapters,
|
|
172
|
+
)
|
|
173
|
+
},
|
|
174
|
+
recordError(error) {
|
|
175
|
+
if (ended) return
|
|
176
|
+
recordedError = true
|
|
177
|
+
span.attributes["error.message"] = errorMessage(error)
|
|
178
|
+
},
|
|
179
|
+
end(endInput = {}) {
|
|
180
|
+
if (ended) return span
|
|
181
|
+
ended = true
|
|
182
|
+
Object.assign(span.attributes, endInput.attributes)
|
|
183
|
+
span.endTime = clock.wallTime()
|
|
184
|
+
span.durationMs = Math.max(0, clock.monotonicTime() - monotonicStart)
|
|
185
|
+
span.status =
|
|
186
|
+
endInput.status ??
|
|
187
|
+
(endInput.statusCode === undefined
|
|
188
|
+
? recordedError
|
|
189
|
+
? "error"
|
|
190
|
+
: "ok"
|
|
191
|
+
: endInput.statusCode >= 500
|
|
192
|
+
? "error"
|
|
193
|
+
: "ok")
|
|
194
|
+
// Publish an immutable completed span: a misbehaving adapter can no longer mutate the shared
|
|
195
|
+
// attributes bag and corrupt what other adapters (or the caller) observe. Adapters are fail-open,
|
|
196
|
+
// so a stray write throws into their own try/catch rather than changing request behavior.
|
|
197
|
+
Object.freeze(span.attributes)
|
|
198
|
+
Object.freeze(span)
|
|
199
|
+
notifyEnd(span, [...spanAdapters])
|
|
200
|
+
return span
|
|
201
|
+
},
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
notifyStart(span, [...spanAdapters])
|
|
205
|
+
return active
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return { start }
|
|
209
|
+
}
|
package/src/span.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The span model + exporter seam. Attribute names follow OpenTelemetry HTTP semantic conventions
|
|
3
3
|
* (`http.request.method`, `url.path`, `http.response.status_code`, …) so a span maps cleanly onto an
|
|
4
|
-
* OTel `Span` when bridged — but nothing here depends on the OTel SDK. You supply
|
|
4
|
+
* OTel `Span` when bridged — but nothing here depends on the OTel SDK. You supply an
|
|
5
|
+
* {@link ObservationAdapter}
|
|
5
6
|
* (a ~10-line adapter to `@opentelemetry/api`, or the bundled {@link consoleSpanExporter}).
|
|
6
7
|
*/
|
|
7
8
|
|
|
@@ -36,11 +37,43 @@ export interface NifraSpan {
|
|
|
36
37
|
* real `Span` from a `Tracer`), ship to a collector, or just log. `onStart` is optional (most
|
|
37
38
|
* backends only need the completed span).
|
|
38
39
|
*/
|
|
39
|
-
export interface
|
|
40
|
+
export interface ObservationAdapter {
|
|
40
41
|
onStart?(span: NifraSpan): void
|
|
41
42
|
onEnd(span: NifraSpan): void
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
/** Backwards-compatible name for an observation sink. */
|
|
46
|
+
export type SpanExporter = ObservationAdapter
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Fan out lifecycle notifications to several adapters. Each adapter is isolated: an exception in
|
|
50
|
+
* one sink cannot prevent the remaining sinks from observing the span.
|
|
51
|
+
*/
|
|
52
|
+
export function combineObservationAdapters(
|
|
53
|
+
adapters: readonly ObservationAdapter[],
|
|
54
|
+
): ObservationAdapter {
|
|
55
|
+
return {
|
|
56
|
+
onStart(span) {
|
|
57
|
+
for (const adapter of adapters) {
|
|
58
|
+
try {
|
|
59
|
+
adapter.onStart?.(span)
|
|
60
|
+
} catch {
|
|
61
|
+
// Observation is fail-open by contract.
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
onEnd(span) {
|
|
66
|
+
for (const adapter of adapters) {
|
|
67
|
+
try {
|
|
68
|
+
adapter.onEnd(span)
|
|
69
|
+
} catch {
|
|
70
|
+
// Observation is fail-open by contract.
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
44
77
|
/** A no-frills exporter that logs each completed span as one structured line. Useful in dev or as a
|
|
45
78
|
* starting point before wiring a real backend. */
|
|
46
79
|
export function consoleSpanExporter(
|
package/src/tracing.ts
CHANGED
|
@@ -7,31 +7,20 @@
|
|
|
7
7
|
|
|
8
8
|
import { definePlugin } from "@nifrajs/core"
|
|
9
9
|
import {
|
|
10
|
-
type
|
|
11
|
-
|
|
12
|
-
type
|
|
13
|
-
|
|
14
|
-
} from "./span.ts"
|
|
15
|
-
import {
|
|
16
|
-
formatTraceparent,
|
|
17
|
-
generateSpanId,
|
|
18
|
-
generateTraceId,
|
|
19
|
-
parseTraceparent,
|
|
20
|
-
} from "./traceparent.ts"
|
|
10
|
+
type ActiveObservation,
|
|
11
|
+
createObservationLifecycle,
|
|
12
|
+
type ObservationContext,
|
|
13
|
+
} from "./lifecycle.ts"
|
|
14
|
+
import { consoleSpanExporter, type ObservationAdapter, type SpanExporter } from "./span.ts"
|
|
21
15
|
|
|
22
16
|
/** The trace context exposed on the handler `c.trace` (typed, threaded via `derive`). */
|
|
23
|
-
export
|
|
24
|
-
readonly traceId: string
|
|
25
|
-
readonly spanId: string
|
|
26
|
-
readonly parentSpanId?: string
|
|
27
|
-
readonly sampled: boolean
|
|
28
|
-
/** This request's outbound `traceparent` — forward it on downstream calls to continue the trace. */
|
|
29
|
-
readonly traceparent: string
|
|
30
|
-
}
|
|
17
|
+
export type TraceContext = ObservationContext
|
|
31
18
|
|
|
32
19
|
export interface TracingOptions {
|
|
33
20
|
/** Where spans are sent. Default: {@link consoleSpanExporter}. */
|
|
34
21
|
readonly exporter?: SpanExporter
|
|
22
|
+
/** Additional observation adapters (DevTools, a private redacting backend, metrics, …). */
|
|
23
|
+
readonly adapters?: readonly ObservationAdapter[]
|
|
35
24
|
/** Sets the `service.name` attribute on every span. */
|
|
36
25
|
readonly serviceName?: string
|
|
37
26
|
/**
|
|
@@ -65,60 +54,56 @@ const pathOf = (url: string): string => {
|
|
|
65
54
|
* ```
|
|
66
55
|
*/
|
|
67
56
|
export function tracing(options: TracingOptions = {}) {
|
|
68
|
-
const
|
|
57
|
+
const adapters: ObservationAdapter[] = [
|
|
58
|
+
...(options.exporter === undefined && options.adapters === undefined
|
|
59
|
+
? [consoleSpanExporter()]
|
|
60
|
+
: options.exporter === undefined
|
|
61
|
+
? []
|
|
62
|
+
: [options.exporter]),
|
|
63
|
+
...(options.adapters ?? []),
|
|
64
|
+
]
|
|
65
|
+
const lifecycle = createObservationLifecycle({ adapters })
|
|
69
66
|
const serviceName = options.serviceName
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
const inFlight = new WeakMap<Request, NifraSpan>()
|
|
67
|
+
// A WeakMap so an abandoned request cannot leak its active observation.
|
|
68
|
+
const inFlight = new WeakMap<Request, ActiveObservation>()
|
|
73
69
|
|
|
74
70
|
return definePlugin("tracing", (app) =>
|
|
75
71
|
app
|
|
76
72
|
.derive((c) => {
|
|
77
|
-
const parent = parseTraceparent(c.req.headers.get("traceparent"))
|
|
78
|
-
const traceId = parent?.traceId ?? generateTraceId()
|
|
79
|
-
const spanId = generateSpanId()
|
|
80
|
-
const sampled = parent?.sampled ?? true
|
|
81
73
|
const path = pathOf(c.req.url)
|
|
82
|
-
const
|
|
83
|
-
"http.request.method": c.req.method,
|
|
84
|
-
"url.path": path,
|
|
85
|
-
}
|
|
86
|
-
if (serviceName !== undefined) attributes["service.name"] = serviceName
|
|
87
|
-
const span: NifraSpan = {
|
|
88
|
-
traceId,
|
|
89
|
-
spanId,
|
|
90
|
-
...(parent !== null ? { parentSpanId: parent.spanId } : {}),
|
|
91
|
-
sampled,
|
|
74
|
+
const observation = lifecycle.start({
|
|
92
75
|
name: `${c.req.method} ${path}`,
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
spanId,
|
|
104
|
-
...(parent !== null ? { parentSpanId: parent.spanId } : {}),
|
|
105
|
-
sampled,
|
|
106
|
-
traceparent,
|
|
107
|
-
}
|
|
108
|
-
return { trace }
|
|
76
|
+
traceparent: c.req.headers.get("traceparent"),
|
|
77
|
+
attributes: {
|
|
78
|
+
"http.request.method": c.req.method,
|
|
79
|
+
"url.path": path,
|
|
80
|
+
...(serviceName === undefined ? {} : { "service.name": serviceName }),
|
|
81
|
+
},
|
|
82
|
+
})
|
|
83
|
+
inFlight.set(c.req, observation)
|
|
84
|
+
if (options.responseHeader) c.set.headers.traceparent = observation.context.traceparent
|
|
85
|
+
return { trace: observation.context, observation }
|
|
109
86
|
})
|
|
110
87
|
.use({
|
|
111
88
|
name: "tracing-end",
|
|
89
|
+
onError: (error, context) => {
|
|
90
|
+
inFlight.get(context.request)?.recordError(error)
|
|
91
|
+
return undefined
|
|
92
|
+
},
|
|
112
93
|
onResponse: (res, req) => {
|
|
113
|
-
const
|
|
114
|
-
if (
|
|
94
|
+
const observation = inFlight.get(req)
|
|
95
|
+
if (observation === undefined) return res
|
|
115
96
|
inFlight.delete(req)
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
97
|
+
const bodySize = Number(res.headers.get("content-length") ?? "0") || 0
|
|
98
|
+
const isrStatus = res.headers.get("x-nifra-isr")
|
|
99
|
+
observation.end({
|
|
100
|
+
statusCode: res.status,
|
|
101
|
+
attributes: {
|
|
102
|
+
"http.response.status_code": res.status,
|
|
103
|
+
"http.response.body.size": bodySize,
|
|
104
|
+
...(isrStatus === null ? {} : { "nifra.isr.status": isrStatus }),
|
|
105
|
+
},
|
|
106
|
+
})
|
|
122
107
|
return res
|
|
123
108
|
},
|
|
124
109
|
}),
|