duron 0.2.1 → 0.3.0-beta.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 (77) hide show
  1. package/dist/action-job.d.ts +2 -0
  2. package/dist/action-job.d.ts.map +1 -1
  3. package/dist/action-job.js +20 -1
  4. package/dist/action-manager.d.ts +2 -0
  5. package/dist/action-manager.d.ts.map +1 -1
  6. package/dist/action-manager.js +3 -0
  7. package/dist/action.d.ts +7 -0
  8. package/dist/action.d.ts.map +1 -1
  9. package/dist/action.js +1 -0
  10. package/dist/adapters/adapter.d.ts +10 -2
  11. package/dist/adapters/adapter.d.ts.map +1 -1
  12. package/dist/adapters/adapter.js +59 -1
  13. package/dist/adapters/postgres/base.d.ts +9 -4
  14. package/dist/adapters/postgres/base.d.ts.map +1 -1
  15. package/dist/adapters/postgres/base.js +269 -19
  16. package/dist/adapters/postgres/schema.d.ts +249 -105
  17. package/dist/adapters/postgres/schema.d.ts.map +1 -1
  18. package/dist/adapters/postgres/schema.default.d.ts +249 -106
  19. package/dist/adapters/postgres/schema.default.d.ts.map +1 -1
  20. package/dist/adapters/postgres/schema.default.js +2 -2
  21. package/dist/adapters/postgres/schema.js +29 -1
  22. package/dist/adapters/schemas.d.ts +140 -7
  23. package/dist/adapters/schemas.d.ts.map +1 -1
  24. package/dist/adapters/schemas.js +52 -4
  25. package/dist/client.d.ts +8 -1
  26. package/dist/client.d.ts.map +1 -1
  27. package/dist/client.js +29 -1
  28. package/dist/errors.d.ts +6 -0
  29. package/dist/errors.d.ts.map +1 -1
  30. package/dist/errors.js +16 -1
  31. package/dist/index.d.ts +3 -1
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +3 -1
  34. package/dist/server.d.ts +220 -16
  35. package/dist/server.d.ts.map +1 -1
  36. package/dist/server.js +123 -8
  37. package/dist/step-manager.d.ts +8 -2
  38. package/dist/step-manager.d.ts.map +1 -1
  39. package/dist/step-manager.js +138 -15
  40. package/dist/telemetry/adapter.d.ts +85 -0
  41. package/dist/telemetry/adapter.d.ts.map +1 -0
  42. package/dist/telemetry/adapter.js +128 -0
  43. package/dist/telemetry/index.d.ts +5 -0
  44. package/dist/telemetry/index.d.ts.map +1 -0
  45. package/dist/telemetry/index.js +4 -0
  46. package/dist/telemetry/local.d.ts +21 -0
  47. package/dist/telemetry/local.d.ts.map +1 -0
  48. package/dist/telemetry/local.js +180 -0
  49. package/dist/telemetry/noop.d.ts +16 -0
  50. package/dist/telemetry/noop.d.ts.map +1 -0
  51. package/dist/telemetry/noop.js +39 -0
  52. package/dist/telemetry/opentelemetry.d.ts +24 -0
  53. package/dist/telemetry/opentelemetry.d.ts.map +1 -0
  54. package/dist/telemetry/opentelemetry.js +202 -0
  55. package/migrations/postgres/20260117231749_clumsy_penance/migration.sql +3 -0
  56. package/migrations/postgres/20260117231749_clumsy_penance/snapshot.json +988 -0
  57. package/migrations/postgres/20260118202533_wealthy_mysterio/migration.sql +24 -0
  58. package/migrations/postgres/20260118202533_wealthy_mysterio/snapshot.json +1362 -0
  59. package/package.json +6 -4
  60. package/src/action-job.ts +35 -0
  61. package/src/action-manager.ts +5 -0
  62. package/src/action.ts +56 -0
  63. package/src/adapters/adapter.ts +151 -0
  64. package/src/adapters/postgres/base.ts +342 -23
  65. package/src/adapters/postgres/schema.default.ts +2 -2
  66. package/src/adapters/postgres/schema.ts +49 -1
  67. package/src/adapters/schemas.ts +81 -5
  68. package/src/client.ts +80 -2
  69. package/src/errors.ts +45 -1
  70. package/src/index.ts +3 -1
  71. package/src/server.ts +163 -8
  72. package/src/step-manager.ts +232 -13
  73. package/src/telemetry/adapter.ts +468 -0
  74. package/src/telemetry/index.ts +17 -0
  75. package/src/telemetry/local.ts +336 -0
  76. package/src/telemetry/noop.ts +95 -0
  77. package/src/telemetry/opentelemetry.ts +310 -0
