duron 0.3.0-beta.9 → 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 +88 -23
- 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 +140 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +152 -9
- 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 +403 -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 +33 -29
- 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 +84 -12
- package/src/index.ts +2 -0
- package/src/server.ts +39 -37
- package/src/step-manager.ts +246 -95
- 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
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
type DelayJobStepOptions,
|
|
25
25
|
type DeleteJobOptions,
|
|
26
26
|
type DeleteJobsOptions,
|
|
27
|
-
type
|
|
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
|
|
37
|
-
type
|
|
38
|
-
type
|
|
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({
|
|
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:
|
|
1017
|
-
actionName:
|
|
1018
|
-
groupKey:
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
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(
|
|
1033
|
-
.where(eq(
|
|
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
|
|
1414
|
+
* Internal method to insert multiple span records in a single batch.
|
|
1366
1415
|
*/
|
|
1367
|
-
protected async
|
|
1368
|
-
if (
|
|
1416
|
+
protected async _insertSpans(spans: InsertSpanOptions[]): Promise<number> {
|
|
1417
|
+
if (spans.length === 0) {
|
|
1369
1418
|
return 0
|
|
1370
1419
|
}
|
|
1371
1420
|
|
|
1372
|
-
const values =
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
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.
|
|
1438
|
+
.insert(this.tables.spansTable)
|
|
1383
1439
|
.values(values)
|
|
1384
|
-
.returning({ id: this.tables.
|
|
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
|
|
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
|
|
1393
|
-
const
|
|
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: '
|
|
1401
|
-
const sortFieldMap: Record<
|
|
1402
|
-
name:
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
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(
|
|
1472
|
+
const total = await this.db.$count(spansTable, where)
|
|
1410
1473
|
if (!total) {
|
|
1411
1474
|
return {
|
|
1412
|
-
|
|
1475
|
+
spans: [],
|
|
1413
1476
|
total: 0,
|
|
1414
1477
|
}
|
|
1415
1478
|
}
|
|
1416
1479
|
|
|
1417
|
-
const
|
|
1418
|
-
const orderByClause =
|
|
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
|
|
1486
|
+
const rows = await this.db
|
|
1421
1487
|
.select({
|
|
1422
|
-
id:
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
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(
|
|
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
|
-
|
|
1518
|
+
spans,
|
|
1438
1519
|
total,
|
|
1439
1520
|
}
|
|
1440
1521
|
}
|
|
1441
1522
|
|
|
1442
1523
|
/**
|
|
1443
|
-
*
|
|
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
|
|
1609
|
+
protected async _deleteSpans(options: DeleteSpansOptions): Promise<number> {
|
|
1446
1610
|
const result = await this.db
|
|
1447
|
-
.delete(this.tables.
|
|
1448
|
-
.where(eq(this.tables.
|
|
1449
|
-
.returning({ id: this.tables.
|
|
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
|
|
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
|
|
1458
|
-
const
|
|
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
|
-
|
|
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(
|
|
1466
|
-
: ilike(
|
|
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?.
|
|
1472
|
-
|
|
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,
|
|
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,
|
|
3
|
+
const { schema, jobsTable, jobStepsTable, spansTable } = createSchema('duron')
|
|
4
4
|
|
|
5
|
-
export { schema, jobsTable, jobStepsTable,
|
|
5
|
+
export { schema, jobsTable, jobStepsTable, spansTable }
|