autotel-cloudflare 3.1.0 → 4.0.1

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.
Files changed (76) hide show
  1. package/dist/agents.d.ts.map +1 -1
  2. package/dist/agents.js +7 -0
  3. package/dist/agents.js.map +1 -1
  4. package/package.json +8 -4
  5. package/src/actors/alarms.ts +0 -225
  6. package/src/actors/index.ts +0 -36
  7. package/src/actors/instrument-actor.test.ts +0 -179
  8. package/src/actors/instrument-actor.ts +0 -574
  9. package/src/actors/sockets.ts +0 -217
  10. package/src/actors/storage.ts +0 -263
  11. package/src/actors/traced-handler.ts +0 -300
  12. package/src/actors/types.ts +0 -98
  13. package/src/actors.ts +0 -50
  14. package/src/agents/agent.ts +0 -327
  15. package/src/agents/base.ts +0 -26
  16. package/src/agents/index.ts +0 -46
  17. package/src/agents/mcp.ts +0 -38
  18. package/src/agents/observability.ts +0 -145
  19. package/src/agents/otel-observability.test.ts +0 -269
  20. package/src/agents/otel-observability.ts +0 -557
  21. package/src/agents/types.ts +0 -61
  22. package/src/agents.ts +0 -87
  23. package/src/bindings/ai.test.ts +0 -170
  24. package/src/bindings/ai.ts +0 -73
  25. package/src/bindings/analytics-engine.test.ts +0 -175
  26. package/src/bindings/analytics-engine.ts +0 -78
  27. package/src/bindings/bindings-cache.test.ts +0 -80
  28. package/src/bindings/bindings-detection.test.ts +0 -235
  29. package/src/bindings/bindings-this-binding.test.ts +0 -294
  30. package/src/bindings/bindings.ts +0 -720
  31. package/src/bindings/browser-rendering.test.ts +0 -160
  32. package/src/bindings/browser-rendering.ts +0 -70
  33. package/src/bindings/common.ts +0 -84
  34. package/src/bindings/hyperdrive.test.ts +0 -176
  35. package/src/bindings/hyperdrive.ts +0 -74
  36. package/src/bindings/images.test.ts +0 -262
  37. package/src/bindings/images.ts +0 -182
  38. package/src/bindings/index.ts +0 -20
  39. package/src/bindings/queue-producer.test.ts +0 -224
  40. package/src/bindings/queue-producer.ts +0 -105
  41. package/src/bindings/rate-limiter.test.ts +0 -140
  42. package/src/bindings/rate-limiter.ts +0 -69
  43. package/src/bindings/vectorize.test.ts +0 -362
  44. package/src/bindings/vectorize.ts +0 -86
  45. package/src/bindings.ts +0 -6
  46. package/src/events.ts +0 -6
  47. package/src/execution-logger.test.ts +0 -127
  48. package/src/execution-logger.ts +0 -112
  49. package/src/global/cache.test.ts +0 -292
  50. package/src/global/cache.ts +0 -164
  51. package/src/global/fetch.test.ts +0 -399
  52. package/src/global/fetch.ts +0 -136
  53. package/src/global/index.ts +0 -7
  54. package/src/handlers/durable-objects.test.ts +0 -524
  55. package/src/handlers/durable-objects.ts +0 -250
  56. package/src/handlers/index.ts +0 -6
  57. package/src/handlers/workflows.test.ts +0 -411
  58. package/src/handlers/workflows.ts +0 -345
  59. package/src/handlers.ts +0 -6
  60. package/src/index.ts +0 -91
  61. package/src/logger.ts +0 -15
  62. package/src/native/native-tracing.test.ts +0 -54
  63. package/src/native/native-tracing.ts +0 -83
  64. package/src/native.ts +0 -12
  65. package/src/parse-error.ts +0 -1
  66. package/src/sampling.ts +0 -6
  67. package/src/testing.ts +0 -6
  68. package/src/wrappers/cf-attributes.test.ts +0 -334
  69. package/src/wrappers/define-worker-fetch.test.ts +0 -102
  70. package/src/wrappers/define-worker-fetch.ts +0 -71
  71. package/src/wrappers/index.ts +0 -13
  72. package/src/wrappers/instrument.integration.test.ts +0 -578
  73. package/src/wrappers/instrument.ts +0 -806
  74. package/src/wrappers/native-instrument.test.ts +0 -153
  75. package/src/wrappers/wrap-do.ts +0 -34
  76. 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
- }