autotel-cloudflare 2.19.0 → 3.1.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.
@@ -2,66 +2,52 @@
2
2
  * OpenTelemetry-based Observability implementation for Cloudflare Agents SDK
3
3
  *
4
4
  * Converts Agent events into OpenTelemetry spans for distributed tracing.
5
- *
6
- * @example
7
- * ```typescript
8
- * import { Agent } from 'agents'
9
- * import { createOtelObservability } from 'autotel-cloudflare/agents'
10
- *
11
- * class MyAgent extends Agent<Env> {
12
- * observability = createOtelObservability({
13
- * service: { name: 'my-agent' },
14
- * exporter: { url: env.OTLP_ENDPOINT }
15
- * })
16
- *
17
- * @callable()
18
- * async doSomething() {
19
- * // This RPC call will be automatically traced
20
- * return 'done'
21
- * }
22
- * }
23
- * ```
24
5
  */
25
6
 
26
7
  import {
27
- trace,
28
- SpanStatusCode,
29
8
  SpanKind,
30
- type Span,
9
+ SpanStatusCode,
10
+ trace,
31
11
  type Attributes,
12
+ type Span,
32
13
  } from '@opentelemetry/api';
33
14
  import { resourceFromAttributes } from '@opentelemetry/resources';
34
15
  import {
35
- createInitialiser,
36
- WorkerTracerProvider,
37
16
  WorkerTracer,
17
+ WorkerTracerProvider,
18
+ createInitialiser,
38
19
  type ResolvedEdgeConfig,
39
20
  } from 'autotel-edge';
40
21
  import type {
22
+ AgentInstrumentationOptions,
23
+ MCPObservabilityEvent,
41
24
  Observability,
42
25
  ObservabilityEvent,
26
+ ObservabilityExecutionContext,
43
27
  OtelObservabilityConfig,
44
- AgentInstrumentationOptions,
45
28
  } from './types';
46
29
 
47
- /**
48
- * Map of active spans keyed by event ID
49
- * Used to correlate start/end events
50
- */
51
- const activeSpans = new Map<string, Span>();
52
-
53
- /**
54
- * Whether the provider has been initialized
55
- */
56
30
  let providerInitialized = false;
57
31
 
