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.
Files changed (91) hide show
  1. package/dist/action-job.d.ts +33 -2
  2. package/dist/action-job.d.ts.map +1 -1
  3. package/dist/action-job.js +93 -26
  4. package/dist/action-manager.d.ts +44 -2
  5. package/dist/action-manager.d.ts.map +1 -1
  6. package/dist/action-manager.js +64 -3
  7. package/dist/action.d.ts +388 -7
  8. package/dist/action.d.ts.map +1 -1
  9. package/dist/action.js +44 -23
  10. package/dist/adapters/adapter.d.ts +365 -8
  11. package/dist/adapters/adapter.d.ts.map +1 -1
  12. package/dist/adapters/adapter.js +221 -15
  13. package/dist/adapters/postgres/base.d.ts +184 -6
  14. package/dist/adapters/postgres/base.d.ts.map +1 -1
  15. package/dist/adapters/postgres/base.js +436 -75
  16. package/dist/adapters/postgres/pglite.d.ts +37 -0
  17. package/dist/adapters/postgres/pglite.d.ts.map +1 -1
  18. package/dist/adapters/postgres/pglite.js +38 -0
  19. package/dist/adapters/postgres/postgres.d.ts +35 -0
  20. package/dist/adapters/postgres/postgres.d.ts.map +1 -1
  21. package/dist/adapters/postgres/postgres.js +42 -0
  22. package/dist/adapters/postgres/schema.d.ts +150 -37
  23. package/dist/adapters/postgres/schema.d.ts.map +1 -1
  24. package/dist/adapters/postgres/schema.default.d.ts +151 -38
  25. package/dist/adapters/postgres/schema.default.d.ts.map +1 -1
  26. package/dist/adapters/postgres/schema.default.js +2 -2
  27. package/dist/adapters/postgres/schema.js +60 -23
  28. package/dist/adapters/schemas.d.ts +124 -80
  29. package/dist/adapters/schemas.d.ts.map +1 -1
  30. package/dist/adapters/schemas.js +139 -26
  31. package/dist/client.d.ts +426 -22
  32. package/dist/client.d.ts.map +1 -1
  33. package/dist/client.js +370 -20
  34. package/dist/constants.js +6 -0
  35. package/dist/errors.d.ts +166 -9
  36. package/dist/errors.d.ts.map +1 -1
  37. package/dist/errors.js +189 -19
  38. package/dist/index.d.ts +2 -1
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/server.d.ts +99 -37
  41. package/dist/server.d.ts.map +1 -1
  42. package/dist/server.js +84 -25
  43. package/dist/step-manager.d.ts +111 -4
  44. package/dist/step-manager.d.ts.map +1 -1
  45. package/dist/step-manager.js +411 -75
  46. package/dist/telemetry/index.d.ts +1 -4
  47. package/dist/telemetry/index.d.ts.map +1 -1
  48. package/dist/telemetry/index.js +2 -4
  49. package/dist/telemetry/local-span-exporter.d.ts +56 -0
  50. package/dist/telemetry/local-span-exporter.d.ts.map +1 -0
  51. package/dist/telemetry/local-span-exporter.js +118 -0
  52. package/dist/utils/p-retry.d.ts +5 -0
  53. package/dist/utils/p-retry.d.ts.map +1 -1
  54. package/dist/utils/p-retry.js +8 -0
  55. package/dist/utils/wait-for-abort.d.ts +1 -0
  56. package/dist/utils/wait-for-abort.d.ts.map +1 -1
  57. package/dist/utils/wait-for-abort.js +1 -0
  58. package/migrations/postgres/{20260119153838_flimsy_thor_girl → 20260121160012_normal_bloodstrike}/migration.sql +32 -20
  59. package/migrations/postgres/{20260119153838_flimsy_thor_girl → 20260121160012_normal_bloodstrike}/snapshot.json +241 -66
  60. package/package.json +42 -26
  61. package/src/action-job.ts +43 -32
  62. package/src/action-manager.ts +5 -5
  63. package/src/action.ts +317 -149
  64. package/src/adapters/adapter.ts +54 -54
  65. package/src/adapters/postgres/base.ts +266 -86
  66. package/src/adapters/postgres/schema.default.ts +2 -2
  67. package/src/adapters/postgres/schema.ts +52 -24
  68. package/src/adapters/schemas.ts +91 -36
  69. package/src/client.ts +322 -68
  70. package/src/errors.ts +141 -30
  71. package/src/index.ts +2 -0
  72. package/src/server.ts +39 -37
  73. package/src/step-manager.ts +254 -91
  74. package/src/telemetry/index.ts +2 -20
  75. package/src/telemetry/local-span-exporter.ts +148 -0
  76. package/dist/telemetry/adapter.d.ts +0 -107
  77. package/dist/telemetry/adapter.d.ts.map +0 -1
  78. package/dist/telemetry/adapter.js +0 -134
  79. package/dist/telemetry/local.d.ts +0 -22
  80. package/dist/telemetry/local.d.ts.map +0 -1
  81. package/dist/telemetry/local.js +0 -243
  82. package/dist/telemetry/noop.d.ts +0 -17
  83. package/dist/telemetry/noop.d.ts.map +0 -1
  84. package/dist/telemetry/noop.js +0 -66
  85. package/dist/telemetry/opentelemetry.d.ts +0 -25
  86. package/dist/telemetry/opentelemetry.d.ts.map +0 -1
  87. package/dist/telemetry/opentelemetry.js +0 -312
  88. package/src/telemetry/adapter.ts +0 -642
  89. package/src/telemetry/local.ts +0 -429
  90. package/src/telemetry/noop.ts +0 -141
  91. package/src/telemetry/opentelemetry.ts +0 -453
