@fiducian/otel 0.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.
@@ -0,0 +1,49 @@
1
+ import { type StoryContext, type TraceEvent } from "@fiducian/conductor";
2
+ import type { FiduciaTrace } from "@fiducian/runtime";
3
+ export declare const OTEL_BAGGAGE_CONTEXT_KEY = "fiducia.context";
4
+ export declare const OTEL_ATTR_FROM = "fiducia.from";
5
+ export declare const OTEL_ATTR_TO = "fiducia.to";
6
+ export declare const OTEL_ATTR_OP = "fiducia.op";
7
+ export interface TraceParentParts {
8
+ version: string;
9
+ traceId: string;
10
+ spanId: string;
11
+ flags: string;
12
+ }
13
+ export interface OtelSpanLike {
14
+ traceId?: string;
15
+ spanId?: string;
16
+ name?: string;
17
+ timestamp?: number;
18
+ attributes?: Record<string, string | number | boolean>;
19
+ }
20
+ export interface HttpLikeRequest {
21
+ headers: Record<string, string | string[] | undefined>;
22
+ }
23
+ export declare function parseTraceParent(traceparent: string): TraceParentParts | undefined;
24
+ export declare function parseBaggage(baggage: string | undefined): Record<string, string>;
25
+ export declare function contextFromOtelHeaders(headers: Record<string, string | string[] | undefined>, ct: FiduciaTrace): StoryContext;
26
+ export declare function otelHeadersFromContext(ctx: StoryContext): Record<string, string>;
27
+ export declare function spanLikeToEvent(span: OtelSpanLike, contextId: string): TraceEvent | undefined;
28
+ export declare function importOtelJsonl(source: string): TraceEvent[];
29
+ export declare function importOtelJsonlFile(path: string): TraceEvent[];
30
+ export interface ExpressRouteMapping {
31
+ method: string;
32
+ path: string;
33
+ from: string;
34
+ to: string;
35
+ payloadKeys?: string[];
36
+ }
37
+ export interface ExpressOtelMiddlewareOptions {
38
+ serviceName: string;
39
+ routes?: ExpressRouteMapping[];
40
+ }
41
+ export declare function createExpressOtelMiddleware(ct: FiduciaTrace, options: ExpressOtelMiddlewareOptions): (req: HttpLikeRequest & {
42
+ method?: string;
43
+ path?: string;
44
+ body?: Record<string, unknown>;
45
+ }, res: {
46
+ on: (event: string, cb: () => void) => void;
47
+ statusCode?: number;
48
+ }, next: () => void) => void;
49
+ export declare function attachOtelContext(ctx: StoryContext, headers?: Record<string, string>): Record<string, string>;
package/dist/index.js ADDED
@@ -0,0 +1,150 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { CONTEXT_HEADER, contextFromHeader, headerFromContext, } from "@fiducian/conductor";
3
+ export const OTEL_BAGGAGE_CONTEXT_KEY = "fiducia.context";
4
+ export const OTEL_ATTR_FROM = "fiducia.from";
5
+ export const OTEL_ATTR_TO = "fiducia.to";
6
+ export const OTEL_ATTR_OP = "fiducia.op";
7
+ function headerValue(headers, name) {
8
+ const raw = headers[name] ?? headers[name.toLowerCase()];
9
+ return Array.isArray(raw) ? raw[0] : raw;
10
+ }
11
+ export function parseTraceParent(traceparent) {
12
+ const parts = traceparent.trim().split("-");
13
+ if (parts.length !== 4)
14
+ return undefined;
15
+ const [version, traceId, spanId, flags] = parts;
16
+ if (version !== "00" || traceId.length !== 32 || spanId.length !== 16) {
17
+ return undefined;
18
+ }
19
+ return { version, traceId, spanId, flags };
20
+ }
21
+ export function parseBaggage(baggage) {
22
+ if (!baggage)
23
+ return {};
24
+ const out = {};
25
+ for (const part of baggage.split(",")) {
26
+ const trimmed = part.trim();
27
+ if (!trimmed)
28
+ continue;
29
+ const eq = trimmed.indexOf("=");
30
+ if (eq === -1)
31
+ continue;
32
+ const key = decodeURIComponent(trimmed.slice(0, eq).trim());
33
+ const value = decodeURIComponent(trimmed.slice(eq + 1).trim());
34
+ out[key] = value;
35
+ }
36
+ return out;
37
+ }
38
+ export function contextFromOtelHeaders(headers, ct) {
39
+ const explicit = headerValue(headers, CONTEXT_HEADER) ??
40
+ parseBaggage(headerValue(headers, "baggage"))[OTEL_BAGGAGE_CONTEXT_KEY];
41
+ if (explicit) {
42
+ return contextFromHeader(explicit) ?? ct.startStory();
43
+ }
44
+ const traceparent = headerValue(headers, "traceparent");
45
+ if (traceparent) {
46
+ const parsed = parseTraceParent(traceparent);
47
+ if (parsed) {
48
+ return ct.conductor.startStoryWithId(parsed.traceId);
49
+ }
50
+ }
51
+ return ct.startStory();
52
+ }
53
+ export function otelHeadersFromContext(ctx) {
54
+ const id = headerFromContext(ctx);
55
+ return {
56
+ [CONTEXT_HEADER]: id,
57
+ baggage: `${OTEL_BAGGAGE_CONTEXT_KEY}=${encodeURIComponent(id)}`,
58
+ };
59
+ }
60
+ export function spanLikeToEvent(span, contextId) {
61
+ const attrs = span.attributes ?? {};
62
+ const from = attrs[OTEL_ATTR_FROM] ??
63
+ inferFromSpanName(span.name, 0);
64
+ const to = attrs[OTEL_ATTR_TO] ??
65
+ inferFromSpanName(span.name, 1);
66
+ if (!from || !to)
67
+ return undefined;
68
+ const payload = {};
69
+ for (const [key, value] of Object.entries(attrs)) {
70
+ if (key.startsWith("fiducia.payload.")) {
71
+ payload[key.slice("fiducia.payload.".length)] = value;
72
+ }
73
+ else if (key.startsWith("fiducia.payload.")) {
74
+ payload[key.slice("fiducia.payload.".length)] = value;
75
+ }
76
+ }
77
+ return {
78
+ contextId,
79
+ timestamp: span.timestamp ?? Date.now(),
80
+ from,
81
+ to,
82
+ op: attrs[OTEL_ATTR_OP] ?? "send",
83
+ payload,
84
+ };
85
+ }
86
+ function inferFromSpanName(name, index) {
87
+ if (!name)
88
+ return undefined;
89
+ const arrow = name.match(/^([^->]+)->([^->]+)$/);
90
+ if (!arrow)
91
+ return undefined;
92
+ return index === 0 ? arrow[1].trim() : arrow[2].trim();
93
+ }
94
+ export function importOtelJsonl(source) {
95
+ const events = [];
96
+ for (const line of source.split("\n")) {
97
+ const trimmed = line.trim();
98
+ if (!trimmed)
99
+ continue;
100
+ const row = JSON.parse(trimmed);
101
+ if ("from" in row && "to" in row && "contextId" in row) {
102
+ events.push(row);
103
+ continue;
104
+ }
105
+ const span = row;
106
+ const contextId = span.traceId ?? "unknown-trace";
107
+ const event = spanLikeToEvent(span, contextId);
108
+ if (event)
109
+ events.push(event);
110
+ }
111
+ return events;
112
+ }
113
+ export function importOtelJsonlFile(path) {
114
+ return importOtelJsonl(readFileSync(path, "utf8"));
115
+ }
116
+ export function createExpressOtelMiddleware(ct, options) {
117
+ const routes = options.routes ?? [];
118
+ return (req, res, next) => {
119
+ const ctx = contextFromOtelHeaders(req.headers, ct);
120
+ req.fiduciaContext =
121
+ ctx;
122
+ const match = routes.find((route) => route.method.toUpperCase() === (req.method ?? "GET").toUpperCase() &&
123
+ route.path === req.path);
124
+ if (match) {
125
+ const payload = {};
126
+ for (const key of match.payloadKeys ?? []) {
127
+ if (req.body && key in req.body)
128
+ payload[key] = req.body[key];
129
+ }
130
+ ct.emit(ctx, {
131
+ from: match.from,
132
+ to: match.to,
133
+ payload,
134
+ });
135
+ }
136
+ res.on("finish", () => {
137
+ if (res.statusCode && res.statusCode >= 400) {
138
+ ct.emit(ctx, {
139
+ from: options.serviceName,
140
+ to: "error",
141
+ payload: { status: res.statusCode, path: req.path ?? "/" },
142
+ });
143
+ }
144
+ });
145
+ next();
146
+ };
147
+ }
148
+ export function attachOtelContext(ctx, headers = {}) {
149
+ return { ...headers, ...otelHeadersFromContext(ctx) };
150
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,57 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { FiduciaTrace, parseWaltzFile } from "@fiducian/runtime";
4
+ import { contextFromOtelHeaders, importOtelJsonl, parseTraceParent, spanLikeToEvent, } from "./index.js";
5
+ const spec = parseWaltzFile(`
6
+ Omega(
7
+ send client->order{orderId} : true
8
+ ;
9
+ send order->kitchen{orderId, accepted} : accepted == true
10
+ ;
11
+ send order->billing{orderId, charged} : charged == true
12
+ )
13
+ `);
14
+ test("parseTraceParent extracts trace id", () => {
15
+ const parsed = parseTraceParent("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01");
16
+ assert.ok(parsed);
17
+ assert.equal(parsed.traceId, "4bf92f3577b34da6a3ce929d0e0e4736");
18
+ });
19
+ test("contextFromOtelHeaders uses traceparent as story id", () => {
20
+ const ct = new FiduciaTrace({ spec });
21
+ const ctx = contextFromOtelHeaders({
22
+ traceparent: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
23
+ }, ct);
24
+ assert.equal(ctx.contextId, "4bf92f3577b34da6a3ce929d0e0e4736");
25
+ });
26
+ test("importOtelJsonl converts span attributes to events", () => {
27
+ const jsonl = [
28
+ JSON.stringify({
29
+ traceId: "trace-1",
30
+ name: "order->kitchen",
31
+ timestamp: 1,
32
+ attributes: {
33
+ "fiducia.payload.orderId": "42",
34
+ "fiducia.payload.accepted": true,
35
+ },
36
+ }),
37
+ JSON.stringify({
38
+ traceId: "trace-1",
39
+ name: "order->billing",
40
+ timestamp: 2,
41
+ attributes: {
42
+ "fiducia.payload.orderId": "42",
43
+ "fiducia.payload.charged": true,
44
+ },
45
+ }),
46
+ ].join("\n");
47
+ const events = importOtelJsonl(jsonl);
48
+ assert.equal(events.length, 2);
49
+ assert.equal(events[0].from, "order");
50
+ assert.equal(events[0].to, "kitchen");
51
+ });
52
+ test("spanLikeToEvent maps arrow span names", () => {
53
+ const event = spanLikeToEvent({ name: "client->order", traceId: "t1", attributes: {} }, "t1");
54
+ assert.ok(event);
55
+ assert.equal(event.from, "client");
56
+ assert.equal(event.to, "order");
57
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,44 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { readFileSync } from "node:fs";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { importOtelJsonl } from "./index.js";
7
+ import { evaluateTrace, parseWaltzFile } from "@fiducian/runtime";
8
+ const root = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
9
+ test("payment-ordering bug fixture fails", () => {
10
+ const spec = parseWaltzFile(readFileSync(join(root, "specs", "payment-ordering.waltz"), "utf8"));
11
+ const events = importOtelJsonl(readFileSync(join(root, "specs", "fixtures", "payment-ordering-bug.otel.jsonl"), "utf8"));
12
+ const result = evaluateTrace(spec, events);
13
+ assert.equal(result.passed, false);
14
+ });
15
+ test("payment-ordering fix fixture passes", () => {
16
+ const spec = parseWaltzFile(readFileSync(join(root, "specs", "payment-ordering.waltz"), "utf8"));
17
+ const events = importOtelJsonl(readFileSync(join(root, "specs", "fixtures", "payment-ordering-fix.otel.jsonl"), "utf8"));
18
+ const result = evaluateTrace(spec, events);
19
+ assert.equal(result.passed, true);
20
+ });
21
+ test("refund-chain bug fixture fails", () => {
22
+ const spec = parseWaltzFile(readFileSync(join(root, "specs", "refund-chain.waltz"), "utf8"));
23
+ const events = importOtelJsonl(readFileSync(join(root, "specs", "fixtures", "refund-chain-bug.otel.jsonl"), "utf8"));
24
+ const result = evaluateTrace(spec, events);
25
+ assert.equal(result.passed, false);
26
+ });
27
+ test("refund-chain fix fixture passes", () => {
28
+ const spec = parseWaltzFile(readFileSync(join(root, "specs", "refund-chain.waltz"), "utf8"));
29
+ const events = importOtelJsonl(readFileSync(join(root, "specs", "fixtures", "refund-chain-fix.otel.jsonl"), "utf8"));
30
+ const result = evaluateTrace(spec, events);
31
+ assert.equal(result.passed, true);
32
+ });
33
+ test("agent-tool-chain bug fixture fails", () => {
34
+ const spec = parseWaltzFile(readFileSync(join(root, "specs", "agent-tool-chain.waltz"), "utf8"));
35
+ const events = importOtelJsonl(readFileSync(join(root, "specs", "fixtures", "agent-tool-chain-bug.otel.jsonl"), "utf8"));
36
+ const result = evaluateTrace(spec, events);
37
+ assert.equal(result.passed, false);
38
+ });
39
+ test("agent-tool-chain fix fixture passes", () => {
40
+ const spec = parseWaltzFile(readFileSync(join(root, "specs", "agent-tool-chain.waltz"), "utf8"));
41
+ const events = importOtelJsonl(readFileSync(join(root, "specs", "fixtures", "agent-tool-chain-fix.otel.jsonl"), "utf8"));
42
+ const result = evaluateTrace(spec, events);
43
+ assert.equal(result.passed, true);
44
+ });
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@fiducian/otel",
3
+ "version": "0.4.0",
4
+ "description": "OpenTelemetry bridge for FiduciaTrace traceparent and JSONL import",
5
+ "license": "AGPL-3.0-or-later",
6
+ "keywords": ["fiducia", "fiducia", "opentelemetry", "otel", "tracing"],
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/thesnmc/Fiducian.git",
10
+ "directory": "trace/packages/otel"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "files": ["dist"],
16
+ "type": "module",
17
+ "main": "./dist/index.js",
18
+ "types": "./dist/index.d.ts",
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "import": "./dist/index.js"
23
+ }
24
+ },
25
+ "dependencies": {
26
+ "@fiducian/conductor": "0.4.0",
27
+ "@fiducian/runtime": "0.4.0"
28
+ },
29
+ "scripts": {
30
+ "build": "tsc -p tsconfig.json",
31
+ "test": "node --test dist/**/*.test.js",
32
+ "prepublishOnly": "npm run build"
33
+ }
34
+ }