autotel-cloudflare 3.0.0 → 4.0.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 +260 -171
- package/dist/agents.js.map +1 -1
- package/package.json +8 -4
- package/src/actors/alarms.ts +0 -225
- package/src/actors/index.ts +0 -36
- package/src/actors/instrument-actor.test.ts +0 -179
- package/src/actors/instrument-actor.ts +0 -574
- package/src/actors/sockets.ts +0 -217
- package/src/actors/storage.ts +0 -263
- package/src/actors/traced-handler.ts +0 -300
- package/src/actors/types.ts +0 -98
- package/src/actors.ts +0 -50
- package/src/agents/index.ts +0 -42
- package/src/agents/otel-observability.test.ts +0 -336
- package/src/agents/otel-observability.ts +0 -474
- package/src/agents/types.ts +0 -167
- package/src/agents.ts +0 -76
- package/src/bindings/ai.test.ts +0 -170
- package/src/bindings/ai.ts +0 -73
- package/src/bindings/analytics-engine.test.ts +0 -175
- package/src/bindings/analytics-engine.ts +0 -78
- package/src/bindings/bindings-cache.test.ts +0 -80
- package/src/bindings/bindings-detection.test.ts +0 -235
- package/src/bindings/bindings-this-binding.test.ts +0 -294
- package/src/bindings/bindings.ts +0 -720
- package/src/bindings/browser-rendering.test.ts +0 -160
- package/src/bindings/browser-rendering.ts +0 -70
- package/src/bindings/common.ts +0 -84
- package/src/bindings/hyperdrive.test.ts +0 -176
- package/src/bindings/hyperdrive.ts +0 -74
- package/src/bindings/images.test.ts +0 -262
- package/src/bindings/images.ts +0 -182
- package/src/bindings/index.ts +0 -20
- package/src/bindings/queue-producer.test.ts +0 -224
- package/src/bindings/queue-producer.ts +0 -105
- package/src/bindings/rate-limiter.test.ts +0 -140
- package/src/bindings/rate-limiter.ts +0 -69
- package/src/bindings/vectorize.test.ts +0 -362
- package/src/bindings/vectorize.ts +0 -86
- package/src/bindings.ts +0 -6
- package/src/events.ts +0 -6
- package/src/execution-logger.test.ts +0 -127
- package/src/execution-logger.ts +0 -112
- package/src/global/cache.test.ts +0 -292
- package/src/global/cache.ts +0 -164
- package/src/global/fetch.test.ts +0 -399
- package/src/global/fetch.ts +0 -136
- package/src/global/index.ts +0 -7
- package/src/handlers/durable-objects.test.ts +0 -524
- package/src/handlers/durable-objects.ts +0 -250
- package/src/handlers/index.ts +0 -6
- package/src/handlers/workflows.test.ts +0 -411
- package/src/handlers/workflows.ts +0 -345
- package/src/handlers.ts +0 -6
- package/src/index.ts +0 -91
- package/src/logger.ts +0 -15
- package/src/native/native-tracing.test.ts +0 -54
- package/src/native/native-tracing.ts +0 -83
- package/src/native.ts +0 -12
- package/src/parse-error.ts +0 -1
- package/src/sampling.ts +0 -6
- package/src/testing.ts +0 -6
- package/src/wrappers/cf-attributes.test.ts +0 -334
- package/src/wrappers/define-worker-fetch.test.ts +0 -102
- package/src/wrappers/define-worker-fetch.ts +0 -71
- package/src/wrappers/index.ts +0 -13
- package/src/wrappers/instrument.integration.test.ts +0 -578
- package/src/wrappers/instrument.ts +0 -806
- package/src/wrappers/native-instrument.test.ts +0 -153
- package/src/wrappers/wrap-do.ts +0 -34
- package/src/wrappers/wrap-module.ts +0 -37
|
@@ -1,806 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Handler instrumentation for Cloudflare Workers
|
|
3
|
-
*
|
|
4
|
-
* Note: This file uses Cloudflare Workers types (ExportedHandler, Request, Response, etc.)
|
|
5
|
-
* which are globally available via @cloudflare/workers-types when listed in tsconfig.json.
|
|
6
|
-
* These types are devDependencies only - they're not runtime dependencies.
|
|
7
|
-
* At runtime, Cloudflare Workers runtime provides the actual implementations.
|
|
8
|
-
*
|
|
9
|
-
* Provides automatic OpenTelemetry tracing for:
|
|
10
|
-
* - HTTP handlers (fetch)
|
|
11
|
-
* - Scheduled/cron handlers
|
|
12
|
-
* - Queue handlers (with message tracking)
|
|
13
|
-
* - Email handlers
|
|
14
|
-
* - Auto-instrumentation of Cloudflare bindings (KV, R2, D1, Service Bindings)
|
|
15
|
-
* - Global fetch and cache instrumentation
|
|
16
|
-
* - Post-processor support for span customization
|
|
17
|
-
* - Tail sampling support
|
|
18
|
-
* - Cold start tracking
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import {
|
|
22
|
-
trace,
|
|
23
|
-
context as api_context,
|
|
24
|
-
propagation,
|
|
25
|
-
SpanStatusCode,
|
|
26
|
-
SpanKind,
|
|
27
|
-
} from '@opentelemetry/api';
|
|
28
|
-
import { resourceFromAttributes } from '@opentelemetry/resources';
|
|
29
|
-
import type {
|
|
30
|
-
ConfigurationOption,
|
|
31
|
-
ResolvedEdgeConfig,
|
|
32
|
-
Trigger,
|
|
33
|
-
HandlerInstrumentation,
|
|
34
|
-
InitialSpanInfo,
|
|
35
|
-
ReadableSpan,
|
|
36
|
-
RouteServiceConfig,
|
|
37
|
-
} from 'autotel-edge';
|
|
38
|
-
import {
|
|
39
|
-
createInitialiser,
|
|
40
|
-
setConfig,
|
|
41
|
-
getActiveConfig,
|
|
42
|
-
getServiceForPath,
|
|
43
|
-
shouldInstrumentPath,
|
|
44
|
-
withNativeTracer,
|
|
45
|
-
ensureGlobalContextManager,
|
|
46
|
-
type Initialiser,
|
|
47
|
-
type NativeTracer,
|
|
48
|
-
WorkerTracerProvider,
|
|
49
|
-
WorkerTracer,
|
|
50
|
-
} from 'autotel-edge';
|
|
51
|
-
import { getNativeTracerFromCtx } from '../native/native-tracing';
|
|
52
|
-
import { proxyExecutionContext, unwrap, wrap, type PromiseTracker } from '../bindings/common';
|
|
53
|
-
import { instrumentGlobalFetch } from '../global/fetch';
|
|
54
|
-
import { instrumentGlobalCache } from '../global/cache';
|
|
55
|
-
import { instrumentBindings } from '../bindings/bindings';
|
|
56
|
-
import type { Attributes, Span, TextMapGetter } from '@opentelemetry/api';
|
|
57
|
-
|
|
58
|
-
// Web API Headers objects require .get() — property access via defaultTextMapGetter won't work
|
|
59
|
-
const headersGetter: TextMapGetter<Headers> = {
|
|
60
|
-
get: (carrier, key) => carrier.get(key) ?? undefined,
|
|
61
|
-
keys: (carrier) => [...carrier.keys()],
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
type FetchHandler = (
|
|
65
|
-
request: Request,
|
|
66
|
-
env: any,
|
|
67
|
-
ctx: ExecutionContext,
|
|
68
|
-
) => Response | Promise<Response>;
|
|
69
|
-
|
|
70
|
-
type ScheduledHandler = (
|
|
71
|
-
event: ScheduledController,
|
|
72
|
-
env: any,
|
|
73
|
-
ctx: ExecutionContext,
|
|
74
|
-
) => void | Promise<void>;
|
|
75
|
-
|
|
76
|
-
type QueueHandler = (
|
|
77
|
-
batch: MessageBatch,
|
|
78
|
-
env: any,
|
|
79
|
-
ctx: ExecutionContext,
|
|
80
|
-
) => void | Promise<void>;
|
|
81
|
-
|
|
82
|
-
type EmailHandler = (
|
|
83
|
-
message: ForwardableEmailMessage,
|
|
84
|
-
env: any,
|
|
85
|
-
ctx: ExecutionContext,
|
|
86
|
-
) => void | Promise<void>;
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Create fetch handler instrumentation with config support for postProcess
|
|
90
|
-
*/
|
|
91
|
-
/**
|
|
92
|
-
* Extract Cloudflare-specific attributes from a request
|
|
93
|
-
*/
|
|
94
|
-
function extractCfAttributes(request: Request): Record<string, string | number | boolean> {
|
|
95
|
-
const cf = (request as any).cf;
|
|
96
|
-
if (!cf) return {};
|
|
97
|
-
|
|
98
|
-
const attrs: Record<string, string | number | boolean> = {};
|
|
99
|
-
const set = (key: string, value: unknown) => {
|
|
100
|
-
if (value !== undefined && value !== null) {
|
|
101
|
-
attrs[key] = value as string | number | boolean;
|
|
102
|
-
}
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
set('cloudflare.colo', cf.colo);
|
|
106
|
-
const ray = request.headers.get('cf-ray');
|
|
107
|
-
if (ray) attrs['cloudflare.ray_id'] = ray;
|
|
108
|
-
set('cloudflare.country', cf.country);
|
|
109
|
-
set('cloudflare.city', cf.city);
|
|
110
|
-
set('cloudflare.region', cf.region);
|
|
111
|
-
set('cloudflare.continent', cf.continent);
|
|
112
|
-
set('cloudflare.timezone', cf.timezone);
|
|
113
|
-
set('cloudflare.latitude', cf.latitude);
|
|
114
|
-
set('cloudflare.longitude', cf.longitude);
|
|
115
|
-
set('cloudflare.asn', cf.asn);
|
|
116
|
-
set('cloudflare.as_organization', cf.asOrganization);
|
|
117
|
-
set('cloudflare.http_protocol', cf.httpProtocol);
|
|
118
|
-
set('cloudflare.tls_version', cf.tlsVersion);
|
|
119
|
-
set('cloudflare.client_tcp_rtt', cf.clientTcpRtt);
|
|
120
|
-
|
|
121
|
-
return attrs;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function createFetchInstrumentation(
|
|
125
|
-
config: ResolvedEdgeConfig,
|
|
126
|
-
): HandlerInstrumentation<Request, Response> {
|
|
127
|
-
return {
|
|
128
|
-
getInitialSpanInfo: (request: Request): InitialSpanInfo => {
|
|
129
|
-
const url = new URL(request.url);
|
|
130
|
-
const routeService = getServiceForPath(
|
|
131
|
-
url.pathname,
|
|
132
|
-
config.handlers.fetch.routes as Record<string, RouteServiceConfig> | undefined,
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
const cfAttrs = (config as any).extractCfAttributes === false
|
|
136
|
-
? {}
|
|
137
|
-
: extractCfAttributes(request);
|
|
138
|
-
|
|
139
|
-
return {
|
|
140
|
-
name: `${request.method} ${url.pathname}`,
|
|
141
|
-
options: {
|
|
142
|
-
kind: SpanKind.SERVER,
|
|
143
|
-
attributes: {
|
|
144
|
-
'http.request.method': request.method,
|
|
145
|
-
'url.full': request.url,
|
|
146
|
-
...(routeService ? { 'service.name': routeService, 'autotel.route.service': routeService } : {}),
|
|
147
|
-
...cfAttrs,
|
|
148
|
-
},
|
|
149
|
-
},
|
|
150
|
-
context: propagation.extract(api_context.active(), request.headers, headersGetter),
|
|
151
|
-
};
|
|
152
|
-
},
|
|
153
|
-
getAttributesFromResult: (response: Response) => ({
|
|
154
|
-
'http.response.status_code': response.status,
|
|
155
|
-
}),
|
|
156
|
-
executionSucces: (span: Span, trigger: Request, result: Response) => {
|
|
157
|
-
// Override span status for server errors (5xx)
|
|
158
|
-
if (result.status >= 500) {
|
|
159
|
-
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Call postProcess callback if configured
|
|
163
|
-
if (config.handlers.fetch.postProcess) {
|
|
164
|
-
const readableSpan = span as unknown as ReadableSpan;
|
|
165
|
-
config.handlers.fetch.postProcess(span, {
|
|
166
|
-
request: trigger,
|
|
167
|
-
response: result,
|
|
168
|
-
readable: readableSpan,
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
},
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Scheduled handler instrumentation
|
|
177
|
-
*/
|
|
178
|
-
const scheduledInstrumentation: HandlerInstrumentation<ScheduledController, void> = {
|
|
179
|
-
getInitialSpanInfo: (event: ScheduledController): InitialSpanInfo => {
|
|
180
|
-
return {
|
|
181
|
-
name: `scheduledHandler ${event.cron || 'unknown'}`,
|
|
182
|
-
options: {
|
|
183
|
-
kind: SpanKind.INTERNAL,
|
|
184
|
-
attributes: {
|
|
185
|
-
'faas.trigger': 'timer',
|
|
186
|
-
'faas.cron': event.cron || 'unknown',
|
|
187
|
-
'faas.scheduled_time': new Date(event.scheduledTime).toISOString(),
|
|
188
|
-
},
|
|
189
|
-
},
|
|
190
|
-
};
|
|
191
|
-
},
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Tracks message status counts for queue processing
|
|
196
|
-
*/
|
|
197
|
-
class MessageStatusCount {
|
|
198
|
-
succeeded = 0;
|
|
199
|
-
failed = 0;
|
|
200
|
-
implicitly_acked = 0;
|
|
201
|
-
implicitly_retried = 0;
|
|
202
|
-
readonly total: number;
|
|
203
|
-
|
|
204
|
-
constructor(total: number) {
|
|
205
|
-
this.total = total;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
ack() {
|
|
209
|
-
this.succeeded = this.succeeded + 1;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
ackRemaining() {
|
|
213
|
-
this.implicitly_acked = this.total - this.succeeded - this.failed;
|
|
214
|
-
this.succeeded = this.total - this.failed;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
retry() {
|
|
218
|
-
this.failed = this.failed + 1;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
retryRemaining() {
|
|
222
|
-
this.implicitly_retried = this.total - this.succeeded - this.failed;
|
|
223
|
-
this.failed = this.total - this.succeeded;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
toAttributes(): Attributes {
|
|
227
|
-
return {
|
|
228
|
-
'queue.messages_count': this.total,
|
|
229
|
-
'queue.messages_success': this.succeeded,
|
|
230
|
-
'queue.messages_failed': this.failed,
|
|
231
|
-
'queue.batch_success': this.succeeded === this.total,
|
|
232
|
-
'queue.implicitly_acked': this.implicitly_acked,
|
|
233
|
-
'queue.implicitly_retried': this.implicitly_retried,
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Add event to active span
|
|
240
|
-
*/
|
|
241
|
-
function addQueueEvent(name: string, msg?: Message, delaySeconds?: number) {
|
|
242
|
-
const attrs: Attributes = {};
|
|
243
|
-
if (msg) {
|
|
244
|
-
attrs['queue.message_id'] = msg.id;
|
|
245
|
-
attrs['queue.message_timestamp'] = msg.timestamp.toISOString();
|
|
246
|
-
// Add attempts if available (from Cloudflare Queues API)
|
|
247
|
-
if ('attempts' in msg && typeof msg.attempts === 'number') {
|
|
248
|
-
attrs['queue.message_attempts'] = msg.attempts;
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
if (delaySeconds !== undefined) {
|
|
252
|
-
attrs['queue.retry_delay_seconds'] = delaySeconds;
|
|
253
|
-
}
|
|
254
|
-
trace.getActiveSpan()?.addEvent(name, attrs);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* Proxy a queue message to track ack/retry operations
|
|
259
|
-
*/
|
|
260
|
-
function proxyQueueMessage<Q>(msg: Message<Q>, count: MessageStatusCount): Message<Q> {
|
|
261
|
-
const msgHandler: ProxyHandler<Message<Q>> = {
|
|
262
|
-
get: (target, prop) => {
|
|
263
|
-
if (prop === 'ack') {
|
|
264
|
-
const ackFn = Reflect.get(target, prop);
|
|
265
|
-
return new Proxy(ackFn, {
|
|
266
|
-
apply: (fnTarget) => {
|
|
267
|
-
addQueueEvent('messageAck', msg);
|
|
268
|
-
count.ack();
|
|
269
|
-
Reflect.apply(fnTarget, msg, []);
|
|
270
|
-
},
|
|
271
|
-
});
|
|
272
|
-
} else if (prop === 'retry') {
|
|
273
|
-
const retryFn = Reflect.get(target, prop);
|
|
274
|
-
return new Proxy(retryFn, {
|
|
275
|
-
apply: (fnTarget, _thisArg, args) => {
|
|
276
|
-
// Extract delay and content type from retry options if provided
|
|
277
|
-
const retryOptions = args[0] as
|
|
278
|
-
| { delaySeconds?: number; contentType?: string }
|
|
279
|
-
| undefined;
|
|
280
|
-
const delaySeconds = retryOptions?.delaySeconds;
|
|
281
|
-
|
|
282
|
-
addQueueEvent('messageRetry', msg, delaySeconds);
|
|
283
|
-
|
|
284
|
-
// Add content type attribute if provided
|
|
285
|
-
if (retryOptions?.contentType) {
|
|
286
|
-
const span = trace.getActiveSpan();
|
|
287
|
-
if (span) {
|
|
288
|
-
span.setAttribute('queue.message.content_type', retryOptions.contentType);
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
count.retry();
|
|
293
|
-
const result = Reflect.apply(fnTarget, msg, args);
|
|
294
|
-
return result;
|
|
295
|
-
},
|
|
296
|
-
});
|
|
297
|
-
} else {
|
|
298
|
-
return Reflect.get(target, prop, msg);
|
|
299
|
-
}
|
|
300
|
-
},
|
|
301
|
-
};
|
|
302
|
-
return wrap(msg, msgHandler);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* Proxy MessageBatch to track ackAll/retryAll operations
|
|
307
|
-
*/
|
|
308
|
-
function proxyMessageBatch(batch: MessageBatch, count: MessageStatusCount): MessageBatch {
|
|
309
|
-
const batchHandler: ProxyHandler<MessageBatch> = {
|
|
310
|
-
get: (target, prop) => {
|
|
311
|
-
if (prop === 'messages') {
|
|
312
|
-
const messages = Reflect.get(target, prop);
|
|
313
|
-
const messagesHandler: ProxyHandler<MessageBatch['messages']> = {
|
|
314
|
-
get: (target, prop) => {
|
|
315
|
-
if (typeof prop === 'string' && !isNaN(parseInt(prop))) {
|
|
316
|
-
const message = Reflect.get(target, prop);
|
|
317
|
-
return proxyQueueMessage(message, count);
|
|
318
|
-
} else {
|
|
319
|
-
return Reflect.get(target, prop);
|
|
320
|
-
}
|
|
321
|
-
},
|
|
322
|
-
};
|
|
323
|
-
return wrap(messages, messagesHandler);
|
|
324
|
-
} else if (prop === 'ackAll') {
|
|
325
|
-
const ackFn = Reflect.get(target, prop);
|
|
326
|
-
return new Proxy(ackFn, {
|
|
327
|
-
apply: (fnTarget) => {
|
|
328
|
-
addQueueEvent('ackAll');
|
|
329
|
-
count.ackRemaining();
|
|
330
|
-
Reflect.apply(fnTarget, batch, []);
|
|
331
|
-
},
|
|
332
|
-
});
|
|
333
|
-
} else if (prop === 'retryAll') {
|
|
334
|
-
const retryFn = Reflect.get(target, prop);
|
|
335
|
-
return new Proxy(retryFn, {
|
|
336
|
-
apply: (fnTarget, _thisArg, args) => {
|
|
337
|
-
// Extract delay from retryAll options if provided
|
|
338
|
-
const retryOptions = args[0] as { delaySeconds?: number } | undefined;
|
|
339
|
-
const delaySeconds = retryOptions?.delaySeconds;
|
|
340
|
-
|
|
341
|
-
addQueueEvent('retryAll', undefined, delaySeconds);
|
|
342
|
-
count.retryRemaining();
|
|
343
|
-
Reflect.apply(fnTarget, batch, args);
|
|
344
|
-
},
|
|
345
|
-
});
|
|
346
|
-
}
|
|
347
|
-
return Reflect.get(target, prop);
|
|
348
|
-
},
|
|
349
|
-
};
|
|
350
|
-
return wrap(batch, batchHandler);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
/**
|
|
354
|
-
* Queue handler instrumentation with message tracking
|
|
355
|
-
*/
|
|
356
|
-
class QueueInstrumentation implements HandlerInstrumentation<MessageBatch, void> {
|
|
357
|
-
private count?: MessageStatusCount;
|
|
358
|
-
|
|
359
|
-
getInitialSpanInfo(batch: MessageBatch): InitialSpanInfo {
|
|
360
|
-
return {
|
|
361
|
-
name: `queueHandler ${batch.queue || 'unknown'}`,
|
|
362
|
-
options: {
|
|
363
|
-
kind: SpanKind.CONSUMER,
|
|
364
|
-
attributes: {
|
|
365
|
-
'faas.trigger': 'pubsub',
|
|
366
|
-
'queue.name': batch.queue || 'unknown',
|
|
367
|
-
},
|
|
368
|
-
},
|
|
369
|
-
};
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
instrumentTrigger(batch: MessageBatch): MessageBatch {
|
|
373
|
-
this.count = new MessageStatusCount(batch.messages.length);
|
|
374
|
-
return proxyMessageBatch(batch, this.count);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
executionSucces(span: Span, _trigger: MessageBatch, _result: void) {
|
|
378
|
-
if (this.count) {
|
|
379
|
-
this.count.ackRemaining();
|
|
380
|
-
span.setAttributes(this.count.toAttributes());
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
executionFailed(span: Span, _trigger: MessageBatch, _error?: any) {
|
|
385
|
-
if (this.count) {
|
|
386
|
-
this.count.retryRemaining();
|
|
387
|
-
span.setAttributes(this.count.toAttributes());
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
/**
|
|
393
|
-
* Converts email headers into OpenTelemetry attributes.
|
|
394
|
-
* When dataSafety.emailHeaderAllowlist is configured, only allowed headers are captured.
|
|
395
|
-
*/
|
|
396
|
-
function headerAttributes(message: { headers: Headers }): Record<string, string> {
|
|
397
|
-
const attrs: Record<string, string> = {};
|
|
398
|
-
if (message.headers instanceof Headers) {
|
|
399
|
-
const config = getActiveConfig();
|
|
400
|
-
const allowlist: string[] | undefined = config?.dataSafety?.emailHeaderAllowlist;
|
|
401
|
-
for (const [key, value] of message.headers.entries()) {
|
|
402
|
-
if (allowlist && !allowlist.includes(key.toLowerCase())) {
|
|
403
|
-
continue;
|
|
404
|
-
}
|
|
405
|
-
attrs[`email.header.${key}`] = value;
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
return attrs;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
/**
|
|
412
|
-
* Email handler instrumentation
|
|
413
|
-
*/
|
|
414
|
-
const emailInstrumentation: HandlerInstrumentation<ForwardableEmailMessage, void> = {
|
|
415
|
-
getInitialSpanInfo: (message: ForwardableEmailMessage): InitialSpanInfo => {
|
|
416
|
-
const attributes: Record<string, string> = {
|
|
417
|
-
'faas.trigger': 'other',
|
|
418
|
-
'messaging.destination.name': message.to || 'unknown',
|
|
419
|
-
};
|
|
420
|
-
|
|
421
|
-
// Add message ID if available
|
|
422
|
-
if ('headers' in message && message.headers instanceof Headers) {
|
|
423
|
-
const messageId = message.headers.get('Message-Id');
|
|
424
|
-
if (messageId) {
|
|
425
|
-
attributes['rpc.message.id'] = messageId;
|
|
426
|
-
}
|
|
427
|
-
// Add all headers as attributes
|
|
428
|
-
Object.assign(attributes, headerAttributes(message));
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
return {
|
|
432
|
-
name: `emailHandler ${message.to || 'unknown'}`,
|
|
433
|
-
options: {
|
|
434
|
-
kind: SpanKind.CONSUMER,
|
|
435
|
-
attributes,
|
|
436
|
-
},
|
|
437
|
-
};
|
|
438
|
-
},
|
|
439
|
-
};
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
/**
|
|
443
|
-
* Export spans after request completes
|
|
444
|
-
*/
|
|
445
|
-
async function exportSpans(
|
|
446
|
-
traceId: string,
|
|
447
|
-
tracker: PromiseTracker | undefined,
|
|
448
|
-
ctx: ExecutionContext,
|
|
449
|
-
) {
|
|
450
|
-
const tracer = trace.getTracer('autotel-edge');
|
|
451
|
-
if (tracer instanceof WorkerTracer) {
|
|
452
|
-
try {
|
|
453
|
-
// scheduler is available on ExecutionContext at runtime
|
|
454
|
-
const ctxWithScheduler = ctx as ExecutionContext & { scheduler?: { wait(ms: number): Promise<void> } };
|
|
455
|
-
if (ctxWithScheduler.scheduler) {
|
|
456
|
-
await ctxWithScheduler.scheduler.wait(1);
|
|
457
|
-
}
|
|
458
|
-
await tracker?.wait();
|
|
459
|
-
await tracer.forceFlush(traceId);
|
|
460
|
-
} catch (error) {
|
|
461
|
-
// Silently handle exporter errors to prevent worker crashes
|
|
462
|
-
// Exporter failures should not affect the worker's ability to process requests
|
|
463
|
-
// In production, consider logging to a monitoring service
|
|
464
|
-
console.error('[autotel-edge] Failed to export spans:', error);
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
/**
|
|
470
|
-
* Create handler flow with instrumentation
|
|
471
|
-
*/
|
|
472
|
-
function createHandlerFlow<T extends Trigger, E, R>(
|
|
473
|
-
instrumentation: HandlerInstrumentation<T, R>,
|
|
474
|
-
) {
|
|
475
|
-
return (
|
|
476
|
-
handlerFn: (trigger: T, env: E, ctx: ExecutionContext) => R | Promise<R>,
|
|
477
|
-
[trigger, env, context]: [T, E, ExecutionContext],
|
|
478
|
-
) => {
|
|
479
|
-
const { ctx: proxiedCtx, tracker } = proxyExecutionContext(context);
|
|
480
|
-
|
|
481
|
-
const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
|
|
482
|
-
|
|
483
|
-
const { name, options, context: spanContext } =
|
|
484
|
-
instrumentation.getInitialSpanInfo(trigger);
|
|
485
|
-
|
|
486
|
-
// Add cold start tracking
|
|
487
|
-
if (options.attributes) {
|
|
488
|
-
options.attributes['faas.coldstart'] = coldStart;
|
|
489
|
-
} else {
|
|
490
|
-
options.attributes = { 'faas.coldstart': coldStart };
|
|
491
|
-
}
|
|
492
|
-
coldStart = false;
|
|
493
|
-
|
|
494
|
-
const parentContext = spanContext || api_context.active();
|
|
495
|
-
|
|
496
|
-
// Instrument trigger if supported (e.g., for queue handler)
|
|
497
|
-
const instrumentedTrigger = instrumentation.instrumentTrigger
|
|
498
|
-
? instrumentation.instrumentTrigger(trigger)
|
|
499
|
-
: trigger;
|
|
500
|
-
|
|
501
|
-
return tracer.startActiveSpan(name, options, parentContext, async (span) => {
|
|
502
|
-
try {
|
|
503
|
-
const result = await handlerFn(instrumentedTrigger, env, proxiedCtx);
|
|
504
|
-
|
|
505
|
-
if (instrumentation.getAttributesFromResult) {
|
|
506
|
-
const attributes = instrumentation.getAttributesFromResult(result);
|
|
507
|
-
span.setAttributes(attributes);
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
// Set default OK status; executionSucces may override (e.g. HTTP 5xx)
|
|
511
|
-
span.setStatus({ code: SpanStatusCode.OK });
|
|
512
|
-
|
|
513
|
-
if (instrumentation.executionSucces) {
|
|
514
|
-
instrumentation.executionSucces(span, trigger, result);
|
|
515
|
-
}
|
|
516
|
-
return result;
|
|
517
|
-
} catch (error) {
|
|
518
|
-
span.recordException(error as Error);
|
|
519
|
-
span.setStatus({
|
|
520
|
-
code: SpanStatusCode.ERROR,
|
|
521
|
-
message: error instanceof Error ? error.message : String(error),
|
|
522
|
-
});
|
|
523
|
-
if (instrumentation.executionFailed) {
|
|
524
|
-
instrumentation.executionFailed(span, trigger, error);
|
|
525
|
-
}
|
|
526
|
-
throw error;
|
|
527
|
-
} finally {
|
|
528
|
-
span.end();
|
|
529
|
-
context.waitUntil(exportSpans(span.spanContext().traceId, tracker, context));
|
|
530
|
-
}
|
|
531
|
-
});
|
|
532
|
-
};
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
let warnedMissingNative = false;
|
|
536
|
-
|
|
537
|
-
/**
|
|
538
|
-
* Resolve whether this invocation should use Cloudflare's native tracer.
|
|
539
|
-
*
|
|
540
|
-
* - `nativeTracing: 'off'` → never use native (always autotel's OTLP pipeline).
|
|
541
|
-
* - `nativeTracing: 'on'` → require native; warn once and fall back if absent.
|
|
542
|
-
* - `nativeTracing: 'auto'` (default) → use native when `ctx.tracing` exists.
|
|
543
|
-
*/
|
|
544
|
-
function resolveNativeTracer(
|
|
545
|
-
config: ResolvedEdgeConfig,
|
|
546
|
-
ctx: ExecutionContext,
|
|
547
|
-
trigger: Trigger,
|
|
548
|
-
): NativeTracer | null {
|
|
549
|
-
const mode = config.nativeTracing ?? 'auto';
|
|
550
|
-
if (mode === 'off') {
|
|
551
|
-
return null;
|
|
552
|
-
}
|
|
553
|
-
// Correlation id: prefer Cloudflare's ray id (also stamped on the native root
|
|
554
|
-
// span + carried by the workers logger), so logs/spans/dashboard share a key.
|
|
555
|
-
// Fall back to a per-invocation uuid for non-fetch triggers.
|
|
556
|
-
const rayId = trigger instanceof Request ? trigger.headers.get('cf-ray') : null;
|
|
557
|
-
const correlationId = rayId ?? crypto.randomUUID();
|
|
558
|
-
const nativeTracer = getNativeTracerFromCtx(ctx, correlationId);
|
|
559
|
-
if (!nativeTracer && mode === 'on' && !warnedMissingNative) {
|
|
560
|
-
warnedMissingNative = true;
|
|
561
|
-
console.warn(
|
|
562
|
-
"[autotel-cloudflare] nativeTracing is 'on' but Cloudflare native tracing was not detected. " +
|
|
563
|
-
'Enable observability.traces in your Wrangler config (and use a recent compatibility_date). ' +
|
|
564
|
-
'Falling back to the autotel OTLP exporter.',
|
|
565
|
-
);
|
|
566
|
-
}
|
|
567
|
-
return nativeTracer;
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
/**
|
|
571
|
-
* Run a handler in native-tracing mode.
|
|
572
|
-
*
|
|
573
|
-
* Cloudflare already creates the root handler span and instruments platform
|
|
574
|
-
* operations (fetch / KV / R2 / D1 / DO), so autotel defers entirely: no
|
|
575
|
-
* binding proxies (avoids duplicate spans), no provider/exporter, no manual
|
|
576
|
-
* flush. We install the native tracer into the active context so that the
|
|
577
|
-
* user's `span()` / `trace()` / `enterSpan()` calls nest inside Cloudflare's
|
|
578
|
-
* native waterfall and are exported by the platform.
|
|
579
|
-
*/
|
|
580
|
-
function runWithNativeTracing<T extends Trigger, E, R>(
|
|
581
|
-
handlerFn: (trigger: T, env: E, ctx: ExecutionContext) => R | Promise<R>,
|
|
582
|
-
trigger: T,
|
|
583
|
-
env: E,
|
|
584
|
-
ctx: ExecutionContext,
|
|
585
|
-
config: ResolvedEdgeConfig,
|
|
586
|
-
nativeTracer: NativeTracer,
|
|
587
|
-
): R | Promise<R> {
|
|
588
|
-
// Register the context manager (we skip initProvider in native mode) so the
|
|
589
|
-
// native tracer propagates through OTel context into span()/trace() — without
|
|
590
|
-
// it, getActiveNativeTracer() returns null and custom spans never route to
|
|
591
|
-
// enterSpan. Bindings are NOT proxied: Cloudflare instruments them natively.
|
|
592
|
-
ensureGlobalContextManager();
|
|
593
|
-
const nativeContext = withNativeTracer(nativeTracer, setConfig(config));
|
|
594
|
-
return api_context.with(nativeContext, () => handlerFn(trigger, env, ctx));
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
/**
|
|
598
|
-
* Create handler proxy
|
|
599
|
-
*/
|
|
600
|
-
function createHandlerProxy<T extends Trigger, E, R>(
|
|
601
|
-
_handler: unknown,
|
|
602
|
-
handlerFn: (trigger: T, env: E, ctx: ExecutionContext) => R | Promise<R>,
|
|
603
|
-
initialiser: Initialiser,
|
|
604
|
-
instrumentation: HandlerInstrumentation<T, R>,
|
|
605
|
-
): (trigger: T, env: E, ctx: ExecutionContext) => ReturnType<typeof handlerFn> {
|
|
606
|
-
return (trigger: T, env: E, ctx: ExecutionContext) => {
|
|
607
|
-
const config = initialiser(env, trigger);
|
|
608
|
-
|
|
609
|
-
// Check if instrumentation is disabled (useful for local dev)
|
|
610
|
-
if (config.instrumentation.disabled) {
|
|
611
|
-
// Return handler as-is without instrumentation
|
|
612
|
-
return handlerFn(trigger, env, ctx);
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
// Prefer Cloudflare native tracing when available — defer to the platform.
|
|
616
|
-
const nativeTracer = resolveNativeTracer(config, ctx, trigger);
|
|
617
|
-
if (nativeTracer) {
|
|
618
|
-
return runWithNativeTracing(handlerFn, trigger, env, ctx, config, nativeTracer);
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
// Auto-instrument Cloudflare bindings in the environment
|
|
622
|
-
const instrumentedEnv = instrumentBindings(env as Record<string, any>) as E;
|
|
623
|
-
|
|
624
|
-
const configContext = setConfig(config);
|
|
625
|
-
|
|
626
|
-
// Initialize provider on first call
|
|
627
|
-
initProvider(config);
|
|
628
|
-
|
|
629
|
-
const flowFn = createHandlerFlow<T, E, R>(instrumentation);
|
|
630
|
-
|
|
631
|
-
// Execute the handler flow within the config context
|
|
632
|
-
return api_context.with(configContext, () => {
|
|
633
|
-
return flowFn(handlerFn, [trigger, instrumentedEnv, ctx]) as ReturnType<typeof handlerFn>;
|
|
634
|
-
});
|
|
635
|
-
};
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
/**
|
|
639
|
-
* Create handler proxy with dynamic instrumentation (for fetch with postProcess)
|
|
640
|
-
*/
|
|
641
|
-
function createHandlerProxyWithConfig<T extends Trigger, E, R>(
|
|
642
|
-
_handler: unknown,
|
|
643
|
-
handlerFn: (trigger: T, env: E, ctx: ExecutionContext) => R | Promise<R>,
|
|
644
|
-
initialiser: Initialiser,
|
|
645
|
-
createInstrumentation: (config: ResolvedEdgeConfig) => HandlerInstrumentation<T, R>,
|
|
646
|
-
): (trigger: T, env: E, ctx: ExecutionContext) => ReturnType<typeof handlerFn> {
|
|
647
|
-
return (trigger: T, env: E, ctx: ExecutionContext) => {
|
|
648
|
-
const config = initialiser(env, trigger);
|
|
649
|
-
|
|
650
|
-
// Check if instrumentation is disabled (useful for local dev)
|
|
651
|
-
if (config.instrumentation.disabled) {
|
|
652
|
-
// Return handler as-is without instrumentation
|
|
653
|
-
return handlerFn(trigger, env, ctx);
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
if (trigger instanceof Request) {
|
|
657
|
-
const pathname = new URL(trigger.url).pathname;
|
|
658
|
-
const fetchCfg = config.handlers.fetch;
|
|
659
|
-
if (
|
|
660
|
-
!shouldInstrumentPath(pathname, {
|
|
661
|
-
include: fetchCfg.include,
|
|
662
|
-
exclude: fetchCfg.exclude,
|
|
663
|
-
})
|
|
664
|
-
) {
|
|
665
|
-
return handlerFn(trigger, env, ctx) as ReturnType<typeof handlerFn>;
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
// Prefer Cloudflare native tracing when available — defer to the platform.
|
|
670
|
-
const nativeTracer = resolveNativeTracer(config, ctx, trigger);
|
|
671
|
-
if (nativeTracer) {
|
|
672
|
-
return runWithNativeTracing(handlerFn, trigger, env, ctx, config, nativeTracer) as ReturnType<typeof handlerFn>;
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
// Auto-instrument Cloudflare bindings in the environment
|
|
676
|
-
const instrumentedEnv = instrumentBindings(env as Record<string, any>) as E;
|
|
677
|
-
|
|
678
|
-
const configContext = setConfig(config);
|
|
679
|
-
|
|
680
|
-
// Initialize provider on first call
|
|
681
|
-
initProvider(config);
|
|
682
|
-
|
|
683
|
-
// Create instrumentation with config
|
|
684
|
-
const instrumentation = createInstrumentation(config);
|
|
685
|
-
const flowFn = createHandlerFlow<T, E, R>(instrumentation);
|
|
686
|
-
|
|
687
|
-
// Execute the handler flow within the config context
|
|
688
|
-
return api_context.with(configContext, () => {
|
|
689
|
-
return flowFn(handlerFn, [trigger, instrumentedEnv, ctx]) as ReturnType<typeof handlerFn>;
|
|
690
|
-
});
|
|
691
|
-
};
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
let providerInitialized = false;
|
|
695
|
-
let coldStart = true;
|
|
696
|
-
|
|
697
|
-
/**
|
|
698
|
-
* Initialize the tracer provider
|
|
699
|
-
*/
|
|
700
|
-
function initProvider(config: ResolvedEdgeConfig): void {
|
|
701
|
-
if (providerInitialized) return;
|
|
702
|
-
|
|
703
|
-
// Install global instrumentations
|
|
704
|
-
if (config.instrumentation.instrumentGlobalFetch) {
|
|
705
|
-
instrumentGlobalFetch();
|
|
706
|
-
}
|
|
707
|
-
if (config.instrumentation.instrumentGlobalCache) {
|
|
708
|
-
instrumentGlobalCache();
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
// Set up propagator
|
|
712
|
-
propagation.setGlobalPropagator(config.propagator);
|
|
713
|
-
|
|
714
|
-
// Create resource
|
|
715
|
-
const resource = resourceFromAttributes({
|
|
716
|
-
'service.name': config.service.name,
|
|
717
|
-
'service.version': config.service.version,
|
|
718
|
-
'service.namespace': config.service.namespace,
|
|
719
|
-
'cloud.provider': 'cloudflare',
|
|
720
|
-
'cloud.platform': 'cloudflare.workers',
|
|
721
|
-
'telemetry.sdk.name': 'autotel-edge',
|
|
722
|
-
'telemetry.sdk.language': 'js',
|
|
723
|
-
});
|
|
724
|
-
|
|
725
|
-
// Create and register provider
|
|
726
|
-
const provider = new WorkerTracerProvider(config.spanProcessors, resource);
|
|
727
|
-
provider.register();
|
|
728
|
-
|
|
729
|
-
// Set head sampler on tracer
|
|
730
|
-
const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
|
|
731
|
-
tracer.setHeadSampler(config.sampling.headSampler);
|
|
732
|
-
|
|
733
|
-
providerInitialized = true;
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
/**
|
|
737
|
-
* Instrument a Cloudflare Workers handler
|
|
738
|
-
*
|
|
739
|
-
* @example
|
|
740
|
-
* ```typescript
|
|
741
|
-
* import { instrument } from 'autotel-edge'
|
|
742
|
-
*
|
|
743
|
-
* const handler = {
|
|
744
|
-
* async fetch(request, env, ctx) {
|
|
745
|
-
* return new Response('Hello World')
|
|
746
|
-
* }
|
|
747
|
-
* }
|
|
748
|
-
*
|
|
749
|
-
* export default instrument(handler, {
|
|
750
|
-
* exporter: {
|
|
751
|
-
* url: env.OTLP_ENDPOINT,
|
|
752
|
-
* headers: { 'x-api-key': env.API_KEY }
|
|
753
|
-
* },
|
|
754
|
-
* service: { name: 'my-worker' }
|
|
755
|
-
* })
|
|
756
|
-
* ```
|
|
757
|
-
*/
|
|
758
|
-
export function instrument<E, Q = any, C = any>(
|
|
759
|
-
handler: ExportedHandler<E, Q, C>,
|
|
760
|
-
config: ConfigurationOption,
|
|
761
|
-
): ExportedHandler<E, Q, C> {
|
|
762
|
-
const initialiser = createInitialiser(config);
|
|
763
|
-
|
|
764
|
-
if (handler.fetch) {
|
|
765
|
-
const fetcher = unwrap(handler.fetch) as FetchHandler;
|
|
766
|
-
// Create fetch instrumentation with config support
|
|
767
|
-
handler.fetch = createHandlerProxyWithConfig(
|
|
768
|
-
handler,
|
|
769
|
-
fetcher,
|
|
770
|
-
initialiser,
|
|
771
|
-
createFetchInstrumentation,
|
|
772
|
-
);
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
if (handler.scheduled) {
|
|
776
|
-
const scheduled = unwrap(handler.scheduled) as ScheduledHandler;
|
|
777
|
-
handler.scheduled = createHandlerProxy(
|
|
778
|
-
handler,
|
|
779
|
-
scheduled,
|
|
780
|
-
initialiser,
|
|
781
|
-
scheduledInstrumentation,
|
|
782
|
-
);
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
if (handler.queue) {
|
|
786
|
-
const queue = unwrap(handler.queue) as QueueHandler;
|
|
787
|
-
handler.queue = createHandlerProxy(
|
|
788
|
-
handler,
|
|
789
|
-
queue,
|
|
790
|
-
initialiser,
|
|
791
|
-
new QueueInstrumentation(),
|
|
792
|
-
);
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
if (handler.email) {
|
|
796
|
-
const email = unwrap(handler.email) as EmailHandler;
|
|
797
|
-
handler.email = createHandlerProxy(
|
|
798
|
-
handler,
|
|
799
|
-
email,
|
|
800
|
-
initialiser,
|
|
801
|
-
emailInstrumentation,
|
|
802
|
-
);
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
return handler;
|
|
806
|
-
}
|