@@ -24,7 +24,7 @@ import {
24
24
  type DelayJobStepOptions,
25
25
  type DeleteJobOptions,
26
26
  type DeleteJobsOptions,
27
- type DeleteMetricsOptions,
27
+ type DeleteSpansOptions,
28
28
  type FailJobOptions,
29
29
  type FailJobStepOptions,
30
30
  type FetchOptions,
@@ -33,17 +33,17 @@ import {
33
33
  type GetJobStepsResult,
34
34
  type GetJobsOptions,
35
35
  type GetJobsResult,
36
- type GetMetricsOptions,
37
- type GetMetricsResult,
38
- type InsertMetricOptions,
36
+ type GetSpansOptions,
37
+ type GetSpansResult,
38
+ type InsertSpanOptions,
39
39
  type Job,
40
40
  type JobSort,
41
41
  type JobStatusResult,
42
42
  type JobStep,
43
43
  type JobStepStatusResult,
44
- type MetricSort,
45
44
  type RecoverJobsOptions,
46
45
  type RetryJobOptions,
46
+ type SpanSort,
47
47
  type TimeTravelJobOptions,
48
48
  } from '../adapter.js'
49
49
  import createSchema from './schema.js'
@@ -141,17 +141,28 @@ export class PostgresBaseAdapter<Database extends DrizzleDatabase, Connection> e
141
141
  *
142
142
  * @returns Promise resolving to the job ID, or `null` if creation failed
143
143
  */
144
- protected async _createJob({ queue, groupKey, input, timeoutMs, checksum, concurrencyLimit }: CreateJobOptions) {
144
+ protected async _createJob({
145
+ queue,
146
+ groupKey,
147
+ input,
148
+ timeoutMs,
149
+ checksum,
150
+ concurrencyLimit,
151
+ concurrencyStepLimit,
152
+ description,
153
+ }: CreateJobOptions) {
145
154
  const [result] = await this.db
146
155
  .insert(this.tables.jobsTable)
147
156
  .values({
148
157
  action_name: queue,
149
158
  group_key: groupKey,
159
+ description: description ?? null,
150
160
  checksum,
151
161
  input,
152
162
  status: JOB_STATUS_CREATED,
153
163
  timeout_ms: timeoutMs,
154
164
  concurrency_limit: concurrencyLimit,
165
+ concurrency_step_limit: concurrencyStepLimit,
155
166
  })
156
167
  .returning({ id: this.tables.jobsTable.id })
157
168
 
@@ -254,11 +265,13 @@ export class PostgresBaseAdapter<Database extends DrizzleDatabase, Connection> e
254
265
  SELECT
255
266
  j.action_name,
256
267
  j.group_key,
268
+ j.description,
257
269
  j.checksum,
258
270
  j.input,
259
271
  j.timeout_ms,
260
272
  j.created_at,
261
- j.concurrency_limit
273
+ j.concurrency_limit,
274
+ j.concurrency_step_limit
262
275
  FROM ${this.tables.jobsTable} j
263
276
  WHERE j.id = ${jobId}
264
277
  AND j.status IN (${JOB_STATUS_COMPLETED}, ${JOB_STATUS_CANCELLED}, ${JOB_STATUS_FAILED})
@@ -283,15 +296,18 @@ export class PostgresBaseAdapter<Database extends DrizzleDatabase, Connection> e
283
296
  INSERT INTO ${this.tables.jobsTable} (
284
297
  action_name,
285
298
  group_key,
299
+ description,
286
300
  checksum,
287
301
  input,
288
302
  status,
289
303
  timeout_ms,
290
- concurrency_limit
304
+ concurrency_limit,
305
+ concurrency_step_limit
291
306
  )
292
307
  SELECT
293
308
  ls.action_name,
294
309
  ls.group_key,
310
+ ls.description,
295
311
  ls.checksum,
296
312
  ls.input,
297
313
  ${JOB_STATUS_CREATED},
@@ -307,7 +323,8 @@ export class PostgresBaseAdapter<Database extends DrizzleDatabase, Connection> e
307
323
  LIMIT 1
308
324
  ),
309
325
  ls.concurrency_limit
310
- )
326
+ ),
327
+ ls.concurrency_step_limit
311
328
  FROM locked_source ls
