duron 0.3.0-beta.10 → 0.3.0-beta.12

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 (90) 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 +87 -22
  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 +146 -3
  8. package/dist/action.d.ts.map +1 -1
  9. package/dist/action.js +131 -0
  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 +174 -5
  14. package/dist/adapters/postgres/base.d.ts.map +1 -1
  15. package/dist/adapters/postgres/base.js +349 -66
  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 +118 -35
  23. package/dist/adapters/postgres/schema.d.ts.map +1 -1
  24. package/dist/adapters/postgres/schema.default.d.ts +119 -36
  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 +55 -22
  28. package/dist/adapters/schemas.d.ts +107 -80
  29. package/dist/adapters/schemas.d.ts.map +1 -1
  30. package/dist/adapters/schemas.js +131 -26
  31. package/dist/client.d.ts +315 -9
  32. package/dist/client.d.ts.map +1 -1
  33. package/dist/client.js +391 -21
  34. package/dist/constants.js +6 -0
  35. package/dist/errors.d.ts +119 -0
  36. package/dist/errors.d.ts.map +1 -1
  37. package/dist/errors.js +111 -0
  38. package/dist/index.d.ts +1 -0
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/server.d.ts +91 -37
  41. package/dist/server.d.ts.map +1 -1
  42. package/dist/server.js +81 -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 +340 -69
  46. package/dist/telemetry/adapter.d.ts +322 -0
  47. package/dist/telemetry/adapter.d.ts.map +1 -1
  48. package/dist/telemetry/adapter.js +145 -0
  49. package/dist/telemetry/index.d.ts +1 -4
  50. package/dist/telemetry/index.d.ts.map +1 -1
  51. package/dist/telemetry/index.js +2 -4
  52. package/dist/telemetry/local-span-exporter.d.ts +56 -0
  53. package/dist/telemetry/local-span-exporter.d.ts.map +1 -0
  54. package/dist/telemetry/local-span-exporter.js +118 -0
  55. package/dist/telemetry/local.d.ts +48 -0
  56. package/dist/telemetry/local.d.ts.map +1 -1
  57. package/dist/telemetry/local.js +102 -0
  58. package/dist/telemetry/noop.d.ts +10 -0
  59. package/dist/telemetry/noop.d.ts.map +1 -1
  60. package/dist/telemetry/noop.js +43 -0
  61. package/dist/telemetry/opentelemetry.d.ts +23 -0
  62. package/dist/telemetry/opentelemetry.d.ts.map +1 -1
  63. package/dist/telemetry/opentelemetry.js +39 -0
  64. package/dist/utils/p-retry.d.ts +5 -0
  65. package/dist/utils/p-retry.d.ts.map +1 -1
  66. package/dist/utils/p-retry.js +8 -0
  67. package/dist/utils/wait-for-abort.d.ts +1 -0
  68. package/dist/utils/wait-for-abort.d.ts.map +1 -1
  69. package/dist/utils/wait-for-abort.js +1 -0
  70. package/migrations/postgres/{20260119153838_flimsy_thor_girl → 20260120154151_mean_magdalene}/migration.sql +27 -19
  71. package/migrations/postgres/{20260119153838_flimsy_thor_girl → 20260120154151_mean_magdalene}/snapshot.json +172 -65
  72. package/package.json +7 -2
  73. package/src/action-job.ts +32 -28
  74. package/src/action-manager.ts +5 -5
  75. package/src/action.ts +7 -7
  76. package/src/adapters/adapter.ts +54 -54
  77. package/src/adapters/postgres/base.ts +140 -77
  78. package/src/adapters/postgres/schema.default.ts +2 -2
  79. package/src/adapters/postgres/schema.ts +47 -23
  80. package/src/adapters/schemas.ts +83 -36
  81. package/src/client.ts +195 -42
  82. package/src/index.ts +1 -0
  83. package/src/server.ts +37 -37
  84. package/src/step-manager.ts +170 -86
  85. package/src/telemetry/index.ts +2 -20
  86. package/src/telemetry/local-span-exporter.ts +148 -0
  87. package/src/telemetry/adapter.ts +0 -642
  88. package/src/telemetry/local.ts +0 -429
  89. package/src/telemetry/noop.ts +0 -141
  90. package/src/telemetry/opentelemetry.ts +0 -453
