duron 0.3.0-beta.5 → 0.3.0-beta.7

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.
@@ -108,6 +108,106 @@ export interface AddSpanAttributeOptions {
108
108
  value: string | number | boolean
109
109
  }
110
110
 
111
+ /**
112
+ * Options for starting a custom span with the tracer.
113
+ */
114
+ export interface StartSpanOptions {
115
+ /**
116
+ * Span kind (internal, client, server, producer, consumer).
117
+ * @default 'internal'
118
+ */
119
+ kind?: 'internal' | 'client' | 'server' | 'producer' | 'consumer'
120
+
121
+ /**
122
+ * Initial attributes for the span.
123
+ */
124
+ attributes?: Record<string, string | number | boolean>
125
+
126
+ /**
127
+ * Parent span to use for context propagation.
128
+ * If not provided, uses the current active context.
129
+ */
130
+ parentSpan?: TracerSpan
131
+ }
132
+
133
+ /**
134
+ * A span created by the Tracer for manual instrumentation.
135
+ */
136
+ export interface TracerSpan {
137
+ /**
138
+ * Set an attribute on the span.
139
+ *
140
+ * @param key - The attribute key
141
+ * @param value - The attribute value
142
+ */
143
+ setAttribute(key: string, value: string | number | boolean): void
144
+
145
+ /**
146
+ * Set multiple attributes on the span.
147
+ *
148
+ * @param attributes - The attributes to set
149
+ */
150
+ setAttributes(attributes: Record<string, string | number | boolean>): void
151
+
152
+ /**
153
+ * Add an event to the span.
154
+ *
155
+ * @param name - The event name
156
+ * @param attributes - Optional event attributes
157
+ */
158
+ addEvent(name: string, attributes?: Record<string, string | number | boolean>): void
159
+
160
+ /**
161
+ * Record an exception on the span.
162
+ *
163
+ * @param error - The error to record
164
+ */
165
+ recordException(error: Error): void
166
+
167
+ /**
168
+ * Set the span status to OK.
169
+ */
170
+ setStatusOk(): void
171
+
172
+ /**
173
+ * Set the span status to error.
174
+ *
175
+ * @param message - Optional error message
176
+ */
177
+ setStatusError(message?: string): void
178
+
179
+ /**
180
+ * End the span.
181
+ * After calling this, no more operations can be performed on the span.
182
+ */
183
+ end(): void
184
+
185
+ /**
186
+ * Check if this span is recording.
187
+ */
188
+ isRecording(): boolean
189
+ }
190
+
191
+ /**
192
+ * A Tracer provides methods for creating spans.
193
+ * Similar to OpenTelemetry's Tracer interface.
194
+ */
195
+ export interface Tracer {
196
+ /**
197
+ * The name of this tracer.
198
+ */
199
+ readonly name: string
200
+
201
+ /**
202
+ * Start a new span.
203
+ *
204
+ * @param name - The name of the span
205
+ * @param options - Optional span configuration
206
+ * @returns A TracerSpan for manual instrumentation
207
+ */
208
+ startSpan(name: string, options?: StartSpanOptions): TracerSpan
209
+ }
210
+
111
211
  /**
112
212
  * Observe context provided to action and step handlers.
113
213
  */
@@ -136,6 +236,37 @@ export interface ObserveContext {
136
236
  * @param attributes - Optional event attributes
137
237
  */
138
238
  addSpanEvent(name: string, attributes?: Record<string, any>): void
239
+
240
+ /**
241
+ * Get a tracer for manual instrumentation.
242
+ * Similar to OpenTelemetry's `trace.getTracer()` method.
243
+ *
244
+ * @param name - The name of the tracer (typically your service or library name)
245
+ * @returns A Tracer for creating custom spans
246
+ *
247
+ * @example
248
+ * ```typescript
249
+ * const tracer = ctx.observe.getTracer('my-service')
250
+ *
251
+ * const span = tracer.startSpan('external-api-call', {
252
+ * kind: 'client',
253
+ * attributes: { 'api.endpoint': '/users' }
254
+ * })
255
+ *
256
+ * try {
257
+ * const result = await fetch('https://api.example.com/users')
258
+ * span.setStatusOk()
259
+ * return result
260
+ * } catch (error) {
261
+ * span.recordException(error)
262
+ * span.setStatusError(error.message)
263
+ * throw error
264
+ * } finally {
265
+ * span.end()
266
+ * }
267
+ * ```
268
+ */
269
+ getTracer(name: string): Tracer
139
270
  }
