abxbus 2.4.19 → 2.4.22

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.
@@ -1,13 +1,20 @@
1
1
  import {
2
2
  ROOT_CONTEXT,
3
+ SpanKind,
3
4
  SpanStatusCode,
4
5
  trace,
5
6
  type Context,
6
7
  type Span,
7
8
  type SpanAttributeValue,
8
9
  type SpanAttributes,
10
+ type SpanContext,
9
11
  type Tracer,
10
12
  } from '@opentelemetry/api'
13
+ import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
14
+ import { resourceFromAttributes } from '@opentelemetry/resources'
15
+ import { BasicTracerProvider, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'
16
+ import type { SpanLimits, SpanProcessor } from '@opentelemetry/sdk-trace-base'
17
+ import { SpanImpl } from '@opentelemetry/sdk-trace-base/build/src/Span.js'
11
18
 
12
19
  import type { BaseEvent } from './base_event.js'
13
20
  import type { EventBus } from './event_bus.js'
@@ -15,11 +22,37 @@ import type { EventResult } from './event_result.js'
15
22
  import type { EventBusMiddleware } from './middlewares.js'
16
23
  import type { EventStatus } from './types.js'
17
24
 
18
- type OpenTelemetryTraceApi = Pick<typeof trace, 'getTracer' | 'setSpan'>
25
+ type OpenTelemetryTraceApi = Pick<typeof trace, 'getTracer' | 'setSpan'> & Partial<Pick<typeof trace, 'setSpanContext'>>
26
+
27
+ export type OtelTracingSpanFactoryInput = {
28
+ name: string
29
+ span_context: SpanContext
30
+ parent_span_context?: SpanContext
31
+ attributes: SpanAttributes
32
+ start_time?: Date
33
+ }
34
+
35
+ export type OtelTracingSpanFactory = (input: OtelTracingSpanFactoryInput) => Span
36
+
37
+ type OtelTracingSpanProviderInternals = {
38
+ _activeSpanProcessor?: SpanProcessor
39
+ _config?: {
40
+ resource?: unknown
41
+ spanLimits?: SpanLimits
42
+ }
43
+ _resource?: unknown
44
+ }
45
+
46
+ export type OtelTracingSpanProvider = object
19
47
 
20
48
  export type OtelTracingMiddlewareOptions = {
21
49
  tracer?: Tracer
22
50
  trace_api?: OpenTelemetryTraceApi
51
+ span_provider?: OtelTracingSpanProvider
52
+ span_factory?: OtelTracingSpanFactory
53
+ otlp_endpoint?: string
54
+ service_name?: string
55
+ instrumentation_name?: string
23
56
  root_span_name?: string | ((eventbus: EventBus, event: BaseEvent) => string)
24
57
  root_span_attributes?: SpanAttributes | ((eventbus: EventBus, event: BaseEvent) => SpanAttributes)
25
58
  }
@@ -27,6 +60,8 @@ export type OtelTracingMiddlewareOptions = {
27
60
  export class OtelTracingMiddleware implements EventBusMiddleware {
28
61
  private readonly tracer: Tracer
29
62
  private readonly trace_api: OpenTelemetryTraceApi
63
+ private readonly span_factory?: OtelTracingSpanFactory
64
+ private readonly span_provider?: OtelTracingSpanProvider
30
65
  private readonly root_span_name: OtelTracingMiddlewareOptions['root_span_name']
31
66
  private readonly root_span_attributes: OtelTracingMiddlewareOptions['root_span_attributes']
32
67
  private readonly root_spans = new Map<string, Span>()
@@ -39,12 +74,21 @@ export class OtelTracingMiddleware implements EventBusMiddleware {
39
74
  constructor(options: OtelTracingMiddlewareOptions = {}) {
40
75
  this.trace_api = options.trace_api ?? trace
41
76
  this.tracer = options.tracer ?? this.trace_api.getTracer('abxbus')
77
+ this.span_provider = options.span_provider ?? (options.otlp_endpoint ? createOtlpSpanProvider(options) : undefined)
78
+ this.span_factory =
79
+ options.span_factory ??
80
+ (this.span_provider
81
+ ? createProviderSpanFactory(this.trace_api, this.span_provider, options.instrumentation_name ?? 'abxbus')
82
+ : undefined)
42
83
  this.root_span_name = options.root_span_name
43
84
  this.root_span_attributes = options.root_span_attributes
44
85
  }
45
86
 
46
87
  onEventChange(eventbus: EventBus, event: BaseEvent, status: EventStatus): void {
47
88
  if (status === 'started') {
89
+ if (this.span_factory) {
90
+ return
91
+ }
48
92
  this.startEventSpan(eventbus, event)
49
93
  return
50
94
  }
@@ -56,12 +100,15 @@ export class OtelTracingMiddleware implements EventBusMiddleware {
56
100
 
57
101
  onEventResultChange(eventbus: EventBus, event: BaseEvent, event_result: EventResult, status: EventStatus): void {
58
102
  if (status === 'started') {
103
+ if (this.span_factory) {
104
+ return
105
+ }
59
106
  this.startHandlerSpan(eventbus, event, event_result)
60
107
  return
61
108
  }
62
109
 
63
110
  if (status === 'completed') {
64
- this.completeHandlerSpan(event_result)
111
+ this.completeHandlerSpan(eventbus, event, event_result)
65
112
  }
66
113
  }
67
114
 
@@ -98,6 +145,11 @@ export class OtelTracingMiddleware implements EventBusMiddleware {
98
145
  }
99
146
 
100
147
  private completeEventSpan(eventbus: EventBus, event: BaseEvent): void {
148
+ if (this.span_factory) {
149
+ this.completeEventSpanWithFactory(eventbus, event)
150
+ return
151
+ }
152
+
101
153
  const span = this.event_spans.get(event.event_id) ?? this.startEventSpan(eventbus, event)
102
154
  if (event.event_errors.length > 0) {
103
155
  recordSpanError(span, event.event_errors[0])
@@ -152,7 +204,12 @@ export class OtelTracingMiddleware implements EventBusMiddleware {
152
204
  return span
153
205
  }
154
206
 
155
- private completeHandlerSpan(event_result: EventResult): void {
207
+ private completeHandlerSpan(eventbus: EventBus, event: BaseEvent, event_result: EventResult): void {
208
+ if (this.span_factory) {
209
+ this.completeHandlerSpanWithFactory(eventbus, event, event_result)
210
+ return
211
+ }
212
+
156
213
  const span = this.handler_spans.get(event_result.id)
157
214
  if (!span) {
158
215
  return
@@ -226,12 +283,244 @@ export class OtelTracingMiddleware implements EventBusMiddleware {
226
283
 
227
284
  return event.event_parent_id ? this.event_contexts.get(event.event_parent_id) : undefined
228
285
  }
286
+
287
+ private completeEventSpanWithFactory(eventbus: EventBus, event: BaseEvent): void {
288
+ const root_event = rootEventForEvent(eventbus, event)
289
+ const trace_id = traceIdForRootEvent(root_event.event_id)
290
+ const event_context = eventSpanContext(trace_id, event.event_id)
291
+ const start_time = dateFromIso(event.event_started_at)
292
+ const end_time = endTimeAfterStart(start_time, dateFromIso(event.event_completed_at))
293
+
294
+ if (!event.event_parent_id) {
295
+ const root_span = this.span_factory!({
296
+ name: resolveRootSpanName(this.root_span_name, eventbus, event),
297
+ span_context: rootSpanContext(trace_id, event.event_id),
298
+ attributes: rootSpanAttributes(this.root_span_attributes, eventbus, event),
299
+ start_time,
300
+ })
301
+ root_span.setStatus({ code: SpanStatusCode.OK })
302
+ root_span.end(end_time)
303
+ }
304
+
305
+ const span = this.span_factory!({
306
+ name: `abxbus.event ${event.event_type}`,
307
+ span_context: event_context,
308
+ parent_span_context: parentSpanContextForEvent(event, trace_id),
309
+ attributes: eventSpanAttributes(eventbus, event),
310
+ start_time,
311
+ })
312
+ if (event.event_errors.length > 0) {
313
+ recordSpanError(span, event.event_errors[0])
314
+ } else {
315
+ span.setStatus({ code: SpanStatusCode.OK })
316
+ }
317
+ span.end(end_time)
318
+ }
319
+
320
+ private completeHandlerSpanWithFactory(eventbus: EventBus, event: BaseEvent, event_result: EventResult): void {
321
+ const root_event = rootEventForEvent(eventbus, event)
322
+ const trace_id = traceIdForRootEvent(root_event.event_id)
323
+ const start_time = dateFromIso(event_result.started_at)
324
+ const span = this.span_factory!({
325
+ name: `abxbus.handler ${event.event_type} ${event_result.handler_name}`,
326
+ span_context: handlerSpanContext(trace_id, event_result.event_id, event_result.handler_id),
327
+ parent_span_context: eventSpanContext(trace_id, event.event_id),
328
+ attributes: handlerSpanAttributes(eventbus, event, event_result),
329
+ start_time,
330
+ })
331
+
332
+ if (event_result.error !== undefined) {
333
+ recordSpanError(span, event_result.error)
334
+ } else {
335
+ span.setStatus({ code: SpanStatusCode.OK })
336
+ }
337
+ span.end(endTimeAfterStart(start_time, dateFromIso(event_result.completed_at)))
338
+ }
229
339
  }
230
340
 
231
341
  function handlerSpanKey(event_id: string, handler_id: string): string {
232
342
  return `${event_id}:${handler_id}`
233
343
  }
234
344
 
345
+ function createOtlpSpanProvider(options: OtelTracingMiddlewareOptions): OtelTracingSpanProvider {
346
+ return new BasicTracerProvider({
347
+ resource: resourceFromAttributes({
348
+ 'service.name': options.service_name ?? 'abxbus',
349
+ }),
350
+ spanProcessors: [
351
+ new SimpleSpanProcessor(
352
+ new OTLPTraceExporter({
353
+ url: normalizeOtlpTracesEndpoint(options.otlp_endpoint!),
354
+ })
355
+ ),
356
+ ],
357
+ })
358
+ }
359
+
360
+ function createProviderSpanFactory(
361
+ trace_api: OpenTelemetryTraceApi,
362
+ provider: OtelTracingSpanProvider,
363
+ instrumentation_name: string
364
+ ): OtelTracingSpanFactory {
365
+ const provider_internals = provider as OtelTracingSpanProviderInternals
366
+ return (input: OtelTracingSpanFactoryInput): Span => {
367
+ const span_processor = provider_internals._activeSpanProcessor
368
+ const span_limits = provider_internals._config?.spanLimits
369
+ const resource = provider_internals._resource ?? provider_internals._config?.resource
370
+ if (!span_processor || !span_limits || !resource) {
371
+ throw new Error('OtelTracingMiddleware span_provider must be an OpenTelemetry SDK trace provider with active span internals')
372
+ }
373
+
374
+ const parent_context = input.parent_span_context
375
+ ? (trace_api.setSpanContext ?? trace.setSpanContext)(ROOT_CONTEXT, input.parent_span_context)
376
+ : ROOT_CONTEXT
377
+ return new SpanImpl({
378
+ resource,
379
+ scope: { name: instrumentation_name },
380
+ context: parent_context,
381
+ spanContext: input.span_context,
382
+ parentSpanContext: input.parent_span_context,
383
+ name: input.name,
384
+ kind: SpanKind.INTERNAL,
385
+ attributes: input.attributes,
386
+ startTime: input.start_time,
387
+ spanProcessor: span_processor,
388
+ spanLimits: span_limits,
389
+ } as ConstructorParameters<typeof SpanImpl>[0])
390
+ }
391
+ }
392
+
393
+ function normalizeOtlpTracesEndpoint(endpoint: string): string {
394
+ const trimmed = endpoint.replace(/\/+$/, '')
395
+ return trimmed.endsWith('/v1/traces') ? trimmed : `${trimmed}/v1/traces`
396
+ }
397
+
398
+ function eventSpanAttributes(eventbus: EventBus, event: BaseEvent): SpanAttributes {
399
+ return compactAttributes({
400
+ 'abxbus.bus.id': eventbus.id,
401
+ 'abxbus.bus.name': eventbus.name,
402
+ 'abxbus.event.id': event.event_id,
403
+ 'abxbus.event.type': event.event_type,
404
+ 'abxbus.event.version': event.event_version,
405
+ 'abxbus.event.session_id': stringValue((event as { session_id?: unknown }).session_id),
406
+ 'abxbus.event.parent_id': event.event_parent_id,
407
+ 'abxbus.event.emitted_by_handler_id': event.event_emitted_by_handler_id,
408
+ 'abxbus.event.path': event.event_path.join(' '),
409
+ 'abxbus.event.status': event.event_status,
410
+ 'abxbus.event.result_count': event.event_results.size,
411
+ 'abxbus.event.error_count': event.event_errors.length,
412
+ 'abxbus.event.child_count': event.event_children.length,
413
+ })
414
+ }
415
+
416
+ function handlerSpanAttributes(eventbus: EventBus, event: BaseEvent, event_result: EventResult): SpanAttributes {
417
+ return compactAttributes({
418
+ 'abxbus.bus.id': eventbus.id,
419
+ 'abxbus.bus.name': eventbus.name,
420
+ 'abxbus.event.id': event.event_id,
421
+ 'abxbus.event.type': event.event_type,
422
+ 'abxbus.handler.id': event_result.handler_id,
423
+ 'abxbus.handler.name': event_result.handler_name,
424
+ 'abxbus.handler.file_path': event_result.handler_file_path,
425
+ 'abxbus.handler.event_pattern': event_result.handler.event_pattern,
426
+ 'abxbus.event_result.id': event_result.id,
427
+ 'abxbus.event_result.status': event_result.status,
428
+ 'abxbus.handler.child_count': event_result.event_children.length,
429
+ })
430
+ }
431
+
432
+ function rootSpanAttributes(
433
+ root_span_attributes: OtelTracingMiddlewareOptions['root_span_attributes'],
434
+ eventbus: EventBus,
435
+ event: BaseEvent
436
+ ): SpanAttributes {
437
+ const session_id = stringValue((event as { session_id?: unknown }).session_id)
438
+ return compactAttributes({
439
+ ...resolveAttributes(root_span_attributes, eventbus, event),
440
+ 'abxbus.trace.root': true,
441
+ 'abxbus.bus.id': eventbus.id,
442
+ 'abxbus.bus.name': eventbus.name,
443
+ 'abxbus.root_event.id': event.event_id,
444
+ 'abxbus.root_event.type': event.event_type,
445
+ 'abxbus.root_event.session_id': session_id,
446
+ 'abxbus.root_event.status': event.event_status,
447
+ 'abxbus.root_event.error_count': event.event_errors.length,
448
+ 'abxbus.root_event.child_count': event.event_children.length,
449
+ })
450
+ }
451
+
452
+ function rootEventForEvent(eventbus: EventBus, event: BaseEvent): BaseEvent {
453
+ let current = event._event_original ?? event
454
+ const seen = new Set<string>()
455
+ while (current.event_parent_id && !seen.has(current.event_id)) {
456
+ seen.add(current.event_id)
457
+ const parent = eventbus.findEventById(current.event_parent_id)
458
+ if (!parent) {
459
+ break
460
+ }
461
+ current = parent._event_original ?? parent
462
+ }
463
+ return current
464
+ }
465
+
466
+ function parentSpanContextForEvent(event: BaseEvent, trace_id: string): SpanContext {
467
+ if (!event.event_parent_id) {
468
+ return rootSpanContext(trace_id, event.event_id)
469
+ }
470
+
471
+ if (event.event_emitted_by_handler_id) {
472
+ return handlerSpanContext(trace_id, event.event_parent_id, event.event_emitted_by_handler_id)
473
+ }
474
+
475
+ return eventSpanContext(trace_id, event.event_parent_id)
476
+ }
477
+
478
+ function rootSpanContext(trace_id: string, event_id: string): SpanContext {
479
+ return {
480
+ traceId: trace_id,
481
+ spanId: deterministicSpanId(`abxbus.root:${event_id}`),
482
+ traceFlags: 1,
483
+ }
484
+ }
485
+
486
+ function eventSpanContext(trace_id: string, event_id: string): SpanContext {
487
+ return {
488
+ traceId: trace_id,
489
+ spanId: deterministicSpanId(`abxbus.event:${event_id}`),
490
+ traceFlags: 1,
491
+ }
492
+ }
493
+
494
+ function handlerSpanContext(trace_id: string, event_id: string, handler_id: string): SpanContext {
495
+ return {
496
+ traceId: trace_id,
497
+ spanId: deterministicSpanId(`abxbus.handler:${event_id}:${handler_id}`),
498
+ traceFlags: 1,
499
+ }
500
+ }
501
+
502
+ function traceIdForRootEvent(event_id: string): string {
503
+ return `${fnv1a64Hex(`abxbus.trace.a:${event_id}`)}${fnv1a64Hex(`abxbus.trace.b:${event_id}`)}`
504
+ }
505
+
506
+ function deterministicSpanId(input: string): string {
507
+ return fnv1a64Hex(input)
508
+ }
509
+
510
+ function fnv1a64Hex(input: string): string {
511
+ let hash = 0xcbf29ce484222325n
512
+ const prime = 0x100000001b3n
513
+ const mask = 0xffffffffffffffffn
514
+ for (let index = 0; index < input.length; index += 1) {
515
+ hash ^= BigInt(input.charCodeAt(index))
516
+ hash = (hash * prime) & mask
517
+ }
518
+ if (hash === 0n) {
519
+ hash = 1n
520
+ }
521
+ return hash.toString(16).padStart(16, '0')
522
+ }
523
+
235
524
  function dateFromIso(value: string | null | undefined): Date | undefined {
236
525
  if (value == null) {
237
526
  return undefined