duron 0.3.0-beta.8 → 0.3.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/dist/action-job.d.ts +33 -2
- package/dist/action-job.d.ts.map +1 -1
- package/dist/action-job.js +93 -26
- package/dist/action-manager.d.ts +44 -2
- package/dist/action-manager.d.ts.map +1 -1
- package/dist/action-manager.js +64 -3
- package/dist/action.d.ts +388 -7
- package/dist/action.d.ts.map +1 -1
- package/dist/action.js +44 -23
- package/dist/adapters/adapter.d.ts +365 -8
- package/dist/adapters/adapter.d.ts.map +1 -1
- package/dist/adapters/adapter.js +221 -15
- package/dist/adapters/postgres/base.d.ts +184 -6
- package/dist/adapters/postgres/base.d.ts.map +1 -1
- package/dist/adapters/postgres/base.js +436 -75
- package/dist/adapters/postgres/pglite.d.ts +37 -0
- package/dist/adapters/postgres/pglite.d.ts.map +1 -1
- package/dist/adapters/postgres/pglite.js +38 -0
- package/dist/adapters/postgres/postgres.d.ts +35 -0
- package/dist/adapters/postgres/postgres.d.ts.map +1 -1
- package/dist/adapters/postgres/postgres.js +42 -0
- package/dist/adapters/postgres/schema.d.ts +150 -37
- package/dist/adapters/postgres/schema.d.ts.map +1 -1
- package/dist/adapters/postgres/schema.default.d.ts +151 -38
- package/dist/adapters/postgres/schema.default.d.ts.map +1 -1
- package/dist/adapters/postgres/schema.default.js +2 -2
- package/dist/adapters/postgres/schema.js +60 -23
- package/dist/adapters/schemas.d.ts +124 -80
- package/dist/adapters/schemas.d.ts.map +1 -1
- package/dist/adapters/schemas.js +139 -26
- package/dist/client.d.ts +426 -22
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +370 -20
- package/dist/constants.js +6 -0
- package/dist/errors.d.ts +166 -9
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +189 -19
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/server.d.ts +99 -37
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +84 -25
- package/dist/step-manager.d.ts +111 -4
- package/dist/step-manager.d.ts.map +1 -1
- package/dist/step-manager.js +411 -75
- package/dist/telemetry/index.d.ts +1 -4
- package/dist/telemetry/index.d.ts.map +1 -1
- package/dist/telemetry/index.js +2 -4
- package/dist/telemetry/local-span-exporter.d.ts +56 -0
- package/dist/telemetry/local-span-exporter.d.ts.map +1 -0
- package/dist/telemetry/local-span-exporter.js +118 -0
- package/dist/utils/p-retry.d.ts +5 -0
- package/dist/utils/p-retry.d.ts.map +1 -1
- package/dist/utils/p-retry.js +8 -0
- package/dist/utils/wait-for-abort.d.ts +1 -0
- package/dist/utils/wait-for-abort.d.ts.map +1 -1
- package/dist/utils/wait-for-abort.js +1 -0
- package/migrations/postgres/{20260119153838_flimsy_thor_girl → 20260121160012_normal_bloodstrike}/migration.sql +32 -20
- package/migrations/postgres/{20260119153838_flimsy_thor_girl → 20260121160012_normal_bloodstrike}/snapshot.json +241 -66
- package/package.json +42 -26
- package/src/action-job.ts +43 -32
- package/src/action-manager.ts +5 -5
- package/src/action.ts +317 -149
- package/src/adapters/adapter.ts +54 -54
- package/src/adapters/postgres/base.ts +266 -86
- package/src/adapters/postgres/schema.default.ts +2 -2
- package/src/adapters/postgres/schema.ts +52 -24
- package/src/adapters/schemas.ts +91 -36
- package/src/client.ts +322 -68
- package/src/errors.ts +141 -30
- package/src/index.ts +2 -0
- package/src/server.ts +39 -37
- package/src/step-manager.ts +254 -91
- package/src/telemetry/index.ts +2 -20
- package/src/telemetry/local-span-exporter.ts +148 -0
- package/dist/telemetry/adapter.d.ts +0 -107
- package/dist/telemetry/adapter.d.ts.map +0 -1
- package/dist/telemetry/adapter.js +0 -134
- package/dist/telemetry/local.d.ts +0 -22
- package/dist/telemetry/local.d.ts.map +0 -1
- package/dist/telemetry/local.js +0 -243
- package/dist/telemetry/noop.d.ts +0 -17
- package/dist/telemetry/noop.d.ts.map +0 -1
- package/dist/telemetry/noop.js +0 -66
- package/dist/telemetry/opentelemetry.d.ts +0 -25
- package/dist/telemetry/opentelemetry.d.ts.map +0 -1
- package/dist/telemetry/opentelemetry.js +0 -312
- package/src/telemetry/adapter.ts +0 -642
- package/src/telemetry/local.ts +0 -429
- package/src/telemetry/noop.ts +0 -141
- package/src/telemetry/opentelemetry.ts +0 -453
package/src/step-manager.ts
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Context,
|
|
3
|
+
context,
|
|
4
|
+
type Span,
|
|
5
|
+
SpanKind,
|
|
6
|
+
type SpanOptions,
|
|
7
|
+
SpanStatusCode,
|
|
8
|
+
type Tracer,
|
|
9
|
+
trace,
|
|
10
|
+
} from '@opentelemetry/api'
|
|
1
11
|
import fastq from 'fastq'
|
|
2
12
|
import type { Logger } from 'pino'
|
|
3
13
|
import type { z } from 'zod'
|
|
@@ -17,51 +27,125 @@ import {
|
|
|
17
27
|
ActionCancelError,
|
|
18
28
|
isCancelError,
|
|
19
29
|
isNonRetriableError,
|
|
30
|
+
isTimeoutError,
|
|
20
31
|
NonRetriableError,
|
|
21
32
|
StepAlreadyExecutedError,
|
|
22
33
|
StepTimeoutError,
|
|
23
34
|
serializeError,
|
|
24
35
|
UnhandledChildStepsError,
|
|
25
36
|
} from './errors.js'
|
|
26
|
-
import type { ObserveContext, Span, TelemetryAdapter, Tracer, TracerSpan } from './telemetry/adapter.js'
|
|
27
37
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Telemetry context provided to action and step handlers.
|
|
40
|
+
* Provides access to OpenTelemetry APIs for recording traces and metrics.
|
|
41
|
+
*/
|
|
42
|
+
export interface TelemetryContext {
|
|
43
|
+
/**
|
|
44
|
+
* Get the active OpenTelemetry span for the current job/step.
|
|
45
|
+
*/
|
|
46
|
+
getActiveSpan(): Span | undefined
|
|
31
47
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Get an OpenTelemetry tracer for creating custom spans.
|
|
50
|
+
*/
|
|
51
|
+
getTracer(name: string): Tracer
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Start a new span as a child of the current job/step span.
|
|
55
|
+
* This is a convenience method that properly links the span to the trace hierarchy.
|
|
56
|
+
*/
|
|
57
|
+
startSpan(name: string, options?: { attributes?: Record<string, any> }): Span
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Record a custom metric as a span event.
|
|
61
|
+
*/
|
|
62
|
+
recordMetric(name: string, value: number, attributes?: Record<string, any>): void
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Inject parent span into a context if we have one.
|
|
67
|
+
*/
|
|
68
|
+
function injectParentSpan(ctx: Context, parentSpan: Span | null): Context {
|
|
69
|
+
return parentSpan ? trace.setSpan(ctx, parentSpan) : ctx
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Create a context-aware tracer wrapper that automatically injects the parent span.
|
|
74
|
+
* This ensures spans created by external libraries (like AI SDK) are properly linked
|
|
75
|
+
* to the current job/step trace hierarchy.
|
|
76
|
+
*/
|
|
77
|
+
function createContextAwareTracer(tracer: Tracer, parentSpan: Span | null): Tracer {
|
|
78
|
+
return {
|
|
79
|
+
startSpan(name: string, options?: SpanOptions, ctx?: Context): Span {
|
|
80
|
+
// Always inject our parent span into the context, regardless of what context is passed.
|
|
81
|
+
// This is necessary because without global registration, context.active() returns
|
|
82
|
+
// ROOT_CONTEXT, so external libraries (like AI SDK) that pass context.active()
|
|
83
|
+
// would otherwise create orphan spans.
|
|
84
|
+
const baseContext = ctx ?? context.active()
|
|
85
|
+
const effectiveContext = injectParentSpan(baseContext, parentSpan)
|
|
86
|
+
return tracer.startSpan(name, options, effectiveContext)
|
|
87
|
+
},
|
|
88
|
+
// startActiveSpan has multiple overloads, we need to handle them all
|
|
89
|
+
startActiveSpan<F extends (span: Span) => unknown>(
|
|
90
|
+
name: string,
|
|
91
|
+
optionsOrFn: SpanOptions | F,
|
|
92
|
+
ctxOrFn?: Context | F,
|
|
93
|
+
fn?: F,
|
|
94
|
+
): ReturnType<F> {
|
|
95
|
+
// Parse the overloaded arguments
|
|
96
|
+
let options: SpanOptions | undefined
|
|
97
|
+
let ctx: Context | undefined
|
|
98
|
+
let callback: F
|
|
99
|
+
|
|
100
|
+
if (typeof optionsOrFn === 'function') {
|
|
101
|
+
// startActiveSpan(name, fn)
|
|
102
|
+
callback = optionsOrFn
|
|
103
|
+
} else if (typeof ctxOrFn === 'function') {
|
|
104
|
+
// startActiveSpan(name, options, fn)
|
|
105
|
+
options = optionsOrFn
|
|
106
|
+
callback = ctxOrFn
|
|
107
|
+
} else {
|
|
108
|
+
// startActiveSpan(name, options, context, fn)
|
|
109
|
+
options = optionsOrFn
|
|
110
|
+
ctx = ctxOrFn
|
|
111
|
+
callback = fn!
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const baseContext = ctx ?? context.active()
|
|
115
|
+
const effectiveContext = injectParentSpan(baseContext, parentSpan)
|
|
116
|
+
|
|
117
|
+
return tracer.startActiveSpan(name, options ?? {}, effectiveContext, callback)
|
|
118
|
+
},
|
|
119
|
+
} as Tracer
|
|
57
120
|
}
|
|
58
121
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
122
|
+
/**
|
|
123
|
+
* Create a TelemetryContext that wraps an OTel span.
|
|
124
|
+
*/
|
|
125
|
+
function createTelemetryContext(span: Span | null, tracer: Tracer): TelemetryContext {
|
|
126
|
+
return {
|
|
127
|
+
getActiveSpan(): Span | undefined {
|
|
128
|
+
return span ?? undefined
|
|
129
|
+
},
|
|
130
|
+
getTracer(_name: string): Tracer {
|
|
131
|
+
// Return a context-aware tracer that automatically links spans to the current trace
|
|
132
|
+
return createContextAwareTracer(tracer, span)
|
|
133
|
+
},
|
|
134
|
+
startSpan(name: string, options?: { attributes?: Record<string, any> }): Span {
|
|
135
|
+
// Create a child span linked to the current span (job or step)
|
|
136
|
+
const parentContext = span ? trace.setSpan(context.active(), span) : context.active()
|
|
137
|
+
return tracer.startSpan(name, { attributes: options?.attributes }, parentContext)
|
|
138
|
+
},
|
|
139
|
+
recordMetric(name: string, value: number, attributes?: Record<string, any>): void {
|
|
140
|
+
if (span) {
|
|
141
|
+
span.addEvent(`metric:${name}`, {
|
|
142
|
+
'metric.value': value,
|
|
143
|
+
...attributes,
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
}
|
|
148
|
+
}
|
|
65
149
|
|
|
66
150
|
import pRetry from './utils/p-retry.js'
|
|
67
151
|
import waitForAbort from './utils/wait-for-abort.js'
|
|
@@ -171,7 +255,7 @@ export interface StepManagerOptions {
|
|
|
171
255
|
jobId: string
|
|
172
256
|
actionName: string
|
|
173
257
|
adapter: Adapter
|
|
174
|
-
|
|
258
|
+
tracer: Tracer
|
|
175
259
|
logger: Logger
|
|
176
260
|
concurrencyLimit: number
|
|
177
261
|
}
|
|
@@ -184,7 +268,7 @@ export class StepManager {
|
|
|
184
268
|
#jobId: string
|
|
185
269
|
#actionName: string
|
|
186
270
|
#stepStore: StepStore
|
|
187
|
-
#
|
|
271
|
+
#tracer: Tracer
|
|
188
272
|
#queue: fastq.queueAsPromised<TaskStep, any>
|
|
189
273
|
#logger: Logger
|
|
190
274
|
// each step name should be executed only once per parent (name + parentStepId)
|
|
@@ -192,7 +276,7 @@ export class StepManager {
|
|
|
192
276
|
// Store step spans for nested step tracking
|
|
193
277
|
#stepSpans = new Map<string, Span>()
|
|
194
278
|
// Store the job span for creating step spans
|
|
195
|
-
#jobSpan
|
|
279
|
+
#jobSpan!: Span
|
|
196
280
|
// Factory function to create run functions with the correct parent step ID and abort signal
|
|
197
281
|
#runFnFactory: ((parentStepId: string | null, abortSignal: AbortSignal) => StepHandlerContext['run']) | null = null
|
|
198
282
|
|
|
@@ -209,7 +293,7 @@ export class StepManager {
|
|
|
209
293
|
this.#jobId = options.jobId
|
|
210
294
|
this.#actionName = options.actionName
|
|
211
295
|
this.#logger = options.logger
|
|
212
|
-
this.#
|
|
296
|
+
this.#tracer = options.tracer
|
|
213
297
|
this.#stepStore = new StepStore(options.adapter)
|
|
214
298
|
this.#queue = fastq.promise(async (task: TaskStep) => {
|
|
215
299
|
// Create composite key: name + parentStepId (allows same name under different parents)
|
|
@@ -253,7 +337,6 @@ export class StepManager {
|
|
|
253
337
|
* @param variables - Variables available to the action
|
|
254
338
|
* @param abortSignal - Abort signal for cancelling the action
|
|
255
339
|
* @param logger - Pino child logger for this job
|
|
256
|
-
* @param observeContext - Observability context for telemetry
|
|
257
340
|
* @returns ActionHandlerContext instance
|
|
258
341
|
*/
|
|
259
342
|
createActionContext<TInput extends z.ZodObject, TOutput extends z.ZodObject, TVariables = Record<string, unknown>>(
|
|
@@ -262,38 +345,21 @@ export class StepManager {
|
|
|
262
345
|
variables: TVariables,
|
|
263
346
|
abortSignal: AbortSignal,
|
|
264
347
|
logger: Logger,
|
|
265
|
-
observeContext: ObserveContext,
|
|
266
348
|
): ActionHandlerContext<TInput, TVariables> {
|
|
267
|
-
|
|
349
|
+
const telemetryContext = createTelemetryContext(this.#jobSpan, this.#tracer)
|
|
350
|
+
return new ActionContext(this, job, action, variables, abortSignal, logger, telemetryContext)
|
|
268
351
|
}
|
|
269
352
|
|
|
270
353
|
/**
|
|
271
|
-
* Create
|
|
354
|
+
* Create a telemetry context for a step.
|
|
272
355
|
*/
|
|
273
|
-
|
|
356
|
+
createStepTelemetryContext(stepId: string): TelemetryContext {
|
|
274
357
|
const stepSpan = this.#stepSpans.get(stepId)
|
|
275
358
|
if (stepSpan) {
|
|
276
|
-
return
|
|
359
|
+
return createTelemetryContext(stepSpan, this.#tracer)
|
|
277
360
|
}
|
|
278
361
|
// Fallback to job span if step span not found
|
|
279
|
-
|
|
280
|
-
return this.#telemetry.createObserveContext(this.#jobId, stepId, this.#jobSpan)
|
|
281
|
-
}
|
|
282
|
-
// No-op observe context
|
|
283
|
-
return {
|
|
284
|
-
recordMetric: () => {
|
|
285
|
-
// No-op
|
|
286
|
-
},
|
|
287
|
-
addSpanAttribute: () => {
|
|
288
|
-
// No-op
|
|
289
|
-
},
|
|
290
|
-
addSpanEvent: () => {
|
|
291
|
-
// No-op
|
|
292
|
-
},
|
|
293
|
-
getTracer: (name: string) => {
|
|
294
|
-
return createNoopTracer(name)
|
|
295
|
-
},
|
|
296
|
-
}
|
|
362
|
+
return createTelemetryContext(this.#jobSpan, this.#tracer)
|
|
297
363
|
}
|
|
298
364
|
|
|
299
365
|
/**
|
|
@@ -380,15 +446,22 @@ export class StepManager {
|
|
|
380
446
|
|
|
381
447
|
step = newStep
|
|
382
448
|
|
|
383
|
-
// Start step
|
|
449
|
+
// Start step span - uses no-op tracer if no SDK is configured
|
|
384
450
|
const parentSpan = parentStepId ? this.#stepSpans.get(parentStepId) : this.#jobSpan
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
451
|
+
const parentContext = parentSpan ? trace.setSpan(context.active(), parentSpan) : context.active()
|
|
452
|
+
const stepSpan = this.#tracer.startSpan(
|
|
453
|
+
`step:${name}`,
|
|
454
|
+
{
|
|
455
|
+
kind: SpanKind.INTERNAL,
|
|
456
|
+
attributes: {
|
|
457
|
+
'duron.job.id': this.#jobId,
|
|
458
|
+
'duron.step.id': step.id,
|
|
459
|
+
'duron.step.name': name,
|
|
460
|
+
'duron.step.parent_id': parentStepId ?? undefined,
|
|
461
|
+
},
|
|
462
|
+
},
|
|
463
|
+
parentContext,
|
|
464
|
+
)
|
|
392
465
|
this.#stepSpans.set(step.id, stepSpan)
|
|
393
466
|
|
|
394
467
|
if (abortSignal.aborted) {
|
|
@@ -426,7 +499,11 @@ export class StepManager {
|
|
|
426
499
|
// Create abort controller for this step's timeout
|
|
427
500
|
const stepAbortController = new AbortController()
|
|
428
501
|
const timeoutId = setTimeout(() => {
|
|
429
|
-
const timeoutError = new StepTimeoutError(name, this.#jobId, expire
|
|
502
|
+
const timeoutError = new StepTimeoutError(name, this.#jobId, expire, {
|
|
503
|
+
stepId: step?.id,
|
|
504
|
+
parentStepId,
|
|
505
|
+
actionName: this.#actionName,
|
|
506
|
+
})
|
|
430
507
|
stepAbortController.abort(timeoutError)
|
|
431
508
|
}, expire)
|
|
432
509
|
|
|
@@ -446,15 +523,15 @@ export class StepManager {
|
|
|
446
523
|
const childAbortController = new AbortController()
|
|
447
524
|
const childSignal = AbortSignal.any([stepSignal, childAbortController.signal])
|
|
448
525
|
|
|
449
|
-
// Create
|
|
450
|
-
const
|
|
526
|
+
// Create telemetry context for this step
|
|
527
|
+
const stepTelemetryContext = this.createStepTelemetryContext(step.id)
|
|
451
528
|
|
|
452
529
|
// Create StepHandlerContext with nested step support
|
|
453
530
|
const stepContext: StepHandlerContext = {
|
|
454
531
|
signal: stepSignal,
|
|
455
532
|
stepId: step.id,
|
|
456
533
|
parentStepId,
|
|
457
|
-
|
|
534
|
+
telemetry: stepTelemetryContext,
|
|
458
535
|
step: <TChildResult>(
|
|
459
536
|
childName: string,
|
|
460
537
|
childCb: (ctx: StepHandlerContext) => Promise<TChildResult>,
|
|
@@ -493,6 +570,7 @@ export class StepManager {
|
|
|
493
570
|
.catch(() => {
|
|
494
571
|
trackedChild.settled = true
|
|
495
572
|
// Swallow the error here - it will be re-thrown to the caller via the returned promise
|
|
573
|
+
// Note: sibling steps will be aborted when the error propagates to the action level
|
|
496
574
|
})
|
|
497
575
|
|
|
498
576
|
return childPromise
|
|
@@ -503,10 +581,15 @@ export class StepManager {
|
|
|
503
581
|
try {
|
|
504
582
|
// Race between abort signal and callback execution
|
|
505
583
|
const abortPromise = waitForAbort(stepSignal)
|
|
506
|
-
|
|
584
|
+
|
|
585
|
+
// Execute callback within the span context so that child spans inherit the trace
|
|
586
|
+
const currentStepSpan = step?.id ? this.#stepSpans.get(step.id) : undefined
|
|
587
|
+
const spanContext = currentStepSpan ? trace.setSpan(context.active(), currentStepSpan) : context.active()
|
|
588
|
+
const callbackPromise = context.with(spanContext, () => cb(stepContext))
|
|
507
589
|
|
|
508
590
|
let result: any = null
|
|
509
591
|
let aborted = false
|
|
592
|
+
let callbackError: Error | null = null
|
|
510
593
|
|
|
511
594
|
await Promise.race([
|
|
512
595
|
abortPromise.promise.then(() => {
|
|
@@ -518,11 +601,25 @@ export class StepManager {
|
|
|
518
601
|
result = res
|
|
519
602
|
}
|
|
520
603
|
})
|
|
604
|
+
.catch((err) => {
|
|
605
|
+
callbackError = err
|
|
606
|
+
})
|
|
521
607
|
.finally(() => {
|
|
522
608
|
abortPromise.release()
|
|
523
609
|
}),
|
|
524
610
|
])
|
|
525
611
|
|
|
612
|
+
// If callback threw an error, abort children and wait for them before re-throwing
|
|
613
|
+
if (callbackError) {
|
|
614
|
+
if (childSteps.length > 0) {
|
|
615
|
+
// Abort all children with the callback error as reason
|
|
616
|
+
childAbortController.abort(callbackError)
|
|
617
|
+
// Wait for all children to settle
|
|
618
|
+
await Promise.allSettled(childSteps.map((c) => c.promise))
|
|
619
|
+
}
|
|
620
|
+
throw callbackError
|
|
621
|
+
}
|
|
622
|
+
|
|
526
623
|
// If aborted, wait for child steps to settle before propagating
|
|
527
624
|
if (aborted) {
|
|
528
625
|
// Wait for all child steps to settle (they'll be aborted via signal propagation)
|
|
@@ -548,7 +645,12 @@ export class StepManager {
|
|
|
548
645
|
)
|
|
549
646
|
|
|
550
647
|
// Abort all pending children
|
|
551
|
-
const unhandledError = new UnhandledChildStepsError(name, unsettledChildren.length
|
|
648
|
+
const unhandledError = new UnhandledChildStepsError(name, unsettledChildren.length, {
|
|
649
|
+
stepId: step.id,
|
|
650
|
+
parentStepId,
|
|
651
|
+
jobId: this.#jobId,
|
|
652
|
+
actionName: this.#actionName,
|
|
653
|
+
})
|
|
552
654
|
childAbortController.abort(unhandledError)
|
|
553
655
|
|
|
554
656
|
// Wait for all children to settle (they'll reject with cancellation)
|
|
@@ -564,10 +666,11 @@ export class StepManager {
|
|
|
564
666
|
throw new Error(`Failed to complete step "${name}" for job "${this.#jobId}" action "${this.#actionName}"`)
|
|
565
667
|
}
|
|
566
668
|
|
|
567
|
-
// End step
|
|
669
|
+
// End step span successfully
|
|
568
670
|
const stepSpan = this.#stepSpans.get(step.id)
|
|
569
671
|
if (stepSpan) {
|
|
570
|
-
|
|
672
|
+
stepSpan.setStatus({ code: SpanStatusCode.OK })
|
|
673
|
+
stepSpan.end()
|
|
571
674
|
this.#stepSpans.delete(step.id)
|
|
572
675
|
}
|
|
573
676
|
|
|
@@ -597,28 +700,65 @@ export class StepManager {
|
|
|
597
700
|
if (
|
|
598
701
|
isNonRetriableError(error) ||
|
|
599
702
|
(error.cause && isNonRetriableError(error.cause)) ||
|
|
600
|
-
(error instanceof Error && error.name === 'AbortError'
|
|
703
|
+
(error instanceof Error && error.name === 'AbortError')
|
|
601
704
|
) {
|
|
602
|
-
|
|
705
|
+
const err = isNonRetriableError(error)
|
|
706
|
+
? error
|
|
707
|
+
: error instanceof Error && error.name === 'AbortError'
|
|
708
|
+
? new NonRetriableError(error.message, { cause: error.cause })
|
|
709
|
+
: (error.cause as NonRetriableError)
|
|
710
|
+
|
|
711
|
+
if (Object.keys(err.metadata).length === 0) {
|
|
712
|
+
err.setMetadata({
|
|
713
|
+
stepId: step?.id,
|
|
714
|
+
parentStepId,
|
|
715
|
+
jobId: this.#jobId,
|
|
716
|
+
actionName: this.#actionName,
|
|
717
|
+
})
|
|
718
|
+
}
|
|
719
|
+
throw err
|
|
603
720
|
}
|
|
604
721
|
|
|
605
722
|
if (ctx.retriesLeft > 0 && step) {
|
|
723
|
+
this.#clearHistoryForStep(step.id)
|
|
606
724
|
const delayed = await this.#stepStore.delay(step.id, ctx.finalDelay, serializeError(error))
|
|
607
725
|
if (!delayed) {
|
|
608
726
|
throw new Error(`Failed to delay step "${name}" for job "${this.#jobId}" action "${this.#actionName}"`)
|
|
609
727
|
}
|
|
728
|
+
} else {
|
|
729
|
+
if (isTimeoutError(error)) {
|
|
730
|
+
;(error as any).nonRetriable = true
|
|
731
|
+
throw error
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const errorMessage = error instanceof Error ? error.message : String(error)
|
|
735
|
+
const err = new NonRetriableError(errorMessage, { cause: error })
|
|
736
|
+
err.setMetadata({
|
|
737
|
+
stepId: step?.id,
|
|
738
|
+
parentStepId,
|
|
739
|
+
jobId: this.#jobId,
|
|
740
|
+
actionName: this.#actionName,
|
|
741
|
+
})
|
|
742
|
+
throw err
|
|
610
743
|
}
|
|
611
744
|
},
|
|
612
745
|
}).catch(async (error) => {
|
|
613
746
|
if (step) {
|
|
614
|
-
// End step
|
|
747
|
+
// End step span with error/cancelled status
|
|
615
748
|
const stepSpan = this.#stepSpans.get(step.id)
|
|
616
749
|
if (stepSpan) {
|
|
617
750
|
if (isCancelError(error)) {
|
|
618
|
-
|
|
751
|
+
stepSpan.setStatus({ code: SpanStatusCode.ERROR, message: 'Step cancelled' })
|
|
619
752
|
} else {
|
|
620
|
-
|
|
753
|
+
stepSpan.setStatus({
|
|
754
|
+
code: SpanStatusCode.ERROR,
|
|
755
|
+
message: error instanceof Error ? error.message : String(error),
|
|
756
|
+
})
|
|
757
|
+
if (error instanceof Error) {
|
|
758
|
+
stepSpan.recordException(error)
|
|
759
|
+
}
|
|
621
760
|
}
|
|
761
|
+
stepSpan.end()
|
|
622
762
|
this.#stepSpans.delete(step.id)
|
|
623
763
|
}
|
|
624
764
|
|
|
@@ -631,6 +771,19 @@ export class StepManager {
|
|
|
631
771
|
throw error
|
|
632
772
|
})
|
|
633
773
|
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* Clear the history of nested steps for a given step.
|
|
777
|
+
* We do't need to clear the history for the root step because it's not a parent step, it's the action itself.
|
|
778
|
+
* @param stepId - The ID of the step to clear the history for
|
|
779
|
+
*/
|
|
780
|
+
#clearHistoryForStep(stepId: string): void {
|
|
781
|
+
this.#historySteps.forEach((stepKey) => {
|
|
782
|
+
if (stepKey.startsWith(stepId)) {
|
|
783
|
+
this.#historySteps.delete(stepKey)
|
|
784
|
+
}
|
|
785
|
+
})
|
|
786
|
+
}
|
|
634
787
|
}
|
|
635
788
|
|
|
636
789
|
// ============================================================================
|
|
@@ -652,7 +805,7 @@ class ActionContext<TInput extends z.ZodObject, TOutput extends z.ZodObject, TVa
|
|
|
652
805
|
#jobId: string
|
|
653
806
|
#groupKey: string = '@default'
|
|
654
807
|
#action: Action<TInput, TOutput, TVariables>
|
|
655
|
-
#
|
|
808
|
+
#telemetryContext: TelemetryContext
|
|
656
809
|
|
|
657
810
|
// ============================================================================
|
|
658
811
|
// Constructor
|
|
@@ -665,7 +818,7 @@ class ActionContext<TInput extends z.ZodObject, TOutput extends z.ZodObject, TVa
|
|
|
665
818
|
variables: TVariables,
|
|
666
819
|
abortSignal: AbortSignal,
|
|
667
820
|
logger: Logger,
|
|
668
|
-
|
|
821
|
+
telemetryContext: TelemetryContext,
|
|
669
822
|
) {
|
|
670
823
|
this.#stepManager = stepManager
|
|
671
824
|
this.#variables = variables
|
|
@@ -674,7 +827,7 @@ class ActionContext<TInput extends z.ZodObject, TOutput extends z.ZodObject, TVa
|
|
|
674
827
|
this.#action = action
|
|
675
828
|
this.#jobId = job.id
|
|
676
829
|
this.#groupKey = job.groupKey ?? '@default'
|
|
677
|
-
this.#
|
|
830
|
+
this.#telemetryContext = telemetryContext
|
|
678
831
|
if (action.input) {
|
|
679
832
|
this.#input = action.input.parse(job.input, {
|
|
680
833
|
error: () => 'Error parsing action input',
|
|
@@ -734,10 +887,10 @@ class ActionContext<TInput extends z.ZodObject, TOutput extends z.ZodObject, TVa
|
|
|
734
887
|
}
|
|
735
888
|
|
|
736
889
|
/**
|
|
737
|
-
* Get the
|
|
890
|
+
* Get the telemetry context for recording metrics and span data.
|
|
738
891
|
*/
|
|
739
|
-
get
|
|
740
|
-
return this.#
|
|
892
|
+
get telemetry(): TelemetryContext {
|
|
893
|
+
return this.#telemetryContext
|
|
741
894
|
}
|
|
742
895
|
|
|
743
896
|
/**
|
|
@@ -758,6 +911,7 @@ class ActionContext<TInput extends z.ZodObject, TOutput extends z.ZodObject, TVa
|
|
|
758
911
|
...this.#action.steps,
|
|
759
912
|
...options,
|
|
760
913
|
})
|
|
914
|
+
|
|
761
915
|
return this.#stepManager.push({
|
|
762
916
|
name,
|
|
763
917
|
cb,
|
|
@@ -806,7 +960,16 @@ class ActionContext<TInput extends z.ZodObject, TOutput extends z.ZodObject, TVa
|
|
|
806
960
|
: (input as z.infer<TStepInput>)
|
|
807
961
|
|
|
808
962
|
// Resolve step name (static or dynamic)
|
|
809
|
-
|
|
963
|
+
// If it's a function, pass the full context including input, variables, jobId, and parentStepId
|
|
964
|
+
const stepName =
|
|
965
|
+
typeof stepDef.name === 'function'
|
|
966
|
+
? stepDef.name({
|
|
967
|
+
input: validatedInput,
|
|
968
|
+
var: this.#variables,
|
|
969
|
+
jobId: this.#jobId,
|
|
970
|
+
parentStepId,
|
|
971
|
+
})
|
|
972
|
+
: stepDef.name
|
|
810
973
|
|
|
811
974
|
// Merge options: action defaults -> step definition -> call-time overrides
|
|
812
975
|
const mergedOptions: z.input<typeof StepOptionsSchema> = {
|
package/src/telemetry/index.ts
CHANGED
|
@@ -1,20 +1,2 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
export {
|
|
4
|
-
type AddSpanAttributeOptions,
|
|
5
|
-
type AddSpanEventOptions,
|
|
6
|
-
type EndSpanOptions,
|
|
7
|
-
type ObserveContext,
|
|
8
|
-
type RecordMetricOptions,
|
|
9
|
-
type Span,
|
|
10
|
-
type StartDatabaseSpanOptions,
|
|
11
|
-
type StartJobSpanOptions,
|
|
12
|
-
type StartSpanOptions,
|
|
13
|
-
type StartStepSpanOptions,
|
|
14
|
-
TelemetryAdapter,
|
|
15
|
-
type Tracer,
|
|
16
|
-
type TracerSpan,
|
|
17
|
-
} from './adapter.js'
|
|
18
|
-
export { LocalTelemetryAdapter, type LocalTelemetryAdapterOptions, localTelemetryAdapter } from './local.js'
|
|
19
|
-
export { NoopTelemetryAdapter, noopTelemetryAdapter } from './noop.js'
|
|
20
|
-
export { OpenTelemetryAdapter, type OpenTelemetryAdapterOptions, openTelemetryAdapter } from './opentelemetry.js'
|
|
1
|
+
// OpenTelemetry-based span exporter for local database persistence
|
|
2
|
+
export { LocalSpanExporter, type LocalSpanExporterOptions } from './local-span-exporter.js'
|