abxbus 2.4.18 → 2.4.20

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,4 +1,14 @@
1
- import { ROOT_CONTEXT, SpanStatusCode, trace, type Context, type Span, type Tracer } from '@opentelemetry/api'
1
+ import {
2
+ ROOT_CONTEXT,
3
+ SpanStatusCode,
4
+ trace,
5
+ type Context,
6
+ type Span,
7
+ type SpanAttributeValue,
8
+ type SpanAttributes,
9
+ type SpanContext,
10
+ type Tracer,
11
+ } from '@opentelemetry/api'
2
12
 
3
13
  import type { BaseEvent } from './base_event.js'
4
14
  import type { EventBus } from './event_bus.js'
@@ -8,14 +18,32 @@ import type { EventStatus } from './types.js'
8
18
 
9
19
  type OpenTelemetryTraceApi = Pick<typeof trace, 'getTracer' | 'setSpan'>
10
20
 
21
+ export type OtelTracingSpanFactoryInput = {
22
+ name: string
23
+ span_context: SpanContext
24
+ parent_span_context?: SpanContext
25
+ attributes: SpanAttributes
26
+ start_time?: Date
27
+ }
28
+
29
+ export type OtelTracingSpanFactory = (input: OtelTracingSpanFactoryInput) => Span
30
+
11
31
  export type OtelTracingMiddlewareOptions = {
12
32
  tracer?: Tracer
13
33
  trace_api?: OpenTelemetryTraceApi
34
+ span_factory?: OtelTracingSpanFactory
35
+ root_span_name?: string | ((eventbus: EventBus, event: BaseEvent) => string)
36
+ root_span_attributes?: SpanAttributes | ((eventbus: EventBus, event: BaseEvent) => SpanAttributes)
14
37
  }
15
38
 
