@drej/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.
Files changed (3) hide show
  1. package/package.json +17 -0
  2. package/src/index.ts +117 -0
  3. package/tsconfig.json +13 -0
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@drej/otel",
3
+ "version": "0.1.0",
4
+ "publishConfig": { "access": "public" },
5
+ "main": "./src/index.ts",
6
+ "peerDependencies": {
7
+ "@opentelemetry/api": "^1.0.0"
8
+ },
9
+ "dependencies": {
10
+ "@drej/core": "workspace:*"
11
+ },
12
+ "devDependencies": {
13
+ "@opentelemetry/api": "^1.9.0",
14
+ "bun-types": "latest",
15
+ "typescript": "latest"
16
+ }
17
+ }
package/src/index.ts ADDED
@@ -0,0 +1,117 @@
1
+ import type { Tracer, Span, SpanStatusCode } from "@opentelemetry/api";
2
+ import { SpanStatusCode as StatusCode, context, trace } from "@opentelemetry/api";
3
+ import type {
4
+ WorkflowHooks,
5
+ StepHookInfo,
6
+ StepCompleteHookInfo,
7
+ StepFailedHookInfo,
8
+ WorkflowCompleteHookInfo,
9
+ WorkflowFailedHookInfo,
10
+ WorkflowHookInfo,
11
+ } from "@drej/core";
12
+
13
+ export interface OtelHooksOptions {
14
+ /** Include sandbox ID as a span attribute when present in step output. Default: true. */
15
+ recordSandboxId?: boolean;
16
+ /** Include exit code as a span attribute on exec_command steps. Default: true. */
17
+ recordExitCode?: boolean;
18
+ }
19
+
20
+ /**
21
+ * Returns `WorkflowHooks` that emit OpenTelemetry traces for every workflow run.
22
+ *
23
+ * Pass the result to `client.run(wf, { hooks: otelHooks(tracer) })`.
24
+ *
25
+ * Span structure:
26
+ * ```
27
+ * workflow.run ← root span, name = workflow name
28
+ * workflow.step ← child per step, name = step type
29
+ * workflow.step
30
+ * ```
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * import { otelHooks } from "@drej/otel";
35
+ * import { trace } from "@opentelemetry/api";
36
+ *
37
+ * const tracer = trace.getTracer("my-app");
38
+ * const run = await client.run(wf, { hooks: otelHooks(tracer) });
39
+ * ```
40
+ */
41
+ export function otelHooks(tracer: Tracer, opts: OtelHooksOptions = {}): WorkflowHooks {
42
+ const { recordSandboxId = true, recordExitCode = true } = opts;
43
+
44
+ let rootSpan: Span | undefined;
45
+ let rootCtx: ReturnType<typeof context.active> | undefined;
46
+ const stepSpans = new Map<number, Span>();
47
+
48
+ return {
49
+ onWorkflowStart({ workflowName, runId }: WorkflowHookInfo) {
50
+ rootCtx = context.active();
51
+ rootSpan = tracer.startSpan(
52
+ "workflow.run",
53
+ {
54
+ attributes: {
55
+ "drej.workflow.name": workflowName,
56
+ "drej.run.id": runId,
57
+ },
58
+ },
59
+ rootCtx,
60
+ );
61
+ },
62
+
63
+ onStepStart({ stepIndex, stepId }: StepHookInfo) {
64
+ if (!rootSpan || !rootCtx) return;
65
+ const spanCtx = trace.setSpan(rootCtx, rootSpan);
66
+ const span = tracer.startSpan(
67
+ "workflow.step",
68
+ {
69
+ attributes: {
70
+ "drej.step.index": stepIndex,
71
+ "drej.step.type": stepId,
72
+ },
73
+ },
74
+ spanCtx,
75
+ );
76
+ stepSpans.set(stepIndex, span);
77
+ },
78
+
79
+ onStepComplete({ stepIndex, output }: StepCompleteHookInfo) {
80
+ const span = stepSpans.get(stepIndex);
81
+ if (!span) return;
82
+ if (recordSandboxId) {
83
+ const sandboxId = (output as Record<string, unknown>)?.sandboxId;
84
+ if (typeof sandboxId === "string") span.setAttribute("drej.sandbox.id", sandboxId);
85
+ }
86
+ if (recordExitCode) {
87
+ const exitCode = (output as Record<string, unknown>)?.exitCode;
88
+ if (typeof exitCode === "number") span.setAttribute("process.exit_code", exitCode);
89
+ }
90
+ span.setStatus({ code: StatusCode.OK });
91
+ span.end();
92
+ stepSpans.delete(stepIndex);
93
+ },
94
+
95
+ onStepFailed({ stepIndex, error }: StepFailedHookInfo) {
96
+ const span = stepSpans.get(stepIndex);
97
+ if (!span) return;
98
+ span.recordException(error);
99
+ span.setStatus({ code: StatusCode.ERROR, message: error.message });
100
+ span.end();
101
+ stepSpans.delete(stepIndex);
102
+ },
103
+
104
+ onWorkflowComplete(_info: WorkflowCompleteHookInfo) {
105
+ rootSpan?.setStatus({ code: StatusCode.OK });
106
+ rootSpan?.end();
107
+ rootSpan = undefined;
108
+ },
109
+
110
+ onWorkflowFailed({ error }: WorkflowFailedHookInfo) {
111
+ rootSpan?.recordException(error);
112
+ rootSpan?.setStatus({ code: StatusCode.ERROR, message: error.message });
113
+ rootSpan?.end();
114
+ rootSpan = undefined;
115
+ },
116
+ };
117
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "noEmit": true,
8
+ "skipLibCheck": true,
9
+ "allowImportingTsExtensions": true,
10
+ "types": ["bun-types"]
11
+ },
12
+ "include": ["src"]
13
+ }