duron 0.3.0-beta.11 → 0.3.0-beta.13
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 +2 -2
- package/dist/action-job.d.ts.map +1 -1
- package/dist/action-job.js +30 -24
- package/dist/action-manager.d.ts +2 -2
- package/dist/action-manager.d.ts.map +1 -1
- package/dist/action-manager.js +3 -3
- package/dist/action.d.ts +7 -7
- package/dist/action.d.ts.map +1 -1
- package/dist/adapters/adapter.d.ts +24 -26
- package/dist/adapters/adapter.d.ts.map +1 -1
- package/dist/adapters/adapter.js +25 -27
- package/dist/adapters/postgres/base.d.ts +21 -9
- package/dist/adapters/postgres/base.d.ts.map +1 -1
- package/dist/adapters/postgres/base.js +157 -62
- package/dist/adapters/postgres/schema.d.ts +118 -35
- package/dist/adapters/postgres/schema.d.ts.map +1 -1
- package/dist/adapters/postgres/schema.default.d.ts +119 -36
- 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 +45 -22
- package/dist/adapters/schemas.d.ts +98 -80
- package/dist/adapters/schemas.d.ts.map +1 -1
- package/dist/adapters/schemas.js +59 -26
- package/dist/client.d.ts +106 -24
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +90 -30
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/server.d.ts +51 -41
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +27 -27
- package/dist/step-manager.d.ts +30 -6
- package/dist/step-manager.d.ts.map +1 -1
- package/dist/step-manager.js +124 -85
- package/dist/telemetry/index.d.ts +1 -4
- package/dist/telemetry/index.d.ts.map +1 -1
- package/dist/telemetry/index.js +2 -5
- 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/migrations/postgres/{20260119153838_flimsy_thor_girl → 20260120154151_mean_magdalene}/migration.sql +27 -19
- package/migrations/postgres/{20260119153838_flimsy_thor_girl → 20260120154151_mean_magdalene}/snapshot.json +172 -65
- package/package.json +7 -2
- package/src/action-job.ts +32 -28
- package/src/action-manager.ts +5 -5
- package/src/action.ts +7 -7
- package/src/adapters/adapter.ts +54 -54
- package/src/adapters/postgres/base.ts +201 -70
- package/src/adapters/postgres/schema.default.ts +2 -2
- package/src/adapters/postgres/schema.ts +47 -23
- package/src/adapters/schemas.ts +72 -35
- package/src/client.ts +195 -42
- package/src/index.ts +1 -0
- package/src/server.ts +37 -37
- package/src/step-manager.ts +170 -86
- package/src/telemetry/index.ts +2 -20
- package/src/telemetry/local-span-exporter.ts +148 -0
- 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/dist/step-manager.js
CHANGED
|
@@ -1,42 +1,84 @@
|
|
|
1
|
+
import { context, SpanKind, SpanStatusCode, trace, } from '@opentelemetry/api';
|
|
1
2
|
import fastq from 'fastq';
|
|
2
3
|
import { StepOptionsSchema, } from './action.js';
|
|
3
4
|
import { STEP_STATUS_CANCELLED, STEP_STATUS_COMPLETED, STEP_STATUS_FAILED } from './constants.js';
|
|
4
5
|
import { ActionCancelError, isCancelError, isNonRetriableError, isTimeoutError, NonRetriableError, StepAlreadyExecutedError, StepTimeoutError, serializeError, UnhandledChildStepsError, } from './errors.js';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Inject parent span into a context if we have one.
|
|
8
|
+
*/
|
|
9
|
+
function injectParentSpan(ctx, parentSpan) {
|
|
10
|
+
return parentSpan ? trace.setSpan(ctx, parentSpan) : ctx;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Create a context-aware tracer wrapper that automatically injects the parent span.
|
|
14
|
+
* This ensures spans created by external libraries (like AI SDK) are properly linked
|
|
15
|
+
* to the current job/step trace hierarchy.
|
|
16
|
+
*/
|
|
17
|
+
function createContextAwareTracer(tracer, parentSpan) {
|
|
18
|
+
return {
|
|
19
|
+
startSpan(name, options, ctx) {
|
|
20
|
+
// Always inject our parent span into the context, regardless of what context is passed.
|
|
21
|
+
// This is necessary because without global registration, context.active() returns
|
|
22
|
+
// ROOT_CONTEXT, so external libraries (like AI SDK) that pass context.active()
|
|
23
|
+
// would otherwise create orphan spans.
|
|
24
|
+
const baseContext = ctx ?? context.active();
|
|
25
|
+
const effectiveContext = injectParentSpan(baseContext, parentSpan);
|
|
26
|
+
return tracer.startSpan(name, options, effectiveContext);
|
|
27
|
+
},
|
|
28
|
+
// startActiveSpan has multiple overloads, we need to handle them all
|
|
29
|
+
startActiveSpan(name, optionsOrFn, ctxOrFn, fn) {
|
|
30
|
+
// Parse the overloaded arguments
|
|
31
|
+
let options;
|
|
32
|
+
let ctx;
|
|
33
|
+
let callback;
|
|
34
|
+
if (typeof optionsOrFn === 'function') {
|
|
35
|
+
// startActiveSpan(name, fn)
|
|
36
|
+
callback = optionsOrFn;
|
|
37
|
+
}
|
|
38
|
+
else if (typeof ctxOrFn === 'function') {
|
|
39
|
+
// startActiveSpan(name, options, fn)
|
|
40
|
+
options = optionsOrFn;
|
|
41
|
+
callback = ctxOrFn;
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
// startActiveSpan(name, options, context, fn)
|
|
45
|
+
options = optionsOrFn;
|
|
46
|
+
ctx = ctxOrFn;
|
|
47
|
+
callback = fn;
|
|
48
|
+
}
|
|
49
|
+
const baseContext = ctx ?? context.active();
|
|
50
|
+
const effectiveContext = injectParentSpan(baseContext, parentSpan);
|
|
51
|
+
return tracer.startActiveSpan(name, options ?? {}, effectiveContext, callback);
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Create a TelemetryContext that wraps an OTel span.
|
|
57
|
+
*/
|
|
58
|
+
function createTelemetryContext(span, tracer) {
|
|
59
|
+
return {
|
|
60
|
+
getActiveSpan() {
|
|
61
|
+
return span ?? undefined;
|
|
62
|
+
},
|
|
63
|
+
getTracer(_name) {
|
|
64
|
+
// Return a context-aware tracer that automatically links spans to the current trace
|
|
65
|
+
return createContextAwareTracer(tracer, span);
|
|
66
|
+
},
|
|
67
|
+
startSpan(name, options) {
|
|
68
|
+
// Create a child span linked to the current span (job or step)
|
|
69
|
+
const parentContext = span ? trace.setSpan(context.active(), span) : context.active();
|
|
70
|
+
return tracer.startSpan(name, { attributes: options?.attributes }, parentContext);
|
|
71
|
+
},
|
|
72
|
+
recordMetric(name, value, attributes) {
|
|
73
|
+
if (span) {
|
|
74
|
+
span.addEvent(`metric:${name}`, {
|
|
75
|
+
'metric.value': value,
|
|
76
|
+
...attributes,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
40
82
|
import pRetry from './utils/p-retry.js';
|
|
41
83
|
import waitForAbort from './utils/wait-for-abort.js';
|
|
42
84
|
/**
|
|
@@ -128,7 +170,7 @@ export class StepManager {
|
|
|
128
170
|
#jobId;
|
|
129
171
|
#actionName;
|
|
130
172
|
#stepStore;
|
|
131
|
-
#
|
|
173
|
+
#tracer;
|
|
132
174
|
#queue;
|
|
133
175
|
#logger;
|
|
134
176
|
// each step name should be executed only once per parent (name + parentStepId)
|
|
@@ -136,7 +178,7 @@ export class StepManager {
|
|
|
136
178
|
// Store step spans for nested step tracking
|
|
137
179
|
#stepSpans = new Map();
|
|
138
180
|
// Store the job span for creating step spans
|
|
139
|
-
#jobSpan
|
|
181
|
+
#jobSpan;
|
|
140
182
|
// Factory function to create run functions with the correct parent step ID and abort signal
|
|
141
183
|
#runFnFactory = null;
|
|
142
184
|
// ============================================================================
|
|
@@ -151,7 +193,7 @@ export class StepManager {
|
|
|
151
193
|
this.#jobId = options.jobId;
|
|
152
194
|
this.#actionName = options.actionName;
|
|
153
195
|
this.#logger = options.logger;
|
|
154
|
-
this.#
|
|
196
|
+
this.#tracer = options.tracer;
|
|
155
197
|
this.#stepStore = new StepStore(options.adapter);
|
|
156
198
|
this.#queue = fastq.promise(async (task) => {
|
|
157
199
|
// Create composite key: name + parentStepId (allows same name under different parents)
|
|
@@ -191,39 +233,22 @@ export class StepManager {
|
|
|
191
233
|
* @param variables - Variables available to the action
|
|
192
234
|
* @param abortSignal - Abort signal for cancelling the action
|
|
193
235
|
* @param logger - Pino child logger for this job
|
|
194
|
-
* @param observeContext - Observability context for telemetry
|
|
195
236
|
* @returns ActionHandlerContext instance
|
|
196
237
|
*/
|
|
197
|
-
createActionContext(job, action, variables, abortSignal, logger
|
|
198
|
-
|
|
238
|
+
createActionContext(job, action, variables, abortSignal, logger) {
|
|
239
|
+
const telemetryContext = createTelemetryContext(this.#jobSpan, this.#tracer);
|
|
240
|
+
return new ActionContext(this, job, action, variables, abortSignal, logger, telemetryContext);
|
|
199
241
|
}
|
|
200
242
|
/**
|
|
201
|
-
* Create
|
|
243
|
+
* Create a telemetry context for a step.
|
|
202
244
|
*/
|
|
203
|
-
|
|
245
|
+
createStepTelemetryContext(stepId) {
|
|
204
246
|
const stepSpan = this.#stepSpans.get(stepId);
|
|
205
247
|
if (stepSpan) {
|
|
206
|
-
return
|
|
248
|
+
return createTelemetryContext(stepSpan, this.#tracer);
|
|
207
249
|
}
|
|
208
250
|
// Fallback to job span if step span not found
|
|
209
|
-
|
|
210
|
-
return this.#telemetry.createObserveContext(this.#jobId, stepId, this.#jobSpan);
|
|
211
|
-
}
|
|
212
|
-
// No-op observe context
|
|
213
|
-
return {
|
|
214
|
-
recordMetric: () => {
|
|
215
|
-
// No-op
|
|
216
|
-
},
|
|
217
|
-
addSpanAttribute: () => {
|
|
218
|
-
// No-op
|
|
219
|
-
},
|
|
220
|
-
addSpanEvent: () => {
|
|
221
|
-
// No-op
|
|
222
|
-
},
|
|
223
|
-
getTracer: (name) => {
|
|
224
|
-
return createNoopTracer(name);
|
|
225
|
-
},
|
|
226
|
-
};
|
|
251
|
+
return createTelemetryContext(this.#jobSpan, this.#tracer);
|
|
227
252
|
}
|
|
228
253
|
/**
|
|
229
254
|
* Queue a step task for execution.
|
|
@@ -283,15 +308,18 @@ export class StepManager {
|
|
|
283
308
|
throw new NonRetriableError(`Failed to create step "${name}" for job "${this.#jobId}" action "${this.#actionName}"`, { cause: 'step not created' });
|
|
284
309
|
}
|
|
285
310
|
step = newStep;
|
|
286
|
-
// Start step
|
|
311
|
+
// Start step span - uses no-op tracer if no SDK is configured
|
|
287
312
|
const parentSpan = parentStepId ? this.#stepSpans.get(parentStepId) : this.#jobSpan;
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
313
|
+
const parentContext = parentSpan ? trace.setSpan(context.active(), parentSpan) : context.active();
|
|
314
|
+
const stepSpan = this.#tracer.startSpan(`step:${name}`, {
|
|
315
|
+
kind: SpanKind.INTERNAL,
|
|
316
|
+
attributes: {
|
|
317
|
+
'duron.job.id': this.#jobId,
|
|
318
|
+
'duron.step.id': step.id,
|
|
319
|
+
'duron.step.name': name,
|
|
320
|
+
'duron.step.parent_id': parentStepId ?? undefined,
|
|
321
|
+
},
|
|
322
|
+
}, parentContext);
|
|
295
323
|
this.#stepSpans.set(step.id, stepSpan);
|
|
296
324
|
if (abortSignal.aborted) {
|
|
297
325
|
throw new ActionCancelError(this.#actionName, this.#jobId, { cause: 'step cancelled after create step' });
|
|
@@ -329,14 +357,14 @@ export class StepManager {
|
|
|
329
357
|
// Create abort controller for child steps (used when parent returns with pending children)
|
|
330
358
|
const childAbortController = new AbortController();
|
|
331
359
|
const childSignal = AbortSignal.any([stepSignal, childAbortController.signal]);
|
|
332
|
-
// Create
|
|
333
|
-
const
|
|
360
|
+
// Create telemetry context for this step
|
|
361
|
+
const stepTelemetryContext = this.createStepTelemetryContext(step.id);
|
|
334
362
|
// Create StepHandlerContext with nested step support
|
|
335
363
|
const stepContext = {
|
|
336
364
|
signal: stepSignal,
|
|
337
365
|
stepId: step.id,
|
|
338
366
|
parentStepId,
|
|
339
|
-
|
|
367
|
+
telemetry: stepTelemetryContext,
|
|
340
368
|
step: (childName, childCb, childOptions = {}) => {
|
|
341
369
|
// Inherit parent step options EXCEPT parallel (each step's parallel status is independent)
|
|
342
370
|
const { parallel: _parentParallel, ...inheritableOptions } = options;
|
|
@@ -377,7 +405,10 @@ export class StepManager {
|
|
|
377
405
|
try {
|
|
378
406
|
// Race between abort signal and callback execution
|
|
379
407
|
const abortPromise = waitForAbort(stepSignal);
|
|
380
|
-
|
|
408
|
+
// Execute callback within the span context so that child spans inherit the trace
|
|
409
|
+
const currentStepSpan = step?.id ? this.#stepSpans.get(step.id) : undefined;
|
|
410
|
+
const spanContext = currentStepSpan ? trace.setSpan(context.active(), currentStepSpan) : context.active();
|
|
411
|
+
const callbackPromise = context.with(spanContext, () => cb(stepContext));
|
|
381
412
|
let result = null;
|
|
382
413
|
let aborted = false;
|
|
383
414
|
let callbackError = null;
|
|
@@ -445,10 +476,11 @@ export class StepManager {
|
|
|
445
476
|
if (!completed) {
|
|
446
477
|
throw new Error(`Failed to complete step "${name}" for job "${this.#jobId}" action "${this.#actionName}"`);
|
|
447
478
|
}
|
|
448
|
-
// End step
|
|
479
|
+
// End step span successfully
|
|
449
480
|
const stepSpan = this.#stepSpans.get(step.id);
|
|
450
481
|
if (stepSpan) {
|
|
451
|
-
|
|
482
|
+
stepSpan.setStatus({ code: SpanStatusCode.OK });
|
|
483
|
+
stepSpan.end();
|
|
452
484
|
this.#stepSpans.delete(step.id);
|
|
453
485
|
}
|
|
454
486
|
// Log step completion
|
|
@@ -514,15 +546,22 @@ export class StepManager {
|
|
|
514
546
|
},
|
|
515
547
|
}).catch(async (error) => {
|
|
516
548
|
if (step) {
|
|
517
|
-
// End step
|
|
549
|
+
// End step span with error/cancelled status
|
|
518
550
|
const stepSpan = this.#stepSpans.get(step.id);
|
|
519
551
|
if (stepSpan) {
|
|
520
552
|
if (isCancelError(error)) {
|
|
521
|
-
|
|
553
|
+
stepSpan.setStatus({ code: SpanStatusCode.ERROR, message: 'Step cancelled' });
|
|
522
554
|
}
|
|
523
555
|
else {
|
|
524
|
-
|
|
556
|
+
stepSpan.setStatus({
|
|
557
|
+
code: SpanStatusCode.ERROR,
|
|
558
|
+
message: error instanceof Error ? error.message : String(error),
|
|
559
|
+
});
|
|
560
|
+
if (error instanceof Error) {
|
|
561
|
+
stepSpan.recordException(error);
|
|
562
|
+
}
|
|
525
563
|
}
|
|
564
|
+
stepSpan.end();
|
|
526
565
|
this.#stepSpans.delete(step.id);
|
|
527
566
|
}
|
|
528
567
|
if (isCancelError(error)) {
|
|
@@ -564,11 +603,11 @@ class ActionContext {
|
|
|
564
603
|
#jobId;
|
|
565
604
|
#groupKey = '@default';
|
|
566
605
|
#action;
|
|
567
|
-
#
|
|
606
|
+
#telemetryContext;
|
|
568
607
|
// ============================================================================
|
|
569
608
|
// Constructor
|
|
570
609
|
// ============================================================================
|
|
571
|
-
constructor(stepManager, job, action, variables, abortSignal, logger,
|
|
610
|
+
constructor(stepManager, job, action, variables, abortSignal, logger, telemetryContext) {
|
|
572
611
|
this.#stepManager = stepManager;
|
|
573
612
|
this.#variables = variables;
|
|
574
613
|
this.#abortSignal = abortSignal;
|
|
@@ -576,7 +615,7 @@ class ActionContext {
|
|
|
576
615
|
this.#action = action;
|
|
577
616
|
this.#jobId = job.id;
|
|
578
617
|
this.#groupKey = job.groupKey ?? '@default';
|
|
579
|
-
this.#
|
|
618
|
+
this.#telemetryContext = telemetryContext;
|
|
580
619
|
if (action.input) {
|
|
581
620
|
this.#input = action.input.parse(job.input, {
|
|
582
621
|
error: () => 'Error parsing action input',
|
|
@@ -629,10 +668,10 @@ class ActionContext {
|
|
|
629
668
|
return this.#logger;
|
|
630
669
|
}
|
|
631
670
|
/**
|
|
632
|
-
* Get the
|
|
671
|
+
* Get the telemetry context for recording metrics and span data.
|
|
633
672
|
*/
|
|
634
|
-
get
|
|
635
|
-
return this.#
|
|
673
|
+
get telemetry() {
|
|
674
|
+
return this.#telemetryContext;
|
|
636
675
|
}
|
|
637
676
|
/**
|
|
638
677
|
* Execute a step within the action.
|
|
@@ -1,5 +1,2 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export { LocalTelemetryAdapter, type LocalTelemetryAdapterOptions, localTelemetryAdapter } from './local.js';
|
|
3
|
-
export { NoopTelemetryAdapter, noopTelemetryAdapter } from './noop.js';
|
|
4
|
-
export { OpenTelemetryAdapter, type OpenTelemetryAdapterOptions, openTelemetryAdapter } from './opentelemetry.js';
|
|
1
|
+
export { LocalSpanExporter, type LocalSpanExporterOptions } from './local-span-exporter.js';
|
|
5
2
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/telemetry/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/telemetry/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,KAAK,wBAAwB,EAAE,MAAM,0BAA0B,CAAA"}
|
package/dist/telemetry/index.js
CHANGED
|
@@ -1,5 +1,2 @@
|
|
|
1
|
-
//
|
|
2
|
-
export {
|
|
3
|
-
export { LocalTelemetryAdapter, localTelemetryAdapter } from './local.js';
|
|
4
|
-
export { NoopTelemetryAdapter, noopTelemetryAdapter } from './noop.js';
|
|
5
|
-
export { OpenTelemetryAdapter, openTelemetryAdapter } from './opentelemetry.js';
|
|
1
|
+
// OpenTelemetry-based span exporter for local database persistence
|
|
2
|
+
export { LocalSpanExporter } from './local-span-exporter.js';
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { type ExportResult } from '@opentelemetry/core';
|
|
2
|
+
import type { ReadableSpan, SpanExporter } from '@opentelemetry/sdk-trace-base';
|
|
3
|
+
import type { Adapter } from '../adapters/adapter.js';
|
|
4
|
+
/**
|
|
5
|
+
* Configuration options for the LocalSpanExporter.
|
|
6
|
+
*/
|
|
7
|
+
export interface LocalSpanExporterOptions {
|
|
8
|
+
/**
|
|
9
|
+
* The database adapter to use for storing spans.
|
|
10
|
+
* This is the same adapter used by the Duron Client.
|
|
11
|
+
*/
|
|
12
|
+
adapter: Adapter;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A custom OpenTelemetry SpanExporter that stores spans locally in the database.
|
|
16
|
+
*
|
|
17
|
+
* This exporter converts OpenTelemetry ReadableSpan objects into database records
|
|
18
|
+
* and inserts them via the Duron Adapter interface.
|
|
19
|
+
*
|
|
20
|
+
* It extracts `duron.job.id` and `duron.step.id` from span attributes to link
|
|
21
|
+
* spans to Duron jobs and steps.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```typescript
|
|
25
|
+
* import { LocalSpanExporter } from 'duron/telemetry'
|
|
26
|
+
* import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base'
|
|
27
|
+
*
|
|
28
|
+
* const exporter = new LocalSpanExporter({ adapter })
|
|
29
|
+
* const processor = new BatchSpanProcessor(exporter)
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export declare class LocalSpanExporter implements SpanExporter {
|
|
33
|
+
#private;
|
|
34
|
+
constructor(options: LocalSpanExporterOptions);
|
|
35
|
+
/**
|
|
36
|
+
* Export spans to the local database.
|
|
37
|
+
*
|
|
38
|
+
* Converts ReadableSpan objects to database records and inserts them.
|
|
39
|
+
* Follows OpenTelemetry exporter rules:
|
|
40
|
+
* - Does not throw exceptions
|
|
41
|
+
* - Does not modify received spans
|
|
42
|
+
* - Does not implement queuing/batching (handled by SpanProcessor)
|
|
43
|
+
*/
|
|
44
|
+
export(spans: ReadableSpan[], resultCallback: (result: ExportResult) => void): void;
|
|
45
|
+
/**
|
|
46
|
+
* Shutdown the exporter.
|
|
47
|
+
* After shutdown, no more spans will be exported.
|
|
48
|
+
*/
|
|
49
|
+
shutdown(): Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* Force flush any pending exports.
|
|
52
|
+
* Since we export synchronously to the database, this is a no-op.
|
|
53
|
+
*/
|
|
54
|
+
forceFlush(): Promise<void>;
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=local-span-exporter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local-span-exporter.d.ts","sourceRoot":"","sources":["../../src/telemetry/local-span-exporter.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,YAAY,EAAoB,MAAM,qBAAqB,CAAA;AACzE,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAA;AAE/E,OAAO,KAAK,EAAE,OAAO,EAAqB,MAAM,wBAAwB,CAAA;AAExE;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC;;;OAGG;IACH,OAAO,EAAE,OAAO,CAAA;CACjB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,iBAAkB,YAAW,YAAY;;gBAIxC,OAAO,EAAE,wBAAwB;IAI7C;;;;;;;;OAQG;IACH,MAAM,CAAC,KAAK,EAAE,YAAY,EAAE,EAAE,cAAc,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,IAAI,GAAG,IAAI;IAenF;;;OAGG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAI/B;;;OAGG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;CAoElC"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { ExportResultCode } from '@opentelemetry/core';
|
|
2
|
+
/**
|
|
3
|
+
* A custom OpenTelemetry SpanExporter that stores spans locally in the database.
|
|
4
|
+
*
|
|
5
|
+
* This exporter converts OpenTelemetry ReadableSpan objects into database records
|
|
6
|
+
* and inserts them via the Duron Adapter interface.
|
|
7
|
+
*
|
|
8
|
+
* It extracts `duron.job.id` and `duron.step.id` from span attributes to link
|
|
9
|
+
* spans to Duron jobs and steps.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* import { LocalSpanExporter } from 'duron/telemetry'
|
|
14
|
+
* import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base'
|
|
15
|
+
*
|
|
16
|
+
* const exporter = new LocalSpanExporter({ adapter })
|
|
17
|
+
* const processor = new BatchSpanProcessor(exporter)
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export class LocalSpanExporter {
|
|
21
|
+
#adapter;
|
|
22
|
+
#shutdown = false;
|
|
23
|
+
constructor(options) {
|
|
24
|
+
this.#adapter = options.adapter;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Export spans to the local database.
|
|
28
|
+
*
|
|
29
|
+
* Converts ReadableSpan objects to database records and inserts them.
|
|
30
|
+
* Follows OpenTelemetry exporter rules:
|
|
31
|
+
* - Does not throw exceptions
|
|
32
|
+
* - Does not modify received spans
|
|
33
|
+
* - Does not implement queuing/batching (handled by SpanProcessor)
|
|
34
|
+
*/
|
|
35
|
+
export(spans, resultCallback) {
|
|
36
|
+
if (this.#shutdown) {
|
|
37
|
+
resultCallback({ code: ExportResultCode.FAILED, error: new Error('Exporter has been shutdown') });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
this.#exportSpans(spans)
|
|
41
|
+
.then(() => {
|
|
42
|
+
resultCallback({ code: ExportResultCode.SUCCESS });
|
|
43
|
+
})
|
|
44
|
+
.catch((error) => {
|
|
45
|
+
resultCallback({ code: ExportResultCode.FAILED, error });
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Shutdown the exporter.
|
|
50
|
+
* After shutdown, no more spans will be exported.
|
|
51
|
+
*/
|
|
52
|
+
async shutdown() {
|
|
53
|
+
this.#shutdown = true;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Force flush any pending exports.
|
|
57
|
+
* Since we export synchronously to the database, this is a no-op.
|
|
58
|
+
*/
|
|
59
|
+
async forceFlush() {
|
|
60
|
+
// No-op: we don't buffer spans, they're exported immediately
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Internal method to export spans to the database.
|
|
64
|
+
*/
|
|
65
|
+
async #exportSpans(spans) {
|
|
66
|
+
if (spans.length === 0) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const records = spans.map((span) => this.#readableSpanToRecord(span));
|
|
70
|
+
await this.#adapter.insertSpans(records);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Convert a ReadableSpan to an InsertSpanOptions record.
|
|
74
|
+
*/
|
|
75
|
+
#readableSpanToRecord(span) {
|
|
76
|
+
const spanContext = span.spanContext();
|
|
77
|
+
// Extract Duron-specific attributes
|
|
78
|
+
const jobId = span.attributes['duron.job.id'];
|
|
79
|
+
const stepId = span.attributes['duron.step.id'];
|
|
80
|
+
// Convert attributes to plain object, excluding only job.id and step.id
|
|
81
|
+
// (those are stored in separate columns)
|
|
82
|
+
const attributes = {};
|
|
83
|
+
for (const [key, value] of Object.entries(span.attributes)) {
|
|
84
|
+
if (key !== 'duron.job.id' && key !== 'duron.step.id') {
|
|
85
|
+
attributes[key] = value;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Convert events
|
|
89
|
+
const events = span.events.map((event) => ({
|
|
90
|
+
name: event.name,
|
|
91
|
+
timeUnixNano: this.#hrTimeToNanos(event.time).toString(),
|
|
92
|
+
attributes: event.attributes,
|
|
93
|
+
}));
|
|
94
|
+
// Get parent span ID from parentSpanContext if available
|
|
95
|
+
const parentSpanId = span.parentSpanContext?.spanId || null;
|
|
96
|
+
return {
|
|
97
|
+
traceId: spanContext.traceId,
|
|
98
|
+
spanId: spanContext.spanId,
|
|
99
|
+
parentSpanId,
|
|
100
|
+
jobId: jobId || null,
|
|
101
|
+
stepId: stepId || null,
|
|
102
|
+
name: span.name,
|
|
103
|
+
kind: span.kind,
|
|
104
|
+
startTimeUnixNano: this.#hrTimeToNanos(span.startTime),
|
|
105
|
+
endTimeUnixNano: span.endTime ? this.#hrTimeToNanos(span.endTime) : null,
|
|
106
|
+
statusCode: span.status.code,
|
|
107
|
+
statusMessage: span.status.message || null,
|
|
108
|
+
attributes,
|
|
109
|
+
events,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Convert HrTime [seconds, nanoseconds] to bigint nanoseconds.
|
|
114
|
+
*/
|
|
115
|
+
#hrTimeToNanos(hrTime) {
|
|
116
|
+
return BigInt(hrTime[0]) * BigInt(1_000_000_000) + BigInt(hrTime[1]);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -43,17 +43,23 @@ CREATE TABLE "duron"."jobs" (
|
|
|
43
43
|
CONSTRAINT "jobs_status_check" CHECK ("status" IN ('created','active','completed','failed','cancelled'))
|
|
44
44
|
);
|
|
45
45
|
--> statement-breakpoint
|
|
46
|
-
CREATE TABLE "duron"."
|
|
47
|
-
"id"
|
|
48
|
-
"
|
|
46
|
+
CREATE TABLE "duron"."spans" (
|
|
47
|
+
"id" bigserial PRIMARY KEY,
|
|
48
|
+
"trace_id" text NOT NULL,
|
|
49
|
+
"span_id" text NOT NULL,
|
|
50
|
+
"parent_span_id" text,
|
|
51
|
+
"job_id" uuid,
|
|
49
52
|
"step_id" uuid,
|
|
50
53
|
"name" text NOT NULL,
|
|
51
|
-
"
|
|
54
|
+
"kind" integer DEFAULT 0 NOT NULL,
|
|
55
|
+
"start_time_unix_nano" bigint NOT NULL,
|
|
56
|
+
"end_time_unix_nano" bigint,
|
|
57
|
+
"status_code" integer DEFAULT 0 NOT NULL,
|
|
58
|
+
"status_message" text,
|
|
52
59
|
"attributes" jsonb DEFAULT '{}' NOT NULL,
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
CONSTRAINT "metrics_type_check" CHECK ("type" IN ('metric', 'span_event', 'span_attribute'))
|
|
60
|
+
"events" jsonb DEFAULT '[]' NOT NULL,
|
|
61
|
+
CONSTRAINT "spans_kind_check" CHECK ("kind" IN (0, 1, 2, 3, 4)),
|
|
62
|
+
CONSTRAINT "spans_status_code_check" CHECK ("status_code" IN (0, 1, 2))
|
|
57
63
|
);
|
|
58
64
|
--> statement-breakpoint
|
|
59
65
|
CREATE INDEX "idx_job_steps_job_id" ON "duron"."job_steps" ("job_id");--> statement-breakpoint
|
|
@@ -77,15 +83,17 @@ CREATE INDEX "idx_jobs_action_status" ON "duron"."jobs" ("action_name","status")
|
|
|
77
83
|
CREATE INDEX "idx_jobs_action_group" ON "duron"."jobs" ("action_name","group_key");--> statement-breakpoint
|
|
78
84
|
CREATE INDEX "idx_jobs_input_fts" ON "duron"."jobs" USING gin (to_tsvector('english', "input"::text));--> statement-breakpoint
|
|
79
85
|
CREATE INDEX "idx_jobs_output_fts" ON "duron"."jobs" USING gin (to_tsvector('english', "output"::text));--> statement-breakpoint
|
|
80
|
-
CREATE INDEX "
|
|
81
|
-
CREATE INDEX "
|
|
82
|
-
CREATE INDEX "
|
|
83
|
-
CREATE INDEX "
|
|
84
|
-
CREATE INDEX "
|
|
85
|
-
CREATE INDEX "
|
|
86
|
-
CREATE INDEX "
|
|
87
|
-
CREATE INDEX "
|
|
88
|
-
CREATE INDEX "
|
|
86
|
+
CREATE INDEX "idx_spans_trace_id" ON "duron"."spans" ("trace_id");--> statement-breakpoint
|
|
87
|
+
CREATE INDEX "idx_spans_span_id" ON "duron"."spans" ("span_id");--> statement-breakpoint
|
|
88
|
+
CREATE INDEX "idx_spans_job_id" ON "duron"."spans" ("job_id");--> statement-breakpoint
|
|
89
|
+
CREATE INDEX "idx_spans_step_id" ON "duron"."spans" ("step_id");--> statement-breakpoint
|
|
90
|
+
CREATE INDEX "idx_spans_name" ON "duron"."spans" ("name");--> statement-breakpoint
|
|
91
|
+
CREATE INDEX "idx_spans_kind" ON "duron"."spans" ("kind");--> statement-breakpoint
|
|
92
|
+
CREATE INDEX "idx_spans_status_code" ON "duron"."spans" ("status_code");--> statement-breakpoint
|
|
93
|
+
CREATE INDEX "idx_spans_job_step" ON "duron"."spans" ("job_id","step_id");--> statement-breakpoint
|
|
94
|
+
CREATE INDEX "idx_spans_trace_parent" ON "duron"."spans" ("trace_id","parent_span_id");--> statement-breakpoint
|
|
95
|
+
CREATE INDEX "idx_spans_attributes" ON "duron"."spans" USING gin ("attributes");--> statement-breakpoint
|
|
96
|
+
CREATE INDEX "idx_spans_events" ON "duron"."spans" USING gin ("events");--> statement-breakpoint
|
|
89
97
|
ALTER TABLE "duron"."job_steps" ADD CONSTRAINT "job_steps_job_id_jobs_id_fkey" FOREIGN KEY ("job_id") REFERENCES "duron"."jobs"("id") ON DELETE CASCADE;--> statement-breakpoint
|
|
90
|
-
ALTER TABLE "duron"."
|
|
91
|
-
ALTER TABLE "duron"."
|
|
98
|
+
ALTER TABLE "duron"."spans" ADD CONSTRAINT "spans_job_id_jobs_id_fkey" FOREIGN KEY ("job_id") REFERENCES "duron"."jobs"("id") ON DELETE CASCADE;--> statement-breakpoint
|
|
99
|
+
ALTER TABLE "duron"."spans" ADD CONSTRAINT "spans_step_id_job_steps_id_fkey" FOREIGN KEY ("step_id") REFERENCES "duron"."job_steps"("id") ON DELETE CASCADE;
|