140
271
 
141
272
  // ============================================================================
@@ -369,6 +500,41 @@ export abstract class TelemetryAdapter {
369
500
  return this._addSpanAttribute(options)
370
501
  }
371
502
 
503
+ // ============================================================================
504
+ // Tracer Methods
505
+ // ============================================================================
506
+
507
+ /**
508
+ * Get a tracer for manual instrumentation.
509
+ * Similar to OpenTelemetry's `trace.getTracer()` method.
510
+ *
511
+ * @param name - The name of the tracer (typically your service or library name)
512
+ * @returns A Tracer for creating custom spans
513
+ *
514
+ * @example
515
+ * ```typescript
516
+ * const tracer = telemetry.getTracer('my-service')
517
+ *
518
+ * const span = tracer.startSpan('process-order', {
519
+ * attributes: { 'order.id': orderId }
520
+ * })
521
+ *
522
+ * try {
523
+ * // Do some work
524
+ * span.addEvent('order.validated')
525
+ * span.setStatusOk()
526
+ * } catch (error) {
527
+ * span.recordException(error)
528
+ * span.setStatusError(error.message)
529
+ * } finally {
530
+ * span.end()
531
+ * }
532
+ * ```
533
+ */
534
+ getTracer(name: string): Tracer {
535
+ return this._getTracer(name)
536
+ }
537
+
372
538
  // ============================================================================
373
539
  // Context Methods
374
540
  // ============================================================================
@@ -404,6 +570,9 @@ export abstract class TelemetryAdapter {
404
570
  this.#logger?.error(err, 'Error adding span event')
405
571
  })
406
572
  },
573
+ getTracer: (name: string) => {
574
+ return this.getTracer(name)
575
+ },
407
576
  }
408
577
  }
409
578
 
@@ -465,4 +634,9 @@ export abstract class TelemetryAdapter {
465
634
  * Internal method to add a span attribute.
466
635
  */
467
636
  protected abstract _addSpanAttribute(options: AddSpanAttributeOptions): Promise<void>
637
+
638
+ /**
639
+ * Internal method to get a tracer for manual instrumentation.
640
+ */
641
+ protected abstract _getTracer(name: string): Tracer
468
642
  }
@@ -9,8 +9,11 @@ export {
9
9
  type Span,
10
10
  type StartDatabaseSpanOptions,
11
11
  type StartJobSpanOptions,
12
+ type StartSpanOptions,
12
13
  type StartStepSpanOptions,
13
14
  TelemetryAdapter,
15
+ type Tracer,
16
+ type TracerSpan,
14
17
  } from './adapter.js'
15
18
  export { LocalTelemetryAdapter, type LocalTelemetryAdapterOptions, localTelemetryAdapter } from './local.js'
16
19
  export { NoopTelemetryAdapter, noopTelemetryAdapter } from './noop.js'
@@ -7,8 +7,11 @@ import {
7
7
  type Span,
8
8
  type StartDatabaseSpanOptions,
9
9
  type StartJobSpanOptions,
10
+ type StartSpanOptions,
10
11
  type StartStepSpanOptions,
11
12
  TelemetryAdapter,
13
+ type Tracer,
14
+ type TracerSpan,
12
15
  } from './adapter.js'
13
16
 