@@ -1,15 +1,15 @@
1
+ import type { Tracer } from '@opentelemetry/api'
1
2
  import fastq from 'fastq'
2
3
  import type { Logger } from 'pino'
3
4
 
4
5
  import type { Action } from './action.js'
5
6
  import { ActionJob } from './action-job.js'
6
7
  import type { Adapter, Job } from './adapters/adapter.js'
7
- import type { TelemetryAdapter } from './telemetry/adapter.js'
8
8
 
9
9
  export interface ActionManagerOptions<TAction extends Action<any, any, any>> {
10
10
  action: TAction
11
11
  database: Adapter
12
- telemetry: TelemetryAdapter
12
+ tracer: Tracer
13
13
  variables: Record<string, unknown>
14
14
  logger: Logger
15
15
  concurrencyLimit: number
@@ -24,7 +24,7 @@ export interface ActionManagerOptions<TAction extends Action<any, any, any>> {
24
24
  export class ActionManager<TAction extends Action<any, any, any>> {
25
25
  #action: TAction
26
26
  #database: Adapter
27
- #telemetry: TelemetryAdapter
27
+ #tracer: Tracer
28
28
  #variables: Record<string, unknown>
29
29
  #logger: Logger
30
30
  #queue: fastq.queueAsPromised<Job, void>
@@ -44,7 +44,7 @@ export class ActionManager<TAction extends Action<any, any, any>> {
44
44
  constructor(options: ActionManagerOptions<TAction>) {
45
45
  this.#action = options.action
46
46
  this.#database = options.database
47
- this.#telemetry = options.telemetry
47
+ this.#tracer = options.tracer
48
48
  this.#variables = options.variables
49
49
  this.#logger = options.logger
50
50
  this.#concurrencyLimit = options.concurrencyLimit
@@ -153,7 +153,7 @@ export class ActionManager<TAction extends Action<any, any, any>> {
153
153
  },
154
154
  action: this.#action,
155
155
  database: this.#database,
156
- telemetry: this.#telemetry,
156
+ tracer: this.#tracer,
157
157
  variables: this.#variables,
158
158
  logger: this.#logger,
159
159
  })
package/src/action.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { Logger } from 'pino'
2
2
  import * as z from 'zod'
3
3
 
4
- import type { ObserveContext } from './telemetry/adapter.js'
4
+ import type { TelemetryContext } from './step-manager.js'
5
5
  import generateChecksum from './utils/checksum.js'
6
6
 
7
7
  export type RetryOptions = z.infer<typeof RetryOptionsSchema>
@@ -16,10 +16,10 @@ export interface ActionHandlerContext<TInput extends z.ZodObject, TVariables = R
16
16
  logger: Logger
17
17
 
18
18
  /**
19
- * Observability context for recording metrics and span data.
20
- * Allows recording custom metrics, span attributes, and events.
19
+ * Telemetry context for recording metrics and span data.
20
+ * Provides access to OpenTelemetry APIs for recording traces and metrics.
21
21
  */
22
- observe: ObserveContext
22
+ telemetry: TelemetryContext
23
23
 
24
24
  /**
25
25
  * Execute an inline step within the action.
@@ -71,10 +71,10 @@ export interface StepHandlerContext {
71
71
  parentStepId: string | null
72
72
 
73
73
  /**
74
- * Observability context for recording metrics and span data.
75
- * Allows recording custom metrics, span attributes, and events.
74
+ * Telemetry context for recording metrics and span data.
75
+ * Provides access to OpenTelemetry APIs for recording traces and metrics.
76
76
  */
77
- observe: ObserveContext
77
+ telemetry: TelemetryContext
78
78
 
79
79
  /**
80
80
  * Create a nested child step.
@@ -24,7 +24,7 @@ import type {
24
24
  DelayJobStepOptions,
25
25
  DeleteJobOptions,
26
26
  DeleteJobsOptions,
27
- DeleteMetricsOptions,
27
+ DeleteSpansOptions,
28
28
  FailJobOptions,
29
29
  FailJobStepOptions,
30
30
  FetchOptions,
@@ -33,9 +33,9 @@ import type {
33
33
  GetJobStepsResult,
34
34
  GetJobsOptions,
35
35
  GetJobsResult,
36
- GetMetricsOptions,
37
- GetMetricsResult,
38
- InsertMetricOptions,
36
+ GetSpansOptions,
37
+ GetSpansResult,
38
+ InsertSpanOptions,
39
39
  Job,
40
40
  JobStatusResult,
41
41
  JobStep,
@@ -56,7 +56,7 @@ import {
56
56
  DelayJobStepOptionsSchema,
57
57
  DeleteJobOptionsSchema,
58
58
  DeleteJobsOptionsSchema,
59
- DeleteMetricsOptionsSchema,
59
+ DeleteSpansOptionsSchema,
60
60
  FailJobOptionsSchema,
61
61
  FailJobStepOptionsSchema,
62
62
  FetchOptionsSchema,
@@ -65,9 +65,9 @@ import {
65
65
  GetJobStepsResultSchema,
66
66
  GetJobsOptionsSchema,
67
67
  GetJobsResultSchema,
68
- GetMetricsOptionsSchema,
69
- GetMetricsResultSchema,
70
- InsertMetricOptionsSchema,
68
+ GetSpansOptionsSchema,
69
+ GetSpansResultSchema,
70
+ InsertSpanOptionsSchema,
71
71
  JobIdResultSchema,
72
72
  JobSchema,
73
73
  JobStatusResultSchema,
@@ -93,7 +93,7 @@ export type {
93
93
  DelayJobStepOptions,
94
94
  DeleteJobOptions,
95
95
  DeleteJobsOptions,
96
- DeleteMetricsOptions,
96
+ DeleteSpansOptions,
97
97
  FailJobOptions,
98
98
  FailJobStepOptions,
99
99
  FetchOptions,
@@ -102,9 +102,9 @@ export type {
102
102
  GetJobStepsResult,
103
103
  GetJobsOptions,
104
104
  GetJobsResult,
105
- GetMetricsOptions,
106
- GetMetricsResult,
107
- InsertMetricOptions,
105
+ GetSpansOptions,
106
+ GetSpansResult,
107
+ InsertSpanOptions,
108
108
  Job,
109
109
  JobFilters,
110
110
  JobSort,
@@ -112,14 +112,16 @@ export type {
112
112
  JobStatusResult,
113
113
  JobStep,
114
114
  JobStepStatusResult,
115
- Metric,
116
- MetricFilters,
117
- MetricSort,
118
- MetricSortField,
119
- MetricType,
120
115
  RecoverJobsOptions,
121
116
  RetryJobOptions,
122
117
  SortOrder,
118
+ Span,
119
+ SpanEvent,
120
+ SpanFilters,
121
+ SpanKind,
122
+ SpanSort,
123
+ SpanSortField,
124
+ SpanStatusCode,
123
125
  TimeTravelJobOptions,
124
126
  } from './schemas.js'
125
127
 
@@ -993,100 +995,98 @@ export abstract class Adapter extends EventEmitter<AdapterEvents> {
993
995
  protected abstract _getActions(): Promise<GetActionsResult>
994
996
 
995
997
  // ============================================================================
996
- // Metrics Methods
998
+ // Span Methods (OpenTelemetry)
997
999
  // ============================================================================
998
1000
 
999
1001
  /**
1000
- * Insert multiple metric records in a single batch operation.
1001
- * Note: This method bypasses telemetry tracing to prevent infinite loops.
1002
+ * Insert multiple span records in a single batch operation.
1003
+ * Used by LocalSpanExporter to store spans from the OpenTelemetry SDK.
1002
1004
  *
1003
- * @param metrics - Array of metric data to insert
1004
- * @returns Promise resolving to the number of metrics inserted
1005
+ * @param spans - Array of span data to insert
1006
+ * @returns Promise resolving to the number of spans inserted
1005
1007
  */
1006
- async insertMetrics(metrics: InsertMetricOptions[]): Promise<number> {
1008
+ async insertSpans(spans: InsertSpanOptions[]): Promise<number> {
1007
1009
  try {
1008
- if (metrics.length === 0) {
1010
+ if (spans.length === 0) {
1009
1011
  return 0
1010
1012
  }
1011
1013
  await this.start()
1012
- const parsedMetrics = metrics.map((m) => InsertMetricOptionsSchema.parse(m))
1013
- const result = await this._insertMetrics(parsedMetrics)
1014
+ const parsedSpans = spans.map((s) => InsertSpanOptionsSchema.parse(s))
1015
+ const result = await this._insertSpans(parsedSpans)
1014
1016
  return NumberResultSchema.parse(result)
1015
1017
  } catch (error) {
1016
- this.#logger?.error(error, 'Error in Adapter.insertMetrics()')
1018
+ this.#logger?.error(error, 'Error in Adapter.insertSpans()')
1017
1019
  throw error
1018
1020
  }
1019
1021
  }
1020
1022
 
1021
1023
  /**
1022
- * Get metrics for a job or step.
1023
- * Note: This method bypasses telemetry tracing to prevent infinite loops.
1024
+ * Get spans for a job or step.
1024
1025
  *
1025
- * @param options - Query options including jobId/stepId, filters, sort, and pagination
1026
- * @returns Promise resolving to metrics result with pagination info
1026
+ * @param options - Query options including jobId/stepId, filters, and sort
1027
+ * @returns Promise resolving to spans result
1027
1028
  */
1028
- async getMetrics(options: GetMetricsOptions): Promise<GetMetricsResult> {
1029
+ async getSpans(options: GetSpansOptions): Promise<GetSpansResult> {
1029
1030
  try {
1030
1031
  await this.start()
1031
- const parsedOptions = GetMetricsOptionsSchema.parse(options)
1032
+ const parsedOptions = GetSpansOptionsSchema.parse(options)
1032
1033
  // Validate that at least one of jobId or stepId is provided
1033
1034
  if (!parsedOptions.jobId && !parsedOptions.stepId) {
1034
1035
  throw new Error('At least one of jobId or stepId must be provided')
1035
1036
  }
1036
- const result = await this._getMetrics(parsedOptions)
1037
- return GetMetricsResultSchema.parse(result)
1037
+ const result = await this._getSpans(parsedOptions)
1038
+ return GetSpansResultSchema.parse(result)
1038
1039
  } catch (error) {
1039
- this.#logger?.error(error, 'Error in Adapter.getMetrics()')
1040
+ this.#logger?.error(error, 'Error in Adapter.getSpans()')
1040
1041
  throw error
1041
1042
  }
1042
1043
  }
1043
1044
 
1044
1045
  /**
1045
- * Delete all metrics for a job.
1046
- * Note: This method bypasses telemetry tracing to prevent infinite loops.
1046
+ * Delete all spans for a job.
1047
1047
  *
1048
1048
  * @param options - Options containing the jobId
1049
- * @returns Promise resolving to the number of metrics deleted
1049
+ * @returns Promise resolving to the number of spans deleted
1050
1050
  */
1051
- async deleteMetrics(options: DeleteMetricsOptions): Promise<number> {
1051
+ async deleteSpans(options: DeleteSpansOptions): Promise<number> {
1052
1052
  try {
1053
1053
  await this.start()
1054
- const parsedOptions = DeleteMetricsOptionsSchema.parse(options)
1055
- const result = await this._deleteMetrics(parsedOptions)
1054
+ const parsedOptions = DeleteSpansOptionsSchema.parse(options)
1055
+ const result = await this._deleteSpans(parsedOptions)
1056
1056
  return NumberResultSchema.parse(result)
1057
1057
  } catch (error) {
1058
- this.#logger?.error(error, 'Error in Adapter.deleteMetrics()')
1058
+ this.#logger?.error(error, 'Error in Adapter.deleteSpans()')
1059
1059
  throw error
1060
1060
  }
1061
1061
  }
1062
1062
 
1063
1063
  // ============================================================================
1064
- // Private Metrics Methods (to be implemented by adapters)
1064
+ // Private Span Methods (to be implemented by adapters)
1065
1065
  // ============================================================================
1066
1066
 
1067
1067
  /**
1068
- * Internal method to insert multiple metric records in a single batch.
1068
+ * Internal method to insert multiple span records in a single batch.
1069
1069
  *
1070
- * @param metrics - Array of validated metric data
1071
- * @returns Promise resolving to the number of metrics inserted
1070
+ * @param spans - Array of validated span data
1071
+ * @returns Promise resolving to the number of spans inserted
1072
1072
  */
1073
- protected abstract _insertMetrics(metrics: InsertMetricOptions[]): Promise<number>
1073
+ protected abstract _insertSpans(spans: InsertSpanOptions[]): Promise<number>
1074
1074
 
1075
1075
  /**
1076
- * Internal method to get metrics for a job or step.
1076
+ * Internal method to get spans for a job or step.
1077
1077
  *
1078
1078
  * @param options - Validated query options
1079
- * @returns Promise resolving to metrics result with pagination info
1079
+ * @returns Promise resolving to spans result
1080
1080
  */
1081
- protected abstract _getMetrics(options: GetMetricsOptions): Promise<GetMetricsResult>
1081
+ protected abstract _getSpans(options: GetSpansOptions): Promise<GetSpansResult>
1082
1082
 
1083
1083
  /**
1084
- * Internal method to delete all metrics for a job.
1084
+ * Internal method to delete all spans for a job.
1085
1085
  *
1086
1086
  * @param options - Validated options containing the jobId
1087
- * @returns Promise resolving to the number of metrics deleted
1087
+ * @returns Promise resolving to the number of spans deleted
1088
1088
  */
1089
- protected abstract _deleteMetrics(options: DeleteMetricsOptions): Promise<number>
1089
+ protected abstract _deleteSpans(options: DeleteSpansOptions): Promise<number>
1090
1090
 
1091
1091
  // ============================================================================
1092
1092
  // Protected Abstract Methods (to be implemented by adapters)
@@ -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'
@@ -788,7 +788,7 @@ export class PostgresBaseAdapter<Database extends DrizzleDatabase, Connection> e
788
788
  step_existed AS (
789
789
  SELECT EXISTS(
790
790
  SELECT 1 FROM ${this.tables.jobStepsTable} s
791
- WHERE s.job_id = ${jobId}
791
+ WHERE s.job_id = ${jobId}
792
792
  AND s.name = ${name}
793
793
  AND s.parent_step_id IS NOT DISTINCT FROM ${parentStepId}
794
794
  ) AS existed
@@ -1011,26 +1011,38 @@ export class PostgresBaseAdapter<Database extends DrizzleDatabase, Connection> e
1011
1011
  * Internal method to get a job by its ID. Does not include step information.
1012
1012
  */
1013
1013
  protected async _getJobById(jobId: string): Promise<Job | null> {
1014
+ const jobsTable = this.tables.jobsTable
1015
+
1016
+ // Calculate duration as a SQL expression (finishedAt - startedAt in milliseconds)
1017
+ const durationMs = sql<number | null>`
1018
+ CASE
1019
+ WHEN ${jobsTable.started_at} IS NOT NULL AND ${jobsTable.finished_at} IS NOT NULL
1020
+ THEN EXTRACT(EPOCH FROM (${jobsTable.finished_at} - ${jobsTable.started_at})) * 1000
1021
+ ELSE NULL
1022
+ END
1023
+ `.as('duration_ms')
1024
+
1014
1025
  const [job] = await this.db
1015
1026
  .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,
1027
+ id: jobsTable.id,
1028
+ actionName: jobsTable.action_name,
1029
+ groupKey: jobsTable.group_key,
1030
+ input: jobsTable.input,
1031
+ output: jobsTable.output,
1032
+ error: jobsTable.error,
1033
+ status: jobsTable.status,
1034
+ timeoutMs: jobsTable.timeout_ms,
1035
+ expiresAt: jobsTable.expires_at,
1036
+ startedAt: jobsTable.started_at,
1037
+ finishedAt: jobsTable.finished_at,
1038
+ createdAt: jobsTable.created_at,
1039
+ updatedAt: jobsTable.updated_at,
1040
+ concurrencyLimit: jobsTable.concurrency_limit,
1041
+ clientId: jobsTable.client_id,
1042
+ durationMs,
1031
1043
  })
1032
- .from(this.tables.jobsTable)
1033
- .where(eq(this.tables.jobsTable.id, jobId))
1044
+ .from(jobsTable)
1045
+ .where(eq(jobsTable.id, jobId))
1034
1046
  .limit(1)
1035
1047
 
1036
1048
  return job ?? null
@@ -1194,6 +1206,15 @@ export class PostgresBaseAdapter<Database extends DrizzleDatabase, Connection> e
1194
1206
  }
1195
1207
  }
1196
1208
 
1209
+ // Calculate duration as a SQL expression (finishedAt - startedAt in milliseconds)
1210
+ const durationMs = sql<number | null>`
1211
+ CASE
1212
+ WHEN ${jobsTable.started_at} IS NOT NULL AND ${jobsTable.finished_at} IS NOT NULL
1213
+ THEN EXTRACT(EPOCH FROM (${jobsTable.finished_at} - ${jobsTable.started_at})) * 1000
1214
+ ELSE NULL
1215
+ END
1216
+ `.as('duration_ms')
1217
+
1197
1218
  const sortFieldMap: Record<JobSort['field'], any> = {
1198
1219
  createdAt: jobsTable.created_at,
1199
1220
  startedAt: jobsTable.started_at,
@@ -1201,6 +1222,7 @@ export class PostgresBaseAdapter<Database extends DrizzleDatabase, Connection> e
1201
1222
  status: jobsTable.status,
1202
1223
  actionName: jobsTable.action_name,
1203
1224
  expiresAt: jobsTable.expires_at,
1225
+ duration: durationMs,
1204
1226
  }
1205
1227
 
1206
1228
  const jobs = await this.db
@@ -1220,6 +1242,7 @@ export class PostgresBaseAdapter<Database extends DrizzleDatabase, Connection> e
1220
1242
  updatedAt: jobsTable.updated_at,
1221
1243
  concurrencyLimit: jobsTable.concurrency_limit,
1222
1244
  clientId: jobsTable.client_id,
1245
+ durationMs,
1223
1246
  })
1224
1247
  .from(jobsTable)
1225
1248
  .where(where)
@@ -1362,54 +1385,60 @@ export class PostgresBaseAdapter<Database extends DrizzleDatabase, Connection> e
1362
1385
  // ============================================================================
1363
1386
 
1364
1387
  /**
1365
- * Internal method to insert multiple metric records in a single batch.
1388
+ * Internal method to insert multiple span records in a single batch.
1366
1389
  */
1367
- protected async _insertMetrics(metrics: InsertMetricOptions[]): Promise<number> {
1368
- if (metrics.length === 0) {
1390
+ protected async _insertSpans(spans: InsertSpanOptions[]): Promise<number> {
1391
+ if (spans.length === 0) {
1369
1392
  return 0
1370
1393
  }
1371
1394
 
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,
1395
+ const values = spans.map((s) => ({
1396
+ trace_id: s.traceId,
1397
+ span_id: s.spanId,
1398
+ parent_span_id: s.parentSpanId,
1399
+ job_id: s.jobId,
1400
+ step_id: s.stepId,
1401
+ name: s.name,
1402
+ kind: s.kind,
1403
+ start_time_unix_nano: s.startTimeUnixNano,
1404
+ end_time_unix_nano: s.endTimeUnixNano,
1405
+ status_code: s.statusCode,
1406
+ status_message: s.statusMessage,
1407
+ attributes: s.attributes ?? {},
1408
+ events: s.events ?? [],
1379
1409
  }))
1380
1410
 
1381
1411
  const result = await this.db
1382
- .insert(this.tables.metricsTable)
1412
+ .insert(this.tables.spansTable)
1383
1413
  .values(values)
1384
- .returning({ id: this.tables.metricsTable.id })
1414
+ .returning({ id: this.tables.spansTable.id })
1385
1415
 
1386
1416
  return result.length
1387
1417
  }
1388
1418
 
1389
1419
  /**
1390
- * Internal method to get metrics for a job or step.
1420
+ * Internal method to get spans for a job or step.
1391
1421
  */
1392
- protected async _getMetrics(options: GetMetricsOptions): Promise<GetMetricsResult> {
1393
- const metricsTable = this.tables.metricsTable
1422
+ protected async _getSpans(options: GetSpansOptions): Promise<GetSpansResult> {
1423
+ const spansTable = this.tables.spansTable
1394
1424
  const filters = options.filters ?? {}
1395
1425
 
1396
1426
  // Build WHERE clause
1397
- const where = this._buildMetricsWhereClause(options.jobId, options.stepId, filters)
1427
+ const where = this._buildSpansWhereClause(options.jobId, options.stepId, filters)
1398
1428
 
1399
1429
  // 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,
1430
+ const sortInput = options.sort ?? { field: 'startTimeUnixNano', order: 'asc' }
1431
+ const sortFieldMap: Record<SpanSort['field'], any> = {
1432
+ name: spansTable.name,
1433
+ startTimeUnixNano: spansTable.start_time_unix_nano,
1434
+ endTimeUnixNano: spansTable.end_time_unix_nano,
1406
1435
  }
1407
1436
 
1408
1437
  // Get total count
1409
- const total = await this.db.$count(metricsTable, where)
1438
+ const total = await this.db.$count(spansTable, where)
1410
1439
  if (!total) {
1411
1440
  return {
1412
- metrics: [],
1441
+ spans: [],
1413
1442
  total: 0,
1414
1443
  }
1415
1444
  }
@@ -1417,62 +1446,96 @@ export class PostgresBaseAdapter<Database extends DrizzleDatabase, Connection> e
1417
1446
  const sortField = sortFieldMap[sortInput.field]
1418
1447
  const orderByClause = sortInput.order === 'asc' ? asc(sortField) : desc(sortField)
1419
1448
 
1420
- const metrics = await this.db
1449
+ const rows = await this.db
1421
1450
  .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,
1451
+ id: spansTable.id,
1452
+ traceId: spansTable.trace_id,
1453
+ spanId: spansTable.span_id,
1454
+ parentSpanId: spansTable.parent_span_id,
1455
+ jobId: spansTable.job_id,
1456
+ stepId: spansTable.step_id,
1457
+ name: spansTable.name,
1458
+ kind: spansTable.kind,
1459
+ startTimeUnixNano: spansTable.start_time_unix_nano,
1460
+ endTimeUnixNano: spansTable.end_time_unix_nano,
1461
+ statusCode: spansTable.status_code,
1462
+ statusMessage: spansTable.status_message,
1463
+ attributes: spansTable.attributes,
1464
+ events: spansTable.events,
1431
1465
  })
1432
- .from(metricsTable)
1466
+ .from(spansTable)
1433
1467
  .where(where)
1434
1468
  .orderBy(orderByClause)
1435
1469
 
1470
+ // Cast kind and statusCode to proper types, convert BigInt to string for JSON serialization
1471
+ const spans = rows.map((row) => ({
1472
+ ...row,
1473
+ kind: row.kind as 0 | 1 | 2 | 3 | 4,
1474
+ statusCode: row.statusCode as 0 | 1 | 2,
1475
+ // Convert BigInt to string for JSON serialization
1476
+ startTimeUnixNano: row.startTimeUnixNano?.toString() ?? null,
1477
+ endTimeUnixNano: row.endTimeUnixNano?.toString() ?? null,
1478
+ }))
1479
+
1436
1480
  return {
1437
- metrics,
1481
+ spans,
1438
1482
  total,
1439
1483
  }
1440
1484
  }
1441
1485
 
1442
1486
  /**
1443
- * Internal method to delete all metrics for a job.
1487
+ * Internal method to delete all spans for a job.
1444
1488
  */
1445
- protected async _deleteMetrics(options: DeleteMetricsOptions): Promise<number> {
1489
+ protected async _deleteSpans(options: DeleteSpansOptions): Promise<number> {
1446
1490
  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 })
1491
+ .delete(this.tables.spansTable)
1492
+ .where(eq(this.tables.spansTable.job_id, options.jobId))
1493
+ .returning({ id: this.tables.spansTable.id })
1450
1494
 
1451
1495
  return result.length
1452
1496
  }
1453
1497
 
1454
1498
  /**
1455
- * Build WHERE clause for metrics queries.
1499
+ * Build WHERE clause for spans queries.
1500
+ * When querying by jobId or stepId, we find all spans that share the same trace_id
1501
+ * as spans with that job/step. This includes spans from external libraries that
1502
+ * don't have the duron.job.id attribute but are part of the same trace.
1456
1503
  */
1457
- protected _buildMetricsWhereClause(jobId?: string, stepId?: string, filters?: GetMetricsOptions['filters']) {
1458
- const metricsTable = this.tables.metricsTable
1504
+ protected _buildSpansWhereClause(jobId?: string, stepId?: string, filters?: GetSpansOptions['filters']) {
1505
+ const spansTable = this.tables.spansTable
1506
+
1507
+ // Build condition for finding spans by trace_id (includes external spans)
1508
+ let traceCondition: ReturnType<typeof eq> | undefined
1509
+
1510
+ if (jobId) {
1511
+ // Find all spans that share a trace_id with any span that has this job_id
1512
+ // This includes external spans (like from AI SDK) that don't have duron.job.id
1513
+ traceCondition = inArray(
1514
+ spansTable.trace_id,
1515
+ this.db.select({ traceId: spansTable.trace_id }).from(spansTable).where(eq(spansTable.job_id, jobId)),
1516
+ )
1517
+ } else if (stepId) {
1518
+ // Find all spans that share a trace_id with any span that has this step_id
1519
+ traceCondition = inArray(
1520
+ spansTable.trace_id,
1521
+ this.db.select({ traceId: spansTable.trace_id }).from(spansTable).where(eq(spansTable.step_id, stepId)),
1522
+ )
1523
+ }
1459
1524
 
1460
1525
  return and(
1461
- jobId ? eq(metricsTable.job_id, jobId) : undefined,
1462
- stepId ? eq(metricsTable.step_id, stepId) : undefined,
1526
+ traceCondition,
1463
1527
  filters?.name
1464
1528
  ? 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])
1529
+ ? or(...filters.name.map((n) => ilike(spansTable.name, `%${n}%`)))
1530
+ : ilike(spansTable.name, `%${filters.name}%`)
1470
1531
  : undefined,
1471
- filters?.timestampRange && filters.timestampRange.length === 2
1472
- ? between(metricsTable.timestamp, filters.timestampRange[0]!, filters.timestampRange[1]!)
1532
+ filters?.kind ? inArray(spansTable.kind, Array.isArray(filters.kind) ? filters.kind : [filters.kind]) : undefined,
1533
+ filters?.statusCode
1534
+ ? inArray(spansTable.status_code, Array.isArray(filters.statusCode) ? filters.statusCode : [filters.statusCode])
1473
1535
  : undefined,
1536
+ filters?.traceId ? eq(spansTable.trace_id, filters.traceId) : undefined,
1474
1537
  ...(filters?.attributesFilter && Object.keys(filters.attributesFilter).length > 0
1475
- ? this.#buildJsonbWhereConditions(filters.attributesFilter, metricsTable.attributes)
1538
+ ? this.#buildJsonbWhereConditions(filters.attributesFilter, spansTable.attributes)
1476
1539
  : []),
1477
1540
  )
1478
1541
  }
@@ -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 }