58
- /**
59
- * Initialize the tracer provider for Agents
60
- */
32
+ type EventCategory =
33
+ | 'state'
34
+ | 'rpc'
35
+ | 'schedule'
36
+ | 'queue'
37
+ | 'submission'
38
+ | 'message'
39
+ | 'chat'
40
+ | 'transcript'
41
+ | 'fiber'
42
+ | 'agentTool'
43
+ | 'lifecycle'
44
+ | 'workflow'
45
+ | 'mcp'
46
+ | 'email';
47
+
61
48
  function initProvider(config: ResolvedEdgeConfig): void {
62
49
  if (providerInitialized) return;
63
50
 
64
- // Create resource with agent-specific attributes
65
51
  const resource = resourceFromAttributes({
66
52
  'service.name': config.service.name,
67
53
  'service.version': config.service.version,
@@ -73,247 +59,410 @@ function initProvider(config: ResolvedEdgeConfig): void {
73
59
  'agent.framework': 'cloudflare-agents',
74
60
  });
75
61
 
76
- // Create and register provider
77
62
  const provider = new WorkerTracerProvider(config.spanProcessors, resource);
78
63
  provider.register();
79
64
 
80
- // Set head sampler on tracer
81
65
  const tracer = trace.getTracer('autotel-cloudflare/agents') as WorkerTracer;
82
66
  tracer.setHeadSampler(config.sampling.headSampler);
83
67
 
84
68
  providerInitialized = true;
85
69
  }
86
70
 
87
- /**
88
- * Get default span name for an event
89
- */
90
- function getDefaultSpanName(event: ObservabilityEvent): string {
91
- switch (event.type) {
71
+ function classifyEvent(type: ObservabilityEvent['type']): EventCategory {
72
+ if (type.startsWith('mcp:')) return 'mcp';
73
+ if (type.startsWith('workflow:')) return 'workflow';
74
+ if (type.startsWith('fiber:')) return 'fiber';
75
+ if (type.startsWith('agent_tool:')) return 'agentTool';
76
+ if (type.startsWith('chat:transcript:')) return 'transcript';
77
+ if (type.startsWith('chat:')) return 'chat';
78
+ if (type.startsWith('submission:')) return 'submission';
79
+ if (type.startsWith('queue:')) return 'queue';
80
+ if (type.startsWith('schedule:')) return 'schedule';
81
+ if (
82
+ type.startsWith('message:') ||
83
+ type.startsWith('tool:') ||
84
+ type === 'rpc:error'
85
+ ) {
86
+ return 'message';
87
+ }
88
+ if (type === 'rpc') return 'rpc';
89
+ if (type.startsWith('state:')) return 'state';
90
+ if (type.startsWith('email:')) return 'email';
91
+ if (type === 'connect' || type === 'disconnect' || type === 'destroy') {
92
+ return 'lifecycle';
93
+ }
94
+ return 'message';
95
+ }
96
+
97
+ function shouldTraceEvent(
98
+ event: ObservabilityEvent,
99
+ options: AgentInstrumentationOptions,
100
+ ): boolean {
101
+ const opts: Required<
102
+ Pick<
103
+ AgentInstrumentationOptions,
104
+ | 'traceRpc'
105
+ | 'traceSchedule'
106
+ | 'traceQueue'
107
+ | 'traceSubmissions'
108
+ | 'traceMcp'
109
+ | 'traceStateUpdates'
110
+ | 'traceMessages'
111
+ | 'traceLifecycle'
112
+ | 'traceChat'
113
+ | 'traceTranscripts'
114
+ | 'traceFibers'
115
+ | 'traceToolRecovery'
116
+ | 'traceWorkflow'
117
+ | 'traceEmail'
118
+ >
119
+ > = {
120
+ traceRpc: true,
121
+ traceSchedule: true,
122
+ traceQueue: true,
123
+ traceSubmissions: true,
124
+ traceMcp: true,
125
+ traceStateUpdates: false,
126
+ traceMessages: true,
127
+ traceLifecycle: true,
128
+ traceChat: true,
129
+ traceTranscripts: true,
130
+ traceFibers: true,
131
+ traceToolRecovery: true,
132
+ traceWorkflow: true,
133
+ traceEmail: true,
134
+ };
135
+ const merged = { ...opts, ...options };
136
+
137
+ switch (classifyEvent(event.type)) {
92
138
  case 'rpc': {
93
- return `agent.rpc ${event.payload.method}`;
139
+ return merged.traceRpc;
94
140
  }
95
- case 'schedule:create': {
96
- return `agent.schedule.create ${event.payload.callback}`;
141
+ case 'schedule': {
142
+ return merged.traceSchedule;
97
143
  }
98
- case 'schedule:execute': {
99
- return `agent.schedule.execute ${event.payload.callback}`;
144
+ case 'queue': {
145
+ return merged.traceQueue;
100
146
  }
101
- case 'schedule:cancel': {
102
- return `agent.schedule.cancel ${event.payload.callback}`;
147
+ case 'submission': {
148
+ return merged.traceSubmissions;
103
149
  }
104
- case 'connect': {
105
- return `agent.connect`;
150
+ case 'mcp': {
151
+ return merged.traceMcp;
106
152
  }
107
- case 'destroy': {
108
- return `agent.destroy`;
153
+ case 'state': {
154
+ return merged.traceStateUpdates;
109
155
  }
110
- case 'state:update': {
111
- return `agent.state.update`;
156
+ case 'message': {
157
+ return merged.traceMessages;
112
158
  }
113
- case 'message:request': {
114
- return `agent.message.request`;
159
+ case 'lifecycle': {
160
+ return merged.traceLifecycle;
115
161
  }
116
- case 'message:response': {
117
- return `agent.message.response`;
162
+ case 'chat': {
163
+ return merged.traceChat;
118
164
  }
119
- case 'message:clear': {
120
- return `agent.message.clear`;
165
+ case 'transcript': {
166
+ return merged.traceTranscripts;
121
167
  }
122
- case 'mcp:client:preconnect': {
123
- return `mcp.preconnect ${event.payload.serverId}`;
168
+ case 'fiber': {
169
+ return merged.traceFibers;
170
+ }
171
+ case 'agentTool': {
172
+ return merged.traceToolRecovery;
173
+ }
174
+ case 'workflow': {
175
+ return merged.traceWorkflow;
176
+ }
177
+ case 'email': {
178
+ return merged.traceEmail;
179
+ }
180
+ }
181
+ }
182
+
183
+ function formatTypeSegment(value: string): string {
184
+ return value.replaceAll(/[:_]/g, '.');
185
+ }
186
+
187
+ function getPayloadName(
188
+ payload: Record<string, unknown>,
189
+ ...keys: string[]
190
+ ): string | undefined {
191
+ for (const key of keys) {
192
+ const value = payload[key];
193
+ if (typeof value === 'string' && value.length > 0) {
194
+ return value;
195
+ }
196
+ }
197
+ return undefined;
198
+ }
199
+
200
+ function getDefaultSpanName(event: ObservabilityEvent): string {
201
+ const payload = event.payload;
202
+
203
+ switch (event.type) {
204
+ case 'rpc':
205
+ case 'rpc:error': {
206
+ return `agent.rpc ${(payload as { method: string }).method}`;
207
+ }
208
+ case 'schedule:create':
209
+ case 'schedule:execute':
210
+ case 'schedule:cancel':
211
+ case 'schedule:retry':
212
+ case 'schedule:error':
213
+ case 'schedule:duplicate_warning': {
214
+ return `agent.${formatTypeSegment(event.type)} ${getPayloadName(payload, 'callback') ?? 'unknown'}`;
215
+ }
216
+ case 'queue:create':
217
+ case 'queue:retry':
218
+ case 'queue:error': {
219
+ return `agent.${formatTypeSegment(event.type)} ${getPayloadName(payload, 'callback') ?? 'unknown'}`;
220
+ }
221
+ case 'submission:create':
222
+ case 'submission:status':
223
+ case 'submission:error': {
224
+ return `agent.${formatTypeSegment(event.type)} ${getPayloadName(payload, 'submissionId') ?? 'unknown'}`;
225
+ }
226
+ case 'connect':
227
+ case 'disconnect': {
228
+ return `agent.${event.type} ${getPayloadName(payload, 'connectionId') ?? 'unknown'}`;
124
229
  }
125
- case 'mcp:client:connect': {
126
- return `mcp.connect ${event.payload.url}`;
230
+ case 'mcp:client:preconnect': {
231
+ return `mcp.client.preconnect ${getPayloadName(payload, 'serverId') ?? 'unknown'}`;
127
232
  }
128
233
  case 'mcp:client:authorize': {
129
- return `mcp.authorize ${event.payload.serverId}`;
234
+ return `mcp.client.authorize ${getPayloadName(payload, 'serverId') ?? 'unknown'}`;
130
235
  }
131
- case 'mcp:client:discover': {
132
- return `mcp.discover`;
236
+ case 'mcp:client:connect':
237
+ case 'mcp:client:discover':
238
+ case 'mcp:client:close': {
239
+ return `mcp.client.${formatTypeSegment(event.type).split('.').slice(-1)[0]} ${
240
+ getPayloadName(payload, 'url', 'capability', 'state') ?? 'unknown'
241
+ }`;
242
+ }
243
+ case 'workflow:start':
244
+ case 'workflow:event':
245
+ case 'workflow:approved':
246
+ case 'workflow:rejected':
247
+ case 'workflow:terminated':
248
+ case 'workflow:paused':
249
+ case 'workflow:resumed':
250
+ case 'workflow:restarted': {
251
+ return `agent.${formatTypeSegment(event.type)} ${
252
+ getPayloadName(payload, 'workflowName', 'workflowId', 'eventType') ?? 'unknown'
253
+ }`;
254
+ }
255
+ case 'email:receive':
256
+ case 'email:reply':
257
+ case 'email:send': {
258
+ return `agent.${formatTypeSegment(event.type)} ${getPayloadName(payload, 'subject') ?? 'message'}`;
259
+ }
260
+ case 'destroy': {
261
+ return 'agent.destroy';
133
262
  }
134
263
  default: {
135
- return `agent.${(event as ObservabilityEvent).type}`;
264
+ return `agent.${formatTypeSegment(event.type)}`;
136
265
  }
137
266
  }
138
267
  }
139
268
 
140
- /**
141
- * Get default attributes for an event
142
- */
269
+ function isPrimitiveAttributeValue(
270
+ value: unknown,
271
+ ): value is string | number | boolean | string[] | number[] | boolean[] {
272
+ if (
273
+ typeof value === 'string' ||
274
+ typeof value === 'number' ||
275
+ typeof value === 'boolean'
276
+ ) {
277
+ return true;
278
+ }
279
+
280
+ if (!Array.isArray(value)) {
281
+ return false;
282
+ }
283
+
284
+ return value.every(
285
+ (entry) =>
286
+ typeof entry === 'string' ||
287
+ typeof entry === 'number' ||
288
+ typeof entry === 'boolean',
289
+ );
290
+ }
291
+
292
+ function setIfPresent(
293
+ attrs: Attributes,
294
+ key: string,
295
+ value: unknown,
296
+ ): void {
297
+ if (value === undefined || value === null) return;
298
+ if (isPrimitiveAttributeValue(value)) {
299
+ attrs[key] = value;
300
+ }
301
+ }
302
+
143
303
  function getDefaultAttributes(event: ObservabilityEvent): Attributes {
144
304
  const attrs: Attributes = {
145
305
  'agent.event.type': event.type,
146
- 'agent.event.id': event.id,
147
306
  };
148
307
 
149
- // Add type-specific attributes
308
+ setIfPresent(attrs, 'agent.event.id', event.id);
309
+ setIfPresent(attrs, 'agent.display_message', event.displayMessage);
310
+ setIfPresent(attrs, 'agent.class', event.agent);
311
+ setIfPresent(attrs, 'agent.instance.name', event.name);
312
+ setIfPresent(attrs, 'gen_ai.agent.name', event.agent);
313
+
150
314
  switch (event.type) {
151
315
  case 'rpc': {
152
316
  attrs['agent.rpc.method'] = event.payload.method;
153
- if (event.payload.streaming !== undefined) {
154
- attrs['agent.rpc.streaming'] = event.payload.streaming;
155
- }
317
+ setIfPresent(attrs, 'agent.rpc.streaming', event.payload.streaming);
318
+ break;
319
+ }
320
+ case 'rpc:error': {
321
+ attrs['agent.rpc.method'] = event.payload.method;
156
322
  break;
157
323
  }
158
-
159
324
  case 'schedule:create':
160
325
  case 'schedule:execute':
161
- case 'schedule:cancel': {
162
- attrs['agent.schedule.callback'] = event.payload.callback;
163
- attrs['agent.schedule.id'] = event.payload.id;
326
+ case 'schedule:cancel':
327
+ case 'schedule:retry':
328
+ case 'schedule:error':
329
+ case 'queue:create':
330
+ case 'queue:retry':
331
+ case 'queue:error': {
332
+ setIfPresent(attrs, 'agent.schedule.callback', event.payload.callback);
333
+ setIfPresent(attrs, 'agent.schedule.id', event.payload.id);
164
334
  break;
165
335
  }
166
-
167
- case 'connect': {
168
- attrs['agent.connection.id'] = event.payload.connectionId;
336
+ case 'schedule:duplicate_warning': {
337
+ setIfPresent(attrs, 'agent.schedule.callback', event.payload.callback);
338
+ break;
339
+ }
340
+ case 'connect':
341
+ case 'disconnect': {
342
+ setIfPresent(attrs, 'agent.connection.id', event.payload.connectionId);
169
343
  break;
170
344
  }
171
-
172
345
  case 'mcp:client:preconnect': {
173
- attrs['agent.mcp.server_id'] = event.payload.serverId;
346
+ setIfPresent(attrs, 'agent.mcp.server_id', event.payload.serverId);
174
347
  break;
175
348
  }
176
-
177
- case 'mcp:client:connect': {
178
- attrs['agent.mcp.url'] = event.payload.url;
179
- attrs['agent.mcp.transport'] = event.payload.transport;
180
- attrs['agent.mcp.state'] = event.payload.state;
181
- if (event.payload.error) {
182
- attrs['agent.mcp.error'] = event.payload.error;
183
- }
349
+ case 'mcp:client:connect':
350
+ case 'mcp:client:close': {
351
+ setIfPresent(attrs, 'agent.mcp.url', event.payload.url);
352
+ setIfPresent(attrs, 'agent.mcp.transport', event.payload.transport);
353
+ setIfPresent(attrs, 'agent.mcp.state', event.payload.state);
184
354
  break;
185
355
  }
186
-
187
356
  case 'mcp:client:authorize': {
188
- attrs['agent.mcp.server_id'] = event.payload.serverId;
189
- attrs['agent.mcp.auth_url'] = event.payload.authUrl;
190
- if (event.payload.clientId) {
191
- attrs['agent.mcp.client_id'] = event.payload.clientId;
192
- }
357
+ setIfPresent(attrs, 'agent.mcp.server_id', event.payload.serverId);
358
+ setIfPresent(attrs, 'agent.mcp.auth_url', event.payload.authUrl);
359
+ setIfPresent(attrs, 'agent.mcp.client_id', event.payload.clientId);
360
+ break;
361
+ }
362
+ case 'mcp:client:discover': {
363
+ setIfPresent(attrs, 'agent.mcp.url', event.payload.url);
364
+ setIfPresent(attrs, 'agent.mcp.state', event.payload.state);
365
+ setIfPresent(attrs, 'agent.mcp.capability', event.payload.capability);
193
366
  break;
194
367
  }
195
368
  }
196
369
 
197
- // Add any additional payload properties as attributes
198
370
  for (const [key, value] of Object.entries(event.payload)) {
199
- if (
200
- attrs[`agent.${key}`] === undefined &&
201
- (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean')
202
- ) {
203
- attrs[`agent.payload.${key}`] = value;
204
- }
371
+ setIfPresent(attrs, `agent.payload.${key}`, value);
205
372
  }
206
373
 
207
374
  return attrs;
208
375
  }
209
376
 
210
- /**
211
- * Determine span kind based on event type
212
- */
213
377
  function getSpanKind(event: ObservabilityEvent): SpanKind {
214
- switch (event.type) {
215
- case 'rpc': {
216
- return SpanKind.SERVER;
217
- }
218
- case 'connect': {
378
+ switch (classifyEvent(event.type)) {
379
+ case 'rpc':
380
+ case 'lifecycle':
381
+ case 'email': {
219
382
  return SpanKind.SERVER;
220
383
  }
221
- case 'mcp:client:connect':
222
- case 'mcp:client:preconnect':
223
- case 'mcp:client:authorize':
224
- case 'mcp:client:discover': {
384
+ case 'mcp': {
225
385
  return SpanKind.CLIENT;
226
386
  }
387
+ case 'schedule':
388
+ case 'queue': {
389
+ return SpanKind.CONSUMER;
390
+ }
227
391
  default: {
228
392
  return SpanKind.INTERNAL;
229
393
  }
230
394
  }
231
395
  }
232
396
 
233
- /**
234
- * Check if an event type should be traced based on options
235
- */
236
- function shouldTraceEvent(
237
- event: ObservabilityEvent,
238
- options: AgentInstrumentationOptions,
239
- ): boolean {
240
- const defaults: AgentInstrumentationOptions = {
241
- traceRpc: true,
242
- traceSchedule: true,
243
- traceMcp: true,
244
- traceStateUpdates: false,
245
- traceMessages: true,
246
- traceLifecycle: true,
247
- };
248
-
249
- const opts = { ...defaults, ...options };
397
+ function inferErrorMessage(event: ObservabilityEvent): string | undefined {
398
+ const payload = event.payload as Record<string, unknown>;
399
+ const direct = payload.error;
400
+ if (typeof direct === 'string' && direct.length > 0) {
401
+ return direct;
402
+ }
250
403
 
251
- switch (event.type) {
252
- case 'rpc': {
253
- return opts.traceRpc ?? true;
404
+ if (event.type === 'workflow:rejected') {
405
+ const reason = payload.reason;
406
+ if (typeof reason === 'string' && reason.length > 0) {
407
+ return reason;
254
408
  }
409
+ return 'workflow rejected';
410
+ }
255
411
 
256
- case 'schedule:create':
257
- case 'schedule:execute':
258
- case 'schedule:cancel': {
259
- return opts.traceSchedule ?? true;
260
- }
412
+ if (event.type.endsWith(':failed') || event.type.endsWith(':error')) {
413
+ return event.type;
414
+ }
261
415
 
262
- case 'mcp:client:preconnect':
263
- case 'mcp:client:connect':
264
- case 'mcp:client:authorize':
265
- case 'mcp:client:discover': {
266
- return opts.traceMcp ?? true;
267
- }
416
+ return undefined;
417
+ }
268
418
 
269
- case 'state:update': {
270
- return opts.traceStateUpdates ?? false;
271
- }
419
+ function isErrorEvent(event: ObservabilityEvent): boolean {
420
+ if (event.type === 'workflow:rejected') return true;
421
+ if (event.type.endsWith(':error') || event.type.endsWith(':failed')) return true;
272
422
 
273
- case 'message:request':
274
- case 'message:response':
275
- case 'message:clear': {
276
- return opts.traceMessages ?? true;
277
- }
423
+ if ('error' in event.payload) {
424
+ return typeof (event.payload as Record<string, unknown>).error === 'string';
425
+ }
278
426
 
279
- case 'connect':
280
- case 'destroy': {
281
- return opts.traceLifecycle ?? true;
282
- }
427
+ return false;
428
+ }
283
429
 
284
- default: {
285
- return true;
286
- }
430
+ function applyEventStatus(span: Span, event: ObservabilityEvent): void {
431
+ if (!isErrorEvent(event)) {
432
+ span.setStatus({ code: SpanStatusCode.OK });
433
+ return;
434
+ }
435
+
436
+ const message = inferErrorMessage(event);
437
+ span.setStatus({
438
+ code: SpanStatusCode.ERROR,
439
+ message,
440
+ });
441
+ span.setAttribute('error', true);
442
+
443
+ if (message) {
444
+ span.setAttribute('exception.message', message);
287
445
  }
288
446
  }
289
447
 
290
- /**
291
- * Export spans asynchronously
292
- */
293
448
  async function exportSpans(
294
449
  traceId: string,
295
- ctx?: DurableObjectState | ExecutionContext,
450
+ ctx?: ObservabilityExecutionContext,
296
451
  ): Promise<void> {
297
452
  const tracer = trace.getTracer('autotel-cloudflare/agents');
298
453
  if (tracer instanceof WorkerTracer) {
299
454
  try {
300
- // scheduler is only available on ExecutionContext, not DurableObjectState
301
- if (ctx && 'scheduler' in ctx) {
302
- const ctxWithScheduler = ctx as ExecutionContext & { scheduler?: { wait(ms: number): Promise<void> } };
303
- if (ctxWithScheduler.scheduler) {
304
- await ctxWithScheduler.scheduler.wait(1);
305
- }
455
+ const scheduler = ctx && 'scheduler' in ctx
456
+ ? (ctx.scheduler as { wait?: (ms: number) => Promise<void> } | undefined)
457
+ : undefined;
458
+ if (scheduler?.wait) {
459
+ await scheduler.wait(1);
306
460
  }
307
461
  await tracer.forceFlush(traceId);
308
462
  } catch (error) {
309
463
  console.error('[autotel-cloudflare/agents] Failed to export spans:', error);
310
464
  }
311
465
  }
312
-
313
- // If we have a DurableObject context, use waitUntil for export
314
- if (ctx && 'waitUntil' in ctx) {
315
- // Already exported above, but could defer more work here
316
- }
317
466
  }
318
467
 
319
468
  /**
@@ -323,133 +472,69 @@ async function exportSpans(
323
472
  * events into OpenTelemetry spans.
324
473
  */
325
474
  export class OtelObservability implements Observability {
326
- private config: ResolvedEdgeConfig;
327
- private options: AgentInstrumentationOptions;
475
+ private readonly config: ResolvedEdgeConfig;
476
+ private readonly options: AgentInstrumentationOptions;
328
477
  private initialized = false;
329
478
 
330
479
  constructor(config: OtelObservabilityConfig) {
331
- // Use createInitialiser to resolve the config
332
480
  const initialiser = createInitialiser(config);
333
481
  this.config = initialiser({}, undefined);
334
482
  this.options = config.agents ?? {};
335
483
  }
336
484
 
337
- /**
338
- * Initialize the tracer provider (called lazily on first emit)
339
- */
340
485
  private initialize(): void {
341
486
  if (this.initialized) return;
342
487
  initProvider(this.config);
343
488
  this.initialized = true;
344
489
  }
345
490
 
346
- /**
347
- * Emit an observability event
348
- *
349
- * Converts the event to an OpenTelemetry span based on the event type.
350
- */
351
- emit(event: ObservabilityEvent, ctx?: DurableObjectState): void {
352
- // Initialize provider on first emit
491
+ emit(event: ObservabilityEvent, ctx?: ObservabilityExecutionContext): void {
353
492
  this.initialize();
354
493
 
355
- // Check if this event type should be traced
356
494
  if (!shouldTraceEvent(event, this.options)) {
357
495
  return;
358
496
  }
359
497
 
360
498
  const tracer = trace.getTracer('autotel-cloudflare/agents');
361
-
362
- // Get span name (custom or default)
363
499
  const spanName = this.options.spanNameFormatter
364
500
  ? this.options.spanNameFormatter(event)
365
501
  : getDefaultSpanName(event);
366
-
367
- // Get attributes (custom + default)
368
502
  const defaultAttrs = getDefaultAttributes(event);
369
503
  const customAttrs = this.options.attributeExtractor
370
504
  ? this.options.attributeExtractor(event)
371
505
  : {};
372
- const attributes = { ...defaultAttrs, ...customAttrs };
373
-
374
- // Determine span kind
375
- const kind = getSpanKind(event);
376
-
377
- // Create span with event timestamp
378
506
  const span = tracer.startSpan(spanName, {
379
- kind,
380
- attributes,
507
+ kind: getSpanKind(event),
508
+ attributes: { ...defaultAttrs, ...customAttrs },
381
509
  startTime: event.timestamp,
382
510
  });
383
511
 
384
- // For short-lived events, end immediately
385
- // For events that have duration (like RPC), we would ideally track start/end
386
- // But the Agents SDK emits single events, so we create point-in-time spans
387
- span.setStatus({ code: SpanStatusCode.OK });
388
- span.end(event.timestamp + 1); // End 1ms after start
512
+ applyEventStatus(span, event);
513
+ span.end(event.timestamp + 1);
389
514
 
390
- // Store span for potential correlation
391
- activeSpans.set(event.id, span);
392
-
393
- // Schedule span export
394
515
  const traceId = span.spanContext().traceId;
395
- if (ctx && 'waitUntil' in ctx && typeof (ctx as any).waitUntil === 'function') {
396
- (ctx as any).waitUntil(exportSpans(traceId, ctx));
397
- } else {
398
- // In environments without waitUntil, export synchronously-ish
399
- void exportSpans(traceId, ctx);
516
+ if (ctx && 'waitUntil' in ctx && typeof ctx.waitUntil === 'function') {
517
+ ctx.waitUntil(exportSpans(traceId, ctx));
518
+ return;
400
519
  }
520
+
521
+ void exportSpans(traceId, ctx);
401
522
  }
402
523
  }
403
524
 
404
- /**
405
- * Create an OtelObservability instance
406
- *
407
- * @example
408
- * ```typescript
409
- * import { Agent } from 'agents'
410
- * import { createOtelObservability } from 'autotel-cloudflare/agents'
411
- *
412
- * class MyAgent extends Agent<Env> {
413
- * observability = createOtelObservability({
414
- * service: { name: 'my-agent' },
415
- * exporter: { url: env.OTLP_ENDPOINT }
416
- * })
417
- * }
418
- * ```
419
- */
420
- export function createOtelObservability(config: OtelObservabilityConfig): OtelObservability {
525
+ export function createOtelObservability(
526
+ config: OtelObservabilityConfig,
527
+ ): OtelObservability {
421
528
  return new OtelObservability(config);
422
529
  }
423
530
 
424
- /**
425
- * Create an OtelObservability instance with environment-based config
426
- *
427
- * Use this when you need to access environment variables for configuration.
428
- *
429
- * @example
430
- * ```typescript
431
- * import { Agent } from 'agents'
432
- * import { createOtelObservabilityFromEnv } from 'autotel-cloudflare/agents'
433
- *
434
- * class MyAgent extends Agent<Env> {
435
- * observability?: OtelObservability
436
- *
437
- * constructor(state: DurableObjectState, env: Env) {
438
- * super(state, env)
439
- * this.observability = createOtelObservabilityFromEnv(env)
440
- * }
441
- * }
442
- * ```
443
- */
444
531
  export function createOtelObservabilityFromEnv(
445
532
  env: Record<string, unknown>,
446
533
  options?: AgentInstrumentationOptions,
447
534
  ): OtelObservability {
448
- // Extract standard OTLP env vars
449
535
  const endpoint = (env.OTEL_EXPORTER_OTLP_ENDPOINT as string) || undefined;
450
536
  const serviceName = (env.OTEL_SERVICE_NAME as string) || 'cloudflare-agent';
451
537
 
452
- // Parse headers if present
453
538
  let headers: Record<string, string> | undefined;
454
539
  const headersStr = env.OTEL_EXPORTER_OTLP_HEADERS as string;
455
540
  if (headersStr) {
@@ -462,13 +547,11 @@ export function createOtelObservabilityFromEnv(
462
547
  }
463
548
  }
464
549
 
465
- // If no endpoint is configured, use a default localhost endpoint
466
- // In production, users should set OTEL_EXPORTER_OTLP_ENDPOINT
467
- const exporterUrl = endpoint || 'http://localhost:4318/v1/traces';
468
-
469
550
  return createOtelObservability({
470
551
  service: { name: serviceName },
471
- exporter: { url: exporterUrl, headers },
552
+ exporter: { url: endpoint || 'http://localhost:4318/v1/traces', headers },
472
553
  agents: options,
473
554
  });
474
555
  }
556
+
557
+ export type { MCPObservabilityEvent };