@@ -0,0 +1,310 @@
1
+ import type { Span as OTelSpan, Tracer, TracerProvider } from '@opentelemetry/api'
2
+
3
+ import {
4
+ type AddSpanAttributeOptions,
5
+ type AddSpanEventOptions,
6
+ type EndSpanOptions,
7
+ type RecordMetricOptions,
8
+ type Span,
9
+ type StartDatabaseSpanOptions,
10
+ type StartJobSpanOptions,
11
+ type StartStepSpanOptions,
12
+ TelemetryAdapter,
13
+ } from './adapter.js'
14
+
15
+ // ============================================================================
16
+ // Types
17
+ // ============================================================================
18
+
19
+ export interface OpenTelemetryAdapterOptions {
20
+ /**
21
+ * Service name for telemetry.
22
+ * Used as the tracer name.
23
+ */
24
+ serviceName?: string
25
+
26
+ /**
27
+ * Optional TracerProvider to use.
28
+ * If not provided, uses the global tracer provider.
29
+ */
30
+ tracerProvider?: TracerProvider
31
+
32
+ /**
33
+ * Whether to trace database queries.
34
+ * @default false
35
+ */
36
+ traceDatabaseQueries?: boolean
37
+ }
38
+
39
+ interface ExtendedSpan extends Span {
40
+ otelSpan: OTelSpan
41
+ }
42
+
43
+ // ============================================================================
44
+ // OpenTelemetry Adapter
45
+ // ============================================================================
46
+
47
+ /**
48
+ * OpenTelemetry telemetry adapter.
49
+ * Exports traces to external systems like Jaeger, OTLP, etc.
50
+ */
51
+ export class OpenTelemetryAdapter extends TelemetryAdapter {
52
+ #serviceName: string
53
+ #tracerProvider: TracerProvider | null
54
+ #traceDatabaseQueries: boolean
55
+ #tracer: Tracer | null = null
56
+ #spanMap = new Map<string, OTelSpan>()
57
+
58
+ constructor(options: OpenTelemetryAdapterOptions = {}) {
59
+ super()
60
+ this.#serviceName = options.serviceName ?? 'duron'
61
+ this.#tracerProvider = options.tracerProvider ?? null
62
+ this.#traceDatabaseQueries = options.traceDatabaseQueries ?? false
63
+ }
64
+
65
+ // ============================================================================
66
+ // Lifecycle Methods
67
+ // ============================================================================
68
+
69
+ protected async _start(): Promise<void> {
70
+ // Dynamically import OpenTelemetry API to make it optional
71
+ const api = await import('@opentelemetry/api')
72
+
73
+ // Get tracer from provider or global
74
+ if (this.#tracerProvider) {
75
+ this.#tracer = this.#tracerProvider.getTracer(this.#serviceName)
76
+ } else {
77
+ this.#tracer = api.trace.getTracer(this.#serviceName)
78
+ }
79
+ }
80
+
81
+ protected async _stop(): Promise<void> {
82
+ this.#spanMap.clear()
83
+ this.#tracer = null
84
+ }
85
+
86
+ // ============================================================================
87
+ // Span Methods
88
+ // ============================================================================
89
+
90
+ protected async _startJobSpan(options: StartJobSpanOptions): Promise<Span> {
91
+ if (!this.#tracer) {
92
+ throw new Error('OpenTelemetry tracer not initialized')
93
+ }
94
+
95
+ const api = await import('@opentelemetry/api')
96
+
97
+ const otelSpan = this.#tracer.startSpan(`job:${options.actionName}`, {
98
+ kind: api.SpanKind.INTERNAL,
99
+ attributes: {
100
+ 'duron.job.id': options.jobId,
101
+ 'duron.job.action_name': options.actionName,
102
+ 'duron.job.group_key': options.groupKey,
103
+ },
104
+ })
105
+
106
+ const spanId = `job:${options.jobId}`
107
+ this.#spanMap.set(spanId, otelSpan)
108
+
109
+ const span: ExtendedSpan = {
110
+ id: spanId,
111
+ jobId: options.jobId,
112
+ stepId: null,
113
+ parentSpanId: null,
114
+ otelSpan,
115
+ }
116
+
117
+ return span
118
+ }
119
+
120
+ protected async _endJobSpan(span: Span, options: EndSpanOptions): Promise<void> {
121
+ const api = await import('@opentelemetry/api')
122
+ const extSpan = span as ExtendedSpan
123
+ const otelSpan = extSpan.otelSpan
124
+
125
+ if (options.status === 'error') {
126
+ otelSpan.setStatus({
127
+ code: api.SpanStatusCode.ERROR,
128
+ message: options.error?.message ?? 'Unknown error',
129
+ })
130
+ if (options.error) {
131
+ otelSpan.recordException(options.error)
132
+ }
133
+ } else if (options.status === 'cancelled') {
134
+ otelSpan.setStatus({
135
+ code: api.SpanStatusCode.OK,
136
+ message: 'Cancelled',
137
+ })
138
+ otelSpan.setAttribute('duron.job.cancelled', true)
139
+ } else {
140
+ otelSpan.setStatus({ code: api.SpanStatusCode.OK })
141
+ }
142
+
143
+ otelSpan.end()
144
+ this.#spanMap.delete(span.id)
145
+ }
146
+
147
+ protected async _startStepSpan(options: StartStepSpanOptions): Promise<Span> {
148
+ if (!this.#tracer) {
149
+ throw new Error('OpenTelemetry tracer not initialized')
150
+ }
151
+
152
+ const api = await import('@opentelemetry/api')
153
+
154
+ // Get parent span context
155
+ let parentContext = api.context.active()
156
+ if (options.parentSpan) {
157
+ const parentExtSpan = options.parentSpan as ExtendedSpan
158
+ if (parentExtSpan.otelSpan) {
159
+ parentContext = api.trace.setSpan(api.context.active(), parentExtSpan.otelSpan)
160
+ }
161
+ }
162
+
163
+ const otelSpan = this.#tracer.startSpan(
164
+ `step:${options.stepName}`,
165
+ {
166
+ kind: api.SpanKind.INTERNAL,
167
+ attributes: {
168
+ 'duron.job.id': options.jobId,
169
+ 'duron.step.id': options.stepId,
170
+ 'duron.step.name': options.stepName,
171
+ 'duron.step.parent_step_id': options.parentStepId ?? undefined,
172
+ },
173
+ },
174
+ parentContext,
175
+ )
176
+
177
+ const spanId = `step:${options.stepId}`
178
+ this.#spanMap.set(spanId, otelSpan)
179
+
180
+ const span: ExtendedSpan = {
181
+ id: spanId,
182
+ jobId: options.jobId,
183
+ stepId: options.stepId,
184
+ parentSpanId: options.parentSpan?.id ?? null,
185
+ otelSpan,
186
+ }
187
+
188
+ return span
189
+ }
190
+
191
+ protected async _endStepSpan(span: Span, options: EndSpanOptions): Promise<void> {
192
+ const api = await import('@opentelemetry/api')
193
+ const extSpan = span as ExtendedSpan
194
+ const otelSpan = extSpan.otelSpan
195
+
196
+ if (options.status === 'error') {
197
+ otelSpan.setStatus({
198
+ code: api.SpanStatusCode.ERROR,
199
+ message: options.error?.message ?? 'Unknown error',
200
+ })
201
+ if (options.error) {
202
+ otelSpan.recordException(options.error)
203
+ }
204
+ } else if (options.status === 'cancelled') {
205
+ otelSpan.setStatus({
206
+ code: api.SpanStatusCode.OK,
207
+ message: 'Cancelled',
208
+ })
209
+ otelSpan.setAttribute('duron.step.cancelled', true)
210
+ } else {
211
+ otelSpan.setStatus({ code: api.SpanStatusCode.OK })
212
+ }
213
+
214
+ otelSpan.end()
215
+ this.#spanMap.delete(span.id)
216
+ }
217
+
218
+ protected async _startDatabaseSpan(options: StartDatabaseSpanOptions): Promise<Span | null> {
219
+ if (!this.#traceDatabaseQueries || !this.#tracer) {
220
+ return null
221
+ }
222
+
223
+ const api = await import('@opentelemetry/api')
224
+
225
+ const otelSpan = this.#tracer.startSpan(`db:${options.operation}`, {
226
+ kind: api.SpanKind.CLIENT,
227
+ attributes: {
228
+ 'db.system': 'postgresql',
229
+ 'db.operation': options.operation,
230
+ 'db.statement': options.query,
231
+ },
232
+ })
233
+
234
+ const spanId = `db:${globalThis.crypto.randomUUID()}`
235
+ this.#spanMap.set(spanId, otelSpan)
236
+
237
+ const span: ExtendedSpan = {
238
+ id: spanId,
239
+ jobId: '',
240
+ stepId: null,
241
+ parentSpanId: null,
242
+ otelSpan,
243
+ }
244
+
245
+ return span
246
+ }
247
+
248
+ protected async _endDatabaseSpan(span: Span, options: EndSpanOptions): Promise<void> {
249
+ const api = await import('@opentelemetry/api')
250
+ const extSpan = span as ExtendedSpan
251
+ const otelSpan = extSpan.otelSpan
252
+
253
+ if (options.status === 'error') {
254
+ otelSpan.setStatus({
255
+ code: api.SpanStatusCode.ERROR,
256
+ message: options.error?.message ?? 'Unknown error',
257
+ })
258
+ if (options.error) {
259
+ otelSpan.recordException(options.error)
260
+ }
261
+ } else {
262
+ otelSpan.setStatus({ code: api.SpanStatusCode.OK })
263
+ }
264
+
265
+ otelSpan.end()
266
+ this.#spanMap.delete(span.id)
267
+ }
268
+
269
+ // ============================================================================
270
+ // Metrics Methods
271
+ // ============================================================================
272
+
273
+ protected async _recordMetric(options: RecordMetricOptions): Promise<void> {
274
+ // OpenTelemetry metrics would require MeterProvider
275
+ // For now, we record as span events on the current active span
276
+ const api = await import('@opentelemetry/api')
277
+ const activeSpan = api.trace.getActiveSpan()
278
+
279
+ if (activeSpan) {
280
+ activeSpan.addEvent('metric', {
281
+ 'metric.name': options.name,
282
+ 'metric.value': options.value,
283
+ ...options.attributes,
284
+ })
285
+ }
286
+ }
287
+
288
+ protected async _addSpanEvent(options: AddSpanEventOptions): Promise<void> {
289
+ const extSpan = options.span as ExtendedSpan
290
+ if (extSpan.otelSpan) {
291
+ extSpan.otelSpan.addEvent(options.name, options.attributes)
292
+ }
293
+ }
294
+
295
+ protected async _addSpanAttribute(options: AddSpanAttributeOptions): Promise<void> {
296
+ const extSpan = options.span as ExtendedSpan
297
+ if (extSpan.otelSpan) {
298
+ extSpan.otelSpan.setAttribute(options.key, options.value)
299
+ }
300
+ }
301
+ }
302
+
303
+ /**
304
+ * Create an OpenTelemetry telemetry adapter.
305
+ * Exports traces to external systems like Jaeger, OTLP, etc.
306
+ *
307
+ * @param options - Configuration options
308
+ * @returns OpenTelemetryAdapter instance
309
+ */
310
+ export const openTelemetryAdapter = (options?: OpenTelemetryAdapterOptions) => new OpenTelemetryAdapter(options)