@nifrajs/otel 0.1.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nifra Authors
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.
package/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # @nifrajs/otel
2
+
3
+ Distributed tracing for nifra. The `tracing()` plugin continues (or starts) a **W3C trace** per
4
+ request, opens an **OpenTelemetry-semantic-convention** span, and exposes `c.trace` so you can
5
+ forward the trace to downstream services. Spans go to a **pluggable exporter** — bridge to the
6
+ OpenTelemetry SDK, or log them directly. No SDK bundled; edge-safe.
7
+
8
+ ```ts
9
+ import { tracing, traceHeaders, consoleSpanExporter } from "@nifrajs/otel"
10
+
11
+ const app = server()
12
+ .use(tracing({ exporter: consoleSpanExporter(), serviceName: "orders-api" }))
13
+ .get("/orders/:id", async (c) => {
14
+ // continue the trace into a downstream call:
15
+ const res = await fetch(`${INVENTORY_URL}/stock`, { headers: traceHeaders(c.trace) })
16
+ return { id: c.params.id, inStock: (await res.json()).ok }
17
+ })
18
+ ```
19
+
20
+ ## What it does per request
21
+
22
+ - **Continues an inbound trace** — parses the `traceparent` header; reuses its `trace-id` and records
23
+ the inbound span as the parent. No inbound header → starts a fresh trace.
24
+ - **Opens a span** with HTTP semantic-convention attributes (`http.request.method`, `url.path`,
25
+ `http.response.status_code`, optional `service.name`), ended on response with duration + status
26
+ (`error` for 5xx, `ok` otherwise).
27
+ - **Exposes `c.trace`** (`{ traceId, spanId, parentSpanId?, sampled, traceparent }`) — spread
28
+ `traceHeaders(c.trace)` into any downstream `fetch`/`ctx.api` call to continue the trace.
29
+ - `responseHeader: true` also sets `traceparent` on the response (browser/client correlation).
30
+
31
+ ## Exporters
32
+
33
+ Implement `SpanExporter` to send spans wherever you collect them:
34
+
35
+ ```ts
36
+ interface SpanExporter {
37
+ onStart?(span: NifraSpan): void
38
+ onEnd(span: NifraSpan): void
39
+ }
40
+ ```
41
+
42
+ - `consoleSpanExporter()` — logs each completed span as one structured line (dev / starting point).
43
+ - **OpenTelemetry SDK bridge** — a ~10-line adapter maps `NifraSpan` onto a real OTel `Span` from a
44
+ `Tracer` (the attribute names already follow OTel conventions, so they pass straight through). Your
45
+ app depends on `@opentelemetry/*`; `@nifrajs/otel` does not.
46
+
47
+ ## Connect your collector
48
+
49
+ Use `traceparent` and the built-in semantic attributes in every request span, then send spans to
50
+ your own collector through an exporter. Keep the package edge-safe by installing the OpenTelemetry
51
+ SDK only in apps that need that exporter.
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @nifrajs/otel — distributed tracing for nifra. The `tracing()` plugin continues (or starts) a W3C
3
+ * trace per request, opens an OpenTelemetry-semantic-convention span, and exposes `c.trace` so you
4
+ * can forward the trace to downstream services with `traceHeaders(c.trace)`. Spans go to a pluggable
5
+ * `SpanExporter` — bridge to the OpenTelemetry SDK, or log with `consoleSpanExporter`. No SDK
6
+ * bundled; edge-safe.
7
+ *
8
+ * import { tracing, traceHeaders, consoleSpanExporter } from "@nifrajs/otel"
9
+ * app.use(tracing({ exporter: consoleSpanExporter(), serviceName: "orders-api" }))
10
+ */
11
+ export { type AttributeValue, consoleSpanExporter, type NifraSpan, type SpanExporter, type SpanStatus, } from "./span.js";
12
+ export { formatTraceparent, generateSpanId, generateTraceId, type ParsedTraceparent, parseTraceparent, } from "./traceparent.js";
13
+ export { type TraceContext, type TracingOptions, traceHeaders, tracing } from "./tracing.js";
14
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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"}
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @nifrajs/otel — distributed tracing for nifra. The `tracing()` plugin continues (or starts) a W3C
3
+ * trace per request, opens an OpenTelemetry-semantic-convention span, and exposes `c.trace` so you
4
+ * can forward the trace to downstream services with `traceHeaders(c.trace)`. Spans go to a pluggable
5
+ * `SpanExporter` — bridge to the OpenTelemetry SDK, or log with `consoleSpanExporter`. No SDK
6
+ * bundled; edge-safe.
7
+ *
8
+ * import { tracing, traceHeaders, consoleSpanExporter } from "@nifrajs/otel"
9
+ * app.use(tracing({ exporter: consoleSpanExporter(), serviceName: "orders-api" }))
10
+ */
11
+ export { consoleSpanExporter, } from "./span.js";
12
+ export { formatTraceparent, generateSpanId, generateTraceId, parseTraceparent, } from "./traceparent.js";
13
+ export { traceHeaders, tracing } from "./tracing.js";
14
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAEL,mBAAmB,GAIpB,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"}
package/dist/span.d.ts ADDED
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The span model + exporter seam. Attribute names follow OpenTelemetry HTTP semantic conventions
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 a {@link SpanExporter}
5
+ * (a ~10-line adapter to `@opentelemetry/api`, or the bundled {@link consoleSpanExporter}).
6
+ */
7
+ export type SpanStatus = "unset" | "ok" | "error";
8
+ export type AttributeValue = string | number | boolean;
9
+ /** A completed (or in-flight) server span for one request. */
10
+ export interface NifraSpan {
11
+ /** 32-hex W3C trace id — shared across every span/service in the trace. */
12
+ readonly traceId: string;
13
+ /** 16-hex id of this span. */
14
+ readonly spanId: string;
15
+ /** The inbound span's id, if this request continued an upstream trace. */
16
+ readonly parentSpanId?: string;
17
+ /** Whether the trace is sampled (the W3C flag). */
18
+ readonly sampled: boolean;
19
+ /** Span name — `"<METHOD> <path>"`. */
20
+ readonly name: string;
21
+ /** Wall-clock start (epoch ms). */
22
+ readonly startTime: number;
23
+ /** Wall-clock end (epoch ms) — set on completion. */
24
+ endTime?: number;
25
+ /** Duration in ms (monotonic). */
26
+ durationMs?: number;
27
+ status: SpanStatus;
28
+ /** OTel-semantic-convention attributes. */
29
+ readonly attributes: Record<string, AttributeValue>;
30
+ }
31
+ /**
32
+ * Where ended spans go. Implement this to bridge to the OpenTelemetry SDK (map each field onto a
33
+ * real `Span` from a `Tracer`), ship to a collector, or just log. `onStart` is optional (most
34
+ * backends only need the completed span).
35
+ */
36
+ export interface SpanExporter {
37
+ onStart?(span: NifraSpan): void;
38
+ onEnd(span: NifraSpan): void;
39
+ }
40
+ /** A no-frills exporter that logs each completed span as one structured line. Useful in dev or as a
41
+ * starting point before wiring a real backend. */
42
+ export declare function consoleSpanExporter(log?: (line: string) => void): SpanExporter;
43
+ //# sourceMappingURL=span.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"span.d.ts","sourceRoot":"","sources":["../src/span.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;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,YAAY;IAC3B,OAAO,CAAC,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI,CAAA;IAC/B,KAAK,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI,CAAA;CAC7B;AAED;kDACkD;AAClD,wBAAgB,mBAAmB,CACjC,GAAG,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAEtB,GACA,YAAY,CAgBd"}
package/dist/span.js ADDED
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The span model + exporter seam. Attribute names follow OpenTelemetry HTTP semantic conventions
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 a {@link SpanExporter}
5
+ * (a ~10-line adapter to `@opentelemetry/api`, or the bundled {@link consoleSpanExporter}).
6
+ */
7
+ /** A no-frills exporter that logs each completed span as one structured line. Useful in dev or as a
8
+ * starting point before wiring a real backend. */
9
+ export function consoleSpanExporter(log = (l) => {
10
+ console.log(l);
11
+ }) {
12
+ return {
13
+ onEnd(span) {
14
+ log(JSON.stringify({
15
+ name: span.name,
16
+ traceId: span.traceId,
17
+ spanId: span.spanId,
18
+ parentSpanId: span.parentSpanId,
19
+ durationMs: span.durationMs,
20
+ status: span.status,
21
+ ...span.attributes,
22
+ }));
23
+ },
24
+ };
25
+ }
26
+ //# sourceMappingURL=span.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"span.js","sourceRoot":"","sources":["../src/span.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAsCH;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"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * W3C Trace Context — `traceparent` parsing/formatting + id generation. This is OpenTelemetry's
3
+ * wire format for propagating a trace across services, so producing/forwarding it correctly is the
4
+ * core of distributed tracing regardless of which backend ends up collecting the spans.
5
+ *
6
+ * traceparent: 00-<32-hex trace-id>-<16-hex span-id>-<2-hex flags>
7
+ *
8
+ * Pure + dependency-free; the request plugin is a thin layer over this.
9
+ */
10
+ /** A parsed inbound `traceparent`. */
11
+ export interface ParsedTraceparent {
12
+ readonly traceId: string;
13
+ readonly spanId: string;
14
+ readonly sampled: boolean;
15
+ }
16
+ /**
17
+ * Parse a `traceparent` header, or `null` if it's absent/malformed/version-unknown — per the spec,
18
+ * a bad header means "start a fresh trace", never an error. Only version `00` is accepted.
19
+ */
20
+ export declare function parseTraceparent(header: string | null | undefined): ParsedTraceparent | null;
21
+ /** Format a `traceparent` header value (version `00`). */
22
+ export declare function formatTraceparent(traceId: string, spanId: string, sampled: boolean): string;
23
+ /** A fresh 16-byte (32-hex) trace id. */
24
+ export declare function generateTraceId(): string;
25
+ /** A fresh 8-byte (16-hex) span id. */
26
+ export declare function generateSpanId(): string;
27
+ //# sourceMappingURL=traceparent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"traceparent.d.ts","sourceRoot":"","sources":["../src/traceparent.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,sCAAsC;AACtC,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;AAMD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,iBAAiB,GAAG,IAAI,CAW5F;AAED,0DAA0D;AAC1D,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM,CAE3F;AAQD,yCAAyC;AACzC,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED,uCAAuC;AACvC,wBAAgB,cAAc,IAAI,MAAM,CAEvC"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * W3C Trace Context — `traceparent` parsing/formatting + id generation. This is OpenTelemetry's
3
+ * wire format for propagating a trace across services, so producing/forwarding it correctly is the
4
+ * core of distributed tracing regardless of which backend ends up collecting the spans.
5
+ *
6
+ * traceparent: 00-<32-hex trace-id>-<16-hex span-id>-<2-hex flags>
7
+ *
8
+ * Pure + dependency-free; the request plugin is a thin layer over this.
9
+ */
10
+ const HEX = /^[0-9a-f]+$/;
11
+ const ZERO_TRACE = "00000000000000000000000000000000";
12
+ const ZERO_SPAN = "0000000000000000";
13
+ /**
14
+ * Parse a `traceparent` header, or `null` if it's absent/malformed/version-unknown — per the spec,
15
+ * a bad header means "start a fresh trace", never an error. Only version `00` is accepted.
16
+ */
17
+ export function parseTraceparent(header) {
18
+ if (!header)
19
+ return null;
20
+ const parts = header.trim().split("-");
21
+ if (parts.length !== 4)
22
+ return null;
23
+ const [version, traceId, spanId, flags] = parts;
24
+ if (version !== "00")
25
+ return null;
26
+ if (traceId.length !== 32 || !HEX.test(traceId) || traceId === ZERO_TRACE)
27
+ return null;
28
+ if (spanId.length !== 16 || !HEX.test(spanId) || spanId === ZERO_SPAN)
29
+ return null;
30
+ if (flags.length !== 2 || !HEX.test(flags))
31
+ return null;
32
+ // bit 0 of the flags byte = sampled
33
+ return { traceId, spanId, sampled: (Number.parseInt(flags, 16) & 0x01) === 1 };
34
+ }
35
+ /** Format a `traceparent` header value (version `00`). */
36
+ export function formatTraceparent(traceId, spanId, sampled) {
37
+ return `00-${traceId}-${spanId}-${sampled ? "01" : "00"}`;
38
+ }
39
+ const toHex = (bytes) => {
40
+ let out = "";
41
+ for (const b of bytes)
42
+ out += b.toString(16).padStart(2, "0");
43
+ return out;
44
+ };
45
+ /** A fresh 16-byte (32-hex) trace id. */
46
+ export function generateTraceId() {
47
+ return toHex(crypto.getRandomValues(new Uint8Array(16)));
48
+ }
49
+ /** A fresh 8-byte (16-hex) span id. */
50
+ export function generateSpanId() {
51
+ return toHex(crypto.getRandomValues(new Uint8Array(8)));
52
+ }
53
+ //# sourceMappingURL=traceparent.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"traceparent.js","sourceRoot":"","sources":["../src/traceparent.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AASH,MAAM,GAAG,GAAG,aAAa,CAAA;AACzB,MAAM,UAAU,GAAG,kCAAkC,CAAA;AACrD,MAAM,SAAS,GAAG,kBAAkB,CAAA;AAEpC;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAAiC;IAChE,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAA;IACxB,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IACtC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IACnC,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,GAAG,KAAyC,CAAA;IACnF,IAAI,OAAO,KAAK,IAAI;QAAE,OAAO,IAAI,CAAA;IACjC,IAAI,OAAO,CAAC,MAAM,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,OAAO,KAAK,UAAU;QAAE,OAAO,IAAI,CAAA;IACtF,IAAI,MAAM,CAAC,MAAM,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,IAAI,CAAA;IAClF,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IACvD,oCAAoC;IACpC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAA;AAChF,CAAC;AAED,0DAA0D;AAC1D,MAAM,UAAU,iBAAiB,CAAC,OAAe,EAAE,MAAc,EAAE,OAAgB;IACjF,OAAO,MAAM,OAAO,IAAI,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;AAC3D,CAAC;AAED,MAAM,KAAK,GAAG,CAAC,KAAiB,EAAU,EAAE;IAC1C,IAAI,GAAG,GAAG,EAAE,CAAA;IACZ,KAAK,MAAM,CAAC,IAAI,KAAK;QAAE,GAAG,IAAI,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAA;IAC7D,OAAO,GAAG,CAAA;AACZ,CAAC,CAAA;AAED,yCAAyC;AACzC,MAAM,UAAU,eAAe;IAC7B,OAAO,KAAK,CAAC,MAAM,CAAC,eAAe,CAAC,IAAI,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;AAC1D,CAAC;AAED,uCAAuC;AACvC,MAAM,UAAU,cAAc;IAC5B,OAAO,KAAK,CAAC,MAAM,CAAC,eAAe,CAAC,IAAI,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;AACzD,CAAC"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The `tracing()` plugin — establishes a span per request, propagates W3C trace context, and exposes
3
+ * `c.trace` for forwarding the trace to downstream services. OpenTelemetry-compatible by wire format
4
+ * (traceparent) and attribute names (HTTP semantic conventions); the span itself is exported through
5
+ * your {@link SpanExporter} (bridge to the OTel SDK, or log via `consoleSpanExporter`).
6
+ */
7
+ import { type SpanExporter } from "./span.js";
8
+ /** The trace context exposed on the handler `c.trace` (typed, threaded via `derive`). */
9
+ export interface TraceContext {
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
+ }
17
+ export interface TracingOptions {
18
+ /** Where spans are sent. Default: {@link consoleSpanExporter}. */
19
+ readonly exporter?: SpanExporter;
20
+ /** Sets the `service.name` attribute on every span. */
21
+ readonly serviceName?: string;
22
+ /**
23
+ * Set the outbound `traceparent` on the RESPONSE too (handy for browser/client correlation).
24
+ * Default false — most setups only propagate downstream, not back to the caller.
25
+ */
26
+ readonly responseHeader?: boolean;
27
+ }
28
+ /** Spread into an outgoing `fetch`/`ctx.api` call's headers to continue the trace downstream:
29
+ * `fetch(url, { headers: traceHeaders(c.trace) })`. */
30
+ export declare function traceHeaders(trace: TraceContext): {
31
+ traceparent: string;
32
+ };
33
+ /**
34
+ * Distributed-tracing plugin. Each request continues the inbound trace (or starts one), opens a
35
+ * server span, and ends it on response with the status + HTTP attributes. Idempotent.
36
+ *
37
+ * ```ts
38
+ * app.use(tracing({ exporter: myOtelExporter, serviceName: "orders-api" }))
39
+ * // in a handler: fetch(url, { headers: traceHeaders(c.trace) }) // continue the trace downstream
40
+ * ```
41
+ */
42
+ export declare function tracing(options?: TracingOptions): import("@nifrajs/core").NifraPlugin<import("@nifrajs/core").AnyServer, import("@nifrajs/core").Server<any, any>>;
43
+ //# sourceMappingURL=tracing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tracing.d.ts","sourceRoot":"","sources":["../src/tracing.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAIL,KAAK,YAAY,EAClB,MAAM,WAAW,CAAA;AAQlB,yFAAyF;AACzF,MAAM,WAAW,YAAY;IAC3B,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,oGAAoG;IACpG,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;CAC7B;AAED,MAAM,WAAW,cAAc;IAC7B,kEAAkE;IAClE,QAAQ,CAAC,QAAQ,CAAC,EAAE,YAAY,CAAA;IAChC,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,oHA2DnD"}
@@ -0,0 +1,92 @@
1
+ /**
2
+ * The `tracing()` plugin — establishes a span per request, propagates W3C trace context, and exposes
3
+ * `c.trace` for forwarding the trace to downstream services. OpenTelemetry-compatible by wire format
4
+ * (traceparent) and attribute names (HTTP semantic conventions); the span itself is exported through
5
+ * your {@link SpanExporter} (bridge to the OTel SDK, or log via `consoleSpanExporter`).
6
+ */
7
+ import { definePlugin } from "@nifrajs/core";
8
+ import { consoleSpanExporter, } from "./span.js";
9
+ import { formatTraceparent, generateSpanId, generateTraceId, parseTraceparent, } from "./traceparent.js";
10
+ /** Spread into an outgoing `fetch`/`ctx.api` call's headers to continue the trace downstream:
11
+ * `fetch(url, { headers: traceHeaders(c.trace) })`. */
12
+ export function traceHeaders(trace) {
13
+ return { traceparent: trace.traceparent };
14
+ }
15
+ const pathOf = (url) => {
16
+ try {
17
+ return new URL(url).pathname;
18
+ }
19
+ catch {
20
+ return url;
21
+ }
22
+ };
23
+ /**
24
+ * Distributed-tracing plugin. Each request continues the inbound trace (or starts one), opens a
25
+ * server span, and ends it on response with the status + HTTP attributes. Idempotent.
26
+ *
27
+ * ```ts
28
+ * app.use(tracing({ exporter: myOtelExporter, serviceName: "orders-api" }))
29
+ * // in a handler: fetch(url, { headers: traceHeaders(c.trace) }) // continue the trace downstream
30
+ * ```
31
+ */
32
+ export function tracing(options = {}) {
33
+ const exporter = options.exporter ?? consoleSpanExporter();
34
+ const serviceName = options.serviceName;
35
+ // Span per in-flight request, keyed by the Request — ended in onResponse (mirrors @nifrajs/middleware
36
+ // timing). A WeakMap so an abandoned request can't leak a span.
37
+ const inFlight = new WeakMap();
38
+ return definePlugin("tracing", (app) => app
39
+ .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
+ const path = pathOf(c.req.url);
45
+ const attributes = {
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,
56
+ name: `${c.req.method} ${path}`,
57
+ startTime: Date.now(),
58
+ status: "unset",
59
+ attributes,
60
+ };
61
+ inFlight.set(c.req, span);
62
+ exporter.onStart?.(span);
63
+ const traceparent = formatTraceparent(traceId, spanId, sampled);
64
+ if (options.responseHeader)
65
+ c.set.headers.traceparent = traceparent;
66
+ const trace = {
67
+ traceId,
68
+ spanId,
69
+ ...(parent !== null ? { parentSpanId: parent.spanId } : {}),
70
+ sampled,
71
+ traceparent,
72
+ };
73
+ return { trace };
74
+ })
75
+ .use({
76
+ name: "tracing-end",
77
+ onResponse: (res, req) => {
78
+ const span = inFlight.get(req);
79
+ if (span === undefined)
80
+ return res;
81
+ inFlight.delete(req);
82
+ span.endTime = Date.now();
83
+ span.durationMs = span.endTime - span.startTime;
84
+ span.attributes["http.response.status_code"] = res.status;
85
+ // 5xx is a server error; everything else (incl. handled 4xx) is a normal outcome.
86
+ span.status = res.status >= 500 ? "error" : "ok";
87
+ exporter.onEnd(span);
88
+ return res;
89
+ },
90
+ }));
91
+ }
92
+ //# sourceMappingURL=tracing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tracing.js","sourceRoot":"","sources":["../src/tracing.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@nifrajs/otel",
3
+ "version": "0.1.0-alpha.1",
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
+ "keywords": [
6
+ "bun",
7
+ "typescript",
8
+ "nifra",
9
+ "web-framework",
10
+ "full-stack",
11
+ "edge",
12
+ "ssr",
13
+ "opentelemetry",
14
+ "tracing",
15
+ "observability",
16
+ "w3c-traceparent"
17
+ ],
18
+ "type": "module",
19
+ "main": "./dist/index.js",
20
+ "module": "./dist/index.js",
21
+ "types": "./dist/index.d.ts",
22
+ "exports": {
23
+ ".": {
24
+ "bun": "./src/index.ts",
25
+ "types": "./dist/index.d.ts",
26
+ "default": "./dist/index.js"
27
+ }
28
+ },
29
+ "files": [
30
+ "dist",
31
+ "src"
32
+ ],
33
+ "sideEffects": false,
34
+ "scripts": {
35
+ "build": "tsc -p tsconfig.build.json && bun run ../../scripts/fix-dts.ts dist"
36
+ },
37
+ "license": "MIT",
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "peerDependencies": {
42
+ "@nifrajs/core": "workspace:*"
43
+ },
44
+ "devDependencies": {
45
+ "@nifrajs/core": "workspace:*"
46
+ },
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "git+https://github.com/nifrajs/nifra.git",
50
+ "directory": "packages/otel"
51
+ },
52
+ "bugs": {
53
+ "url": "https://github.com/nifrajs/nifra/issues"
54
+ },
55
+ "homepage": "https://github.com/nifrajs/nifra#readme"
56
+ }
package/src/index.ts ADDED
@@ -0,0 +1,26 @@
1
+ /**
2
+ * @nifrajs/otel — distributed tracing for nifra. The `tracing()` plugin continues (or starts) a W3C
3
+ * trace per request, opens an OpenTelemetry-semantic-convention span, and exposes `c.trace` so you
4
+ * can forward the trace to downstream services with `traceHeaders(c.trace)`. Spans go to a pluggable
5
+ * `SpanExporter` — bridge to the OpenTelemetry SDK, or log with `consoleSpanExporter`. No SDK
6
+ * bundled; edge-safe.
7
+ *
8
+ * import { tracing, traceHeaders, consoleSpanExporter } from "@nifrajs/otel"
9
+ * app.use(tracing({ exporter: consoleSpanExporter(), serviceName: "orders-api" }))
10
+ */
11
+
12
+ export {
13
+ type AttributeValue,
14
+ consoleSpanExporter,
15
+ type NifraSpan,
16
+ type SpanExporter,
17
+ type SpanStatus,
18
+ } from "./span.ts"
19
+ export {
20
+ formatTraceparent,
21
+ generateSpanId,
22
+ generateTraceId,
23
+ type ParsedTraceparent,
24
+ parseTraceparent,
25
+ } from "./traceparent.ts"
26
+ export { type TraceContext, type TracingOptions, traceHeaders, tracing } from "./tracing.ts"
package/src/span.ts ADDED
@@ -0,0 +1,66 @@
1
+ /**
2
+ * The span model + exporter seam. Attribute names follow OpenTelemetry HTTP semantic conventions
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 a {@link SpanExporter}
5
+ * (a ~10-line adapter to `@opentelemetry/api`, or the bundled {@link consoleSpanExporter}).
6
+ */
7
+
8
+ export type SpanStatus = "unset" | "ok" | "error"
9
+ export type AttributeValue = string | number | boolean
10
+
11
+ /** A completed (or in-flight) server span for one request. */
12
+ export interface NifraSpan {
13
+ /** 32-hex W3C trace id — shared across every span/service in the trace. */
14
+ readonly traceId: string
15
+ /** 16-hex id of this span. */
16
+ readonly spanId: string
17
+ /** The inbound span's id, if this request continued an upstream trace. */
18
+ readonly parentSpanId?: string
19
+ /** Whether the trace is sampled (the W3C flag). */
20
+ readonly sampled: boolean
21
+ /** Span name — `"<METHOD> <path>"`. */
22
+ readonly name: string
23
+ /** Wall-clock start (epoch ms). */
24
+ readonly startTime: number
25
+ /** Wall-clock end (epoch ms) — set on completion. */
26
+ endTime?: number
27
+ /** Duration in ms (monotonic). */
28
+ durationMs?: number
29
+ status: SpanStatus
30
+ /** OTel-semantic-convention attributes. */
31
+ readonly attributes: Record<string, AttributeValue>
32
+ }
33
+
34
+ /**
35
+ * Where ended spans go. Implement this to bridge to the OpenTelemetry SDK (map each field onto a
36
+ * real `Span` from a `Tracer`), ship to a collector, or just log. `onStart` is optional (most
37
+ * backends only need the completed span).
38
+ */
39
+ export interface SpanExporter {
40
+ onStart?(span: NifraSpan): void
41
+ onEnd(span: NifraSpan): void
42
+ }
43
+
44
+ /** A no-frills exporter that logs each completed span as one structured line. Useful in dev or as a
45
+ * starting point before wiring a real backend. */
46
+ export function consoleSpanExporter(
47
+ log: (line: string) => void = (l) => {
48
+ console.log(l)
49
+ },
50
+ ): SpanExporter {
51
+ return {
52
+ onEnd(span) {
53
+ log(
54
+ JSON.stringify({
55
+ name: span.name,
56
+ traceId: span.traceId,
57
+ spanId: span.spanId,
58
+ parentSpanId: span.parentSpanId,
59
+ durationMs: span.durationMs,
60
+ status: span.status,
61
+ ...span.attributes,
62
+ }),
63
+ )
64
+ },
65
+ }
66
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * W3C Trace Context — `traceparent` parsing/formatting + id generation. This is OpenTelemetry's
3
+ * wire format for propagating a trace across services, so producing/forwarding it correctly is the
4
+ * core of distributed tracing regardless of which backend ends up collecting the spans.
5
+ *
6
+ * traceparent: 00-<32-hex trace-id>-<16-hex span-id>-<2-hex flags>
7
+ *
8
+ * Pure + dependency-free; the request plugin is a thin layer over this.
9
+ */
10
+
11
+ /** A parsed inbound `traceparent`. */
12
+ export interface ParsedTraceparent {
13
+ readonly traceId: string
14
+ readonly spanId: string
15
+ readonly sampled: boolean
16
+ }
17
+
18
+ const HEX = /^[0-9a-f]+$/
19
+ const ZERO_TRACE = "00000000000000000000000000000000"
20
+ const ZERO_SPAN = "0000000000000000"
21
+
22
+ /**
23
+ * Parse a `traceparent` header, or `null` if it's absent/malformed/version-unknown — per the spec,
24
+ * a bad header means "start a fresh trace", never an error. Only version `00` is accepted.
25
+ */
26
+ export function parseTraceparent(header: string | null | undefined): ParsedTraceparent | null {
27
+ if (!header) return null
28
+ const parts = header.trim().split("-")
29
+ if (parts.length !== 4) return null
30
+ const [version, traceId, spanId, flags] = parts as [string, string, string, string]
31
+ if (version !== "00") return null
32
+ if (traceId.length !== 32 || !HEX.test(traceId) || traceId === ZERO_TRACE) return null
33
+ if (spanId.length !== 16 || !HEX.test(spanId) || spanId === ZERO_SPAN) return null
34
+ if (flags.length !== 2 || !HEX.test(flags)) return null
35
+ // bit 0 of the flags byte = sampled
36
+ return { traceId, spanId, sampled: (Number.parseInt(flags, 16) & 0x01) === 1 }
37
+ }
38
+
39
+ /** Format a `traceparent` header value (version `00`). */
40
+ export function formatTraceparent(traceId: string, spanId: string, sampled: boolean): string {
41
+ return `00-${traceId}-${spanId}-${sampled ? "01" : "00"}`
42
+ }
43
+
44
+ const toHex = (bytes: Uint8Array): string => {
45
+ let out = ""
46
+ for (const b of bytes) out += b.toString(16).padStart(2, "0")
47
+ return out
48
+ }
49
+
50
+ /** A fresh 16-byte (32-hex) trace id. */
51
+ export function generateTraceId(): string {
52
+ return toHex(crypto.getRandomValues(new Uint8Array(16)))
53
+ }
54
+
55
+ /** A fresh 8-byte (16-hex) span id. */
56
+ export function generateSpanId(): string {
57
+ return toHex(crypto.getRandomValues(new Uint8Array(8)))
58
+ }
package/src/tracing.ts ADDED
@@ -0,0 +1,126 @@
1
+ /**
2
+ * The `tracing()` plugin — establishes a span per request, propagates W3C trace context, and exposes
3
+ * `c.trace` for forwarding the trace to downstream services. OpenTelemetry-compatible by wire format
4
+ * (traceparent) and attribute names (HTTP semantic conventions); the span itself is exported through
5
+ * your {@link SpanExporter} (bridge to the OTel SDK, or log via `consoleSpanExporter`).
6
+ */
7
+
8
+ import { definePlugin } from "@nifrajs/core"
9
+ import {
10
+ type AttributeValue,
11
+ consoleSpanExporter,
12
+ type NifraSpan,
13
+ type SpanExporter,
14
+ } from "./span.ts"
15
+ import {
16
+ formatTraceparent,
17
+ generateSpanId,
18
+ generateTraceId,
19
+ parseTraceparent,
20
+ } from "./traceparent.ts"
21
+
22
+ /** The trace context exposed on the handler `c.trace` (typed, threaded via `derive`). */
23
+ export interface TraceContext {
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
+ }
31
+
32
+ export interface TracingOptions {
33
+ /** Where spans are sent. Default: {@link consoleSpanExporter}. */
34
+ readonly exporter?: SpanExporter
35
+ /** Sets the `service.name` attribute on every span. */
36
+ readonly serviceName?: string
37
+ /**
38
+ * Set the outbound `traceparent` on the RESPONSE too (handy for browser/client correlation).
39
+ * Default false — most setups only propagate downstream, not back to the caller.
40
+ */
41
+ readonly responseHeader?: boolean
42
+ }
43
+
44
+ /** Spread into an outgoing `fetch`/`ctx.api` call's headers to continue the trace downstream:
45
+ * `fetch(url, { headers: traceHeaders(c.trace) })`. */
46
+ export function traceHeaders(trace: TraceContext): { traceparent: string } {
47
+ return { traceparent: trace.traceparent }
48
+ }
49
+
50
+ const pathOf = (url: string): string => {
51
+ try {
52
+ return new URL(url).pathname
53
+ } catch {
54
+ return url
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Distributed-tracing plugin. Each request continues the inbound trace (or starts one), opens a
60
+ * server span, and ends it on response with the status + HTTP attributes. Idempotent.
61
+ *
62
+ * ```ts
63
+ * app.use(tracing({ exporter: myOtelExporter, serviceName: "orders-api" }))
64
+ * // in a handler: fetch(url, { headers: traceHeaders(c.trace) }) // continue the trace downstream
65
+ * ```
66
+ */
67
+ export function tracing(options: TracingOptions = {}) {
68
+ const exporter = options.exporter ?? consoleSpanExporter()
69
+ const serviceName = options.serviceName
70
+ // Span per in-flight request, keyed by the Request — ended in onResponse (mirrors @nifrajs/middleware
71
+ // timing). A WeakMap so an abandoned request can't leak a span.
72
+ const inFlight = new WeakMap<Request, NifraSpan>()
73
+
74
+ return definePlugin("tracing", (app) =>
75
+ app
76
+ .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
+ const path = pathOf(c.req.url)
82
+ const attributes: Record<string, AttributeValue> = {
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,
92
+ name: `${c.req.method} ${path}`,
93
+ startTime: Date.now(),
94
+ status: "unset",
95
+ attributes,
96
+ }
97
+ inFlight.set(c.req, span)
98
+ exporter.onStart?.(span)
99
+ const traceparent = formatTraceparent(traceId, spanId, sampled)
100
+ if (options.responseHeader) c.set.headers.traceparent = traceparent
101
+ const trace: TraceContext = {
102
+ traceId,
103
+ spanId,
104
+ ...(parent !== null ? { parentSpanId: parent.spanId } : {}),
105
+ sampled,
106
+ traceparent,
107
+ }
108
+ return { trace }
109
+ })
110
+ .use({
111
+ name: "tracing-end",
112
+ onResponse: (res, req) => {
113
+ const span = inFlight.get(req)
114
+ if (span === undefined) return res
115
+ inFlight.delete(req)
116
+ span.endTime = Date.now()
117
+ span.durationMs = span.endTime - span.startTime
118
+ span.attributes["http.response.status_code"] = res.status
119
+ // 5xx is a server error; everything else (incl. handled 4xx) is a normal outcome.
120
+ span.status = res.status >= 500 ? "error" : "ok"
121
+ exporter.onEnd(span)
122
+ return res
123
+ },
124
+ }),
125
+ )
126
+ }