@nagi-js/otel 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.js +203 -0
- package/dist/index.js.map +1 -0
- package/package.json +65 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lymo, Inc.
|
|
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/dist/index.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { FlowHooks, StepCtx } from '@nagi-js/core';
|
|
2
|
+
import { Span, Tracer, Attributes } from '@opentelemetry/api';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Fan out a single `FlowHooks` call site to multiple subscribers, awaiting each in
|
|
6
|
+
* declaration order. A throw from one subscriber is logged via `console.error` and
|
|
7
|
+
* does NOT prevent later subscribers (or the workflow runtime) from running.
|
|
8
|
+
*
|
|
9
|
+
* Nagi's runtime accepts a single `hooks` value — this is how users wire
|
|
10
|
+
* `otelHooks()` alongside their own logger / metrics hooks.
|
|
11
|
+
*/
|
|
12
|
+
declare function composeHooks(...hooks: readonly FlowHooks[]): FlowHooks;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Returns the OTel `Span` tracking the current step attempt, or `undefined` when
|
|
16
|
+
* no `otelHooks()` is wired or the step is not in-flight (e.g. running under a
|
|
17
|
+
* different `nagi()` instance, or after the step has completed).
|
|
18
|
+
*
|
|
19
|
+
* Useful inside user handlers to add custom attributes or open child spans:
|
|
20
|
+
*
|
|
21
|
+
* ```ts
|
|
22
|
+
* b.task({
|
|
23
|
+
* run: async (ctx) => {
|
|
24
|
+
* const span = getStepSpan(ctx);
|
|
25
|
+
* span?.setAttribute("my.custom.attr", 42);
|
|
26
|
+
* ...
|
|
27
|
+
* },
|
|
28
|
+
* });
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* Note: this span is NOT installed as the active context — `trace.getActiveSpan()`
|
|
32
|
+
* inside the handler returns the caller's context, not this span.
|
|
33
|
+
*/
|
|
34
|
+
declare function getStepSpan(ctx: Pick<StepCtx<unknown>, "runId" | "stepId" | "attempt">): Span | undefined;
|
|
35
|
+
|
|
36
|
+
interface OtelHooksOpts {
|
|
37
|
+
/**
|
|
38
|
+
* Custom `Tracer`. Defaults to `trace.getTracer("@nagi-js/otel", "0.0.0")` —
|
|
39
|
+
* a no-op when no SDK is registered (per the OTel API contract).
|
|
40
|
+
*/
|
|
41
|
+
readonly tracer?: Tracer;
|
|
42
|
+
/** Override `"flow"`/`"step"` span-name prefixes (low cardinality; do NOT use IDs). */
|
|
43
|
+
readonly spanNamePrefix?: {
|
|
44
|
+
readonly flow?: string;
|
|
45
|
+
readonly step?: string;
|
|
46
|
+
};
|
|
47
|
+
/** Extra attributes stamped on every span (e.g. `deployment.environment`). */
|
|
48
|
+
readonly defaultAttributes?: Attributes;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* `FlowHooks` adapter that maps Nagi lifecycle events to OpenTelemetry spans.
|
|
52
|
+
*
|
|
53
|
+
* Hierarchy: one `flow {flowId}` span per run; per-step-attempt sibling `step
|
|
54
|
+
* {stepId}` spans underneath. All spans are `INTERNAL` kind. Errors call
|
|
55
|
+
* `span.recordException` + `setStatus(ERROR)`; success leaves status `UNSET`
|
|
56
|
+
* per the Tracing API spec recommendation for instrumentation libraries.
|
|
57
|
+
*
|
|
58
|
+
* Adapter errors are swallowed: a misconfigured tracer must never crash a
|
|
59
|
+
* workflow. Throws are logged via `console.error`.
|
|
60
|
+
*/
|
|
61
|
+
declare function otelHooks(opts?: OtelHooksOpts): FlowHooks;
|
|
62
|
+
|
|
63
|
+
export { type OtelHooksOpts, composeHooks, getStepSpan, otelHooks };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { trace, SpanKind, context, SpanStatusCode } from '@opentelemetry/api';
|
|
2
|
+
|
|
3
|
+
// src/compose.ts
|
|
4
|
+
function composeHooks(...hooks) {
|
|
5
|
+
const result = {};
|
|
6
|
+
const fs = pick(hooks, "onFlowStart");
|
|
7
|
+
if (fs.length > 0) result.onFlowStart = fanout("onFlowStart", fs);
|
|
8
|
+
const fc = pick(hooks, "onFlowComplete");
|
|
9
|
+
if (fc.length > 0) result.onFlowComplete = fanout("onFlowComplete", fc);
|
|
10
|
+
const fe = pick(hooks, "onFlowError");
|
|
11
|
+
if (fe.length > 0) result.onFlowError = fanout("onFlowError", fe);
|
|
12
|
+
const ss = pick(hooks, "onStepStart");
|
|
13
|
+
if (ss.length > 0) result.onStepStart = fanout("onStepStart", ss);
|
|
14
|
+
const sc = pick(hooks, "onStepComplete");
|
|
15
|
+
if (sc.length > 0) result.onStepComplete = fanout("onStepComplete", sc);
|
|
16
|
+
const se = pick(hooks, "onStepError");
|
|
17
|
+
if (se.length > 0) result.onStepError = fanout("onStepError", se);
|
|
18
|
+
const sr = pick(hooks, "onStepRetry");
|
|
19
|
+
if (sr.length > 0) result.onStepRetry = fanout("onStepRetry", sr);
|
|
20
|
+
const sigS = pick(hooks, "onSignalSent");
|
|
21
|
+
if (sigS.length > 0) result.onSignalSent = fanout("onSignalSent", sigS);
|
|
22
|
+
const sigR = pick(hooks, "onSignalReceived");
|
|
23
|
+
if (sigR.length > 0)
|
|
24
|
+
result.onSignalReceived = fanout("onSignalReceived", sigR);
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
function pick(hooks, name) {
|
|
28
|
+
const out = [];
|
|
29
|
+
for (const h of hooks) {
|
|
30
|
+
const fn = h[name];
|
|
31
|
+
if (fn !== void 0) out.push(fn);
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
function fanout(name, subs) {
|
|
36
|
+
return async (event) => {
|
|
37
|
+
for (const fn of subs) {
|
|
38
|
+
try {
|
|
39
|
+
await fn(event);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
console.error(
|
|
42
|
+
`[@nagi-js/otel] composeHooks: ${name} subscriber threw`,
|
|
43
|
+
err
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// src/context.ts
|
|
51
|
+
var SEP = "";
|
|
52
|
+
function stepKey(runId, stepId, attempt) {
|
|
53
|
+
return `${runId}${SEP}${stepId}${SEP}${attempt}`;
|
|
54
|
+
}
|
|
55
|
+
var stepSpanRegistry = /* @__PURE__ */ new Map();
|
|
56
|
+
function getStepSpan(ctx) {
|
|
57
|
+
return stepSpanRegistry.get(stepKey(ctx.runId, ctx.stepId, ctx.attempt));
|
|
58
|
+
}
|
|
59
|
+
var TRACER_NAME = "@nagi-js/otel";
|
|
60
|
+
var TRACER_VERSION = "0.0.0";
|
|
61
|
+
var DEFAULT_FLOW_PREFIX = "flow";
|
|
62
|
+
var DEFAULT_STEP_PREFIX = "step";
|
|
63
|
+
function otelHooks(opts = {}) {
|
|
64
|
+
const tracer = opts.tracer ?? trace.getTracer(TRACER_NAME, TRACER_VERSION);
|
|
65
|
+
const flowPrefix = opts.spanNamePrefix?.flow ?? DEFAULT_FLOW_PREFIX;
|
|
66
|
+
const stepPrefix = opts.spanNamePrefix?.step ?? DEFAULT_STEP_PREFIX;
|
|
67
|
+
const baseAttrs = opts.defaultAttributes ?? {};
|
|
68
|
+
const flowCtxs = /* @__PURE__ */ new Map();
|
|
69
|
+
const stepStartTimes = /* @__PURE__ */ new Map();
|
|
70
|
+
function withGuard(name, fn) {
|
|
71
|
+
return (event) => {
|
|
72
|
+
try {
|
|
73
|
+
fn(event);
|
|
74
|
+
} catch (err) {
|
|
75
|
+
console.error(`[@nagi-js/otel] ${name} hook failed`, err);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function flowAttrs(event) {
|
|
80
|
+
return {
|
|
81
|
+
...baseAttrs,
|
|
82
|
+
"nagi.flow.id": event.flowId,
|
|
83
|
+
"nagi.run.id": event.runId
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function stepAttrs(event) {
|
|
87
|
+
return {
|
|
88
|
+
...flowAttrs(event),
|
|
89
|
+
"nagi.step.id": event.stepId,
|
|
90
|
+
"nagi.step.attempt": event.attempt,
|
|
91
|
+
"nagi.step.kind": event.kind
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function startStepSpan(event) {
|
|
95
|
+
const parentCtx = flowCtxs.get(event.runId) ?? context.active();
|
|
96
|
+
return tracer.startSpan(
|
|
97
|
+
`${stepPrefix} ${event.stepId}`,
|
|
98
|
+
{
|
|
99
|
+
kind: SpanKind.INTERNAL,
|
|
100
|
+
startTime: event.at,
|
|
101
|
+
attributes: stepAttrs(event)
|
|
102
|
+
},
|
|
103
|
+
parentCtx
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
function endStepSpanOk(event, span) {
|
|
107
|
+
const k = stepKey(event.runId, event.stepId, event.attempt);
|
|
108
|
+
const startedAt = stepStartTimes.get(k);
|
|
109
|
+
const durationMs = event.kind === "match" && startedAt ? event.at.getTime() - startedAt.getTime() : event.durationMs;
|
|
110
|
+
span.setAttribute("nagi.step.duration_ms", durationMs);
|
|
111
|
+
span.end(event.at);
|
|
112
|
+
}
|
|
113
|
+
function endStepSpanErr(span, error, endAt) {
|
|
114
|
+
span.recordException(toError(error));
|
|
115
|
+
span.setAttribute("error.type", error.name);
|
|
116
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
|
|
117
|
+
span.end(endAt);
|
|
118
|
+
}
|
|
119
|
+
function consumeStepSpan(runId, stepId, attempt) {
|
|
120
|
+
const k = stepKey(runId, stepId, attempt);
|
|
121
|
+
const span = stepSpanRegistry.get(k);
|
|
122
|
+
stepSpanRegistry.delete(k);
|
|
123
|
+
stepStartTimes.delete(k);
|
|
124
|
+
return span;
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
onFlowStart: withGuard("onFlowStart", (event) => {
|
|
128
|
+
const span = tracer.startSpan(`${flowPrefix} ${event.flowId}`, {
|
|
129
|
+
kind: SpanKind.INTERNAL,
|
|
130
|
+
startTime: event.at,
|
|
131
|
+
attributes: flowAttrs(event)
|
|
132
|
+
});
|
|
133
|
+
flowCtxs.set(event.runId, trace.setSpan(context.active(), span));
|
|
134
|
+
}),
|
|
135
|
+
onFlowComplete: withGuard("onFlowComplete", (event) => {
|
|
136
|
+
const ctx = flowCtxs.get(event.runId);
|
|
137
|
+
flowCtxs.delete(event.runId);
|
|
138
|
+
const span = ctx ? trace.getSpan(ctx) : void 0;
|
|
139
|
+
span?.end(event.at);
|
|
140
|
+
}),
|
|
141
|
+
onFlowError: withGuard("onFlowError", (event) => {
|
|
142
|
+
const ctx = flowCtxs.get(event.runId);
|
|
143
|
+
flowCtxs.delete(event.runId);
|
|
144
|
+
const span = ctx ? trace.getSpan(ctx) : void 0;
|
|
145
|
+
if (span) endStepSpanErr(span, event.error, event.at);
|
|
146
|
+
}),
|
|
147
|
+
onStepStart: withGuard("onStepStart", (event) => {
|
|
148
|
+
const span = startStepSpan(event);
|
|
149
|
+
const k = stepKey(event.runId, event.stepId, event.attempt);
|
|
150
|
+
stepSpanRegistry.set(k, span);
|
|
151
|
+
stepStartTimes.set(k, event.at);
|
|
152
|
+
}),
|
|
153
|
+
onStepComplete: withGuard("onStepComplete", (event) => {
|
|
154
|
+
const span = consumeStepSpan(event.runId, event.stepId, event.attempt);
|
|
155
|
+
if (span) endStepSpanOk(event, span);
|
|
156
|
+
}),
|
|
157
|
+
onStepError: withGuard("onStepError", (event) => {
|
|
158
|
+
const span = consumeStepSpan(event.runId, event.stepId, event.attempt);
|
|
159
|
+
if (span) endStepSpanErr(span, event.error, event.at);
|
|
160
|
+
}),
|
|
161
|
+
onStepRetry: withGuard("onStepRetry", (event) => {
|
|
162
|
+
const span = consumeStepSpan(event.runId, event.stepId, event.attempt);
|
|
163
|
+
if (span) endStepSpanErr(span, event.error, event.at);
|
|
164
|
+
const flowCtx = flowCtxs.get(event.runId);
|
|
165
|
+
const flowSpan = flowCtx ? trace.getSpan(flowCtx) : void 0;
|
|
166
|
+
flowSpan?.addEvent(
|
|
167
|
+
"nagi.retry.scheduled",
|
|
168
|
+
{
|
|
169
|
+
"nagi.step.id": event.stepId,
|
|
170
|
+
"nagi.step.attempt": event.attempt,
|
|
171
|
+
"nagi.next_attempt_at": event.nextAttemptAt.toISOString()
|
|
172
|
+
},
|
|
173
|
+
event.at
|
|
174
|
+
);
|
|
175
|
+
}),
|
|
176
|
+
// onSignalSent is declared in core/types.ts but never fired by the runtime;
|
|
177
|
+
// intentionally omitted until core wires a sender path.
|
|
178
|
+
onSignalReceived: withGuard(
|
|
179
|
+
"onSignalReceived",
|
|
180
|
+
(event) => {
|
|
181
|
+
const span = consumeStepSpan(event.runId, event.stepId, event.attempt);
|
|
182
|
+
if (!span) return;
|
|
183
|
+
span.addEvent(
|
|
184
|
+
"nagi.signal.received",
|
|
185
|
+
{ "nagi.signal.payload_present": event.payload !== null },
|
|
186
|
+
event.at
|
|
187
|
+
);
|
|
188
|
+
span.setAttribute("nagi.step.duration_ms", 0);
|
|
189
|
+
span.end(event.at);
|
|
190
|
+
}
|
|
191
|
+
)
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function toError(err) {
|
|
195
|
+
const e = new Error(err.message);
|
|
196
|
+
e.name = err.name;
|
|
197
|
+
if (err.stack !== void 0) e.stack = err.stack;
|
|
198
|
+
return e;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export { composeHooks, getStepSpan, otelHooks };
|
|
202
|
+
//# sourceMappingURL=index.js.map
|
|
203
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/compose.ts","../src/context.ts","../src/hooks.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type {\n FlowCompleteEvent,\n FlowErrorEvent,\n FlowHooks,\n FlowStartEvent,\n SignalReceivedEvent,\n SignalSentEvent,\n StepCompleteEvent,\n StepErrorEvent,\n StepRetryEvent,\n StepStartEvent,\n} from \"@nagi-js/core\";\n\n/**\n * Fan out a single `FlowHooks` call site to multiple subscribers, awaiting each in\n * declaration order. A throw from one subscriber is logged via `console.error` and\n * does NOT prevent later subscribers (or the workflow runtime) from running.\n *\n * Nagi's runtime accepts a single `hooks` value — this is how users wire\n * `otelHooks()` alongside their own logger / metrics hooks.\n */\nexport function composeHooks(...hooks: readonly FlowHooks[]): FlowHooks {\n // Build a local FlowHooks-shaped object. Optional fields are only set when at\n // least one subscriber provides them — required by `exactOptionalPropertyTypes`.\n const result: {\n onFlowStart?: (event: FlowStartEvent) => Promise<void>;\n onFlowComplete?: (event: FlowCompleteEvent) => Promise<void>;\n onFlowError?: (event: FlowErrorEvent) => Promise<void>;\n onStepStart?: (event: StepStartEvent) => Promise<void>;\n onStepComplete?: (event: StepCompleteEvent) => Promise<void>;\n onStepError?: (event: StepErrorEvent) => Promise<void>;\n onStepRetry?: (event: StepRetryEvent) => Promise<void>;\n onSignalSent?: (event: SignalSentEvent) => Promise<void>;\n onSignalReceived?: (event: SignalReceivedEvent) => Promise<void>;\n } = {};\n\n const fs = pick(hooks, \"onFlowStart\");\n if (fs.length > 0) result.onFlowStart = fanout(\"onFlowStart\", fs);\n\n const fc = pick(hooks, \"onFlowComplete\");\n if (fc.length > 0) result.onFlowComplete = fanout(\"onFlowComplete\", fc);\n\n const fe = pick(hooks, \"onFlowError\");\n if (fe.length > 0) result.onFlowError = fanout(\"onFlowError\", fe);\n\n const ss = pick(hooks, \"onStepStart\");\n if (ss.length > 0) result.onStepStart = fanout(\"onStepStart\", ss);\n\n const sc = pick(hooks, \"onStepComplete\");\n if (sc.length > 0) result.onStepComplete = fanout(\"onStepComplete\", sc);\n\n const se = pick(hooks, \"onStepError\");\n if (se.length > 0) result.onStepError = fanout(\"onStepError\", se);\n\n const sr = pick(hooks, \"onStepRetry\");\n if (sr.length > 0) result.onStepRetry = fanout(\"onStepRetry\", sr);\n\n const sigS = pick(hooks, \"onSignalSent\");\n if (sigS.length > 0) result.onSignalSent = fanout(\"onSignalSent\", sigS);\n\n const sigR = pick(hooks, \"onSignalReceived\");\n if (sigR.length > 0)\n result.onSignalReceived = fanout(\"onSignalReceived\", sigR);\n\n return result;\n}\n\nfunction pick<K extends keyof FlowHooks>(\n hooks: readonly FlowHooks[],\n name: K,\n): Array<NonNullable<FlowHooks[K]>> {\n const out: Array<NonNullable<FlowHooks[K]>> = [];\n for (const h of hooks) {\n const fn = h[name];\n if (fn !== undefined) out.push(fn);\n }\n return out;\n}\n\nfunction fanout<E>(\n name: string,\n subs: ReadonlyArray<(event: E) => void | Promise<void>>,\n): (event: E) => Promise<void> {\n return async (event: E) => {\n for (const fn of subs) {\n try {\n await fn(event);\n } catch (err) {\n console.error(\n `[@nagi-js/otel] composeHooks: ${name} subscriber threw`,\n err,\n );\n }\n }\n };\n}\n","import type { AttemptNumber, RunId, StepCtx, StepId } from \"@nagi-js/core\";\nimport type { Span } from \"@opentelemetry/api\";\n\n/**\n * ASCII unit separator. Composite-key delimiter for `(runId, stepId, attempt)`.\n * Unambiguous because `runId` is `run-<uuid>` and `stepId` is a dotted identifier;\n * neither contains control chars.\n */\nconst SEP = \"\\x1f\";\n\nexport function stepKey(\n runId: RunId,\n stepId: StepId,\n attempt: AttemptNumber,\n): string {\n return `${runId}${SEP}${stepId}${SEP}${attempt}`;\n}\n\n/**\n * Process-global registry of in-flight step spans, keyed by `(runId, stepId, attempt)`.\n * Populated by `otelHooks()` on `onStepStart`; cleared on completion / error / retry.\n *\n * Single-process, single-runtime: Nagi already assumes a runtime per process, so a\n * module-level Map is the right granularity. Bounded by steps-in-flight, which the\n * runtime itself bounds.\n */\nexport const stepSpanRegistry = new Map<string, Span>();\n\n/**\n * Returns the OTel `Span` tracking the current step attempt, or `undefined` when\n * no `otelHooks()` is wired or the step is not in-flight (e.g. running under a\n * different `nagi()` instance, or after the step has completed).\n *\n * Useful inside user handlers to add custom attributes or open child spans:\n *\n * ```ts\n * b.task({\n * run: async (ctx) => {\n * const span = getStepSpan(ctx);\n * span?.setAttribute(\"my.custom.attr\", 42);\n * ...\n * },\n * });\n * ```\n *\n * Note: this span is NOT installed as the active context — `trace.getActiveSpan()`\n * inside the handler returns the caller's context, not this span.\n */\nexport function getStepSpan(\n ctx: Pick<StepCtx<unknown>, \"runId\" | \"stepId\" | \"attempt\">,\n): Span | undefined {\n return stepSpanRegistry.get(stepKey(ctx.runId, ctx.stepId, ctx.attempt));\n}\n","import type {\n AttemptNumber,\n FlowCompleteEvent,\n FlowErrorEvent,\n FlowHooks,\n FlowStartEvent,\n RunId,\n SerializedError,\n SignalReceivedEvent,\n StepCompleteEvent,\n StepErrorEvent,\n StepEvent,\n StepId,\n StepRetryEvent,\n StepStartEvent,\n} from \"@nagi-js/core\";\nimport {\n type Attributes,\n type Context,\n context,\n type Span,\n SpanKind,\n SpanStatusCode,\n type Tracer,\n trace,\n} from \"@opentelemetry/api\";\nimport { stepKey, stepSpanRegistry } from \"./context\";\n\nconst TRACER_NAME = \"@nagi-js/otel\";\nconst TRACER_VERSION = \"0.0.0\";\n\nconst DEFAULT_FLOW_PREFIX = \"flow\";\nconst DEFAULT_STEP_PREFIX = \"step\";\n\nexport interface OtelHooksOpts {\n /**\n * Custom `Tracer`. Defaults to `trace.getTracer(\"@nagi-js/otel\", \"0.0.0\")` —\n * a no-op when no SDK is registered (per the OTel API contract).\n */\n readonly tracer?: Tracer;\n /** Override `\"flow\"`/`\"step\"` span-name prefixes (low cardinality; do NOT use IDs). */\n readonly spanNamePrefix?: { readonly flow?: string; readonly step?: string };\n /** Extra attributes stamped on every span (e.g. `deployment.environment`). */\n readonly defaultAttributes?: Attributes;\n}\n\n/**\n * `FlowHooks` adapter that maps Nagi lifecycle events to OpenTelemetry spans.\n *\n * Hierarchy: one `flow {flowId}` span per run; per-step-attempt sibling `step\n * {stepId}` spans underneath. All spans are `INTERNAL` kind. Errors call\n * `span.recordException` + `setStatus(ERROR)`; success leaves status `UNSET`\n * per the Tracing API spec recommendation for instrumentation libraries.\n *\n * Adapter errors are swallowed: a misconfigured tracer must never crash a\n * workflow. Throws are logged via `console.error`.\n */\nexport function otelHooks(opts: OtelHooksOpts = {}): FlowHooks {\n const tracer: Tracer =\n opts.tracer ?? trace.getTracer(TRACER_NAME, TRACER_VERSION);\n const flowPrefix = opts.spanNamePrefix?.flow ?? DEFAULT_FLOW_PREFIX;\n const stepPrefix = opts.spanNamePrefix?.step ?? DEFAULT_STEP_PREFIX;\n const baseAttrs: Attributes = opts.defaultAttributes ?? {};\n\n // Per-run flow-span context. Used as the parent for step spans.\n const flowCtxs = new Map<RunId, Context>();\n // Per-step match-duration tracking: dispatch.ts hard-codes durationMs=0 on\n // match completion, so we compute it ourselves from the stashed start time.\n const stepStartTimes = new Map<string, Date>();\n\n function withGuard<E>(\n name: string,\n fn: (event: E) => void,\n ): (event: E) => void {\n return (event: E) => {\n try {\n fn(event);\n } catch (err) {\n console.error(`[@nagi-js/otel] ${name} hook failed`, err);\n }\n };\n }\n\n function flowAttrs(event: {\n readonly runId: RunId;\n readonly flowId: string;\n }): Attributes {\n return {\n ...baseAttrs,\n \"nagi.flow.id\": event.flowId,\n \"nagi.run.id\": event.runId,\n };\n }\n\n function stepAttrs(event: StepEvent): Attributes {\n return {\n ...flowAttrs(event),\n \"nagi.step.id\": event.stepId,\n \"nagi.step.attempt\": event.attempt,\n \"nagi.step.kind\": event.kind,\n };\n }\n\n function startStepSpan(event: StepStartEvent): Span {\n const parentCtx = flowCtxs.get(event.runId) ?? context.active();\n return tracer.startSpan(\n `${stepPrefix} ${event.stepId}`,\n {\n kind: SpanKind.INTERNAL,\n startTime: event.at,\n attributes: stepAttrs(event),\n },\n parentCtx,\n );\n }\n\n function endStepSpanOk(event: StepCompleteEvent, span: Span): void {\n const k = stepKey(event.runId, event.stepId, event.attempt);\n const startedAt = stepStartTimes.get(k);\n // Match steps get a hard-coded durationMs=0 from the runtime; recover the\n // real value from the stashed start time when available.\n const durationMs =\n event.kind === \"match\" && startedAt\n ? event.at.getTime() - startedAt.getTime()\n : event.durationMs;\n span.setAttribute(\"nagi.step.duration_ms\", durationMs);\n span.end(event.at);\n }\n\n function endStepSpanErr(\n span: Span,\n error: SerializedError,\n endAt: Date,\n ): void {\n span.recordException(toError(error));\n span.setAttribute(\"error.type\", error.name);\n span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });\n span.end(endAt);\n }\n\n function consumeStepSpan(\n runId: RunId,\n stepId: StepId,\n attempt: AttemptNumber,\n ): Span | undefined {\n const k = stepKey(runId, stepId, attempt);\n const span = stepSpanRegistry.get(k);\n stepSpanRegistry.delete(k);\n stepStartTimes.delete(k);\n return span;\n }\n\n return {\n onFlowStart: withGuard<FlowStartEvent>(\"onFlowStart\", (event) => {\n const span = tracer.startSpan(`${flowPrefix} ${event.flowId}`, {\n kind: SpanKind.INTERNAL,\n startTime: event.at,\n attributes: flowAttrs(event),\n });\n flowCtxs.set(event.runId, trace.setSpan(context.active(), span));\n }),\n\n onFlowComplete: withGuard<FlowCompleteEvent>(\"onFlowComplete\", (event) => {\n const ctx = flowCtxs.get(event.runId);\n flowCtxs.delete(event.runId);\n const span = ctx ? trace.getSpan(ctx) : undefined;\n span?.end(event.at);\n }),\n\n onFlowError: withGuard<FlowErrorEvent>(\"onFlowError\", (event) => {\n const ctx = flowCtxs.get(event.runId);\n flowCtxs.delete(event.runId);\n const span = ctx ? trace.getSpan(ctx) : undefined;\n if (span) endStepSpanErr(span, event.error, event.at);\n }),\n\n onStepStart: withGuard<StepStartEvent>(\"onStepStart\", (event) => {\n const span = startStepSpan(event);\n const k = stepKey(event.runId, event.stepId, event.attempt);\n stepSpanRegistry.set(k, span);\n stepStartTimes.set(k, event.at);\n }),\n\n onStepComplete: withGuard<StepCompleteEvent>(\"onStepComplete\", (event) => {\n const span = consumeStepSpan(event.runId, event.stepId, event.attempt);\n if (span) endStepSpanOk(event, span);\n }),\n\n onStepError: withGuard<StepErrorEvent>(\"onStepError\", (event) => {\n const span = consumeStepSpan(event.runId, event.stepId, event.attempt);\n if (span) endStepSpanErr(span, event.error, event.at);\n }),\n\n onStepRetry: withGuard<StepRetryEvent>(\"onStepRetry\", (event) => {\n // End the failed attempt's span with ERROR status. The next onStepStart\n // for attempt+1 opens a fresh sibling span under the same flow span.\n const span = consumeStepSpan(event.runId, event.stepId, event.attempt);\n if (span) endStepSpanErr(span, event.error, event.at);\n\n // Surface the retry on the flow span so users can see retry latency in\n // the run-level trace without inspecting individual step attempts.\n const flowCtx = flowCtxs.get(event.runId);\n const flowSpan = flowCtx ? trace.getSpan(flowCtx) : undefined;\n flowSpan?.addEvent(\n \"nagi.retry.scheduled\",\n {\n \"nagi.step.id\": event.stepId,\n \"nagi.step.attempt\": event.attempt,\n \"nagi.next_attempt_at\": event.nextAttemptAt.toISOString(),\n },\n event.at,\n );\n }),\n\n // onSignalSent is declared in core/types.ts but never fired by the runtime;\n // intentionally omitted until core wires a sender path.\n onSignalReceived: withGuard<SignalReceivedEvent>(\n \"onSignalReceived\",\n (event) => {\n const span = consumeStepSpan(event.runId, event.stepId, event.attempt);\n if (!span) return;\n span.addEvent(\n \"nagi.signal.received\",\n { \"nagi.signal.payload_present\": event.payload !== null },\n event.at,\n );\n span.setAttribute(\"nagi.step.duration_ms\", 0);\n span.end(event.at);\n },\n ),\n };\n}\n\nfunction toError(err: SerializedError): Error {\n // `recordException` accepts `{ name, message, stack }`. We rebuild a\n // shape-compatible object rather than a real Error to preserve the original\n // name/stack exactly.\n const e = new Error(err.message);\n e.name = err.name;\n if (err.stack !== undefined) e.stack = err.stack;\n return e;\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nagi-js/otel",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OpenTelemetry hooks adapter for nagi.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"keywords": [
|
|
9
|
+
"nagi",
|
|
10
|
+
"workflow",
|
|
11
|
+
"opentelemetry",
|
|
12
|
+
"otel",
|
|
13
|
+
"observability",
|
|
14
|
+
"tracing",
|
|
15
|
+
"adapter"
|
|
16
|
+
],
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=22"
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public",
|
|
22
|
+
"registry": "https://registry.npmjs.org/",
|
|
23
|
+
"provenance": false
|
|
24
|
+
},
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"import": "./dist/index.js",
|
|
29
|
+
"default": "./dist/index.js"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"README.md",
|
|
35
|
+
"LICENSE"
|
|
36
|
+
],
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@nagi-js/core": "0.1.0"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"@opentelemetry/api": "^1.9.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@opentelemetry/api": "^1.9.0",
|
|
45
|
+
"@opentelemetry/resources": "^1.30.0",
|
|
46
|
+
"@opentelemetry/sdk-trace-base": "^1.30.0"
|
|
47
|
+
},
|
|
48
|
+
"repository": {
|
|
49
|
+
"type": "git",
|
|
50
|
+
"url": "git+https://github.com/lymo-inc/nagi.git",
|
|
51
|
+
"directory": "packages/otel"
|
|
52
|
+
},
|
|
53
|
+
"homepage": "https://github.com/lymo-inc/nagi#readme",
|
|
54
|
+
"bugs": {
|
|
55
|
+
"url": "https://github.com/lymo-inc/nagi/issues"
|
|
56
|
+
},
|
|
57
|
+
"scripts": {
|
|
58
|
+
"build": "tsup",
|
|
59
|
+
"dev": "tsup --watch",
|
|
60
|
+
"test": "vitest run",
|
|
61
|
+
"test:watch": "vitest",
|
|
62
|
+
"test:types": "vitest run --typecheck",
|
|
63
|
+
"typecheck": "tsc --noEmit"
|
|
64
|
+
}
|
|
65
|
+
}
|