duron 0.3.0-beta.0 → 0.3.0-beta.10
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.map +1 -1
- package/dist/action-job.js +9 -7
- package/dist/action.d.ts +21 -0
- package/dist/action.d.ts.map +1 -1
- package/dist/action.js +10 -2
- package/dist/adapters/postgres/base.d.ts.map +1 -1
- package/dist/adapters/postgres/base.js +5 -2
- package/dist/adapters/postgres/schema.d.ts.map +1 -1
- package/dist/adapters/postgres/schema.js +3 -1
- package/dist/client.d.ts +25 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +102 -2
- package/dist/errors.d.ts +47 -9
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +78 -19
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/step-manager.d.ts +1 -0
- package/dist/step-manager.d.ts.map +1 -1
- package/dist/step-manager.js +151 -7
- package/dist/telemetry/adapter.d.ts +22 -0
- package/dist/telemetry/adapter.d.ts.map +1 -1
- package/dist/telemetry/adapter.js +6 -0
- package/dist/telemetry/index.d.ts +1 -1
- package/dist/telemetry/index.d.ts.map +1 -1
- package/dist/telemetry/local.d.ts +2 -1
- package/dist/telemetry/local.d.ts.map +1 -1
- package/dist/telemetry/local.js +63 -0
- package/dist/telemetry/noop.d.ts +2 -1
- package/dist/telemetry/noop.d.ts.map +1 -1
- package/dist/telemetry/noop.js +27 -0
- package/dist/telemetry/opentelemetry.d.ts +2 -1
- package/dist/telemetry/opentelemetry.d.ts.map +1 -1
- package/dist/telemetry/opentelemetry.js +110 -0
- package/migrations/postgres/{20251203223656_conscious_johnny_blaze → 20260119153838_flimsy_thor_girl}/migration.sql +29 -2
- package/migrations/postgres/{20260118202533_wealthy_mysterio → 20260119153838_flimsy_thor_girl}/snapshot.json +5 -5
- package/package.json +1 -1
- package/src/action-job.ts +14 -7
- package/src/action.ts +156 -3
- package/src/adapters/postgres/base.ts +5 -2
- package/src/adapters/postgres/schema.ts +5 -2
- package/src/client.ts +187 -8
- package/src/errors.ts +141 -30
- package/src/index.ts +7 -1
- package/src/step-manager.ts +230 -8
- package/src/telemetry/adapter.ts +174 -0
- package/src/telemetry/index.ts +3 -0
- package/src/telemetry/local.ts +93 -0
- package/src/telemetry/noop.ts +46 -0
- package/src/telemetry/opentelemetry.ts +145 -2
- package/migrations/postgres/20251203223656_conscious_johnny_blaze/snapshot.json +0 -941
- package/migrations/postgres/20260117231749_clumsy_penance/migration.sql +0 -3
- package/migrations/postgres/20260117231749_clumsy_penance/snapshot.json +0 -988
- package/migrations/postgres/20260118202533_wealthy_mysterio/migration.sql +0 -24
package/src/step-manager.ts
CHANGED
|
@@ -5,6 +5,8 @@ import type { z } from 'zod'
|
|
|
5
5
|
import {
|
|
6
6
|
type Action,
|
|
7
7
|
type ActionHandlerContext,
|
|
8
|
+
type StepDefinition,
|
|
9
|
+
type StepDefinitionHandlerContext,
|
|
8
10
|
type StepHandlerContext,
|
|
9
11
|
type StepOptions,
|
|
10
12
|
StepOptionsSchema,
|
|
@@ -15,13 +17,53 @@ import {
|
|
|
15
17
|
ActionCancelError,
|
|
16
18
|
isCancelError,
|
|
17
19
|
isNonRetriableError,
|
|
20
|
+
isTimeoutError,
|
|
18
21
|
NonRetriableError,
|
|
19
22
|
StepAlreadyExecutedError,
|
|
20
23
|
StepTimeoutError,
|
|
21
24
|
serializeError,
|
|
22
25
|
UnhandledChildStepsError,
|
|
23
26
|
} from './errors.js'
|
|
24
|
-
import type { ObserveContext, Span, TelemetryAdapter } from './telemetry/adapter.js'
|
|
27
|
+
import type { ObserveContext, Span, TelemetryAdapter, Tracer, TracerSpan } from './telemetry/adapter.js'
|
|
28
|
+
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// Noop Tracer (for fallback observe context)
|
|
31
|
+
// ============================================================================
|
|
32
|
+
|
|
33
|
+
const noopTracerSpan: TracerSpan = {
|
|
34
|
+
setAttribute() {
|
|
35
|
+
// No-op
|
|
36
|
+
},
|
|
37
|
+
setAttributes() {
|
|
38
|
+
// No-op
|
|
39
|
+
},
|
|
40
|
+
addEvent() {
|
|
41
|
+
// No-op
|
|
42
|
+
},
|
|
43
|
+
recordException() {
|
|
44
|
+
// No-op
|
|
45
|
+
},
|
|
46
|
+
setStatusOk() {
|
|
47
|
+
// No-op
|
|
48
|
+
},
|
|
49
|
+
setStatusError() {
|
|
50
|
+
// No-op
|
|
51
|
+
},
|
|
52
|
+
end() {
|
|
53
|
+
// No-op
|
|
54
|
+
},
|
|
55
|
+
isRecording() {
|
|
56
|
+
return false
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const createNoopTracer = (name: string): Tracer => ({
|
|
61
|
+
name,
|
|
62
|
+
startSpan(): TracerSpan {
|
|
63
|
+
return noopTracerSpan
|
|
64
|
+
},
|
|
65
|
+
})
|
|
66
|
+
|
|
25
67
|
import pRetry from './utils/p-retry.js'
|
|
26
68
|
import waitForAbort from './utils/wait-for-abort.js'
|
|
27
69
|
|
|
@@ -146,12 +188,14 @@ export class StepManager {
|
|
|
146
188
|
#telemetry: TelemetryAdapter
|
|
147
189
|
#queue: fastq.queueAsPromised<TaskStep, any>
|
|
148
190
|
#logger: Logger
|
|
149
|
-
// each step name should be executed only once per
|
|
191
|
+
// each step name should be executed only once per parent (name + parentStepId)
|
|
150
192
|
#historySteps = new Set<string>()
|
|
151
193
|
// Store step spans for nested step tracking
|
|
152
194
|
#stepSpans = new Map<string, Span>()
|
|
153
195
|
// Store the job span for creating step spans
|
|
154
196
|
#jobSpan: Span | null = null
|
|
197
|
+
// Factory function to create run functions with the correct parent step ID and abort signal
|
|
198
|
+
#runFnFactory: ((parentStepId: string | null, abortSignal: AbortSignal) => StepHandlerContext['run']) | null = null
|
|
155
199
|
|
|
156
200
|
// ============================================================================
|
|
157
201
|
// Constructor
|
|
@@ -169,10 +213,12 @@ export class StepManager {
|
|
|
169
213
|
this.#telemetry = options.telemetry
|
|
170
214
|
this.#stepStore = new StepStore(options.adapter)
|
|
171
215
|
this.#queue = fastq.promise(async (task: TaskStep) => {
|
|
172
|
-
|
|
216
|
+
// Create composite key: name + parentStepId (allows same name under different parents)
|
|
217
|
+
const stepKey = `${task.parentStepId ?? 'root'}:${task.name}`
|
|
218
|
+
if (this.#historySteps.has(stepKey)) {
|
|
173
219
|
throw new StepAlreadyExecutedError(task.name, this.#jobId, this.#actionName)
|
|
174
220
|
}
|
|
175
|
-
this.#historySteps.add(
|
|
221
|
+
this.#historySteps.add(stepKey)
|
|
176
222
|
return this.#executeStep(task.name, task.cb, task.options, task.abortSignal, task.parentStepId, task.parallel)
|
|
177
223
|
}, options.concurrencyLimit)
|
|
178
224
|
}
|
|
@@ -185,6 +231,16 @@ export class StepManager {
|
|
|
185
231
|
this.#jobSpan = span
|
|
186
232
|
}
|
|
187
233
|
|
|
234
|
+
/**
|
|
235
|
+
* Set the run function factory for executing step definitions from inline steps.
|
|
236
|
+
* Called from ActionContext after it's initialized.
|
|
237
|
+
*
|
|
238
|
+
* @param factory - A function that creates run functions with the correct parent step ID and abort signal
|
|
239
|
+
*/
|
|
240
|
+
setRunFnFactory(factory: (parentStepId: string | null, abortSignal: AbortSignal) => StepHandlerContext['run']): void {
|
|
241
|
+
this.#runFnFactory = factory
|
|
242
|
+
}
|
|
243
|
+
|
|
188
244
|
// ============================================================================
|
|
189
245
|
// Public API Methods
|
|
190
246
|
// ============================================================================
|
|
@@ -235,6 +291,9 @@ export class StepManager {
|
|
|
235
291
|
addSpanEvent: () => {
|
|
236
292
|
// No-op
|
|
237
293
|
},
|
|
294
|
+
getTracer: (name: string) => {
|
|
295
|
+
return createNoopTracer(name)
|
|
296
|
+
},
|
|
238
297
|
}
|
|
239
298
|
}
|
|
240
299
|
|
|
@@ -245,6 +304,21 @@ export class StepManager {
|
|
|
245
304
|
* @returns Promise resolving to the step result
|
|
246
305
|
*/
|
|
247
306
|
async push(task: TaskStep): Promise<any> {
|
|
307
|
+
// Warn about potential starvation when child steps are queued and all slots are occupied
|
|
308
|
+
if (task.parentStepId !== null && this.#queue.running() >= this.#queue.concurrency) {
|
|
309
|
+
this.#logger.warn(
|
|
310
|
+
{
|
|
311
|
+
jobId: this.#jobId,
|
|
312
|
+
actionName: this.#actionName,
|
|
313
|
+
stepName: task.name,
|
|
314
|
+
parentStepId: task.parentStepId,
|
|
315
|
+
running: this.#queue.running(),
|
|
316
|
+
waiting: this.#queue.length(),
|
|
317
|
+
concurrency: this.#queue.concurrency,
|
|
318
|
+
},
|
|
319
|
+
'[StepManager] Potential starvation: child step queued while all concurrency slots are occupied by parent steps. Consider increasing steps.concurrency.',
|
|
320
|
+
)
|
|
321
|
+
}
|
|
248
322
|
return this.#queue.push(task)
|
|
249
323
|
}
|
|
250
324
|
|
|
@@ -353,7 +427,11 @@ export class StepManager {
|
|
|
353
427
|
// Create abort controller for this step's timeout
|
|
354
428
|
const stepAbortController = new AbortController()
|
|
355
429
|
const timeoutId = setTimeout(() => {
|
|
356
|
-
const timeoutError = new StepTimeoutError(name, this.#jobId, expire
|
|
430
|
+
const timeoutError = new StepTimeoutError(name, this.#jobId, expire, {
|
|
431
|
+
stepId: step?.id,
|
|
432
|
+
parentStepId,
|
|
433
|
+
actionName: this.#actionName,
|
|
434
|
+
})
|
|
357
435
|
stepAbortController.abort(timeoutError)
|
|
358
436
|
}, expire)
|
|
359
437
|
|
|
@@ -420,10 +498,12 @@ export class StepManager {
|
|
|
420
498
|
.catch(() => {
|
|
421
499
|
trackedChild.settled = true
|
|
422
500
|
// Swallow the error here - it will be re-thrown to the caller via the returned promise
|
|
501
|
+
// Note: sibling steps will be aborted when the error propagates to the action level
|
|
423
502
|
})
|
|
424
503
|
|
|
425
504
|
return childPromise
|
|
426
505
|
},
|
|
506
|
+
run: this.#runFnFactory!(step.id, childSignal),
|
|
427
507
|
}
|
|
428
508
|
|
|
429
509
|
try {
|
|
@@ -433,6 +513,7 @@ export class StepManager {
|
|
|
433
513
|
|
|
434
514
|
let result: any = null
|
|
435
515
|
let aborted = false
|
|
516
|
+
let callbackError: Error | null = null
|
|
436
517
|
|
|
437
518
|
await Promise.race([
|
|
438
519
|
abortPromise.promise.then(() => {
|
|
@@ -444,11 +525,25 @@ export class StepManager {
|
|
|
444
525
|
result = res
|
|
445
526
|
}
|
|
446
527
|
})
|
|
528
|
+
.catch((err) => {
|
|
529
|
+
callbackError = err
|
|
530
|
+
})
|
|
447
531
|
.finally(() => {
|
|
448
532
|
abortPromise.release()
|
|
449
533
|
}),
|
|
450
534
|
])
|
|
451
535
|
|
|
536
|
+
// If callback threw an error, abort children and wait for them before re-throwing
|
|
537
|
+
if (callbackError) {
|
|
538
|
+
if (childSteps.length > 0) {
|
|
539
|
+
// Abort all children with the callback error as reason
|
|
540
|
+
childAbortController.abort(callbackError)
|
|
541
|
+
// Wait for all children to settle
|
|
542
|
+
await Promise.allSettled(childSteps.map((c) => c.promise))
|
|
543
|
+
}
|
|
544
|
+
throw callbackError
|
|
545
|
+
}
|
|
546
|
+
|
|
452
547
|
// If aborted, wait for child steps to settle before propagating
|
|
453
548
|
if (aborted) {
|
|
454
549
|
// Wait for all child steps to settle (they'll be aborted via signal propagation)
|
|
@@ -474,7 +569,12 @@ export class StepManager {
|
|
|
474
569
|
)
|
|
475
570
|
|
|
476
571
|
// Abort all pending children
|
|
477
|
-
const unhandledError = new UnhandledChildStepsError(name, unsettledChildren.length
|
|
572
|
+
const unhandledError = new UnhandledChildStepsError(name, unsettledChildren.length, {
|
|
573
|
+
stepId: step.id,
|
|
574
|
+
parentStepId,
|
|
575
|
+
jobId: this.#jobId,
|
|
576
|
+
actionName: this.#actionName,
|
|
577
|
+
})
|
|
478
578
|
childAbortController.abort(unhandledError)
|
|
479
579
|
|
|
480
580
|
// Wait for all children to settle (they'll reject with cancellation)
|
|
@@ -523,16 +623,46 @@ export class StepManager {
|
|
|
523
623
|
if (
|
|
524
624
|
isNonRetriableError(error) ||
|
|
525
625
|
(error.cause && isNonRetriableError(error.cause)) ||
|
|
526
|
-
(error instanceof Error && error.name === 'AbortError'
|
|
626
|
+
(error instanceof Error && error.name === 'AbortError')
|
|
527
627
|
) {
|
|
528
|
-
|
|
628
|
+
const err = isNonRetriableError(error)
|
|
629
|
+
? error
|
|
630
|
+
: error instanceof Error && error.name === 'AbortError'
|
|
631
|
+
? new NonRetriableError(error.message, { cause: error.cause })
|
|
632
|
+
: (error.cause as NonRetriableError)
|
|
633
|
+
|
|
634
|
+
if (Object.keys(err.metadata).length === 0) {
|
|
635
|
+
err.setMetadata({
|
|
636
|
+
stepId: step?.id,
|
|
637
|
+
parentStepId,
|
|
638
|
+
jobId: this.#jobId,
|
|
639
|
+
actionName: this.#actionName,
|
|
640
|
+
})
|
|
641
|
+
}
|
|
642
|
+
throw err
|
|
529
643
|
}
|
|
530
644
|
|
|
531
645
|
if (ctx.retriesLeft > 0 && step) {
|
|
646
|
+
this.#clearHistoryForStep(step.id)
|
|
532
647
|
const delayed = await this.#stepStore.delay(step.id, ctx.finalDelay, serializeError(error))
|
|
533
648
|
if (!delayed) {
|
|
534
649
|
throw new Error(`Failed to delay step "${name}" for job "${this.#jobId}" action "${this.#actionName}"`)
|
|
535
650
|
}
|
|
651
|
+
} else {
|
|
652
|
+
if (isTimeoutError(error)) {
|
|
653
|
+
;(error as any).nonRetriable = true
|
|
654
|
+
throw error
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const errorMessage = error instanceof Error ? error.message : String(error)
|
|
658
|
+
const err = new NonRetriableError(errorMessage, { cause: error })
|
|
659
|
+
err.setMetadata({
|
|
660
|
+
stepId: step?.id,
|
|
661
|
+
parentStepId,
|
|
662
|
+
jobId: this.#jobId,
|
|
663
|
+
actionName: this.#actionName,
|
|
664
|
+
})
|
|
665
|
+
throw err
|
|
536
666
|
}
|
|
537
667
|
},
|
|
538
668
|
}).catch(async (error) => {
|
|
@@ -557,6 +687,19 @@ export class StepManager {
|
|
|
557
687
|
throw error
|
|
558
688
|
})
|
|
559
689
|
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Clear the history of nested steps for a given step.
|
|
693
|
+
* We do't need to clear the history for the root step because it's not a parent step, it's the action itself.
|
|
694
|
+
* @param stepId - The ID of the step to clear the history for
|
|
695
|
+
*/
|
|
696
|
+
#clearHistoryForStep(stepId: string): void {
|
|
697
|
+
this.#historySteps.forEach((stepKey) => {
|
|
698
|
+
if (stepKey.startsWith(stepId)) {
|
|
699
|
+
this.#historySteps.delete(stepKey)
|
|
700
|
+
}
|
|
701
|
+
})
|
|
702
|
+
}
|
|
560
703
|
}
|
|
561
704
|
|
|
562
705
|
// ============================================================================
|
|
@@ -609,6 +752,11 @@ class ActionContext<TInput extends z.ZodObject, TOutput extends z.ZodObject, TVa
|
|
|
609
752
|
}
|
|
610
753
|
this.#input = job.input ?? {}
|
|
611
754
|
this.step = this.step.bind(this)
|
|
755
|
+
this.run = this.run.bind(this)
|
|
756
|
+
// Set the run function factory so inline steps can call step definitions with correct parent
|
|
757
|
+
this.#stepManager.setRunFnFactory((parentStepId, abortSignal) => {
|
|
758
|
+
return (stepDef, input, options) => this.#runInternal(stepDef, input, options, parentStepId, abortSignal)
|
|
759
|
+
})
|
|
612
760
|
}
|
|
613
761
|
|
|
614
762
|
// ============================================================================
|
|
@@ -679,6 +827,7 @@ class ActionContext<TInput extends z.ZodObject, TOutput extends z.ZodObject, TVa
|
|
|
679
827
|
...this.#action.steps,
|
|
680
828
|
...options,
|
|
681
829
|
})
|
|
830
|
+
|
|
682
831
|
return this.#stepManager.push({
|
|
683
832
|
name,
|
|
684
833
|
cb,
|
|
@@ -688,4 +837,77 @@ class ActionContext<TInput extends z.ZodObject, TOutput extends z.ZodObject, TVa
|
|
|
688
837
|
parallel: parsedOptions.parallel, // Pass parallel option
|
|
689
838
|
})
|
|
690
839
|
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* Execute a reusable step definition created with createStep().
|
|
843
|
+
* This is the public method called from action handlers.
|
|
844
|
+
*
|
|
845
|
+
* @param stepDef - The step definition to execute
|
|
846
|
+
* @param input - The input data for the step (validated against the step's input schema)
|
|
847
|
+
* @param options - Optional step configuration overrides
|
|
848
|
+
* @returns Promise resolving to the step result
|
|
849
|
+
*/
|
|
850
|
+
async run<TStepInput extends z.ZodObject, TResult>(
|
|
851
|
+
stepDef: StepDefinition<TStepInput, TResult, TVariables>,
|
|
852
|
+
input: z.input<TStepInput>,
|
|
853
|
+
options: Partial<z.input<typeof StepOptionsSchema>> = {},
|
|
854
|
+
): Promise<TResult> {
|
|
855
|
+
return this.#runInternal(stepDef, input, options, null, this.#abortSignal)
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* Internal method to execute a step definition with explicit parent step ID and abort signal.
|
|
860
|
+
* Used by both the public run method and the run functions passed to step contexts.
|
|
861
|
+
*/
|
|
862
|
+
async #runInternal<TStepInput extends z.ZodObject, TResult>(
|
|
863
|
+
stepDef: StepDefinition<TStepInput, TResult, TVariables>,
|
|
864
|
+
input: z.input<TStepInput>,
|
|
865
|
+
options: Partial<z.input<typeof StepOptionsSchema>> = {},
|
|
866
|
+
parentStepId: string | null,
|
|
867
|
+
abortSignal: AbortSignal,
|
|
868
|
+
): Promise<TResult> {
|
|
869
|
+
// Validate input against the step's schema if provided
|
|
870
|
+
// After parsing, validatedInput is z.output<TStepInput> (same as z.infer<TStepInput>)
|
|
871
|
+
const validatedInput: z.infer<TStepInput> = stepDef.input
|
|
872
|
+
? stepDef.input.parse(input, {
|
|
873
|
+
error: () => 'Error parsing step input',
|
|
874
|
+
reportInput: true,
|
|
875
|
+
})
|
|
876
|
+
: (input as z.infer<TStepInput>)
|
|
877
|
+
|
|
878
|
+
// Resolve step name (static or dynamic)
|
|
879
|
+
const stepName = typeof stepDef.name === 'function' ? stepDef.name({ input: validatedInput }) : stepDef.name
|
|
880
|
+
|
|
881
|
+
// Merge options: action defaults -> step definition -> call-time overrides
|
|
882
|
+
const mergedOptions: z.input<typeof StepOptionsSchema> = {
|
|
883
|
+
...this.#action.steps,
|
|
884
|
+
...(stepDef.retry !== undefined && { retry: stepDef.retry }),
|
|
885
|
+
...(stepDef.expire !== undefined && { expire: stepDef.expire }),
|
|
886
|
+
...(stepDef.parallel !== undefined && { parallel: stepDef.parallel }),
|
|
887
|
+
...options,
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
const parsedOptions = StepOptionsSchema.parse(mergedOptions)
|
|
891
|
+
|
|
892
|
+
// Create a wrapper callback that provides the extended context
|
|
893
|
+
const wrappedCb = async (baseCtx: StepHandlerContext): Promise<TResult> => {
|
|
894
|
+
const extendedCtx: StepDefinitionHandlerContext<TStepInput, TVariables> = {
|
|
895
|
+
...baseCtx,
|
|
896
|
+
input: validatedInput,
|
|
897
|
+
var: this.#variables,
|
|
898
|
+
logger: this.#logger,
|
|
899
|
+
jobId: this.#jobId,
|
|
900
|
+
}
|
|
901
|
+
return stepDef.handler(extendedCtx)
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
return this.#stepManager.push({
|
|
905
|
+
name: stepName,
|
|
906
|
+
cb: wrappedCb,
|
|
907
|
+
options: parsedOptions,
|
|
908
|
+
abortSignal,
|
|
909
|
+
parentStepId,
|
|
910
|
+
parallel: parsedOptions.parallel,
|
|
911
|
+
})
|
|
912
|
+
}
|
|
691
913
|
}
|
package/src/telemetry/adapter.ts
CHANGED
|
@@ -108,6 +108,106 @@ export interface AddSpanAttributeOptions {
|
|
|
108
108
|
value: string | number | boolean
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Options for starting a custom span with the tracer.
|
|
113
|
+
*/
|
|
114
|
+
export interface StartSpanOptions {
|
|
115
|
+
/**
|
|
116
|
+
* Span kind (internal, client, server, producer, consumer).
|
|
117
|
+
* @default 'internal'
|
|
118
|
+
*/
|
|
119
|
+
kind?: 'internal' | 'client' | 'server' | 'producer' | 'consumer'
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Initial attributes for the span.
|
|
123
|
+
*/
|
|
124
|
+
attributes?: Record<string, string | number | boolean>
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Parent span to use for context propagation.
|
|
128
|
+
* If not provided, uses the current active context.
|
|
129
|
+
*/
|
|
130
|
+
parentSpan?: TracerSpan
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* A span created by the Tracer for manual instrumentation.
|
|
135
|
+
*/
|
|
136
|
+
export interface TracerSpan {
|
|
137
|
+
/**
|
|
138
|
+
* Set an attribute on the span.
|
|
139
|
+
*
|
|
140
|
+
* @param key - The attribute key
|
|
141
|
+
* @param value - The attribute value
|
|
142
|
+
*/
|
|
143
|
+
setAttribute(key: string, value: string | number | boolean): void
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Set multiple attributes on the span.
|
|
147
|
+
*
|
|
148
|
+
* @param attributes - The attributes to set
|
|
149
|
+
*/
|
|
150
|
+
setAttributes(attributes: Record<string, string | number | boolean>): void
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Add an event to the span.
|
|
154
|
+
*
|
|
155
|
+
* @param name - The event name
|
|
156
|
+
* @param attributes - Optional event attributes
|
|
157
|
+
*/
|
|
158
|
+
addEvent(name: string, attributes?: Record<string, string | number | boolean>): void
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Record an exception on the span.
|
|
162
|
+
*
|
|
163
|
+
* @param error - The error to record
|
|
164
|
+
*/
|
|
165
|
+
recordException(error: Error): void
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Set the span status to OK.
|
|
169
|
+
*/
|
|
170
|
+
setStatusOk(): void
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Set the span status to error.
|
|
174
|
+
*
|
|
175
|
+
* @param message - Optional error message
|
|
176
|
+
*/
|
|
177
|
+
setStatusError(message?: string): void
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* End the span.
|
|
181
|
+
* After calling this, no more operations can be performed on the span.
|
|
182
|
+
*/
|
|
183
|
+
end(): void
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Check if this span is recording.
|
|
187
|
+
*/
|
|
188
|
+
isRecording(): boolean
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* A Tracer provides methods for creating spans.
|
|
193
|
+
* Similar to OpenTelemetry's Tracer interface.
|
|
194
|
+
*/
|
|
195
|
+
export interface Tracer {
|
|
196
|
+
/**
|
|
197
|
+
* The name of this tracer.
|
|
198
|
+
*/
|
|
199
|
+
readonly name: string
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Start a new span.
|
|
203
|
+
*
|
|
204
|
+
* @param name - The name of the span
|
|
205
|
+
* @param options - Optional span configuration
|
|
206
|
+
* @returns A TracerSpan for manual instrumentation
|
|
207
|
+
*/
|
|
208
|
+
startSpan(name: string, options?: StartSpanOptions): TracerSpan
|
|
209
|
+
}
|
|
210
|
+
|
|
111
211
|
/**
|
|
112
212
|
* Observe context provided to action and step handlers.
|
|
113
213
|
*/
|
|
@@ -136,6 +236,37 @@ export interface ObserveContext {
|
|
|
136
236
|
* @param attributes - Optional event attributes
|
|
137
237
|
*/
|
|
138
238
|
addSpanEvent(name: string, attributes?: Record<string, any>): void
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Get a tracer for manual instrumentation.
|
|
242
|
+
* Similar to OpenTelemetry's `trace.getTracer()` method.
|
|
243
|
+
*
|
|
244
|
+
* @param name - The name of the tracer (typically your service or library name)
|
|
245
|
+
* @returns A Tracer for creating custom spans
|
|
246
|
+
*
|
|
247
|
+
* @example
|
|
248
|
+
* ```typescript
|
|
249
|
+
* const tracer = ctx.observe.getTracer('my-service')
|
|
250
|
+
*
|
|
251
|
+
* const span = tracer.startSpan('external-api-call', {
|
|
252
|
+
* kind: 'client',
|
|
253
|
+
* attributes: { 'api.endpoint': '/users' }
|
|
254
|
+
* })
|
|
255
|
+
*
|
|
256
|
+
* try {
|
|
257
|
+
* const result = await fetch('https://api.example.com/users')
|
|
258
|
+
* span.setStatusOk()
|
|
259
|
+
* return result
|
|
260
|
+
* } catch (error) {
|
|
261
|
+
* span.recordException(error)
|
|
262
|
+
* span.setStatusError(error.message)
|
|
263
|
+
* throw error
|
|
264
|
+
* } finally {
|
|
265
|
+
* span.end()
|
|
266
|
+
* }
|
|
267
|
+
* ```
|
|
268
|
+
*/
|
|
269
|
+
getTracer(name: string): Tracer
|
|
139
270
|
}
|
|
140
271
|
|
|
141
272
|
// ============================================================================
|
|
@@ -369,6 +500,41 @@ export abstract class TelemetryAdapter {
|
|
|
369
500
|
return this._addSpanAttribute(options)
|
|
370
501
|
}
|
|
371
502
|
|
|
503
|
+
// ============================================================================
|
|
504
|
+
// Tracer Methods
|
|
505
|
+
// ============================================================================
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Get a tracer for manual instrumentation.
|
|
509
|
+
* Similar to OpenTelemetry's `trace.getTracer()` method.
|
|
510
|
+
*
|
|
511
|
+
* @param name - The name of the tracer (typically your service or library name)
|
|
512
|
+
* @returns A Tracer for creating custom spans
|
|
513
|
+
*
|
|
514
|
+
* @example
|
|
515
|
+
* ```typescript
|
|
516
|
+
* const tracer = telemetry.getTracer('my-service')
|
|
517
|
+
*
|
|
518
|
+
* const span = tracer.startSpan('process-order', {
|
|
519
|
+
* attributes: { 'order.id': orderId }
|
|
520
|
+
* })
|
|
521
|
+
*
|
|
522
|
+
* try {
|
|
523
|
+
* // Do some work
|
|
524
|
+
* span.addEvent('order.validated')
|
|
525
|
+
* span.setStatusOk()
|
|
526
|
+
* } catch (error) {
|
|
527
|
+
* span.recordException(error)
|
|
528
|
+
* span.setStatusError(error.message)
|
|
529
|
+
* } finally {
|
|
530
|
+
* span.end()
|
|
531
|
+
* }
|
|
532
|
+
* ```
|
|
533
|
+
*/
|
|
534
|
+
getTracer(name: string): Tracer {
|
|
535
|
+
return this._getTracer(name)
|
|
536
|
+
}
|
|
537
|
+
|
|
372
538
|
// ============================================================================
|
|
373
539
|
// Context Methods
|
|
374
540
|
// ============================================================================
|
|
@@ -404,6 +570,9 @@ export abstract class TelemetryAdapter {
|
|
|
404
570
|
this.#logger?.error(err, 'Error adding span event')
|
|
405
571
|
})
|
|
406
572
|
},
|
|
573
|
+
getTracer: (name: string) => {
|
|
574
|
+
return this.getTracer(name)
|
|
575
|
+
},
|
|
407
576
|
}
|
|
408
577
|
}
|
|
409
578
|
|
|
@@ -465,4 +634,9 @@ export abstract class TelemetryAdapter {
|
|
|
465
634
|
* Internal method to add a span attribute.
|
|
466
635
|
*/
|
|
467
636
|
protected abstract _addSpanAttribute(options: AddSpanAttributeOptions): Promise<void>
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Internal method to get a tracer for manual instrumentation.
|
|
640
|
+
*/
|
|
641
|
+
protected abstract _getTracer(name: string): Tracer
|
|
468
642
|
}
|
package/src/telemetry/index.ts
CHANGED
|
@@ -9,8 +9,11 @@ export {
|
|
|
9
9
|
type Span,
|
|
10
10
|
type StartDatabaseSpanOptions,
|
|
11
11
|
type StartJobSpanOptions,
|
|
12
|
+
type StartSpanOptions,
|
|
12
13
|
type StartStepSpanOptions,
|
|
13
14
|
TelemetryAdapter,
|
|
15
|
+
type Tracer,
|
|
16
|
+
type TracerSpan,
|
|
14
17
|
} from './adapter.js'
|
|
15
18
|
export { LocalTelemetryAdapter, type LocalTelemetryAdapterOptions, localTelemetryAdapter } from './local.js'
|
|
16
19
|
export { NoopTelemetryAdapter, noopTelemetryAdapter } from './noop.js'
|
package/src/telemetry/local.ts
CHANGED
|
@@ -7,8 +7,11 @@ import {
|
|
|
7
7
|
type Span,
|
|
8
8
|
type StartDatabaseSpanOptions,
|
|
9
9
|
type StartJobSpanOptions,
|
|
10
|
+
type StartSpanOptions,
|
|
10
11
|
type StartStepSpanOptions,
|
|
11
12
|
TelemetryAdapter,
|
|
13
|
+
type Tracer,
|
|
14
|
+
type TracerSpan,
|
|
12
15
|
} from './adapter.js'
|
|
13
16
|
|
|
14
17
|
// ============================================================================
|
|
@@ -304,6 +307,96 @@ export class LocalTelemetryAdapter extends TelemetryAdapter {
|
|
|
304
307
|
},
|
|
305
308
|
})
|
|
306
309
|
}
|
|
310
|
+
|
|
311
|
+
// ============================================================================
|
|
312
|
+
// Tracer Methods
|
|
313
|
+
// ============================================================================
|
|
314
|
+
|
|
315
|
+
protected _getTracer(name: string): Tracer {
|
|
316
|
+
const adapter = this
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
name,
|
|
320
|
+
|
|
321
|
+
startSpan(spanName: string, options?: StartSpanOptions): TracerSpan {
|
|
322
|
+
const spanId = `tracer:${name}:${globalThis.crypto.randomUUID()}`
|
|
323
|
+
const startTime = Date.now()
|
|
324
|
+
let ended = false
|
|
325
|
+
const attributes: Record<string, string | number | boolean> = {
|
|
326
|
+
...options?.attributes,
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Note: Local adapter tracer spans don't have a jobId context,
|
|
330
|
+
// so they can't be stored in the database. They're essentially no-ops
|
|
331
|
+
// but provide a consistent API for code that needs a tracer.
|
|
332
|
+
// For actual metrics storage, use ctx.observe within action/step handlers.
|
|
333
|
+
|
|
334
|
+
const tracerSpan: TracerSpan = {
|
|
335
|
+
setAttribute(key: string, value: string | number | boolean): void {
|
|
336
|
+
if (!ended) {
|
|
337
|
+
attributes[key] = value
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
|
|
341
|
+
setAttributes(attrs: Record<string, string | number | boolean>): void {
|
|
342
|
+
if (!ended) {
|
|
343
|
+
Object.assign(attributes, attrs)
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
|
|
347
|
+
addEvent(eventName: string, eventAttrs?: Record<string, string | number | boolean>): void {
|
|
348
|
+
if (!ended) {
|
|
349
|
+
adapter.logger?.debug({ spanId, event: eventName, attributes: eventAttrs }, 'Tracer span event')
|
|
350
|
+
}
|
|
351
|
+
},
|
|
352
|
+
|
|
353
|
+
recordException(error: Error): void {
|
|
354
|
+
if (!ended) {
|
|
355
|
+
attributes['error.message'] = error.message
|
|
356
|
+
attributes['error.name'] = error.name
|
|
357
|
+
adapter.logger?.debug({ spanId, error: error.message }, 'Tracer span exception')
|
|
358
|
+
}
|
|
359
|
+
},
|
|
360
|
+
|
|
361
|
+
setStatusOk(): void {
|
|
362
|
+
if (!ended) {
|
|
363
|
+
// biome-ignore lint/complexity/useLiteralKeys: Index signature requires bracket notation
|
|
364
|
+
attributes['status'] = 'ok'
|
|
365
|
+
}
|
|
366
|
+
},
|
|
367
|
+
|
|
368
|
+
setStatusError(message?: string): void {
|
|
369
|
+
if (!ended) {
|
|
370
|
+
// biome-ignore lint/complexity/useLiteralKeys: Index signature requires bracket notation
|
|
371
|
+
attributes['status'] = 'error'
|
|
372
|
+
if (message) {
|
|
373
|
+
attributes['status.message'] = message
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
},
|
|
377
|
+
|
|
378
|
+
end(): void {
|
|
379
|
+
if (!ended) {
|
|
380
|
+
ended = true
|
|
381
|
+
const duration = Date.now() - startTime
|
|
382
|
+
adapter.logger?.debug(
|
|
383
|
+
{ spanId, spanName, tracerName: name, durationMs: duration, attributes },
|
|
384
|
+
'Tracer span ended',
|
|
385
|
+
)
|
|
386
|
+
}
|
|
387
|
+
},
|
|
388
|
+
|
|
389
|
+
isRecording(): boolean {
|
|
390
|
+
return !ended
|
|
391
|
+
},
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
adapter.logger?.debug({ spanId, spanName, tracerName: name }, 'Tracer span started')
|
|
395
|
+
|
|
396
|
+
return tracerSpan
|
|
397
|
+
},
|
|
398
|
+
}
|
|
399
|
+
}
|
|
307
400
|
}
|
|
308
401
|
|
|
309
402
|
/**
|