14
17
  // ============================================================================
@@ -304,6 +307,96 @@ export class LocalTelemetryAdapter extends TelemetryAdapter {
304
307
  },
305
308
  })
306
309
  }
310
+
311
+ // ============================================================================
312
+ // Tracer Methods
313
+ // ============================================================================
314
+
315
+ protected _getTracer(name: string): Tracer {
316
+ const adapter = this
317
+
318
+ return {
319
+ name,
320
+
321
+ startSpan(spanName: string, options?: StartSpanOptions): TracerSpan {
322
+ const spanId = `tracer:${name}:${globalThis.crypto.randomUUID()}`
323
+ const startTime = Date.now()
324
+ let ended = false
325
+ const attributes: Record<string, string | number | boolean> = {
326
+ ...options?.attributes,
327
+ }
328
+
329
+ // Note: Local adapter tracer spans don't have a jobId context,
330
+ // so they can't be stored in the database. They're essentially no-ops
331
+ // but provide a consistent API for code that needs a tracer.
332
+ // For actual metrics storage, use ctx.observe within action/step handlers.
333
+
334
+ const tracerSpan: TracerSpan = {
335
+ setAttribute(key: string, value: string | number | boolean): void {
336
+ if (!ended) {
337
+ attributes[key] = value
338
+ }
339
+ },
340
+
341
+ setAttributes(attrs: Record<string, string | number | boolean>): void {
342
+ if (!ended) {
343
+ Object.assign(attributes, attrs)
344
+ }
345
+ },
346
+
347
+ addEvent(eventName: string, eventAttrs?: Record<string, string | number | boolean>): void {
348
+ if (!ended) {
349
+ adapter.logger?.debug({ spanId, event: eventName, attributes: eventAttrs }, 'Tracer span event')
350
+ }
351
+ },
352
+
353
+ recordException(error: Error): void {
354
+ if (!ended) {
355
+ attributes['error.message'] = error.message
356
+ attributes['error.name'] = error.name
357
+ adapter.logger?.debug({ spanId, error: error.message }, 'Tracer span exception')
358
+ }
359
+ },
360
+
361
+ setStatusOk(): void {
362
+ if (!ended) {
363
+ // biome-ignore lint/complexity/useLiteralKeys: Index signature requires bracket notation
364
+ attributes['status'] = 'ok'
365
+ }
366
+ },
367
+
368
+ setStatusError(message?: string): void {
369
+ if (!ended) {
370
+ // biome-ignore lint/complexity/useLiteralKeys: Index signature requires bracket notation
371
+ attributes['status'] = 'error'
372
+ if (message) {
373
+ attributes['status.message'] = message
374
+ }
375
+ }
376
+ },
377
+
378
+ end(): void {
379
+ if (!ended) {
380
+ ended = true
381
+ const duration = Date.now() - startTime
382
+ adapter.logger?.debug(
383
+ { spanId, spanName, tracerName: name, durationMs: duration, attributes },
384
+ 'Tracer span ended',
385
+ )
386
+ }
387
+ },
388
+
389
+ isRecording(): boolean {
390
+ return !ended
391
+ },
392
+ }
393
+
394
+ adapter.logger?.debug({ spanId, spanName, tracerName: name }, 'Tracer span started')
395
+
396
+ return tracerSpan
397
+ },
398
+ }
399
+ }
307
400
  }
308
401
 
