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,344 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { instrumentGlobalFetch } from './fetch';
3
+ import { trace, SpanStatusCode, SpanKind, context as api_context } from '@opentelemetry/api';
4
+ import { setConfig, parseConfig } from 'autotel-edge';
5
+
6
+ describe('Global Fetch Instrumentation', () => {
7
+ let mockTracer: any;
8
+ let mockSpan: any;
9
+ let getTracerSpy: any;
10
+ let originalFetch: typeof globalThis.fetch;
11
+
12
+ beforeEach(() => {
13
+ // Save original fetch
14
+ originalFetch = globalThis.fetch;
15
+
16
+ mockSpan = {
17
+ spanContext: () => ({
18
+ traceId: 'test-trace-id',
19
+ spanId: 'test-span-id',
20
+ traceFlags: 1,
21
+ }),
22
+ setAttribute: vi.fn(),
23
+ setAttributes: vi.fn(),
24
+ setStatus: vi.fn(),
25
+ recordException: vi.fn(),
26
+ end: vi.fn(),
27
+ isRecording: () => true,
28
+ updateName: vi.fn(),
29
+ addEvent: vi.fn(),
30
+ };
31
+
32
+ mockTracer = {
33
+ startActiveSpan: vi.fn((name, options, fn) => {
34
+ return fn(mockSpan);
35
+ }),
36
+ };
37
+
38
+ getTracerSpy = vi.spyOn(trace, 'getTracer').mockReturnValue(mockTracer as any);
39
+
40
+ // Mock underlying fetch to return test responses
41
+ globalThis.fetch = vi.fn(async (_input) => {
42
+ return new Response('{"data": "test"}', {
43
+ status: 200,
44
+ headers: { 'content-type': 'application/json' },
45
+ });
46
+ }) as any;
47
+ });
48
+
49
+ afterEach(() => {
50
+ // Restore original fetch
51
+ globalThis.fetch = originalFetch;
52
+ getTracerSpy.mockRestore();
53
+ });
54
+
55
+ describe('instrumentGlobalFetch()', () => {
56
+ it('should wrap globalThis.fetch', () => {
57
+ const originalFetch = globalThis.fetch;
58
+ instrumentGlobalFetch();
59
+
60
+ expect(globalThis.fetch).not.toBe(originalFetch);
61
+ expect(typeof globalThis.fetch).toBe('function');
62
+ });
63
+
64
+ it('should create span for HTTP requests', async () => {
65
+ // Set up config
66
+ const config = parseConfig({
67
+ service: { name: 'test-service' },
68
+ });
69
+ const ctx = setConfig(config);
70
+
71
+ instrumentGlobalFetch();
72
+
73
+ await api_context.with(ctx, async () => {
74
+ await fetch('https://api.example.com/users');
75
+
76
+ expect(mockTracer.startActiveSpan).toHaveBeenCalled();
77
+
78
+ const spanName = mockTracer.startActiveSpan.mock.calls[0][0];
79
+ expect(spanName).toContain('api.example.com');
80
+ expect(spanName).toContain('GET');
81
+ });
82
+ });
83
+
84
+ it('should add HTTP attributes (method, URL, status, headers)', async () => {
85
+ const config = parseConfig({
86
+ service: { name: 'test-service' },
87
+ });
88
+ const ctx = setConfig(config);
89
+
90
+ instrumentGlobalFetch();
91
+
92
+ await api_context.with(ctx, async () => {
93
+ await fetch('https://api.example.com/users', {
94
+ method: 'POST',
95
+ headers: { 'user-agent': 'test-client/1.0' },
96
+ });
97
+
98
+ const options = mockTracer.startActiveSpan.mock.calls[0][1];
99
+ expect(options.kind).toBe(SpanKind.CLIENT);
100
+ expect(options.attributes['http.request.method']).toBe('POST');
101
+ expect(options.attributes['url.full']).toBe('https://api.example.com/users');
102
+ expect(options.attributes['server.address']).toBe('api.example.com');
103
+ expect(options.attributes['url.scheme']).toBe('https');
104
+ });
105
+ });
106
+
107
+ it('should add response attributes', async () => {
108
+ const config = parseConfig({
109
+ service: { name: 'test-service' },
110
+ });
111
+ const ctx = setConfig(config);
112
+
113
+ instrumentGlobalFetch();
114
+
115
+ await api_context.with(ctx, async () => {
116
+ await fetch('https://api.example.com/users');
117
+
118
+ expect(mockSpan.setAttributes).toHaveBeenCalled();
119
+
120
+ // Find the call with response attributes
121
+ const responseAttributesCall = mockSpan.setAttributes.mock.calls.find(
122
+ (call: any) => call[0]['http.response.status_code'] !== undefined
123
+ );
124
+
125
+ expect(responseAttributesCall).toBeDefined();
126
+ expect(responseAttributesCall[0]['http.response.status_code']).toBe(200);
127
+ });
128
+ });
129
+
130
+ it('should inject traceparent header for context propagation by default', async () => {
131
+ const config = parseConfig({
132
+ service: { name: 'test-service' },
133
+ });
134
+ const ctx = setConfig(config);
135
+
136
+ instrumentGlobalFetch();
137
+
138
+ await api_context.with(ctx, async () => {
139
+ await fetch('https://api.example.com/users');
140
+
141
+ // Span should have been created
142
+ expect(mockTracer.startActiveSpan).toHaveBeenCalled();
143
+
144
+ // In a real scenario, traceparent would be injected via propagation.inject()
145
+ // For this test, we just verify the span was created
146
+ expect(mockSpan.end).toHaveBeenCalled();
147
+ });
148
+ });
149
+
150
+ it('should NOT inject traceparent when includeTraceContext = false', async () => {
151
+ const config = parseConfig({
152
+ service: { name: 'test-service' },
153
+ fetch: { includeTraceContext: false },
154
+ });
155
+ setConfig(config);
156
+
157
+ instrumentGlobalFetch();
158
+
159
+ // Create a spy to check if headers.set was called
160
+ const headersSpy = vi.spyOn(Headers.prototype, 'set');
161
+
162
+ await fetch('https://api.example.com/users');
163
+
164
+ // Should not have tried to inject traceparent
165
+ const traceparentCalls = headersSpy.mock.calls.filter(
166
+ (call) => call[0] === 'traceparent'
167
+ );
168
+ expect(traceparentCalls.length).toBe(0);
169
+
170
+ headersSpy.mockRestore();
171
+ });
172
+
173
+ it('should skip non-HTTP requests (file://, data://)', async () => {
174
+ const config = parseConfig({
175
+ service: { name: 'test-service' },
176
+ });
177
+ setConfig(config);
178
+
179
+ instrumentGlobalFetch();
180
+
181
+ // Try to fetch a file:// URL (should skip instrumentation)
182
+ try {
183
+ await fetch('file:///path/to/file.txt');
184
+ } catch (_e) {
185
+ // file:// will fail, that's expected
186
+ }
187
+
188
+ // Should not have created a span
189
+ expect(mockTracer.startActiveSpan).not.toHaveBeenCalled();
190
+ });
191
+
192
+ it('should skip when no active config (not initialized)', async () => {
193
+ // Don't set config
194
+ setConfig(null as any);
195
+
196
+ instrumentGlobalFetch();
197
+
198
+ await fetch('https://api.example.com/users');
199
+
200
+ // Should not have created a span
201
+ expect(mockTracer.startActiveSpan).not.toHaveBeenCalled();
202
+
203
+ // Original fetch should have been called (returning mocked response)
204
+ // We can't easily verify this without more complex mocking,
205
+ // but the key assertion is that no span was created
206
+ });
207
+
208
+ it('should handle successful responses (200-299)', async () => {
209
+ const config = parseConfig({
210
+ service: { name: 'test-service' },
211
+ });
212
+ const ctx = setConfig(config);
213
+
214
+ // Mock successful response
215
+ globalThis.fetch = vi.fn(async () => {
216
+ return new Response('OK', { status: 200 });
217
+ }) as any;
218
+
219
+ instrumentGlobalFetch();
220
+
221
+ await api_context.with(ctx, async () => {
222
+ await fetch('https://api.example.com/users');
223
+
224
+ expect(mockSpan.setStatus).toHaveBeenCalledWith({ code: SpanStatusCode.OK });
225
+ expect(mockSpan.end).toHaveBeenCalled();
226
+ });
227
+ });
228
+
229
+ it('should handle error responses (400-599)', async () => {
230
+ const config = parseConfig({
231
+ service: { name: 'test-service' },
232
+ });
233
+ const ctx = setConfig(config);
234
+
235
+ // Mock error response
236
+ globalThis.fetch = vi.fn(async () => {
237
+ return new Response('Not Found', { status: 404 });
238
+ }) as any;
239
+
240
+ instrumentGlobalFetch();
241
+
242
+ await api_context.with(ctx, async () => {
243
+ await fetch('https://api.example.com/users');
244
+
245
+ expect(mockSpan.setStatus).toHaveBeenCalledWith({ code: SpanStatusCode.ERROR });
246
+ expect(mockSpan.end).toHaveBeenCalled();
247
+ });
248
+ });
249
+
250
+ it('should handle network errors', async () => {
251
+ const config = parseConfig({
252
+ service: { name: 'test-service' },
253
+ });
254
+ const ctx = setConfig(config);
255
+
256
+ // Mock network error
257
+ globalThis.fetch = vi.fn(async () => {
258
+ throw new Error('Network error');
259
+ }) as any;
260
+
261
+ instrumentGlobalFetch();
262
+
263
+ await api_context.with(ctx, async () => {
264
+ await expect(fetch('https://api.example.com/users')).rejects.toThrow('Network error');
265
+
266
+ expect(mockSpan.recordException).toHaveBeenCalled();
267
+ expect(mockSpan.setStatus).toHaveBeenCalledWith({
268
+ code: SpanStatusCode.ERROR,
269
+ message: 'Network error',
270
+ });
271
+ expect(mockSpan.end).toHaveBeenCalled();
272
+ });
273
+ });
274
+
275
+ it('should allow custom includeTraceContext function', async () => {
276
+ const includeTraceContextFn = vi.fn((request: Request) => {
277
+ // Only include for specific domains
278
+ return request.url.includes('internal.example.com');
279
+ });
280
+
281
+ const config = parseConfig({
282
+ service: { name: 'test-service' },
283
+ fetch: { includeTraceContext: includeTraceContextFn },
284
+ });
285
+ const ctx = setConfig(config);
286
+
287
+ instrumentGlobalFetch();
288
+
289
+ await api_context.with(ctx, async () => {
290
+ // Fetch internal domain - should include context
291
+ await fetch('https://internal.example.com/api');
292
+ expect(includeTraceContextFn).toHaveBeenCalledTimes(1);
293
+
294
+ // Fetch external domain - should not include context
295
+ await fetch('https://external.com/api');
296
+ expect(includeTraceContextFn).toHaveBeenCalledTimes(2);
297
+ });
298
+ });
299
+
300
+ it('should handle Request objects as input', async () => {
301
+ const config = parseConfig({
302
+ service: { name: 'test-service' },
303
+ });
304
+ const ctx = setConfig(config);
305
+
306
+ instrumentGlobalFetch();
307
+
308
+ await api_context.with(ctx, async () => {
309
+ const request = new Request('https://api.example.com/users', {
310
+ method: 'POST',
311
+ headers: { 'content-type': 'application/json' },
312
+ body: JSON.stringify({ name: 'test' }),
313
+ });
314
+
315
+ await fetch(request);
316
+
317
+ expect(mockTracer.startActiveSpan).toHaveBeenCalled();
318
+
319
+ const spanName = mockTracer.startActiveSpan.mock.calls[0][0];
320
+ expect(spanName).toContain('POST');
321
+ expect(spanName).toContain('api.example.com');
322
+ });
323
+ });
324
+
325
+ it('should handle URL objects as input', async () => {
326
+ const config = parseConfig({
327
+ service: { name: 'test-service' },
328
+ });
329
+ const ctx = setConfig(config);
330
+
331
+ instrumentGlobalFetch();
332
+
333
+ await api_context.with(ctx, async () => {
334
+ const url = new URL('https://api.example.com/users');
335
+ await fetch(url);
336
+
337
+ expect(mockTracer.startActiveSpan).toHaveBeenCalled();
338
+
339
+ const spanName = mockTracer.startActiveSpan.mock.calls[0][0];
340
+ expect(spanName).toContain('api.example.com');
341
+ });
342
+ });
343
+ });
344
+ });
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Global fetch() instrumentation for autotel-edge
3
+ *
4
+ * Automatically traces all outgoing fetch() calls with:
5
+ * - HTTP method, URL, status code
6
+ * - Request/response headers
7
+ * - Automatic context propagation
8
+ * - Error tracking
9
+ */
10
+
11
+ import {
12
+ trace,
13
+ context as api_context,
14
+ propagation,
15
+ SpanStatusCode,
16
+ SpanKind,
17
+ } from '@opentelemetry/api';
18
+ import { getActiveConfig, WorkerTracer } from 'autotel-edge';
19
+
20
+ /**
21
+ * Gather HTTP request attributes following OpenTelemetry semantic conventions
22
+ */
23
+ function gatherRequestAttributes(request: Request): Record<string, any> {
24
+ const url = new URL(request.url);
25
+
26
+ return {
27
+ 'http.request.method': request.method.toUpperCase(),
28
+ 'url.full': request.url,
29
+ 'url.scheme': url.protocol.replace(':', ''),
30
+ 'server.address': url.host,
31
+ 'url.path': url.pathname,
32
+ 'url.query': url.search,
33
+ 'network.protocol.name': 'http',
34
+ 'user_agent.original': request.headers.get('user-agent') || undefined,
35
+ };
36
+ }
37
+
38
+ /**
39
+ * Gather HTTP response attributes
40
+ */
41
+ function gatherResponseAttributes(response: Response): Record<string, any> {
42
+ return {
43
+ 'http.response.status_code': response.status,
44
+ 'http.response.body.size': response.headers.get('content-length') || undefined,
45
+ };
46
+ }
47
+
48
+ /**
49
+ * Instrument the global fetch function
50
+ *
51
+ * This wraps globalThis.fetch to automatically create spans for all outgoing HTTP requests.
52
+ *
53
+ * **Note:** This is called automatically when the library is initialized with
54
+ * `instrumentation.instrumentGlobalFetch: true` (default).
55
+ */
56
+ export function instrumentGlobalFetch(): void {
57
+ const originalFetch = globalThis.fetch;
58
+
59
+ const instrumentedFetch = function fetch(
60
+ input: RequestInfo | URL,
61
+ init?: RequestInit,
62
+ ): Promise<Response> {
63
+ const request = new Request(input, init);
64
+
65
+ // Skip non-HTTP requests
66
+ if (!request.url.startsWith('http')) {
67
+ return originalFetch(input, init);
68
+ }
69
+
70
+ // Skip if no active config (not initialized yet)
71
+ const config = getActiveConfig();
72
+ if (!config) {
73
+ return originalFetch(input, init);
74
+ }
75
+
76
+ const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
77
+ const url = new URL(request.url);
78
+ const spanName = `${request.method} ${url.host}`;
79
+
80
+ return tracer.startActiveSpan(
81
+ spanName,
82
+ {
83
+ kind: SpanKind.CLIENT,
84
+ attributes: gatherRequestAttributes(request),
85
+ },
86
+ async (span) => {
87
+ try {
88
+ // Inject trace context into request headers for distributed tracing
89
+ const shouldIncludeContext =
90
+ typeof config.fetch?.includeTraceContext === 'function'
91
+ ? config.fetch.includeTraceContext(request)
92
+ : (config.fetch?.includeTraceContext ?? true);
93
+
94
+ if (shouldIncludeContext) {
95
+ propagation.inject(api_context.active(), request.headers, {
96
+ set: (headers, key, value) => {
97
+ if (typeof value === 'string') {
98
+ headers.set(key, value);
99
+ }
100
+ },
101
+ });
102
+ }
103
+
104
+ // Make the actual fetch call
105
+ const response = await originalFetch(request);
106
+
107
+ // Add response attributes
108
+ span.setAttributes(gatherResponseAttributes(response));
109
+
110
+ // Set span status based on response
111
+ if (response.ok) {
112
+ span.setStatus({ code: SpanStatusCode.OK });
113
+ } else {
114
+ span.setStatus({ code: SpanStatusCode.ERROR });
115
+ }
116
+
117
+ return response;
118
+ } catch (error) {
119
+ span.recordException(error as Error);
120
+ span.setStatus({
121
+ code: SpanStatusCode.ERROR,
122
+ message: error instanceof Error ? error.message : String(error),
123
+ });
124
+ throw error;
125
+ } finally {
126
+ span.end();
127
+ }
128
+ },
129
+ );
130
+ };
131
+
132
+ // Replace global fetch
133
+ globalThis.fetch = instrumentedFetch as typeof fetch;
134
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Global instrumentation for Cloudflare Workers
3
+ * Automatically instrument fetch() and cache APIs
4
+ */
5
+
6
+ export { instrumentGlobalFetch } from './fetch';
7
+ export { instrumentGlobalCache } from './cache';