autotel-cloudflare 3.0.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.
- package/dist/agents.d.ts +626 -123
- package/dist/agents.d.ts.map +1 -1
- package/dist/agents.js +253 -171
- package/dist/agents.js.map +1 -1
- package/package.json +1 -1
- package/src/agents/agent.ts +327 -0
- package/src/agents/base.ts +26 -0
- package/src/agents/index.ts +5 -1
- package/src/agents/mcp.ts +38 -0
- package/src/agents/observability.ts +145 -0
- package/src/agents/otel-observability.test.ts +165 -232
- package/src/agents/otel-observability.ts +350 -267
- package/src/agents/types.ts +26 -132
- package/src/agents.ts +17 -6
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
|
139
|
+
return merged.traceRpc;
|
|
94
140
|
}
|
|
95
|
-
case 'schedule
|
|
96
|
-
return
|
|
141
|
+
case 'schedule': {
|
|
142
|
+
return merged.traceSchedule;
|
|
97
143
|
}
|
|
98
|
-
case '
|
|
99
|
-
return
|
|
144
|
+
case 'queue': {
|
|
145
|
+
return merged.traceQueue;
|
|
100
146
|
}
|
|
101
|
-
case '
|
|
102
|
-
return
|
|
147
|
+
case 'submission': {
|
|
148
|
+
return merged.traceSubmissions;
|
|
103
149
|
}
|
|
104
|
-
case '
|
|
105
|
-
return
|
|
150
|
+
case 'mcp': {
|
|
151
|
+
return merged.traceMcp;
|
|
106
152
|
}
|
|
107
|
-
case '
|
|
108
|
-
return
|
|
153
|
+
case 'state': {
|
|
154
|
+
return merged.traceStateUpdates;
|
|
109
155
|
}
|
|
110
|
-
case '
|
|
111
|
-
return
|
|
156
|
+
case 'message': {
|
|
157
|
+
return merged.traceMessages;
|
|
112
158
|
}
|
|
113
|
-
case '
|
|
114
|
-
return
|
|
159
|
+
case 'lifecycle': {
|
|
160
|
+
return merged.traceLifecycle;
|
|
115
161
|
}
|
|
116
|
-
case '
|
|
117
|
-
return
|
|
162
|
+
case 'chat': {
|
|
163
|
+
return merged.traceChat;
|
|
118
164
|
}
|
|
119
|
-
case '
|
|
120
|
-
return
|
|
165
|
+
case 'transcript': {
|
|
166
|
+
return merged.traceTranscripts;
|
|
121
167
|
}
|
|
122
|
-
case '
|
|
123
|
-
return
|
|
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:
|
|
126
|
-
return `mcp.
|
|
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 ${
|
|
234
|
+
return `mcp.client.authorize ${getPayloadName(payload, 'serverId') ?? 'unknown'}`;
|
|
130
235
|
}
|
|
131
|
-
case 'mcp:client:
|
|
132
|
-
|
|
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
|
|
264
|
+
return `agent.${formatTypeSegment(event.type)}`;
|
|
136
265
|
}
|
|
137
266
|
}
|
|
138
267
|
}
|
|
139
268
|
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
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
|
-
|
|
154
|
-
|
|
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
|
-
|
|
163
|
-
|
|
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
|
-
|
|
168
|
-
|
|
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
|
|
346
|
+
setIfPresent(attrs, 'agent.mcp.server_id', event.payload.serverId);
|
|
174
347
|
break;
|
|
175
348
|
}
|
|
176
|
-
|
|
177
|
-
case 'mcp:client:
|
|
178
|
-
attrs
|
|
179
|
-
attrs
|
|
180
|
-
attrs
|
|
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
|
|
189
|
-
attrs
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
return opts.traceSchedule ?? true;
|
|
260
|
-
}
|
|
412
|
+
if (event.type.endsWith(':failed') || event.type.endsWith(':error')) {
|
|
413
|
+
return event.type;
|
|
414
|
+
}
|
|
261
415
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
case 'mcp:client:authorize':
|
|
265
|
-
case 'mcp:client:discover': {
|
|
266
|
-
return opts.traceMcp ?? true;
|
|
267
|
-
}
|
|
416
|
+
return undefined;
|
|
417
|
+
}
|
|
268
418
|
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
return opts.traceLifecycle ?? true;
|
|
282
|
-
}
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
283
429
|
|
|
284
|
-
|
|
285
|
-
|
|
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?:
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
385
|
-
|
|
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
|
|
396
|
-
|
|
397
|
-
|
|
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
|
-
|
|
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:
|
|
552
|
+
exporter: { url: endpoint || 'http://localhost:4318/v1/traces', headers },
|
|
472
553
|
agents: options,
|
|
473
554
|
});
|
|
474
555
|
}
|
|
556
|
+
|
|
557
|
+
export type { MCPObservabilityEvent };
|