309
402
  /**
@@ -8,8 +8,41 @@ import {
8
8
  type StartJobSpanOptions,
9
9
  type StartStepSpanOptions,
10
10
  TelemetryAdapter,
11
+ type Tracer,
12
+ type TracerSpan,
11
13
  } from './adapter.js'
12
14
 
15
+ // ============================================================================
16
+ // Noop Tracer Span
17
+ // ============================================================================
18
+
19
+ const noopTracerSpan: TracerSpan = {
20
+ setAttribute() {
21
+ // No-op
22
+ },
23
+ setAttributes() {
24
+ // No-op
25
+ },
26
+ addEvent() {
27
+ // No-op
28
+ },
29
+ recordException() {
30
+ // No-op
31
+ },
32
+ setStatusOk() {
33
+ // No-op
34
+ },
35
+ setStatusError() {
36
+ // No-op
37
+ },
38
+ end() {
39
+ // No-op
40
+ },
41
+ isRecording() {
42
+ return false
43
+ },
44
+ }
45
+
13
46
  // ============================================================================
14
47
  // Noop Telemetry Adapter
15
48
  // ============================================================================
@@ -84,6 +117,19 @@ export class NoopTelemetryAdapter extends TelemetryAdapter {
84
117
  protected async _addSpanAttribute(_options: AddSpanAttributeOptions): Promise<void> {
85
118
  // No-op
86
119
  }
120
+
121
+ // ============================================================================
122
+ // Tracer Methods
123
+ // ============================================================================
124
+
125
+ protected _getTracer(name: string): Tracer {
126
+ return {
127
+ name,
128
+ startSpan(): TracerSpan {
129
+ return noopTracerSpan
130
+ },
131
+ }
132
+ }
87
133
  }
88
134
 
89
135
  /**
@@ -1,4 +1,4 @@
1
- import type { Span as OTelSpan, Tracer, TracerProvider } from '@opentelemetry/api'
1
+ import type { Span as OTelSpan, Tracer as OTelTracer, TracerProvider } from '@opentelemetry/api'
2
2
 
3
3
  import {
4
4
  type AddSpanAttributeOptions,
@@ -8,8 +8,11 @@ import {
8
8
  type Span,
9
9
  type StartDatabaseSpanOptions,
10
10
  type StartJobSpanOptions,
11
+ type StartSpanOptions,
11
12
  type StartStepSpanOptions,
12
13
  TelemetryAdapter,
14
+ type Tracer,
15
+ type TracerSpan,
13
16
  } from './adapter.js'
14
17
 
15
18
  // ============================================================================
@@ -52,8 +55,9 @@ export class OpenTelemetryAdapter extends TelemetryAdapter {
52
55
  #serviceName: string
53
56
  #tracerProvider: TracerProvider | null
54
57
  #traceDatabaseQueries: boolean
55
- #tracer: Tracer | null = null
58
+ #tracer: OTelTracer | null = null
56
59
  #spanMap = new Map<string, OTelSpan>()
60
+ #tracerCache = new Map<string, Tracer>()
57
61
 
58
62
  constructor(options: OpenTelemetryAdapterOptions = {}) {
59
63
  super()
@@ -298,6 +302,145 @@ export class OpenTelemetryAdapter extends TelemetryAdapter {
298
302
  extSpan.otelSpan.setAttribute(options.key, options.value)
299
303
  }
300
304
  }
305
+
306
+ // ============================================================================
307
+ // Tracer Methods
308
+ // ============================================================================
309
+
310
+ protected _getTracer(name: string): Tracer {
311
+ // Return cached tracer if available
312
+ const cached = this.#tracerCache.get(name)
313
+ if (cached) {
314
+ return cached
315
+ }
316
+
317
+ const adapter = this
318
+
319
+ const tracer: Tracer = {
320
+ name,
321
+
322
+ startSpan(spanName: string, options?: StartSpanOptions): TracerSpan {
323
+ // We need to dynamically get the OpenTelemetry API
324
+ // Since _getTracer is synchronous, we need to handle this carefully
325
+ let otelSpan: OTelSpan | null = null
326
+ let api: typeof import('@opentelemetry/api') | null = null
327
+ let ended = false
328
+
329
+ // Initialize the span asynchronously but return synchronously
330
+ const initPromise = (async () => {
331
+ api = await import('@opentelemetry/api')
332
+
333
+ // Get the tracer
334
+ let otelTracer: OTelTracer
335
+ if (adapter.#tracerProvider) {
336
+ otelTracer = adapter.#tracerProvider.getTracer(name)
337
+ } else {
338
+ otelTracer = api.trace.getTracer(name)
339
+ }
340
+
341
+ // Map kind
342
+ let spanKind = api.SpanKind.INTERNAL
343
+ if (options?.kind === 'client') spanKind = api.SpanKind.CLIENT
344
+ else if (options?.kind === 'server') spanKind = api.SpanKind.SERVER
345
+ else if (options?.kind === 'producer') spanKind = api.SpanKind.PRODUCER
346
+ else if (options?.kind === 'consumer') spanKind = api.SpanKind.CONSUMER
347
+
348
+ // Get parent context
349
+ let parentContext = api.context.active()
350
+ if (options?.parentSpan) {
351
+ const parentOtelSpan = (options.parentSpan as any)._otelSpan as OTelSpan | undefined
352
+ if (parentOtelSpan) {
353
+ parentContext = api.trace.setSpan(api.context.active(), parentOtelSpan)
354
+ }
355
+ }
356
+
357
+ otelSpan = otelTracer.startSpan(
358
+ spanName,
359
+ {
360
+ kind: spanKind,
361
+ attributes: options?.attributes,
362
+ },
363
+ parentContext,
364
+ )
365
+ })()
366
+
367
+ const tracerSpan: TracerSpan & { _otelSpan?: OTelSpan } = {
368
+ setAttribute(key: string, value: string | number | boolean): void {
369
+ initPromise.then(() => {
370
+ if (otelSpan && !ended) {
371
+ otelSpan.setAttribute(key, value)
372
+ }
373
+ })
374
+ },
375
+
376
+ setAttributes(attributes: Record<string, string | number | boolean>): void {
377
+ initPromise.then(() => {
378
+ if (otelSpan && !ended) {
379
+ otelSpan.setAttributes(attributes)
380
+ }
381
+ })
382
+ },
383
+
384
+ addEvent(eventName: string, attributes?: Record<string, string | number | boolean>): void {
385
+ initPromise.then(() => {
386
+ if (otelSpan && !ended) {
387
+ otelSpan.addEvent(eventName, attributes)
388
+ }
389
+ })
390
+ },
391
+
392
+ recordException(error: Error): void {
393
+ initPromise.then(() => {
394
+ if (otelSpan && !ended) {
395
+ otelSpan.recordException(error)
396
+ }
397
+ })
398
+ },
399
+
400
+ setStatusOk(): void {
401
+ initPromise.then(() => {
402
+ if (otelSpan && api && !ended) {
403
+ otelSpan.setStatus({ code: api.SpanStatusCode.OK })
404
+ }
405
+ })
406
+ },
407
+
408
+ setStatusError(message?: string): void {
409
+ initPromise.then(() => {
410
+ if (otelSpan && api && !ended) {
411
+ otelSpan.setStatus({ code: api.SpanStatusCode.ERROR, message })
412
+ }
413
+ })
414
+ },
415
+
416
+ end(): void {
417
+ initPromise.then(() => {
418
+ if (otelSpan && !ended) {
419
+ ended = true
420
+ otelSpan.end()
421
+ }
422
+ })
423
+ },
424
+
425
+ isRecording(): boolean {
426
+ return otelSpan?.isRecording() ?? false
427
+ },
428
+ }
429
+
430
+ // Store reference for parent context propagation
431
+ initPromise.then(() => {
432
+ if (otelSpan) {
433
+ ;(tracerSpan as any)._otelSpan = otelSpan
434
+ }
435
+ })
436
+
437
+ return tracerSpan
438
+ },
439
+ }
440
+
441
+ this.#tracerCache.set(name, tracer)
442
+ return tracer
443
+ }
301
444
  }
302
445
 
303
446
  /**