312
329
  WHERE NOT EXISTS (SELECT 1 FROM existing_retry)
313
330
  RETURNING id
@@ -657,6 +674,7 @@ export class PostgresBaseAdapter<Database extends DrizzleDatabase, Connection> e
657
674
  j.id,
658
675
  j.action_name as "actionName",
659
676
  j.group_key as "groupKey",
677
+ j.description,
660
678
  j.input,
661
679
  j.output,
662
680
  j.error,
@@ -667,7 +685,8 @@ export class PostgresBaseAdapter<Database extends DrizzleDatabase, Connection> e
667
685
  j.finished_at as "finishedAt",
668
686
  j.created_at as "createdAt",
669
687
  j.updated_at as "updatedAt",
670
- j.concurrency_limit as "concurrencyLimit"
688
+ j.concurrency_limit as "concurrencyLimit",
689
+ j.concurrency_step_limit as "concurrencyStepLimit"
671
690
  `),
672
691
  )
673
692
 
@@ -788,7 +807,7 @@ export class PostgresBaseAdapter<Database extends DrizzleDatabase, Connection> e
788
807
  step_existed AS (
789
808
  SELECT EXISTS(
790
809
  SELECT 1 FROM ${this.tables.jobStepsTable} s
791
- WHERE s.job_id = ${jobId}
810
+ WHERE s.job_id = ${jobId}
792
811
  AND s.name = ${name}
793
812
  AND s.parent_step_id IS NOT DISTINCT FROM ${parentStepId}
794
813
  ) AS existed
@@ -1011,26 +1030,40 @@ export class PostgresBaseAdapter<Database extends DrizzleDatabase, Connection> e
1011
1030
  * Internal method to get a job by its ID. Does not include step information.
1012
1031
  */
1013
1032
  protected async _getJobById(jobId: string): Promise<Job | null> {
1033
+ const jobsTable = this.tables.jobsTable
1034
+
1035
+ // Calculate duration as a SQL expression (finishedAt - startedAt in milliseconds)
1036
+ const durationMs = sql<number | null>`
1037
+ CASE
1038
+ WHEN ${jobsTable.started_at} IS NOT NULL AND ${jobsTable.finished_at} IS NOT NULL
1039
+ THEN EXTRACT(EPOCH FROM (${jobsTable.finished_at} - ${jobsTable.started_at})) * 1000
1040
+ ELSE NULL
1041
+ END
1042
+ `.as('duration_ms')
1043
+
1014
1044
  const [job] = await this.db
1015
1045
  .select({
1016
- id: this.tables.jobsTable.id,
1017
- actionName: this.tables.jobsTable.action_name,
1018
- groupKey: this.tables.jobsTable.group_key,
1019
- input: this.tables.jobsTable.input,
1020
- output: this.tables.jobsTable.output,
1021
- error: this.tables.jobsTable.error,
1022
- status: this.tables.jobsTable.status,
1023
- timeoutMs: this.tables.jobsTable.timeout_ms,
1024
- expiresAt: this.tables.jobsTable.expires_at,
1025
- startedAt: this.tables.jobsTable.started_at,
1026
- finishedAt: this.tables.jobsTable.finished_at,
1027
- createdAt: this.tables.jobsTable.created_at,
1028
- updatedAt: this.tables.jobsTable.updated_at,
1029
- concurrencyLimit: this.tables.jobsTable.concurrency_limit,
1030
- clientId: this.tables.jobsTable.client_id,
1046
+ id: jobsTable.id,
1047
+ actionName: jobsTable.action_name,
1048
+ groupKey: jobsTable.group_key,
1049
+ description: jobsTable.description,
1050
+ input: jobsTable.input,
1051
+ output: jobsTable.output,
1052
+ error: jobsTable.error,
1053
+ status: jobsTable.status,
1054
+ timeoutMs: jobsTable.timeout_ms,
1055
+ expiresAt: jobsTable.expires_at,
1056
+ startedAt: jobsTable.started_at,
1057
+ finishedAt: jobsTable.finished_at,
1058
+ createdAt: jobsTable.created_at,
1059
+ updatedAt: jobsTable.updated_at,
1060
+ concurrencyLimit: jobsTable.concurrency_limit,
1061
+ concurrencyStepLimit: jobsTable.concurrency_step_limit,
1062
+ clientId: jobsTable.client_id,
1063
+ durationMs,
1031
1064
  })
1032
- .from(this.tables.jobsTable)
1033
- .where(eq(this.tables.jobsTable.id, jobId))
1065
+ .from(jobsTable)
1066
+ .where(eq(jobsTable.id, jobId))
1034
1067
  .limit(1)
1035
1068
 
1036
1069
  return job ?? null
@@ -1117,6 +1150,7 @@ export class PostgresBaseAdapter<Database extends DrizzleDatabase, Connection> e
1117
1150
  filters.clientId
1118
1151
  ? inArray(jobsTable.client_id, Array.isArray(filters.clientId) ? filters.clientId : [filters.clientId])
1119
1152
  : undefined,
1153
+ filters.description ? ilike(jobsTable.description, `%${filters.description}%`) : undefined,
1120
1154
  filters.createdAt && Array.isArray(filters.createdAt)
1121
1155
  ? between(
1122
1156
  sql`date_trunc('second', ${jobsTable.created_at})`,
@@ -1154,6 +1188,7 @@ export class PostgresBaseAdapter<Database extends DrizzleDatabase, Connection> e
1154
1188
  ? or(
1155
1189
  ilike(jobsTable.action_name, `%${fuzzySearch}%`),
1156
1190
  ilike(jobsTable.group_key, `%${fuzzySearch}%`),
1191
+ ilike(jobsTable.description, `%${fuzzySearch}%`),
1157
1192
  ilike(jobsTable.client_id, `%${fuzzySearch}%`),
1158
1193
  sql`${jobsTable.id}::text ilike ${`%${fuzzySearch}%`}`,
1159
1194
  sql`to_tsvector('english', ${jobsTable.input}::text) @@ plainto_tsquery('english', ${fuzzySearch})`,
@@ -1194,6 +1229,15 @@ export class PostgresBaseAdapter<Database extends DrizzleDatabase, Connection> e
1194
1229
  }
1195
1230
  }
1196
1231
 
1232
+ // Calculate duration as a SQL expression (finishedAt - startedAt in milliseconds)
1233
+ const durationMs = sql<number | null>`
1234
+ CASE
1235
+ WHEN ${jobsTable.started_at} IS NOT NULL AND ${jobsTable.finished_at} IS NOT NULL
1236
+ THEN EXTRACT(EPOCH FROM (${jobsTable.finished_at} - ${jobsTable.started_at})) * 1000
1237
+ ELSE NULL
1238
+ END
1239
+ `.as('duration_ms')
1240
+
1197
1241
  const sortFieldMap: Record<JobSort['field'], any> = {
1198
1242
  createdAt: jobsTable.created_at,
1199
1243
  startedAt: jobsTable.started_at,
@@ -1201,6 +1245,8 @@ export class PostgresBaseAdapter<Database extends DrizzleDatabase, Connection> e
1201
1245
  status: jobsTable.status,
1202
1246
  actionName: jobsTable.action_name,
1203
1247
  expiresAt: jobsTable.expires_at,
1248
+ duration: durationMs,
1249
+ description: jobsTable.description,
1204
1250
  }
1205
1251
 
1206
1252
  const jobs = await this.db
@@ -1208,6 +1254,7 @@ export class PostgresBaseAdapter<Database extends DrizzleDatabase, Connection> e
1208
1254
  id: jobsTable.id,
1209
1255
  actionName: jobsTable.action_name,
1210
1256
  groupKey: jobsTable.group_key,
1257
+ description: jobsTable.description,
1211
1258
  input: jobsTable.input,
1212
1259
  output: jobsTable.output,
1213
1260
  error: jobsTable.error,
@@ -1219,7 +1266,9 @@ export class PostgresBaseAdapter<Database extends DrizzleDatabase, Connection> e
1219
1266
  createdAt: jobsTable.created_at,
1220
1267
  updatedAt: jobsTable.updated_at,
1221
1268
  concurrencyLimit: jobsTable.concurrency_limit,
1269
+ concurrencyStepLimit: jobsTable.concurrency_step_limit,
1222
1270
  clientId: jobsTable.client_id,
1271
+ durationMs,
1223
1272
  })
1224
1273
  .from(jobsTable)
1225
1274
  .where(where)
@@ -1362,117 +1411,248 @@ export class PostgresBaseAdapter<Database extends DrizzleDatabase, Connection> e
1362
1411
  // ============================================================================
1363
1412
 
1364
1413
  /**
1365
- * Internal method to insert multiple metric records in a single batch.
1414
+ * Internal method to insert multiple span records in a single batch.
1366
1415
  */
1367
- protected async _insertMetrics(metrics: InsertMetricOptions[]): Promise<number> {
1368
- if (metrics.length === 0) {
1416
+ protected async _insertSpans(spans: InsertSpanOptions[]): Promise<number> {
1417
+ if (spans.length === 0) {
1369
1418
  return 0
1370
1419
  }
1371
1420
 
1372
- const values = metrics.map((m) => ({
1373
- job_id: m.jobId,
1374
- step_id: m.stepId ?? null,
1375
- name: m.name,
1376
- value: m.value,
1377
- attributes: m.attributes ?? {},
1378
- type: m.type,
1421
+ const values = spans.map((s) => ({
1422
+ trace_id: s.traceId,
1423
+ span_id: s.spanId,
1424
+ parent_span_id: s.parentSpanId,
1425
+ job_id: s.jobId,
1426
+ step_id: s.stepId,
1427
+ name: s.name,
1428
+ kind: s.kind,
1429
+ start_time_unix_nano: s.startTimeUnixNano,
1430
+ end_time_unix_nano: s.endTimeUnixNano,
1431
+ status_code: s.statusCode,
1432
+ status_message: s.statusMessage,
1433
+ attributes: s.attributes ?? {},
1434
+ events: s.events ?? [],
1379
1435
  }))
1380
1436
 
1381
1437
  const result = await this.db
1382
- .insert(this.tables.metricsTable)
1438
+ .insert(this.tables.spansTable)
1383
1439
  .values(values)
1384
- .returning({ id: this.tables.metricsTable.id })
1440
+ .returning({ id: this.tables.spansTable.id })
1385
1441
 
1386
1442
  return result.length
1387
1443
  }
1388
1444
 
1389
1445
  /**
1390
- * Internal method to get metrics for a job or step.
1446
+ * Internal method to get spans for a job or step.
1447
+ * For step queries, uses a recursive CTE to find all descendant spans.
1391
1448
  */
1392
- protected async _getMetrics(options: GetMetricsOptions): Promise<GetMetricsResult> {
1393
- const metricsTable = this.tables.metricsTable
1449
+ protected async _getSpans(options: GetSpansOptions): Promise<GetSpansResult> {
1450
+ const spansTable = this.tables.spansTable
1394
1451
  const filters = options.filters ?? {}
1395
1452
 
1396
- // Build WHERE clause
1397
- const where = this._buildMetricsWhereClause(options.jobId, options.stepId, filters)
1398
-
1399
1453
  // Build sort
1400
- const sortInput = options.sort ?? { field: 'timestamp', order: 'desc' }
1401
- const sortFieldMap: Record<MetricSort['field'], any> = {
1402
- name: metricsTable.name,
1403
- value: metricsTable.value,
1404
- timestamp: metricsTable.timestamp,
1405
- createdAt: metricsTable.created_at,
1454
+ const sortInput = options.sort ?? { field: 'startTimeUnixNano', order: 'asc' }
1455
+ const sortFieldMap: Record<SpanSort['field'], string> = {
1456
+ name: 'name',
1457
+ startTimeUnixNano: 'start_time_unix_nano',
1458
+ endTimeUnixNano: 'end_time_unix_nano',
1459
+ }
1460
+ const sortField = sortFieldMap[sortInput.field]
1461
+ const sortOrder = sortInput.order === 'asc' ? 'ASC' : 'DESC'
1462
+
1463
+ // For step queries, use a recursive CTE to get descendant spans
1464
+ if (options.stepId) {
1465
+ return this._getStepSpansRecursive(options.stepId, sortField, sortOrder, filters)
1406
1466
  }
1407
1467
 
1468
+ // Build WHERE clause for job queries
1469
+ const where = this._buildSpansWhereClause(options.jobId, undefined, filters)
1470
+
1408
1471
  // Get total count
1409
- const total = await this.db.$count(metricsTable, where)
1472
+ const total = await this.db.$count(spansTable, where)
1410
1473
  if (!total) {
1411
1474
  return {
1412
- metrics: [],
1475
+ spans: [],
1413
1476
  total: 0,
1414
1477
  }
1415
1478
  }
1416
1479
 
1417
- const sortField = sortFieldMap[sortInput.field]
1418
- const orderByClause = sortInput.order === 'asc' ? asc(sortField) : desc(sortField)
1480
+ const sortFieldColumn = sortFieldMap[sortInput.field]
1481
+ const orderByClause =
1482
+ sortInput.order === 'asc'
1483
+ ? asc(spansTable[sortFieldColumn as keyof typeof spansTable] as any)
1484
+ : desc(spansTable[sortFieldColumn as keyof typeof spansTable] as any)
1419
1485
 
1420
- const metrics = await this.db
1486
+ const rows = await this.db
1421
1487
  .select({
1422
- id: metricsTable.id,
1423
- jobId: metricsTable.job_id,
1424
- stepId: metricsTable.step_id,
1425
- name: metricsTable.name,
1426
- value: metricsTable.value,
1427
- attributes: metricsTable.attributes,
1428
- type: metricsTable.type,
1429
- timestamp: metricsTable.timestamp,
1430
- createdAt: metricsTable.created_at,
1488
+ id: spansTable.id,
1489
+ traceId: spansTable.trace_id,
1490
+ spanId: spansTable.span_id,
1491
+ parentSpanId: spansTable.parent_span_id,
1492
+ jobId: spansTable.job_id,
1493
+ stepId: spansTable.step_id,
1494
+ name: spansTable.name,
1495
+ kind: spansTable.kind,
1496
+ startTimeUnixNano: spansTable.start_time_unix_nano,
1497
+ endTimeUnixNano: spansTable.end_time_unix_nano,
1498
+ statusCode: spansTable.status_code,
1499
+ statusMessage: spansTable.status_message,
1500
+ attributes: spansTable.attributes,
1501
+ events: spansTable.events,
1431
1502
  })
1432
- .from(metricsTable)
1503
+ .from(spansTable)
1433
1504
  .where(where)
1434
1505
  .orderBy(orderByClause)
1435
1506
 
1507
+ // Cast kind and statusCode to proper types, convert BigInt to string for JSON serialization
1508
+ const spans = rows.map((row) => ({
1509
+ ...row,
1510
+ kind: row.kind as 0 | 1 | 2 | 3 | 4,
1511
+ statusCode: row.statusCode as 0 | 1 | 2,
1512
+ // Convert BigInt to string for JSON serialization
1513
+ startTimeUnixNano: row.startTimeUnixNano?.toString() ?? null,
1514
+ endTimeUnixNano: row.endTimeUnixNano?.toString() ?? null,
1515
+ }))
1516
+
1436
1517
  return {
1437
- metrics,
1518
+ spans,
1438
1519
  total,
1439
1520
  }
1440
1521
  }
1441
1522
 
1442
1523
  /**
1443
- * Internal method to delete all metrics for a job.
1524
+ * Get spans for a step using a recursive CTE to traverse the span hierarchy.
1525
+ * This returns the step's span and all its descendant spans (children, grandchildren, etc.)
1526
+ */
1527
+ protected async _getStepSpansRecursive(
1528
+ stepId: string,
1529
+ sortField: string,
1530
+ sortOrder: string,
1531
+ _filters?: GetSpansOptions['filters'],
1532
+ ): Promise<GetSpansResult> {
1533
+ const schemaName = this.schema
1534
+
1535
+ // Use a recursive CTE to find all descendant spans
1536
+ // 1. Base case: find the span with step_id = stepId
1537
+ // 2. Recursive case: find all spans where parent_span_id = span_id of a span we've already found
1538
+ const query = sql`
1539
+ WITH RECURSIVE span_tree AS (
1540
+ -- Base case: the span(s) for the step
1541
+ SELECT * FROM ${sql.identifier(schemaName)}.spans WHERE step_id = ${stepId}::uuid
1542
+ UNION ALL
1543
+ -- Recursive case: children of spans we've found
1544
+ SELECT s.* FROM ${sql.identifier(schemaName)}.spans s
1545
+ INNER JOIN span_tree st ON s.parent_span_id = st.span_id
1546
+ )
1547
+ SELECT
1548
+ id,
1549
+ trace_id as "traceId",
1550
+ span_id as "spanId",
1551
+ parent_span_id as "parentSpanId",
1552
+ job_id as "jobId",
1553
+ step_id as "stepId",
1554
+ name,
1555
+ kind,
1556
+ start_time_unix_nano as "startTimeUnixNano",
1557
+ end_time_unix_nano as "endTimeUnixNano",
1558
+ status_code as "statusCode",
1559
+ status_message as "statusMessage",
1560
+ attributes,
1561
+ events
1562
+ FROM span_tree
1563
+ ORDER BY ${sql.identifier(sortField)} ${sql.raw(sortOrder)}
1564
+ `
1565
+
1566
+ // Raw SQL returns numeric types as strings, so we type them as such
1567
+ const rows = (await this.db.execute(query)) as unknown as Array<{
1568
+ id: string | number
1569
+ traceId: string
1570
+ spanId: string
1571
+ parentSpanId: string | null
1572
+ jobId: string | null
1573
+ stepId: string | null
1574
+ name: string
1575
+ kind: string | number
1576
+ startTimeUnixNano: string | bigint | null
1577
+ endTimeUnixNano: string | bigint | null
1578
+ statusCode: string | number
1579
+ statusMessage: string | null
1580
+ attributes: Record<string, any>
1581
+ events: Array<{ name: string; timeUnixNano: string; attributes?: Record<string, any> }>
1582
+ }>
1583
+
1584
+ // Convert types: raw SQL returns numeric types as strings
1585
+ const spans = rows.map((row) => ({
1586
+ ...row,
1587
+ // Convert id to number (bigserial comes as string from raw SQL)
1588
+ id: typeof row.id === 'string' ? Number.parseInt(row.id, 10) : row.id,
1589
+ // Convert kind and statusCode to proper types
1590
+ kind: (typeof row.kind === 'string' ? Number.parseInt(row.kind, 10) : row.kind) as 0 | 1 | 2 | 3 | 4,
1591
+ statusCode: (typeof row.statusCode === 'string' ? Number.parseInt(row.statusCode, 10) : row.statusCode) as
1592
+ | 0
1593
+ | 1
1594
+ | 2,
1595
+ // Convert BigInt to string for JSON serialization
1596
+ startTimeUnixNano: row.startTimeUnixNano?.toString() ?? null,
1597
+ endTimeUnixNano: row.endTimeUnixNano?.toString() ?? null,
1598
+ }))
1599
+
1600
+ return {
1601
+ spans,
1602
+ total: spans.length,
1603
+ }
1604
+ }
1605
+
1606
+ /**
1607
+ * Internal method to delete all spans for a job.
1444
1608
  */
1445
- protected async _deleteMetrics(options: DeleteMetricsOptions): Promise<number> {
1609
+ protected async _deleteSpans(options: DeleteSpansOptions): Promise<number> {
1446
1610
  const result = await this.db
1447
- .delete(this.tables.metricsTable)
1448
- .where(eq(this.tables.metricsTable.job_id, options.jobId))
1449
- .returning({ id: this.tables.metricsTable.id })
1611
+ .delete(this.tables.spansTable)
1612
+ .where(eq(this.tables.spansTable.job_id, options.jobId))
1613
+ .returning({ id: this.tables.spansTable.id })
1450
1614
 
1451
1615
  return result.length
1452
1616
  }
1453
1617
 
1454
1618
  /**
1455
- * Build WHERE clause for metrics queries.
1619
+ * Build WHERE clause for spans queries (used for job queries only).
1620
+ * When querying by jobId, we find all spans that share the same trace_id
1621
+ * as spans with that job. This includes spans from external libraries that
1622
+ * don't have the duron.job.id attribute but are part of the same trace.
1623
+ *
1624
+ * Note: Step queries are handled separately by _getStepSpansRecursive using
1625
+ * a recursive CTE to traverse the span hierarchy.
1456
1626
  */
1457
- protected _buildMetricsWhereClause(jobId?: string, stepId?: string, filters?: GetMetricsOptions['filters']) {
1458
- const metricsTable = this.tables.metricsTable
1627
+ protected _buildSpansWhereClause(jobId?: string, _stepId?: string, filters?: GetSpansOptions['filters']) {
1628
+ const spansTable = this.tables.spansTable
1629
+
1630
+ // Build condition for finding spans by trace_id (includes external spans)
1631
+ let traceCondition: ReturnType<typeof eq> | undefined
1632
+
1633
+ if (jobId) {
1634
+ // Find all spans that share a trace_id with any span that has this job_id
1635
+ // This includes external spans (like from AI SDK) that don't have duron.job.id
1636
+ traceCondition = inArray(
1637
+ spansTable.trace_id,
1638
+ this.db.select({ traceId: spansTable.trace_id }).from(spansTable).where(eq(spansTable.job_id, jobId)),
1639
+ )
1640
+ }
1459
1641
 
1460
1642
  return and(
1461
- jobId ? eq(metricsTable.job_id, jobId) : undefined,
1462
- stepId ? eq(metricsTable.step_id, stepId) : undefined,
1643
+ traceCondition,
1463
1644
  filters?.name
1464
1645
  ? Array.isArray(filters.name)
1465
- ? or(...filters.name.map((n) => ilike(metricsTable.name, `%${n}%`)))
1466
- : ilike(metricsTable.name, `%${filters.name}%`)
1467
- : undefined,
1468
- filters?.type
1469
- ? inArray(metricsTable.type, Array.isArray(filters.type) ? filters.type : [filters.type])
1646
+ ? or(...filters.name.map((n) => ilike(spansTable.name, `%${n}%`)))
1647
+ : ilike(spansTable.name, `%${filters.name}%`)
1470
1648
  : undefined,
1471
- filters?.timestampRange && filters.timestampRange.length === 2
1472
- ? between(metricsTable.timestamp, filters.timestampRange[0]!, filters.timestampRange[1]!)
1649
+ filters?.kind ? inArray(spansTable.kind, Array.isArray(filters.kind) ? filters.kind : [filters.kind]) : undefined,
1650
+ filters?.statusCode
1651
+ ? inArray(spansTable.status_code, Array.isArray(filters.statusCode) ? filters.statusCode : [filters.statusCode])
1473
1652
  : undefined,
1653
+ filters?.traceId ? eq(spansTable.trace_id, filters.traceId) : undefined,
1474
1654
  ...(filters?.attributesFilter && Object.keys(filters.attributesFilter).length > 0
1475
- ? this.#buildJsonbWhereConditions(filters.attributesFilter, metricsTable.attributes)
1655
+ ? this.#buildJsonbWhereConditions(filters.attributesFilter, spansTable.attributes)
1476
1656
  : []),
1477
1657
  )
1478
1658
  }
@@ -1,5 +1,5 @@
1
1
  import createSchema from './schema.js'
2
2
 
3
- const { schema, jobsTable, jobStepsTable, metricsTable } = createSchema('duron')
3
+ const { schema, jobsTable, jobStepsTable, spansTable } = createSchema('duron')
4
4
 
5
- export { schema, jobsTable, jobStepsTable, metricsTable }
5
+ export { schema, jobsTable, jobStepsTable, spansTable }