16
39
  export class OtelTracingMiddleware implements EventBusMiddleware {
17
40
  private readonly tracer: Tracer
18
41
  private readonly trace_api: OpenTelemetryTraceApi
42
+ private readonly span_factory?: OtelTracingSpanFactory
43
+ private readonly root_span_name: OtelTracingMiddlewareOptions['root_span_name']
44
+ private readonly root_span_attributes: OtelTracingMiddlewareOptions['root_span_attributes']
45
+ private readonly root_spans = new Map<string, Span>()
46
+ private readonly root_contexts = new Map<string, Context>()
19
47
  private readonly event_spans = new Map<string, Span>()
20
48
  private readonly event_contexts = new Map<string, Context>()
21
49
  private readonly handler_spans = new Map<string, Span>()
@@ -24,10 +52,16 @@ export class OtelTracingMiddleware implements EventBusMiddleware {
24
52
  constructor(options: OtelTracingMiddlewareOptions = {}) {
25
53
  this.trace_api = options.trace_api ?? trace
26
54
  this.tracer = options.tracer ?? this.trace_api.getTracer('abxbus')
55
+ this.span_factory = options.span_factory
56
+ this.root_span_name = options.root_span_name
57
+ this.root_span_attributes = options.root_span_attributes
27
58
  }
28
59
 
29
60
  onEventChange(eventbus: EventBus, event: BaseEvent, status: EventStatus): void {
30
61
  if (status === 'started') {
62
+ if (this.span_factory) {
63
+ return
64
+ }
31
65
  this.startEventSpan(eventbus, event)
32
66
  return
33
67
  }
@@ -39,12 +73,15 @@ export class OtelTracingMiddleware implements EventBusMiddleware {
39
73
 
40
74
  onEventResultChange(eventbus: EventBus, event: BaseEvent, event_result: EventResult, status: EventStatus): void {
41
75
  if (status === 'started') {
76
+ if (this.span_factory) {
77
+ return
78
+ }
42
79
  this.startHandlerSpan(eventbus, event, event_result)
43
80
  return
44
81
  }
45
82
 
46
83
  if (status === 'completed') {
47
- this.completeHandlerSpan(event_result)
84
+ this.completeHandlerSpan(eventbus, event, event_result)
48
85
  }
49
86
  }
50
87
 
@@ -54,7 +91,8 @@ export class OtelTracingMiddleware implements EventBusMiddleware {
54
91
  return existing
55
92
  }
56
93
 
57
- const parent_context = this.parentContextForEvent(event) ?? ROOT_CONTEXT
94
+ const parent_context = this.parentContextForEvent(event) ?? this.startRootSpan(eventbus, event)
95
+ const start_time = dateFromIso(event.event_started_at)
58
96
  const span = this.tracer.startSpan(
59
97
  `abxbus.event ${event.event_type}`,
60
98
  {
@@ -64,11 +102,12 @@ export class OtelTracingMiddleware implements EventBusMiddleware {
64
102
  'abxbus.event.id': event.event_id,
65
103
  'abxbus.event.type': event.event_type,
66
104
  'abxbus.event.version': event.event_version,
105
+ 'abxbus.event.session_id': stringValue((event as { session_id?: unknown }).session_id),
67
106
  'abxbus.event.parent_id': event.event_parent_id,
68
107
  'abxbus.event.emitted_by_handler_id': event.event_emitted_by_handler_id,
69
108
  'abxbus.event.path': event.event_path.join(' '),
70
109
  }),
71
- startTime: dateFromIso(event.event_started_at),
110
+ startTime: start_time,
72
111
  },
73
112
  parent_context
74
113
  )
@@ -79,6 +118,11 @@ export class OtelTracingMiddleware implements EventBusMiddleware {
79
118
  }
80
119
 
81
120
  private completeEventSpan(eventbus: EventBus, event: BaseEvent): void {
121
+ if (this.span_factory) {
122
+ this.completeEventSpanWithFactory(eventbus, event)
123
+ return
124
+ }
125
+
82
126
  const span = this.event_spans.get(event.event_id) ?? this.startEventSpan(eventbus, event)
83
127
  if (event.event_errors.length > 0) {
84
128
  recordSpanError(span, event.event_errors[0])
@@ -93,9 +137,12 @@ export class OtelTracingMiddleware implements EventBusMiddleware {
93
137
  'abxbus.event.child_count': event.event_children.length,
94
138
  })
95
139
  )
96
- span.end(dateFromIso(event.event_completed_at))
140
+ const start_time = dateFromIso(event.event_started_at)
141
+ const end_time = endTimeAfterStart(start_time, dateFromIso(event.event_completed_at))
142
+ span.end(end_time)
97
143
  this.event_spans.delete(event.event_id)
98
144
  this.event_contexts.delete(event.event_id)
145
+ this.completeRootSpan(event.event_id, start_time, end_time)
99
146
  }
100
147
 
101
148
  private startHandlerSpan(eventbus: EventBus, event: BaseEvent, event_result: EventResult): Span {
@@ -130,7 +177,12 @@ export class OtelTracingMiddleware implements EventBusMiddleware {
130
177
  return span
131
178
  }
132
179
 
133
- private completeHandlerSpan(event_result: EventResult): void {
180
+ private completeHandlerSpan(eventbus: EventBus, event: BaseEvent, event_result: EventResult): void {
181
+ if (this.span_factory) {
182
+ this.completeHandlerSpanWithFactory(eventbus, event, event_result)
183
+ return
184
+ }
185
+
134
186
  const span = this.handler_spans.get(event_result.id)
135
187
  if (!span) {
136
188
  return
@@ -147,11 +199,53 @@ export class OtelTracingMiddleware implements EventBusMiddleware {
147
199
  'abxbus.handler.child_count': event_result.event_children.length,
148
200
  })
149
201
  )
150
- span.end(dateFromIso(event_result.completed_at))
202
+ span.end(endTimeAfterStart(dateFromIso(event_result.started_at), dateFromIso(event_result.completed_at)))
151
203
  this.handler_spans.delete(event_result.id)
152
204
  this.handler_contexts.delete(handlerSpanKey(event_result.event_id, event_result.handler_id))
153
205
  }
154
206
 
207
+ private startRootSpan(eventbus: EventBus, event: BaseEvent): Context {
208
+ const existing = this.root_contexts.get(event.event_id)
209
+ if (existing) {
210
+ return existing
211
+ }
212
+
213
+ const session_id = stringValue((event as { session_id?: unknown }).session_id)
214
+ const root_attributes = resolveAttributes(this.root_span_attributes, eventbus, event)
215
+ const root_span = this.tracer.startSpan(
216
+ resolveRootSpanName(this.root_span_name, eventbus, event),
217
+ {
218
+ attributes: compactAttributes({
219
+ ...root_attributes,
220
+ 'abxbus.trace.root': true,
221
+ 'abxbus.bus.id': eventbus.id,
222
+ 'abxbus.bus.name': eventbus.name,
223
+ 'abxbus.root_event.id': event.event_id,
224
+ 'abxbus.root_event.type': event.event_type,
225
+ 'abxbus.root_event.session_id': session_id,
226
+ }),
227
+ startTime: dateFromIso(event.event_started_at),
228
+ },
229
+ ROOT_CONTEXT
230
+ )
231
+ const root_context = this.trace_api.setSpan(ROOT_CONTEXT, root_span)
232
+ this.root_spans.set(event.event_id, root_span)
233
+ this.root_contexts.set(event.event_id, root_context)
234
+ return root_context
235
+ }
236
+
237
+ private completeRootSpan(event_id: string, start_time: Date | undefined, end_time: Date | undefined): void {
238
+ const root_span = this.root_spans.get(event_id)
239
+ if (!root_span) {
240
+ return
241
+ }
242
+
243
+ root_span.setStatus({ code: SpanStatusCode.OK })
244
+ root_span.end(endTimeAfterStart(start_time, end_time))
245
+ this.root_spans.delete(event_id)
246
+ this.root_contexts.delete(event_id)
247
+ }
248
+
155
249
  private parentContextForEvent(event: BaseEvent): Context | undefined {
156
250
  if (event.event_parent_id && event.event_emitted_by_handler_id) {
157
251
  const handler_context = this.handler_contexts.get(handlerSpanKey(event.event_parent_id, event.event_emitted_by_handler_id))
@@ -162,12 +256,191 @@ export class OtelTracingMiddleware implements EventBusMiddleware {
162
256
 
163
257
  return event.event_parent_id ? this.event_contexts.get(event.event_parent_id) : undefined
164
258
  }
259
+
260
+ private completeEventSpanWithFactory(eventbus: EventBus, event: BaseEvent): void {
261
+ const root_event = rootEventForEvent(eventbus, event)
262
+ const trace_id = traceIdForRootEvent(root_event.event_id)
263
+ const event_context = eventSpanContext(trace_id, event.event_id)
264
+ const start_time = dateFromIso(event.event_started_at)
265
+ const end_time = endTimeAfterStart(start_time, dateFromIso(event.event_completed_at))
266
+
267
+ if (!event.event_parent_id) {
268
+ const root_span = this.span_factory!({
269
+ name: resolveRootSpanName(this.root_span_name, eventbus, event),
270
+ span_context: rootSpanContext(trace_id, event.event_id),
271
+ attributes: rootSpanAttributes(this.root_span_attributes, eventbus, event),
272
+ start_time,
273
+ })
274
+ root_span.setStatus({ code: SpanStatusCode.OK })
275
+ root_span.end(end_time)
276
+ }
277
+
278
+ const span = this.span_factory!({
279
+ name: `abxbus.event ${event.event_type}`,
280
+ span_context: event_context,
281
+ parent_span_context: parentSpanContextForEvent(event, trace_id),
282
+ attributes: eventSpanAttributes(eventbus, event),
283
+ start_time,
284
+ })
285
+ if (event.event_errors.length > 0) {
286
+ recordSpanError(span, event.event_errors[0])
287
+ } else {
288
+ span.setStatus({ code: SpanStatusCode.OK })
289
+ }
290
+ span.end(end_time)
291
+ }
292
+
293
+ private completeHandlerSpanWithFactory(eventbus: EventBus, event: BaseEvent, event_result: EventResult): void {
294
+ const root_event = rootEventForEvent(eventbus, event)
295
+ const trace_id = traceIdForRootEvent(root_event.event_id)
296
+ const start_time = dateFromIso(event_result.started_at)
297
+ const span = this.span_factory!({
298
+ name: `abxbus.handler ${event.event_type} ${event_result.handler_name}`,
299
+ span_context: handlerSpanContext(trace_id, event_result.event_id, event_result.handler_id),
300
+ parent_span_context: eventSpanContext(trace_id, event.event_id),
301
+ attributes: handlerSpanAttributes(eventbus, event, event_result),
302
+ start_time,
303
+ })
304
+
305
+ if (event_result.error !== undefined) {
306
+ recordSpanError(span, event_result.error)
307
+ } else {
308
+ span.setStatus({ code: SpanStatusCode.OK })
309
+ }
310
+ span.end(endTimeAfterStart(start_time, dateFromIso(event_result.completed_at)))
311
+ }
165
312
  }
166
313
 
167
314
  function handlerSpanKey(event_id: string, handler_id: string): string {
168
315
  return `${event_id}:${handler_id}`
169
316
  }
170
317
 
318
+ function eventSpanAttributes(eventbus: EventBus, event: BaseEvent): SpanAttributes {
319
+ return compactAttributes({
320
+ 'abxbus.bus.id': eventbus.id,
321
+ 'abxbus.bus.name': eventbus.name,
322
+ 'abxbus.event.id': event.event_id,
323
+ 'abxbus.event.type': event.event_type,
324
+ 'abxbus.event.version': event.event_version,
325
+ 'abxbus.event.session_id': stringValue((event as { session_id?: unknown }).session_id),
326
+ 'abxbus.event.parent_id': event.event_parent_id,
327
+ 'abxbus.event.emitted_by_handler_id': event.event_emitted_by_handler_id,
328
+ 'abxbus.event.path': event.event_path.join(' '),
329
+ 'abxbus.event.status': event.event_status,
330
+ 'abxbus.event.result_count': event.event_results.size,
331
+ 'abxbus.event.error_count': event.event_errors.length,
332
+ 'abxbus.event.child_count': event.event_children.length,
333
+ })
334
+ }
335
+
336
+ function handlerSpanAttributes(eventbus: EventBus, event: BaseEvent, event_result: EventResult): SpanAttributes {
337
+ return compactAttributes({
338
+ 'abxbus.bus.id': eventbus.id,
339
+ 'abxbus.bus.name': eventbus.name,
340
+ 'abxbus.event.id': event.event_id,
341
+ 'abxbus.event.type': event.event_type,
342
+ 'abxbus.handler.id': event_result.handler_id,
343
+ 'abxbus.handler.name': event_result.handler_name,
344
+ 'abxbus.handler.file_path': event_result.handler_file_path,
345
+ 'abxbus.handler.event_pattern': event_result.handler.event_pattern,
346
+ 'abxbus.event_result.id': event_result.id,
347
+ 'abxbus.event_result.status': event_result.status,
348
+ 'abxbus.handler.child_count': event_result.event_children.length,
349
+ })
350
+ }
351
+
352
+ function rootSpanAttributes(
353
+ root_span_attributes: OtelTracingMiddlewareOptions['root_span_attributes'],
354
+ eventbus: EventBus,
355
+ event: BaseEvent
356
+ ): SpanAttributes {
357
+ const session_id = stringValue((event as { session_id?: unknown }).session_id)
358
+ return compactAttributes({
359
+ ...resolveAttributes(root_span_attributes, eventbus, event),
360
+ 'abxbus.trace.root': true,
361
+ 'abxbus.bus.id': eventbus.id,
362
+ 'abxbus.bus.name': eventbus.name,
363
+ 'abxbus.root_event.id': event.event_id,
364
+ 'abxbus.root_event.type': event.event_type,
365
+ 'abxbus.root_event.session_id': session_id,
366
+ 'abxbus.root_event.status': event.event_status,
367
+ 'abxbus.root_event.error_count': event.event_errors.length,
368
+ 'abxbus.root_event.child_count': event.event_children.length,
369
+ })
370
+ }
371
+
372
+ function rootEventForEvent(eventbus: EventBus, event: BaseEvent): BaseEvent {
373
+ let current = event._event_original ?? event
374
+ const seen = new Set<string>()
375
+ while (current.event_parent_id && !seen.has(current.event_id)) {
376
+ seen.add(current.event_id)
377
+ const parent = eventbus.findEventById(current.event_parent_id)
378
+ if (!parent) {
379
+ break
380
+ }
381
+ current = parent._event_original ?? parent
382
+ }
383
+ return current
384
+ }
385
+
386
+ function parentSpanContextForEvent(event: BaseEvent, trace_id: string): SpanContext {
387
+ if (!event.event_parent_id) {
388
+ return rootSpanContext(trace_id, event.event_id)
389
+ }
390
+
391
+ if (event.event_emitted_by_handler_id) {
392
+ return handlerSpanContext(trace_id, event.event_parent_id, event.event_emitted_by_handler_id)
393
+ }
394
+
395
+ return eventSpanContext(trace_id, event.event_parent_id)
396
+ }
397
+
398
+ function rootSpanContext(trace_id: string, event_id: string): SpanContext {
399
+ return {
400
+ traceId: trace_id,
401
+ spanId: deterministicSpanId(`abxbus.root:${event_id}`),
402
+ traceFlags: 1,
403
+ }
404
+ }
405
+
406
+ function eventSpanContext(trace_id: string, event_id: string): SpanContext {
407
+ return {
408
+ traceId: trace_id,
409
+ spanId: deterministicSpanId(`abxbus.event:${event_id}`),
410
+ traceFlags: 1,
411
+ }
412
+ }
413
+
414
+ function handlerSpanContext(trace_id: string, event_id: string, handler_id: string): SpanContext {
415
+ return {
416
+ traceId: trace_id,
417
+ spanId: deterministicSpanId(`abxbus.handler:${event_id}:${handler_id}`),
418
+ traceFlags: 1,
419
+ }
420
+ }
421
+
422
+ function traceIdForRootEvent(event_id: string): string {
423
+ return `${fnv1a64Hex(`abxbus.trace.a:${event_id}`)}${fnv1a64Hex(`abxbus.trace.b:${event_id}`)}`
424
+ }
425
+
426
+ function deterministicSpanId(input: string): string {
427
+ return fnv1a64Hex(input)
428
+ }
429
+
430
+ function fnv1a64Hex(input: string): string {
431
+ let hash = 0xcbf29ce484222325n
432
+ const prime = 0x100000001b3n
433
+ const mask = 0xffffffffffffffffn
434
+ for (let index = 0; index < input.length; index += 1) {
435
+ hash ^= BigInt(input.charCodeAt(index))
436
+ hash = (hash * prime) & mask
437
+ }
438
+ if (hash === 0n) {
439
+ hash = 1n
440
+ }
441
+ return hash.toString(16).padStart(16, '0')
442
+ }
443
+
171
444
  function dateFromIso(value: string | null | undefined): Date | undefined {
172
445
  if (value == null) {
173
446
  return undefined
@@ -176,10 +449,35 @@ function dateFromIso(value: string | null | undefined): Date | undefined {
176
449
  return Number.isNaN(date.getTime()) ? undefined : date
177
450
  }
178
451
 
179
- function compactAttributes(
180
- attributes: Record<string, string | number | boolean | null | undefined>
181
- ): Record<string, string | number | boolean> {
182
- const compacted: Record<string, string | number | boolean> = {}
452
+ function endTimeAfterStart(start_time: Date | undefined, end_time: Date | undefined): Date | undefined {
453
+ if (!start_time || !end_time) {
454
+ return end_time
455
+ }
456
+
457
+ return end_time.getTime() > start_time.getTime() ? end_time : new Date(start_time.getTime() + 1)
458
+ }
459
+
460
+ function resolveRootSpanName(root_span_name: OtelTracingMiddlewareOptions['root_span_name'], eventbus: EventBus, event: BaseEvent): string {
461
+ if (typeof root_span_name === 'function') {
462
+ return root_span_name(eventbus, event)
463
+ }
464
+ return root_span_name ?? `abxbus.trace ${eventbus.name}`
465
+ }
466
+
467
+ function resolveAttributes(
468
+ attributes: OtelTracingMiddlewareOptions['root_span_attributes'],
469
+ eventbus: EventBus,
470
+ event: BaseEvent
471
+ ): SpanAttributes {
472
+ return typeof attributes === 'function' ? attributes(eventbus, event) : (attributes ?? {})
473
+ }
474
+
475
+ function stringValue(value: unknown): string | undefined {
476
+ return typeof value === 'string' && value.length > 0 ? value : undefined
477
+ }
478
+
479
+ function compactAttributes(attributes: Record<string, SpanAttributeValue | null | undefined>): SpanAttributes {
480
+ const compacted: SpanAttributes = {}
183
481
  for (const [key, value] of Object.entries(attributes)) {
184
482
  if (value !== null && value !== undefined) {
185
483
  compacted[key] = value