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.
- package/LICENSE +21 -0
- package/README.md +432 -0
- package/dist/actors.d.ts +248 -0
- package/dist/actors.js +1030 -0
- package/dist/actors.js.map +1 -0
- package/dist/agents.d.ts +219 -0
- package/dist/agents.js +276 -0
- package/dist/agents.js.map +1 -0
- package/dist/bindings.d.ts +40 -0
- package/dist/bindings.js +4 -0
- package/dist/bindings.js.map +1 -0
- package/dist/chunk-JDPN3HND.js +520 -0
- package/dist/chunk-JDPN3HND.js.map +1 -0
- package/dist/chunk-QXFYTHQF.js +298 -0
- package/dist/chunk-QXFYTHQF.js.map +1 -0
- package/dist/chunk-SKKRPS5K.js +50 -0
- package/dist/chunk-SKKRPS5K.js.map +1 -0
- package/dist/events.d.ts +1 -0
- package/dist/events.js +3 -0
- package/dist/events.js.map +1 -0
- package/dist/handlers.d.ts +121 -0
- package/dist/handlers.js +4 -0
- package/dist/handlers.js.map +1 -0
- package/dist/index.d.ts +144 -0
- package/dist/index.js +576 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +1 -0
- package/dist/logger.js +3 -0
- package/dist/logger.js.map +1 -0
- package/dist/sampling.d.ts +4 -0
- package/dist/sampling.js +3 -0
- package/dist/sampling.js.map +1 -0
- package/dist/testing.d.ts +1 -0
- package/dist/testing.js +3 -0
- package/dist/testing.js.map +1 -0
- package/package.json +107 -0
- package/src/actors/alarms.ts +225 -0
- package/src/actors/index.ts +36 -0
- package/src/actors/instrument-actor.test.ts +179 -0
- package/src/actors/instrument-actor.ts +574 -0
- package/src/actors/sockets.ts +217 -0
- package/src/actors/storage.ts +263 -0
- package/src/actors/traced-handler.ts +300 -0
- package/src/actors/types.ts +98 -0
- package/src/actors.ts +50 -0
- package/src/agents/index.ts +42 -0
- package/src/agents/otel-observability.test.ts +329 -0
- package/src/agents/otel-observability.ts +465 -0
- package/src/agents/types.ts +167 -0
- package/src/agents.ts +76 -0
- package/src/bindings/bindings.ts +621 -0
- package/src/bindings/common.ts +75 -0
- package/src/bindings/index.ts +12 -0
- package/src/bindings.ts +6 -0
- package/src/events.ts +6 -0
- package/src/global/cache.test.ts +292 -0
- package/src/global/cache.ts +164 -0
- package/src/global/fetch.test.ts +344 -0
- package/src/global/fetch.ts +134 -0
- package/src/global/index.ts +7 -0
- package/src/handlers/durable-objects.test.ts +524 -0
- package/src/handlers/durable-objects.ts +250 -0
- package/src/handlers/index.ts +6 -0
- package/src/handlers/workflows.ts +318 -0
- package/src/handlers.ts +6 -0
- package/src/index.ts +57 -0
- package/src/logger.ts +6 -0
- package/src/sampling.ts +6 -0
- package/src/testing.ts +6 -0
- package/src/wrappers/index.ts +8 -0
- package/src/wrappers/instrument.integration.test.ts +468 -0
- package/src/wrappers/instrument.ts +643 -0
- package/src/wrappers/wrap-do.ts +34 -0
- package/src/wrappers/wrap-module.ts +37 -0
|
@@ -0,0 +1,621 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-instrumentation for Cloudflare Workers bindings
|
|
3
|
+
*
|
|
4
|
+
* Note: This file uses Cloudflare Workers types (KVNamespace, R2Bucket, D1Database, Fetcher, 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
|
+
* This module provides automatic tracing for Cloudflare bindings:
|
|
10
|
+
* - KV (key-value operations)
|
|
11
|
+
* - R2 (object storage operations)
|
|
12
|
+
* - D1 (database operations)
|
|
13
|
+
* - Service Bindings
|
|
14
|
+
* - Events Engine
|
|
15
|
+
* - Workers AI
|
|
16
|
+
* - Vectorize
|
|
17
|
+
* - Hyperdrive
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
trace,
|
|
22
|
+
SpanKind,
|
|
23
|
+
SpanStatusCode,
|
|
24
|
+
} from '@opentelemetry/api';
|
|
25
|
+
import { WorkerTracer } from 'autotel-edge';
|
|
26
|
+
import { wrap } from './common';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Instrument KV namespace
|
|
30
|
+
*/
|
|
31
|
+
export function instrumentKV<K extends KVNamespace>(kv: K, namespaceName?: string): K {
|
|
32
|
+
const name = namespaceName || 'kv';
|
|
33
|
+
|
|
34
|
+
const kvHandler: ProxyHandler<K> = {
|
|
35
|
+
get(target, prop) {
|
|
36
|
+
const value = Reflect.get(target, prop);
|
|
37
|
+
|
|
38
|
+
if (prop === 'get' && typeof value === 'function') {
|
|
39
|
+
return new Proxy(value, {
|
|
40
|
+
apply: (fnTarget, thisArg, args) => {
|
|
41
|
+
const [key, options] = args as [string, KVNamespaceGetOptions<unknown> | undefined];
|
|
42
|
+
const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
|
|
43
|
+
|
|
44
|
+
return tracer.startActiveSpan(
|
|
45
|
+
`KV ${name}: get`,
|
|
46
|
+
{
|
|
47
|
+
kind: SpanKind.CLIENT,
|
|
48
|
+
attributes: {
|
|
49
|
+
'db.system': 'cloudflare-kv',
|
|
50
|
+
'db.operation': 'get',
|
|
51
|
+
'db.namespace': name,
|
|
52
|
+
'db.key': key,
|
|
53
|
+
'db.cache_hit': options?.cacheTtl !== undefined,
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
async (span) => {
|
|
57
|
+
try {
|
|
58
|
+
const result = await Reflect.apply(fnTarget, thisArg, args);
|
|
59
|
+
span.setAttribute('db.result.type', result === null ? 'null' : typeof result);
|
|
60
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
61
|
+
return result;
|
|
62
|
+
} catch (error) {
|
|
63
|
+
span.recordException(error as Error);
|
|
64
|
+
span.setStatus({
|
|
65
|
+
code: SpanStatusCode.ERROR,
|
|
66
|
+
message: error instanceof Error ? error.message : String(error),
|
|
67
|
+
});
|
|
68
|
+
throw error;
|
|
69
|
+
} finally {
|
|
70
|
+
span.end();
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
);
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (prop === 'put' && typeof value === 'function') {
|
|
79
|
+
return new Proxy(value, {
|
|
80
|
+
apply: (fnTarget, thisArg, args) => {
|
|
81
|
+
const [key] = args as [string, unknown, KVNamespacePutOptions | undefined];
|
|
82
|
+
const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
|
|
83
|
+
|
|
84
|
+
return tracer.startActiveSpan(
|
|
85
|
+
`KV ${name}: put`,
|
|
86
|
+
{
|
|
87
|
+
kind: SpanKind.CLIENT,
|
|
88
|
+
attributes: {
|
|
89
|
+
'db.system': 'cloudflare-kv',
|
|
90
|
+
'db.operation': 'put',
|
|
91
|
+
'db.namespace': name,
|
|
92
|
+
'db.key': key,
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
async (span) => {
|
|
96
|
+
try {
|
|
97
|
+
const result = await Reflect.apply(fnTarget, thisArg, args);
|
|
98
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
99
|
+
return result;
|
|
100
|
+
} catch (error) {
|
|
101
|
+
span.recordException(error as Error);
|
|
102
|
+
span.setStatus({
|
|
103
|
+
code: SpanStatusCode.ERROR,
|
|
104
|
+
message: error instanceof Error ? error.message : String(error),
|
|
105
|
+
});
|
|
106
|
+
throw error;
|
|
107
|
+
} finally {
|
|
108
|
+
span.end();
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
);
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (prop === 'delete' && typeof value === 'function') {
|
|
117
|
+
return new Proxy(value, {
|
|
118
|
+
apply: (fnTarget, thisArg, args) => {
|
|
119
|
+
const [key] = args as [string];
|
|
120
|
+
const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
|
|
121
|
+
|
|
122
|
+
return tracer.startActiveSpan(
|
|
123
|
+
`KV ${name}: delete`,
|
|
124
|
+
{
|
|
125
|
+
kind: SpanKind.CLIENT,
|
|
126
|
+
attributes: {
|
|
127
|
+
'db.system': 'cloudflare-kv',
|
|
128
|
+
'db.operation': 'delete',
|
|
129
|
+
'db.namespace': name,
|
|
130
|
+
'db.key': key,
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
async (span) => {
|
|
134
|
+
try {
|
|
135
|
+
const result = await Reflect.apply(fnTarget, thisArg, args);
|
|
136
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
137
|
+
return result;
|
|
138
|
+
} catch (error) {
|
|
139
|
+
span.recordException(error as Error);
|
|
140
|
+
span.setStatus({
|
|
141
|
+
code: SpanStatusCode.ERROR,
|
|
142
|
+
message: error instanceof Error ? error.message : String(error),
|
|
143
|
+
});
|
|
144
|
+
throw error;
|
|
145
|
+
} finally {
|
|
146
|
+
span.end();
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
);
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (prop === 'list' && typeof value === 'function') {
|
|
155
|
+
return new Proxy(value, {
|
|
156
|
+
apply: (fnTarget, thisArg, args) => {
|
|
157
|
+
const [options] = args as [KVNamespaceListOptions | undefined];
|
|
158
|
+
const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
|
|
159
|
+
|
|
160
|
+
return tracer.startActiveSpan(
|
|
161
|
+
`KV ${name}: list`,
|
|
162
|
+
{
|
|
163
|
+
kind: SpanKind.CLIENT,
|
|
164
|
+
attributes: {
|
|
165
|
+
'db.system': 'cloudflare-kv',
|
|
166
|
+
'db.operation': 'list',
|
|
167
|
+
'db.namespace': name,
|
|
168
|
+
'db.prefix': options?.prefix || undefined,
|
|
169
|
+
'db.limit': options?.limit || undefined,
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
async (span) => {
|
|
173
|
+
try {
|
|
174
|
+
const result = await Reflect.apply(fnTarget, thisArg, args);
|
|
175
|
+
span.setAttribute('db.result.keys_count', result.keys.length);
|
|
176
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
177
|
+
return result;
|
|
178
|
+
} catch (error) {
|
|
179
|
+
span.recordException(error as Error);
|
|
180
|
+
span.setStatus({
|
|
181
|
+
code: SpanStatusCode.ERROR,
|
|
182
|
+
message: error instanceof Error ? error.message : String(error),
|
|
183
|
+
});
|
|
184
|
+
throw error;
|
|
185
|
+
} finally {
|
|
186
|
+
span.end();
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
);
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return value;
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
return wrap(kv, kvHandler);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Instrument R2 bucket
|
|
203
|
+
*/
|
|
204
|
+
export function instrumentR2<R extends R2Bucket>(r2: R, bucketName?: string): R {
|
|
205
|
+
const name = bucketName || 'r2';
|
|
206
|
+
|
|
207
|
+
const r2Handler: ProxyHandler<R> = {
|
|
208
|
+
get(target, prop) {
|
|
209
|
+
const value = Reflect.get(target, prop);
|
|
210
|
+
|
|
211
|
+
if (prop === 'get' && typeof value === 'function') {
|
|
212
|
+
return new Proxy(value, {
|
|
213
|
+
apply: (fnTarget, thisArg, args) => {
|
|
214
|
+
const [key] = args as [string, R2GetOptions | undefined];
|
|
215
|
+
const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
|
|
216
|
+
|
|
217
|
+
return tracer.startActiveSpan(
|
|
218
|
+
`R2 ${name}: get`,
|
|
219
|
+
{
|
|
220
|
+
kind: SpanKind.CLIENT,
|
|
221
|
+
attributes: {
|
|
222
|
+
'db.system': 'cloudflare-r2',
|
|
223
|
+
'db.operation': 'get',
|
|
224
|
+
'db.bucket': name,
|
|
225
|
+
'db.key': key,
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
async (span) => {
|
|
229
|
+
try {
|
|
230
|
+
const result = await Reflect.apply(fnTarget, thisArg, args);
|
|
231
|
+
if (result) {
|
|
232
|
+
span.setAttribute('db.result.size', result.size);
|
|
233
|
+
span.setAttribute('db.result.etag', result.etag);
|
|
234
|
+
span.setAttribute('db.result.content_type', result.httpMetadata?.contentType);
|
|
235
|
+
} else {
|
|
236
|
+
span.setAttribute('db.result.exists', false);
|
|
237
|
+
}
|
|
238
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
239
|
+
return result;
|
|
240
|
+
} catch (error) {
|
|
241
|
+
span.recordException(error as Error);
|
|
242
|
+
span.setStatus({
|
|
243
|
+
code: SpanStatusCode.ERROR,
|
|
244
|
+
message: error instanceof Error ? error.message : String(error),
|
|
245
|
+
});
|
|
246
|
+
throw error;
|
|
247
|
+
} finally {
|
|
248
|
+
span.end();
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
);
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (prop === 'put' && typeof value === 'function') {
|
|
257
|
+
return new Proxy(value, {
|
|
258
|
+
apply: (fnTarget, thisArg, args) => {
|
|
259
|
+
const [key] = args as [string, ReadableStream | ArrayBuffer | ArrayBufferView | string | null | Blob, R2PutOptions | undefined];
|
|
260
|
+
const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
|
|
261
|
+
|
|
262
|
+
return tracer.startActiveSpan(
|
|
263
|
+
`R2 ${name}: put`,
|
|
264
|
+
{
|
|
265
|
+
kind: SpanKind.CLIENT,
|
|
266
|
+
attributes: {
|
|
267
|
+
'db.system': 'cloudflare-r2',
|
|
268
|
+
'db.operation': 'put',
|
|
269
|
+
'db.bucket': name,
|
|
270
|
+
'db.key': key,
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
async (span) => {
|
|
274
|
+
try {
|
|
275
|
+
const result = await Reflect.apply(fnTarget, thisArg, args);
|
|
276
|
+
span.setAttribute('db.result.etag', result.etag);
|
|
277
|
+
span.setAttribute('db.result.uploaded', result.uploaded);
|
|
278
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
279
|
+
return result;
|
|
280
|
+
} catch (error) {
|
|
281
|
+
span.recordException(error as Error);
|
|
282
|
+
span.setStatus({
|
|
283
|
+
code: SpanStatusCode.ERROR,
|
|
284
|
+
message: error instanceof Error ? error.message : String(error),
|
|
285
|
+
});
|
|
286
|
+
throw error;
|
|
287
|
+
} finally {
|
|
288
|
+
span.end();
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
);
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (prop === 'delete' && typeof value === 'function') {
|
|
297
|
+
return new Proxy(value, {
|
|
298
|
+
apply: (fnTarget, thisArg, args) => {
|
|
299
|
+
const keys = args as string[];
|
|
300
|
+
const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
|
|
301
|
+
|
|
302
|
+
return tracer.startActiveSpan(
|
|
303
|
+
`R2 ${name}: delete`,
|
|
304
|
+
{
|
|
305
|
+
kind: SpanKind.CLIENT,
|
|
306
|
+
attributes: {
|
|
307
|
+
'db.system': 'cloudflare-r2',
|
|
308
|
+
'db.operation': 'delete',
|
|
309
|
+
'db.bucket': name,
|
|
310
|
+
'db.keys_count': keys.length,
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
async (span) => {
|
|
314
|
+
try {
|
|
315
|
+
const result = await Reflect.apply(fnTarget, thisArg, args);
|
|
316
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
317
|
+
return result;
|
|
318
|
+
} catch (error) {
|
|
319
|
+
span.recordException(error as Error);
|
|
320
|
+
span.setStatus({
|
|
321
|
+
code: SpanStatusCode.ERROR,
|
|
322
|
+
message: error instanceof Error ? error.message : String(error),
|
|
323
|
+
});
|
|
324
|
+
throw error;
|
|
325
|
+
} finally {
|
|
326
|
+
span.end();
|
|
327
|
+
}
|
|
328
|
+
},
|
|
329
|
+
);
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (prop === 'list' && typeof value === 'function') {
|
|
335
|
+
return new Proxy(value, {
|
|
336
|
+
apply: (fnTarget, thisArg, args) => {
|
|
337
|
+
const [options] = args as [R2ListOptions | undefined];
|
|
338
|
+
const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
|
|
339
|
+
|
|
340
|
+
return tracer.startActiveSpan(
|
|
341
|
+
`R2 ${name}: list`,
|
|
342
|
+
{
|
|
343
|
+
kind: SpanKind.CLIENT,
|
|
344
|
+
attributes: {
|
|
345
|
+
'db.system': 'cloudflare-r2',
|
|
346
|
+
'db.operation': 'list',
|
|
347
|
+
'db.bucket': name,
|
|
348
|
+
'db.prefix': options?.prefix || undefined,
|
|
349
|
+
'db.limit': options?.limit || undefined,
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
async (span) => {
|
|
353
|
+
try {
|
|
354
|
+
const result = await Reflect.apply(fnTarget, thisArg, args);
|
|
355
|
+
span.setAttribute('db.result.objects_count', result.objects.length);
|
|
356
|
+
span.setAttribute('db.result.truncated', result.truncated);
|
|
357
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
358
|
+
return result;
|
|
359
|
+
} catch (error) {
|
|
360
|
+
span.recordException(error as Error);
|
|
361
|
+
span.setStatus({
|
|
362
|
+
code: SpanStatusCode.ERROR,
|
|
363
|
+
message: error instanceof Error ? error.message : String(error),
|
|
364
|
+
});
|
|
365
|
+
throw error;
|
|
366
|
+
} finally {
|
|
367
|
+
span.end();
|
|
368
|
+
}
|
|
369
|
+
},
|
|
370
|
+
);
|
|
371
|
+
},
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return value;
|
|
376
|
+
},
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
return wrap(r2, r2Handler);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Instrument D1 database
|
|
384
|
+
*/
|
|
385
|
+
export function instrumentD1<D extends D1Database>(d1: D, databaseName?: string): D {
|
|
386
|
+
const name = databaseName || 'd1';
|
|
387
|
+
|
|
388
|
+
const d1Handler: ProxyHandler<D> = {
|
|
389
|
+
get(target, prop) {
|
|
390
|
+
const value = Reflect.get(target, prop);
|
|
391
|
+
|
|
392
|
+
if (prop === 'prepare' && typeof value === 'function') {
|
|
393
|
+
return new Proxy(value, {
|
|
394
|
+
apply: (fnTarget, thisArg, args) => {
|
|
395
|
+
const [query] = args as [string];
|
|
396
|
+
const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
|
|
397
|
+
|
|
398
|
+
const prepared = Reflect.apply(fnTarget, thisArg, args);
|
|
399
|
+
|
|
400
|
+
// Instrument the prepared statement
|
|
401
|
+
const preparedHandler: ProxyHandler<typeof prepared> = {
|
|
402
|
+
get(target, prop) {
|
|
403
|
+
const value = Reflect.get(target, prop);
|
|
404
|
+
|
|
405
|
+
if (prop === 'first' || prop === 'run' || prop === 'all' || prop === 'raw') {
|
|
406
|
+
return new Proxy(value, {
|
|
407
|
+
apply: (fnTarget, thisArg, args) => {
|
|
408
|
+
return tracer.startActiveSpan(
|
|
409
|
+
`D1 ${name}: ${prop}`,
|
|
410
|
+
{
|
|
411
|
+
kind: SpanKind.CLIENT,
|
|
412
|
+
attributes: {
|
|
413
|
+
'db.system': 'cloudflare-d1',
|
|
414
|
+
'db.operation': prop,
|
|
415
|
+
'db.name': name,
|
|
416
|
+
'db.statement': query,
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
async (span) => {
|
|
420
|
+
try {
|
|
421
|
+
const result = await Reflect.apply(fnTarget, thisArg, args);
|
|
422
|
+
if (prop === 'all' && Array.isArray(result)) {
|
|
423
|
+
span.setAttribute('db.result.rows_count', result.length);
|
|
424
|
+
} else if (prop === 'first' && result) {
|
|
425
|
+
span.setAttribute('db.result.exists', true);
|
|
426
|
+
}
|
|
427
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
428
|
+
return result;
|
|
429
|
+
} catch (error) {
|
|
430
|
+
span.recordException(error as Error);
|
|
431
|
+
span.setStatus({
|
|
432
|
+
code: SpanStatusCode.ERROR,
|
|
433
|
+
message: error instanceof Error ? error.message : String(error),
|
|
434
|
+
});
|
|
435
|
+
throw error;
|
|
436
|
+
} finally {
|
|
437
|
+
span.end();
|
|
438
|
+
}
|
|
439
|
+
},
|
|
440
|
+
);
|
|
441
|
+
},
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return value;
|
|
446
|
+
},
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
return wrap(prepared, preparedHandler);
|
|
450
|
+
},
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (prop === 'exec' && typeof value === 'function') {
|
|
455
|
+
return new Proxy(value, {
|
|
456
|
+
apply: (fnTarget, thisArg, args) => {
|
|
457
|
+
const [query] = args as [string];
|
|
458
|
+
const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
|
|
459
|
+
|
|
460
|
+
return tracer.startActiveSpan(
|
|
461
|
+
`D1 ${name}: exec`,
|
|
462
|
+
{
|
|
463
|
+
kind: SpanKind.CLIENT,
|
|
464
|
+
attributes: {
|
|
465
|
+
'db.system': 'cloudflare-d1',
|
|
466
|
+
'db.operation': 'exec',
|
|
467
|
+
'db.name': name,
|
|
468
|
+
'db.statement': query,
|
|
469
|
+
},
|
|
470
|
+
},
|
|
471
|
+
async (span) => {
|
|
472
|
+
try {
|
|
473
|
+
const result = await Reflect.apply(fnTarget, thisArg, args);
|
|
474
|
+
span.setAttribute('db.result.count', result.count);
|
|
475
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
476
|
+
return result;
|
|
477
|
+
} catch (error) {
|
|
478
|
+
span.recordException(error as Error);
|
|
479
|
+
span.setStatus({
|
|
480
|
+
code: SpanStatusCode.ERROR,
|
|
481
|
+
message: error instanceof Error ? error.message : String(error),
|
|
482
|
+
});
|
|
483
|
+
throw error;
|
|
484
|
+
} finally {
|
|
485
|
+
span.end();
|
|
486
|
+
}
|
|
487
|
+
},
|
|
488
|
+
);
|
|
489
|
+
},
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return value;
|
|
494
|
+
},
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
return wrap(d1, d1Handler);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Instrument service binding (Fetcher)
|
|
502
|
+
*/
|
|
503
|
+
export function instrumentServiceBinding<F extends Fetcher>(fetcher: F, serviceName?: string): F {
|
|
504
|
+
const name = serviceName || 'service';
|
|
505
|
+
|
|
506
|
+
const fetcherHandler: ProxyHandler<F> = {
|
|
507
|
+
get(target, prop) {
|
|
508
|
+
const value = Reflect.get(target, prop);
|
|
509
|
+
|
|
510
|
+
if (prop === 'fetch' && typeof value === 'function') {
|
|
511
|
+
return new Proxy(value, {
|
|
512
|
+
apply: (fnTarget, thisArg, args) => {
|
|
513
|
+
const [input, init] = args as [RequestInfo | URL, RequestInit | undefined];
|
|
514
|
+
const request = new Request(input, init);
|
|
515
|
+
const tracer = trace.getTracer('autotel-edge') as WorkerTracer;
|
|
516
|
+
|
|
517
|
+
return tracer.startActiveSpan(
|
|
518
|
+
`Service ${name}: ${request.method}`,
|
|
519
|
+
{
|
|
520
|
+
kind: SpanKind.CLIENT,
|
|
521
|
+
attributes: {
|
|
522
|
+
'rpc.system': 'cloudflare-service-binding',
|
|
523
|
+
'rpc.service': name,
|
|
524
|
+
'http.request.method': request.method,
|
|
525
|
+
'url.full': request.url,
|
|
526
|
+
},
|
|
527
|
+
},
|
|
528
|
+
async (span) => {
|
|
529
|
+
try {
|
|
530
|
+
const response = await Reflect.apply(fnTarget, thisArg, args);
|
|
531
|
+
span.setAttribute('http.response.status_code', response.status);
|
|
532
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
533
|
+
return response;
|
|
534
|
+
} catch (error) {
|
|
535
|
+
span.recordException(error as Error);
|
|
536
|
+
span.setStatus({
|
|
537
|
+
code: SpanStatusCode.ERROR,
|
|
538
|
+
message: error instanceof Error ? error.message : String(error),
|
|
539
|
+
});
|
|
540
|
+
throw error;
|
|
541
|
+
} finally {
|
|
542
|
+
span.end();
|
|
543
|
+
}
|
|
544
|
+
},
|
|
545
|
+
);
|
|
546
|
+
},
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
return value;
|
|
551
|
+
},
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
return wrap(fetcher, fetcherHandler);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Auto-instrument all Cloudflare bindings in the environment
|
|
559
|
+
*/
|
|
560
|
+
export function instrumentBindings(env: Record<string, any>): Record<string, any> {
|
|
561
|
+
const instrumented: Record<string, any> = {};
|
|
562
|
+
|
|
563
|
+
for (const [key, value] of Object.entries(env)) {
|
|
564
|
+
if (!value || typeof value !== 'object') {
|
|
565
|
+
instrumented[key] = value;
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Check for KV namespace
|
|
570
|
+
if ('get' in value && 'put' in value && 'delete' in value && 'list' in value) {
|
|
571
|
+
// Likely KV namespace
|
|
572
|
+
try {
|
|
573
|
+
instrumented[key] = instrumentKV(value as KVNamespace, key);
|
|
574
|
+
continue;
|
|
575
|
+
} catch {
|
|
576
|
+
// Not KV, continue checking
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Check for R2 bucket
|
|
581
|
+
if ('get' in value && 'put' in value && 'delete' in value && 'list' in value && 'head' in value) {
|
|
582
|
+
// Likely R2 bucket
|
|
583
|
+
try {
|
|
584
|
+
instrumented[key] = instrumentR2(value as R2Bucket, key);
|
|
585
|
+
continue;
|
|
586
|
+
} catch {
|
|
587
|
+
// Not R2, continue checking
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Check for D1 database
|
|
592
|
+
if ('prepare' in value && 'exec' in value && typeof value.prepare === 'function') {
|
|
593
|
+
// Likely D1 database
|
|
594
|
+
try {
|
|
595
|
+
instrumented[key] = instrumentD1(value as D1Database, key);
|
|
596
|
+
continue;
|
|
597
|
+
} catch {
|
|
598
|
+
// Not D1, continue checking
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Check for Service Binding (Fetcher)
|
|
603
|
+
if ('fetch' in value && typeof value.fetch === 'function') {
|
|
604
|
+
// Likely service binding
|
|
605
|
+
try {
|
|
606
|
+
instrumented[key] = instrumentServiceBinding(value as Fetcher, key);
|
|
607
|
+
continue;
|
|
608
|
+
} catch {
|
|
609
|
+
// Not a service binding, continue checking
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// For other bindings (Events Engine, Workers AI, Vectorize, Hyperdrive),
|
|
614
|
+
// they don't have standard interfaces we can detect, so we pass them through
|
|
615
|
+
// Users can manually instrument them if needed
|
|
616
|
+
instrumented[key] = value;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return instrumented;
|
|
620
|
+
}
|
|
621
|
+
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Common instrumentation utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Promise tracker for waitUntil
|
|
7
|
+
*/
|
|
8
|
+
export class PromiseTracker {
|
|
9
|
+
private promises: Promise<unknown>[] = [];
|
|
10
|
+
|
|
11
|
+
track(promise: Promise<unknown>): void {
|
|
12
|
+
this.promises.push(promise);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async wait(): Promise<void> {
|
|
16
|
+
await Promise.allSettled(this.promises);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Proxy ExecutionContext to track waitUntil promises
|
|
22
|
+
*/
|
|
23
|
+
export function proxyExecutionContext(ctx: ExecutionContext): {
|
|
24
|
+
ctx: ExecutionContext;
|
|
25
|
+
tracker: PromiseTracker;
|
|
26
|
+
} {
|
|
27
|
+
const tracker = new PromiseTracker();
|
|
28
|
+
|
|
29
|
+
const proxied = new Proxy(ctx, {
|
|
30
|
+
get(target, prop) {
|
|
31
|
+
if (prop === 'waitUntil') {
|
|
32
|
+
return (promise: Promise<unknown>) => {
|
|
33
|
+
tracker.track(promise);
|
|
34
|
+
return target.waitUntil(promise);
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return Reflect.get(target, prop);
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return { ctx: proxied, tracker };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Helper to wrap/unwrap proxied objects
|
|
46
|
+
*/
|
|
47
|
+
const unwrapSymbol = Symbol('unwrap');
|
|
48
|
+
|
|
49
|
+
type Wrapped<T> = { [unwrapSymbol]: T } & T;
|
|
50
|
+
|
|
51
|
+
export function isWrapped<T>(item: T): item is Wrapped<T> {
|
|
52
|
+
return item && !!(item as Wrapped<T>)[unwrapSymbol];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function unwrap<T extends object>(item: T): T {
|
|
56
|
+
if (item && isWrapped(item)) {
|
|
57
|
+
return item[unwrapSymbol];
|
|
58
|
+
} else {
|
|
59
|
+
return item;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function wrap<T extends object>(
|
|
64
|
+
item: T,
|
|
65
|
+
handler: ProxyHandler<T>,
|
|
66
|
+
): Wrapped<T> {
|
|
67
|
+
const proxy = new Proxy(item, handler) as Wrapped<T>;
|
|
68
|
+
Object.defineProperty(proxy, unwrapSymbol, {
|
|
69
|
+
value: item,
|
|
70
|
+
writable: false,
|
|
71
|
+
enumerable: false,
|
|
72
|
+
configurable: false,
|
|
73
|
+
});
|
|
74
|
+
return proxy;
|
|
75
|
+
}
|