autotel-cloudflare 2.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.
Files changed (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +432 -0
  3. package/dist/actors.d.ts +248 -0
  4. package/dist/actors.js +1030 -0
  5. package/dist/actors.js.map +1 -0
  6. package/dist/agents.d.ts +219 -0
  7. package/dist/agents.js +276 -0
  8. package/dist/agents.js.map +1 -0
  9. package/dist/bindings.d.ts +40 -0
  10. package/dist/bindings.js +4 -0
  11. package/dist/bindings.js.map +1 -0
  12. package/dist/chunk-JDPN3HND.js +520 -0
  13. package/dist/chunk-JDPN3HND.js.map +1 -0
  14. package/dist/chunk-QXFYTHQF.js +298 -0
  15. package/dist/chunk-QXFYTHQF.js.map +1 -0
  16. package/dist/chunk-SKKRPS5K.js +50 -0
  17. package/dist/chunk-SKKRPS5K.js.map +1 -0
  18. package/dist/events.d.ts +1 -0
  19. package/dist/events.js +3 -0
  20. package/dist/events.js.map +1 -0
  21. package/dist/handlers.d.ts +121 -0
  22. package/dist/handlers.js +4 -0
  23. package/dist/handlers.js.map +1 -0
  24. package/dist/index.d.ts +144 -0
  25. package/dist/index.js +576 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/logger.d.ts +1 -0
  28. package/dist/logger.js +3 -0
  29. package/dist/logger.js.map +1 -0
  30. package/dist/sampling.d.ts +4 -0
  31. package/dist/sampling.js +3 -0
  32. package/dist/sampling.js.map +1 -0
  33. package/dist/testing.d.ts +1 -0
  34. package/dist/testing.js +3 -0
  35. package/dist/testing.js.map +1 -0
  36. package/package.json +107 -0
  37. package/src/actors/alarms.ts +225 -0
  38. package/src/actors/index.ts +36 -0
  39. package/src/actors/instrument-actor.test.ts +179 -0
  40. package/src/actors/instrument-actor.ts +574 -0
  41. package/src/actors/sockets.ts +217 -0
  42. package/src/actors/storage.ts +263 -0
  43. package/src/actors/traced-handler.ts +300 -0
  44. package/src/actors/types.ts +98 -0
  45. package/src/actors.ts +50 -0
  46. package/src/agents/index.ts +42 -0
  47. package/src/agents/otel-observability.test.ts +329 -0
  48. package/src/agents/otel-observability.ts +465 -0
  49. package/src/agents/types.ts +167 -0
  50. package/src/agents.ts +76 -0
  51. package/src/bindings/bindings.ts +621 -0
  52. package/src/bindings/common.ts +75 -0
  53. package/src/bindings/index.ts +12 -0
  54. package/src/bindings.ts +6 -0
  55. package/src/events.ts +6 -0
  56. package/src/global/cache.test.ts +292 -0
  57. package/src/global/cache.ts +164 -0
  58. package/src/global/fetch.test.ts +344 -0
  59. package/src/global/fetch.ts +134 -0
  60. package/src/global/index.ts +7 -0
  61. package/src/handlers/durable-objects.test.ts +524 -0
  62. package/src/handlers/durable-objects.ts +250 -0
  63. package/src/handlers/index.ts +6 -0
  64. package/src/handlers/workflows.ts +318 -0
  65. package/src/handlers.ts +6 -0
  66. package/src/index.ts +57 -0
  67. package/src/logger.ts +6 -0
  68. package/src/sampling.ts +6 -0
  69. package/src/testing.ts +6 -0
  70. package/src/wrappers/index.ts +8 -0
  71. package/src/wrappers/instrument.integration.test.ts +468 -0
  72. package/src/wrappers/instrument.ts +643 -0
  73. package/src/wrappers/wrap-do.ts +34 -0
  74. package/src/wrappers/wrap-module.ts +37 -0
@@ -0,0 +1,643 @@
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
+ } from 'autotel-edge';
37
+ import {
38
+ createInitialiser,
39
+ setConfig,
40
+ type Initialiser,
41
+ WorkerTracerProvider,
42
+ WorkerTracer,
43
+ } from 'autotel-edge';
44
+ import { proxyExecutionContext, unwrap, wrap, type PromiseTracker } from '../bindings/common';
45
+ import { instrumentGlobalFetch } from '../global/fetch';
46
+ import { instrumentGlobalCache } from '../global/cache';
47
+ import { instrumentBindings } from '../bindings/bindings';
48
+ import type { Attributes, Span } from '@opentelemetry/api';
49
+
50
+ type FetchHandler = (
51
+ request: Request,
52
+ env: any,
53
+ ctx: ExecutionContext,
54
+ ) => Response | Promise<Response>;
55
+
56
+ type ScheduledHandler = (
57
+ event: ScheduledController,
58
+ env: any,
59
+ ctx: ExecutionContext,
60
+ ) => void | Promise<void>;
61
+
62
+ type QueueHandler = (
63
+ batch: MessageBatch,
64
+ env: any,
65
+ ctx: ExecutionContext,
66
+ ) => void | Promise<void>;
67
+
68
+ type EmailHandler = (
69
+ message: ForwardableEmailMessage,
70
+ env: any,
71
+ ctx: ExecutionContext,
72
+ ) => void | Promise<void>;
73
+
74
+
75
+ /**
76
+ * Create fetch handler instrumentation with config support for postProcess
77
+ */
78
+ function createFetchInstrumentation(
79
+ config: ResolvedEdgeConfig,
80
+ ): HandlerInstrumentation<Request, Response> {
81
+ return {
82
+ getInitialSpanInfo: (request: Request): InitialSpanInfo => {
83
+ const url = new URL(request.url);
84
+
85
+ return {
86
+ name: `${request.method} ${url.pathname}`,
87
+ options: {
88
+ kind: SpanKind.SERVER,
89
+ attributes: {
90
+ 'http.request.method': request.method,
91
+ 'url.full': request.url,
92
+ },
93
+ },
94
+ context: propagation.extract(api_context.active(), request.headers),
95
+ };
96
+ },
97
+ getAttributesFromResult: (response: Response) => ({
98
+ 'http.response.status_code': response.status,
99
+ }),
100
+ executionSucces: (span: Span, trigger: Request, result: Response) => {
101
+ // Call postProcess callback if configured
102
+ if (config.handlers.fetch.postProcess) {
103
+ const readableSpan = span as unknown as ReadableSpan;
104
+ config.handlers.fetch.postProcess(span, {
105
+ request: trigger,
106
+ response: result,
107
+ readable: readableSpan,
108
+ });
109
+ }
110
+ },
111
+ };
112
+ }
113
+
114
+ /**
115
+ * Scheduled handler instrumentation
116
+ */
117
+ const scheduledInstrumentation: HandlerInstrumentation<ScheduledController, void> = {
118
+ getInitialSpanInfo: (event: ScheduledController): InitialSpanInfo => {
119
+ return {
120
+ name: `scheduledHandler ${event.cron || 'unknown'}`,
121
+ options: {
122
+ kind: SpanKind.INTERNAL,
123
+ attributes: {
124
+ 'faas.trigger': 'timer',
125
+ 'faas.cron': event.cron || 'unknown',
126
+ 'faas.scheduled_time': new Date(event.scheduledTime).toISOString(),
127
+ },
128
+ },
129
+ };
130
+ },
131
+ };
132
+
133
+ /**
134
+ * Tracks message status counts for queue processing
135
+ */
136
+ class MessageStatusCount {
137
+ succeeded = 0;
138
+ failed = 0;
139
+ implicitly_acked = 0;
140
+ implicitly_retried = 0;
141
+ readonly total: number;
142
+
143
+ constructor(total: number) {
144
+ this.total = total;
145
+ }
146
+
147
+ ack() {
148
+ this.succeeded = this.succeeded + 1;
149
+ }
150
+
151
+ ackRemaining() {
152
+ this.implicitly_acked = this.total - this.succeeded - this.failed;
153
+ this.succeeded = this.total - this.failed;
154
+ }
155
+
156
+ retry() {
157
+ this.failed = this.failed + 1;
158
+ }
159
+
160
+ retryRemaining() {
161
+ this.implicitly_retried = this.total - this.succeeded - this.failed;
162
+ this.failed = this.total - this.succeeded;
163
+ }
164
+
165
+ toAttributes(): Attributes {
166
+ return {
167
+ 'queue.messages_count': this.total,
168
+ 'queue.messages_success': this.succeeded,
169
+ 'queue.messages_failed': this.failed,
170
+ 'queue.batch_success': this.succeeded === this.total,
171
+ 'queue.implicitly_acked': this.implicitly_acked,
172
+ 'queue.implicitly_retried': this.implicitly_retried,
173
+ };
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Add event to active span
179
+ */
180
+ function addQueueEvent(name: string, msg?: Message, delaySeconds?: number) {
181
+ const attrs: Attributes = {};
182
+ if (msg) {
183
+ attrs['queue.message_id'] = msg.id;
184
+ attrs['queue.message_timestamp'] = msg.timestamp.toISOString();
185
+ // Add attempts if available (from Cloudflare Queues API)
186
+ if ('attempts' in msg && typeof msg.attempts === 'number') {
187
+ attrs['queue.message_attempts'] = msg.attempts;
188
+ }
189
+ }
190
+ if (delaySeconds !== undefined) {
191
+ attrs['queue.retry_delay_seconds'] = delaySeconds;
192
+ }
193
+ trace.getActiveSpan()?.addEvent(name, attrs);
194
+ }
195
+
196
+ /**
197
+ * Proxy a queue message to track ack/retry operations
198
+ */
199
+ function proxyQueueMessage<Q>(msg: Message<Q>, count: MessageStatusCount): Message<Q> {
200
+ const msgHandler: ProxyHandler<Message<Q>> = {
201
+ get: (target, prop) => {
202
+ if (prop === 'ack') {
203
+ const ackFn = Reflect.get(target, prop);
204
+ return new Proxy(ackFn, {
205
+ apply: (fnTarget) => {
206
+ addQueueEvent('messageAck', msg);
207
+ count.ack();
208
+ Reflect.apply(fnTarget, msg, []);
209
+ },
210
+ });
211
+ } else if (prop === 'retry') {
212
+ const retryFn = Reflect.get(target, prop);
213
+ return new Proxy(retryFn, {
214
+ apply: (fnTarget, _thisArg, args) => {
215
+ // Extract delay and content type from retry options if provided
216
+ const retryOptions = args[0] as
217
+ | { delaySeconds?: number; contentType?: string }
218
+ | undefined;
219
+ const delaySeconds = retryOptions?.delaySeconds;
220
+
221
+ addQueueEvent('messageRetry', msg, delaySeconds);
222
+
223
+ // Add content type attribute if provided
224
+ if (retryOptions?.contentType) {
225
+ const span = trace.getActiveSpan();
226
+ if (span) {
227
+ span.setAttribute('queue.message.content_type', retryOptions.contentType);
228
+ }
229
+ }
230
+
231
+ count.retry();
232
+ const result = Reflect.apply(fnTarget, msg, args);
233
+ return result;
234
+ },
235
+ });
236
+ } else {
237
+ return Reflect.get(target, prop, msg);
238
+ }
239
+ },
240
+ };
241
+ return wrap(msg, msgHandler);
242
+ }
243
+
244
+ /**
245
+ * Proxy MessageBatch to track ackAll/retryAll operations
246
+ */
247
+ function proxyMessageBatch(batch: MessageBatch, count: MessageStatusCount): MessageBatch {
248
+ const batchHandler: ProxyHandler<MessageBatch> = {
249
+ get: (target, prop) => {
250
+ if (prop === 'messages') {
251
+ const messages = Reflect.get(target, prop);
252
+ const messagesHandler: ProxyHandler<MessageBatch['messages']> = {
253
+ get: (target, prop) => {
254
+ if (typeof prop === 'string' && !isNaN(parseInt(prop))) {
255
+ const message = Reflect.get(target, prop);
256
+ return proxyQueueMessage(message, count);
257
+ } else {
258
+ return Reflect.get(target, prop);
259
+ }
260
+ },
261
+ };
262
+ return wrap(messages, messagesHandler);
263
+ } else if (prop === 'ackAll') {
264
+ const ackFn = Reflect.get(target, prop);
265
+ return new Proxy(ackFn, {
266
+ apply: (fnTarget) => {
267
+ addQueueEvent('ackAll');
268
+ count.ackRemaining();
269
+ Reflect.apply(fnTarget, batch, []);
270
+ },
271
+ });
272
+ } else if (prop === 'retryAll') {
273
+ const retryFn = Reflect.get(target, prop);
274
+ return new Proxy(retryFn, {
275
+ apply: (fnTarget, _thisArg, args) => {
276
+ // Extract delay from retryAll options if provided
277
+ const retryOptions = args[0] as { delaySeconds?: number } | undefined;
278
+ const delaySeconds = retryOptions?.delaySeconds;
279
+
280
+ addQueueEvent('retryAll', undefined, delaySeconds);
281
+ count.retryRemaining();
282
+ Reflect.apply(fnTarget, batch, args);
283
+ },
284
+ });
285
+ }
286
+ return Reflect.get(target, prop);
287
+ },
288
+ };
289
+ return wrap(batch, batchHandler);
290
+ }
291
+
292
+ /**
293
+ * Queue handler instrumentation with message tracking
294
+ */
295
+ class QueueInstrumentation implements HandlerInstrumentation<MessageBatch, void> {
296
+ private count?: MessageStatusCount;
297
+
298
+ getInitialSpanInfo(batch: MessageBatch): InitialSpanInfo {
299
+ return {
300
+ name: `queueHandler ${batch.queue || 'unknown'}`,
301
+ options: {
302
+ kind: SpanKind.CONSUMER,
303
+ attributes: {
304
+ 'faas.trigger': 'pubsub',
305
+ 'queue.name': batch.queue || 'unknown',
306
+ },
307
+ },
308
+ };
309
+ }
310
+
311
+ instrumentTrigger(batch: MessageBatch): MessageBatch {
312
+ this.count = new MessageStatusCount(batch.messages.length);
313
+ return proxyMessageBatch(batch, this.count);
314
+ }
315
+
316
+ executionSucces(span: Span, _trigger: MessageBatch, _result: void) {
317
+ if (this.count) {
318
+ this.count.ackRemaining();
319
+ span.setAttributes(this.count.toAttributes());
320
+ }
321
+ }
322
+
323
+ executionFailed(span: Span, _trigger: MessageBatch, _error?: any) {
324
+ if (this.count) {
325
+ this.count.retryRemaining();
326
+ span.setAttributes(this.count.toAttributes());
327
+ }
328
+ }
329
+ }
330
+
331
+ /**
332
+ * Converts email headers into OpenTelemetry attributes
333
+ */
334
+ function headerAttributes(message: { headers: Headers }): Record<string, string> {
335
+ const attrs: Record<string, string> = {};
336
+ if (message.headers instanceof Headers) {
337
+ for (const [key, value] of message.headers.entries()) {
338
+ attrs[`email.header.${key}`] = value;
339
+ }
340
+ }
341
+ return attrs;
342
+ }
343
+
344
+ /**
345
+ * Email handler instrumentation
346
+ */
347
+ const emailInstrumentation: HandlerInstrumentation<ForwardableEmailMessage, void> = {
348
+ getInitialSpanInfo: (message: ForwardableEmailMessage): InitialSpanInfo => {
349
+ const attributes: Record<string, string> = {
350
+ 'faas.trigger': 'other',
351
+ 'messaging.destination.name': message.to || 'unknown',
352
+ };
353
+
354
+ // Add message ID if available
355
+ if ('headers' in message && message.headers instanceof Headers) {
356
+ const messageId = message.headers.get('Message-Id');
357
+ if (messageId) {
358
+ attributes['rpc.message.id'] = messageId;
359
+ }
360
+ // Add all headers as attributes
361
+ Object.assign(attributes, headerAttributes(message));
362
+ }
363
+
364
+ return {
365
+ name: `emailHandler ${message.to || 'unknown'}`,
366
+ options: {
367
+ kind: SpanKind.CONSUMER,
368
+ attributes,
369
+ },
370
+ };
371
+ },
372
+ };
373
+
374
+
375
+ /**
376
+ * Export spans after request completes
377
+ */
378
+ async function exportSpans(traceId: string, tracker?: PromiseTracker) {
379
+ const tracer = trace.getTracer('autotel-edge');
380
+ if (tracer instanceof WorkerTracer) {
381
+ try {
382
+ await scheduler.wait(1);
383
+ await tracker?.wait();
384
+ await tracer.forceFlush(traceId);
385
+ } catch (error) {
386
+ // Silently handle exporter errors to prevent worker crashes
387
+ // Exporter failures should not affect the worker's ability to process requests
388
+ // In production, consider logging to a monitoring service
389
+ console.error('[autotel-edge] Failed to export spans:', error);
390
+ }
391
+ }
392
+ }
393
+
394
+ /**
395
+ * Create handler flow with instrumentation
396
+ */
397
+ function createHandlerFlow<T extends Trigger, E, R>(
398
+ instrumentation: HandlerInstrumentation<T, R>,
399
+ ) {
400
+ return (
401
+ handlerFn: (trigger: T, env: E, ctx: ExecutionContext) => R | Promise<R>,
402
+ [trigger, env, context]: [T, E, ExecutionContext],
403
+ ) => {
404
+ const { ctx: proxiedCtx, tracker } = proxyExecutionContext(context);
405
+
406
+ const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
407
+
408
+ const { name, options, context: spanContext } =
409
+ instrumentation.getInitialSpanInfo(trigger);
410
+
411
+ // Add cold start tracking
412
+ if (options.attributes) {
413
+ options.attributes['faas.coldstart'] = coldStart;
414
+ } else {
415
+ options.attributes = { 'faas.coldstart': coldStart };
416
+ }
417
+ coldStart = false;
418
+
419
+ const parentContext = spanContext || api_context.active();
420
+
421
+ // Instrument trigger if supported (e.g., for queue handler)
422
+ const instrumentedTrigger = instrumentation.instrumentTrigger
423
+ ? instrumentation.instrumentTrigger(trigger)
424
+ : trigger;
425
+
426
+ return tracer.startActiveSpan(name, options, parentContext, async (span) => {
427
+ try {
428
+ const result = await handlerFn(instrumentedTrigger, env, proxiedCtx);
429
+
430
+ if (instrumentation.getAttributesFromResult) {
431
+ const attributes = instrumentation.getAttributesFromResult(result);
432
+ span.setAttributes(attributes);
433
+ }
434
+
435
+ if (instrumentation.executionSucces) {
436
+ instrumentation.executionSucces(span, trigger, result);
437
+ }
438
+
439
+ span.setStatus({ code: SpanStatusCode.OK });
440
+ return result;
441
+ } catch (error) {
442
+ span.recordException(error as Error);
443
+ span.setStatus({
444
+ code: SpanStatusCode.ERROR,
445
+ message: error instanceof Error ? error.message : String(error),
446
+ });
447
+ if (instrumentation.executionFailed) {
448
+ instrumentation.executionFailed(span, trigger, error);
449
+ }
450
+ throw error;
451
+ } finally {
452
+ span.end();
453
+ context.waitUntil(exportSpans(span.spanContext().traceId, tracker));
454
+ }
455
+ });
456
+ };
457
+ }
458
+
459
+ /**
460
+ * Create handler proxy
461
+ */
462
+ function createHandlerProxy<T extends Trigger, E, R>(
463
+ _handler: unknown,
464
+ handlerFn: (trigger: T, env: E, ctx: ExecutionContext) => R | Promise<R>,
465
+ initialiser: Initialiser,
466
+ instrumentation: HandlerInstrumentation<T, R>,
467
+ ): (trigger: T, env: E, ctx: ExecutionContext) => ReturnType<typeof handlerFn> {
468
+ return (trigger: T, env: E, ctx: ExecutionContext) => {
469
+ const config = initialiser(env, trigger);
470
+
471
+ // Check if instrumentation is disabled (useful for local dev)
472
+ if (config.instrumentation.disabled) {
473
+ // Return handler as-is without instrumentation
474
+ return handlerFn(trigger, env, ctx);
475
+ }
476
+
477
+ // Auto-instrument Cloudflare bindings in the environment
478
+ const instrumentedEnv = instrumentBindings(env as Record<string, any>) as E;
479
+
480
+ const configContext = setConfig(config);
481
+
482
+ // Initialize provider on first call
483
+ initProvider(config);
484
+
485
+ const flowFn = createHandlerFlow<T, E, R>(instrumentation);
486
+
487
+ // Execute the handler flow within the config context
488
+ return api_context.with(configContext, () => {
489
+ return flowFn(handlerFn, [trigger, instrumentedEnv, ctx]) as ReturnType<typeof handlerFn>;
490
+ });
491
+ };
492
+ }
493
+
494
+ /**
495
+ * Create handler proxy with dynamic instrumentation (for fetch with postProcess)
496
+ */
497
+ function createHandlerProxyWithConfig<T extends Trigger, E, R>(
498
+ _handler: unknown,
499
+ handlerFn: (trigger: T, env: E, ctx: ExecutionContext) => R | Promise<R>,
500
+ initialiser: Initialiser,
501
+ createInstrumentation: (config: ResolvedEdgeConfig) => HandlerInstrumentation<T, R>,
502
+ ): (trigger: T, env: E, ctx: ExecutionContext) => ReturnType<typeof handlerFn> {
503
+ return (trigger: T, env: E, ctx: ExecutionContext) => {
504
+ const config = initialiser(env, trigger);
505
+
506
+ // Check if instrumentation is disabled (useful for local dev)
507
+ if (config.instrumentation.disabled) {
508
+ // Return handler as-is without instrumentation
509
+ return handlerFn(trigger, env, ctx);
510
+ }
511
+
512
+ // Auto-instrument Cloudflare bindings in the environment
513
+ const instrumentedEnv = instrumentBindings(env as Record<string, any>) as E;
514
+
515
+ const configContext = setConfig(config);
516
+
517
+ // Initialize provider on first call
518
+ initProvider(config);
519
+
520
+ // Create instrumentation with config
521
+ const instrumentation = createInstrumentation(config);
522
+ const flowFn = createHandlerFlow<T, E, R>(instrumentation);
523
+
524
+ // Execute the handler flow within the config context
525
+ return api_context.with(configContext, () => {
526
+ return flowFn(handlerFn, [trigger, instrumentedEnv, ctx]) as ReturnType<typeof handlerFn>;
527
+ });
528
+ };
529
+ }
530
+
531
+ let providerInitialized = false;
532
+ let coldStart = true;
533
+
534
+ /**
535
+ * Initialize the tracer provider
536
+ */
537
+ function initProvider(config: ResolvedEdgeConfig): void {
538
+ if (providerInitialized) return;
539
+
540
+ // Install global instrumentations
541
+ if (config.instrumentation.instrumentGlobalFetch) {
542
+ instrumentGlobalFetch();
543
+ }
544
+ if (config.instrumentation.instrumentGlobalCache) {
545
+ instrumentGlobalCache();
546
+ }
547
+
548
+ // Set up propagator
549
+ propagation.setGlobalPropagator(config.propagator);
550
+
551
+ // Create resource
552
+ const resource = resourceFromAttributes({
553
+ 'service.name': config.service.name,
554
+ 'service.version': config.service.version,
555
+ 'service.namespace': config.service.namespace,
556
+ 'cloud.provider': 'cloudflare',
557
+ 'cloud.platform': 'cloudflare.workers',
558
+ 'telemetry.sdk.name': 'autotel-edge',
559
+ 'telemetry.sdk.language': 'js',
560
+ });
561
+
562
+ // Create and register provider
563
+ const provider = new WorkerTracerProvider(config.spanProcessors, resource);
564
+ provider.register();
565
+
566
+ // Set head sampler on tracer
567
+ const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
568
+ tracer.setHeadSampler(config.sampling.headSampler);
569
+
570
+ providerInitialized = true;
571
+ }
572
+
573
+ /**
574
+ * Instrument a Cloudflare Workers handler
575
+ *
576
+ * @example
577
+ * ```typescript
578
+ * import { instrument } from 'autotel-edge'
579
+ *
580
+ * const handler = {
581
+ * async fetch(request, env, ctx) {
582
+ * return new Response('Hello World')
583
+ * }
584
+ * }
585
+ *
586
+ * export default instrument(handler, {
587
+ * exporter: {
588
+ * url: env.OTLP_ENDPOINT,
589
+ * headers: { 'x-api-key': env.API_KEY }
590
+ * },
591
+ * service: { name: 'my-worker' }
592
+ * })
593
+ * ```
594
+ */
595
+ export function instrument<E, Q = any, C = any>(
596
+ handler: ExportedHandler<E, Q, C>,
597
+ config: ConfigurationOption,
598
+ ): ExportedHandler<E, Q, C> {
599
+ const initialiser = createInitialiser(config);
600
+
601
+ if (handler.fetch) {
602
+ const fetcher = unwrap(handler.fetch) as FetchHandler;
603
+ // Create fetch instrumentation with config support
604
+ handler.fetch = createHandlerProxyWithConfig(
605
+ handler,
606
+ fetcher,
607
+ initialiser,
608
+ createFetchInstrumentation,
609
+ );
610
+ }
611
+
612
+ if (handler.scheduled) {
613
+ const scheduled = unwrap(handler.scheduled) as ScheduledHandler;
614
+ handler.scheduled = createHandlerProxy(
615
+ handler,
616
+ scheduled,
617
+ initialiser,
618
+ scheduledInstrumentation,
619
+ );
620
+ }
621
+
622
+ if (handler.queue) {
623
+ const queue = unwrap(handler.queue) as QueueHandler;
624
+ handler.queue = createHandlerProxy(
625
+ handler,
626
+ queue,
627
+ initialiser,
628
+ new QueueInstrumentation(),
629
+ );
630
+ }
631
+
632
+ if (handler.email) {
633
+ const email = unwrap(handler.email) as EmailHandler;
634
+ handler.email = createHandlerProxy(
635
+ handler,
636
+ email,
637
+ initialiser,
638
+ emailInstrumentation,
639
+ );
640
+ }
641
+
642
+ return handler;
643
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Durable Object wrapper
3
+ *
4
+ * @example
5
+ * ```typescript
6
+ * import { wrapDurableObject } from 'autotel-cloudflare'
7
+ *
8
+ * class Counter implements DurableObject {
9
+ * async fetch(request: Request) {
10
+ * return new Response('count')
11
+ * }
12
+ * }
13
+ *
14
+ * export default wrapDurableObject({ service: { name: 'counter-do' } }, Counter)
15
+ * ```
16
+ */
17
+
18
+ import { instrumentDO } from '../handlers/durable-objects';
19
+ import type { ConfigurationOption } from 'autotel-edge';
20
+
21
+ /**
22
+ * Wrap a Durable Object class with instrumentation
23
+ * Alternative API style inspired by workers-honeycomb-logger
24
+ *
25
+ * @param config Configuration (can be static object or function)
26
+ * @param doClass The Durable Object class to wrap
27
+ * @returns Instrumented Durable Object class
28
+ */
29
+ export function wrapDurableObject<T extends DurableObject>(
30
+ config: ConfigurationOption,
31
+ doClass: new (state: DurableObjectState, env: any) => T,
32
+ ): new (state: DurableObjectState, env: any) => T {
33
+ return instrumentDO(doClass, config);
34
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * workers-honeycomb-logger style wrapper API
3
+ *
4
+ * @example
5
+ * ```typescript
6
+ * import { wrapModule } from 'autotel-cloudflare'
7
+ *
8
+ * const handler = {
9
+ * async fetch(req, env, ctx) {
10
+ * return new Response('Hello')
11
+ * }
12
+ * }
13
+ *
14
+ * export default wrapModule(
15
+ * { service: { name: 'my-worker' } },
16
+ * handler
17
+ * )
18
+ * ```
19
+ */
20
+
21
+ import { instrument } from './instrument';
22
+ import type { ConfigurationOption } from 'autotel-edge';
23
+
24
+ /**
25
+ * Wrap a Cloudflare Workers module-style handler
26
+ * Alternative API style inspired by workers-honeycomb-logger
27
+ *
28
+ * @param config Configuration (can be static object or function)
29
+ * @param handler The worker handler to wrap
30
+ * @returns Instrumented handler
31
+ */
32
+ export function wrapModule<E, Q = any, C = any>(
33
+ config: ConfigurationOption,
34
+ handler: ExportedHandler<E, Q, C>,
35
+ ): ExportedHandler<E, Q, C> {
36
+ return instrument(handler, config);
37
+ }