@heystack/otel 0.14.0 → 0.16.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/README.md +40 -5
- package/dist/traceparent.d.ts +8 -0
- package/dist/traceparent.js +5 -0
- package/dist/workers-agents.d.ts +1 -1
- package/dist/workers-agents.js +1 -1
- package/dist/workers-bindings.d.ts +14 -1
- package/dist/workers-bindings.js +478 -7
- package/dist/workers.d.ts +91 -20
- package/dist/workers.js +962 -114
- package/package.json +1 -1
package/dist/workers-bindings.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
2
|
// Cloudflare binding instrumentation for @heystack/otel/workers.
|
|
3
3
|
//
|
|
4
|
-
// Wraps
|
|
5
|
-
//
|
|
6
|
-
// as a child of the active request span.
|
|
4
|
+
// Wraps supported Cloudflare data, AI, messaging, Durable Object, Workflow,
|
|
5
|
+
// and Service binding operations with OTel child spans.
|
|
7
6
|
//
|
|
8
|
-
// WinterCG-safe: no
|
|
7
|
+
// WinterCG-safe: no Node-builtin imports. Span factory is injected so the logic
|
|
9
8
|
// is pure and unit-testable without a global provider.
|
|
10
9
|
// ---------------------------------------------------------------------------
|
|
11
10
|
import { context, SpanKind, SpanStatusCode } from "@opentelemetry/api";
|
|
12
11
|
import { isTracingSuppressed } from "@opentelemetry/core";
|
|
12
|
+
import { makeTraceparent } from "./traceparent.js";
|
|
13
13
|
// ---------------------------------------------------------------------------
|
|
14
14
|
// Duck-type detectors — conservative; require the distinctive method set
|
|
15
15
|
// exactly as documented in the task brief.
|
|
@@ -45,6 +45,17 @@ function isQueueLike(b) {
|
|
|
45
45
|
return (typeof b?.send === "function" &&
|
|
46
46
|
typeof b?.sendBatch === "function");
|
|
47
47
|
}
|
|
48
|
+
function isWorkflowLike(b) {
|
|
49
|
+
return (typeof b?.get === "function" &&
|
|
50
|
+
typeof b?.create === "function" &&
|
|
51
|
+
typeof b?.createBatch === "function");
|
|
52
|
+
}
|
|
53
|
+
function isRateLimitLike(b) {
|
|
54
|
+
return typeof b?.limit === "function";
|
|
55
|
+
}
|
|
56
|
+
function isAnalyticsEngineLike(b) {
|
|
57
|
+
return typeof b?.writeDataPoint === "function";
|
|
58
|
+
}
|
|
48
59
|
/** Service binding / Fetcher: last branch — anything with `.fetch`. Prior branches
|
|
49
60
|
* exclude D1/R2/KV/Vectorize/AI/Queue, so order alone is sufficient. */
|
|
50
61
|
function isServiceLike(b) {
|
|
@@ -53,7 +64,12 @@ function isServiceLike(b) {
|
|
|
53
64
|
// ---------------------------------------------------------------------------
|
|
54
65
|
// Span lifecycle helper
|
|
55
66
|
// ---------------------------------------------------------------------------
|
|
56
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Run `fn`, ending `span` in `finally` and recording exceptions + ERROR status
|
|
69
|
+
* on failure (re-throwing). Internal helper shared with the class-level
|
|
70
|
+
* wrappers in workers.ts.
|
|
71
|
+
*/
|
|
72
|
+
export async function runWithSpan(span, fn) {
|
|
57
73
|
try {
|
|
58
74
|
return await fn();
|
|
59
75
|
}
|
|
@@ -69,12 +85,100 @@ async function runWithSpan(span, fn) {
|
|
|
69
85
|
span.end();
|
|
70
86
|
}
|
|
71
87
|
}
|
|
88
|
+
function runSyncWithSpan(span, fn) {
|
|
89
|
+
try {
|
|
90
|
+
return fn();
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
span.recordException(err instanceof Error ? err : new Error(String(err)));
|
|
94
|
+
span.setStatus({
|
|
95
|
+
code: SpanStatusCode.ERROR,
|
|
96
|
+
message: err instanceof Error ? err.message : String(err),
|
|
97
|
+
});
|
|
98
|
+
throw err;
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
span.end();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function isPromiseLike(value) {
|
|
105
|
+
return (((typeof value === "object" && value !== null) || typeof value === "function") &&
|
|
106
|
+
typeof value.then === "function");
|
|
107
|
+
}
|
|
108
|
+
function recordSpanError(span, error) {
|
|
109
|
+
span.recordException(error instanceof Error ? error : new Error(String(error)));
|
|
110
|
+
span.setStatus({
|
|
111
|
+
code: SpanStatusCode.ERROR,
|
|
112
|
+
message: error instanceof Error ? error.message : String(error),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
/** Trace a call without replacing synchronous, Promise, or custom thenable results. */
|
|
116
|
+
function runWithSpanPreservingReturn(span, fn) {
|
|
117
|
+
try {
|
|
118
|
+
const result = fn();
|
|
119
|
+
if (isPromiseLike(result)) {
|
|
120
|
+
let settled = false;
|
|
121
|
+
const finish = (error, failed = false) => {
|
|
122
|
+
if (settled)
|
|
123
|
+
return;
|
|
124
|
+
settled = true;
|
|
125
|
+
if (failed) {
|
|
126
|
+
try {
|
|
127
|
+
recordSpanError(span, error);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// Instrumentation must not alter the observed thenable's behaviour.
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
span.end();
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
// Instrumentation must not alter the observed thenable's behaviour.
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
try {
|
|
141
|
+
const observer = result.then(() => finish(), (error) => finish(error, true));
|
|
142
|
+
// A non-compliant custom thenable may reject the observer despite the
|
|
143
|
+
// rejection callback above. Consume that rejection without replacing
|
|
144
|
+
// or awaiting the original RPC/Promise object returned to the caller.
|
|
145
|
+
if (isPromiseLike(observer)) {
|
|
146
|
+
try {
|
|
147
|
+
observer.then(undefined, () => undefined);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// Calling then() is best-effort for exotic host thenables.
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
finish(error, true);
|
|
156
|
+
}
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
span.end();
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
recordSpanError(span, error);
|
|
164
|
+
span.end();
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/** Transform either a synchronous result or a promise without changing its category. */
|
|
169
|
+
function mapReturn(result, map) {
|
|
170
|
+
return isPromiseLike(result) ? Promise.resolve(result).then(map) : map(result);
|
|
171
|
+
}
|
|
72
172
|
// ---------------------------------------------------------------------------
|
|
73
173
|
// Proxy helper — intercepts listed handlers, falls through to prototype for rest
|
|
74
174
|
// ---------------------------------------------------------------------------
|
|
75
|
-
|
|
175
|
+
const rawTarget = Symbol("heystack.rawBindingTarget");
|
|
176
|
+
/** Internal proxy builder shared with the class-level wrappers in workers.ts. */
|
|
177
|
+
export function makeProxy(target, handlers) {
|
|
76
178
|
return new Proxy(target, {
|
|
77
179
|
get(t, prop, receiver) {
|
|
180
|
+
if (prop === rawTarget)
|
|
181
|
+
return t;
|
|
78
182
|
if (typeof prop === "string" && prop in handlers)
|
|
79
183
|
return handlers[prop];
|
|
80
184
|
const val = Reflect.get(t, prop, receiver);
|
|
@@ -82,6 +186,9 @@ function makeProxy(target, handlers) {
|
|
|
82
186
|
},
|
|
83
187
|
});
|
|
84
188
|
}
|
|
189
|
+
function unwrapTarget(value) {
|
|
190
|
+
return (value?.[rawTarget] ?? value);
|
|
191
|
+
}
|
|
85
192
|
// ---------------------------------------------------------------------------
|
|
86
193
|
// D1 wrappers
|
|
87
194
|
// ---------------------------------------------------------------------------
|
|
@@ -112,6 +219,47 @@ function wrapD1(binding, opts) {
|
|
|
112
219
|
prepare(sql) {
|
|
113
220
|
return wrapD1Statement(binding.prepare(sql), sql, opts);
|
|
114
221
|
},
|
|
222
|
+
batch(statements) {
|
|
223
|
+
if (isTracingSuppressed(context.active())) {
|
|
224
|
+
return binding.batch(statements.map(unwrapTarget));
|
|
225
|
+
}
|
|
226
|
+
const span = opts.startSpan("D1 batch", {
|
|
227
|
+
"db.system": "d1",
|
|
228
|
+
"db.operation.name": "batch",
|
|
229
|
+
"db.operation.batch.size": statements.length,
|
|
230
|
+
});
|
|
231
|
+
return runWithSpan(span, () => binding.batch(statements.map(unwrapTarget)));
|
|
232
|
+
},
|
|
233
|
+
exec(query) {
|
|
234
|
+
if (isTracingSuppressed(context.active()))
|
|
235
|
+
return binding.exec(query);
|
|
236
|
+
const span = opts.startSpan("D1 exec", {
|
|
237
|
+
"db.system": "d1",
|
|
238
|
+
"db.operation.name": "exec",
|
|
239
|
+
});
|
|
240
|
+
return runWithSpan(span, () => binding.exec(query));
|
|
241
|
+
},
|
|
242
|
+
withSession(...args) {
|
|
243
|
+
return wrapD1Session(binding.withSession(...args), opts);
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
function wrapD1Session(session, opts) {
|
|
248
|
+
return makeProxy(session, {
|
|
249
|
+
prepare(sql) {
|
|
250
|
+
return wrapD1Statement(session.prepare(sql), sql, opts);
|
|
251
|
+
},
|
|
252
|
+
batch(statements) {
|
|
253
|
+
if (isTracingSuppressed(context.active())) {
|
|
254
|
+
return session.batch(statements.map(unwrapTarget));
|
|
255
|
+
}
|
|
256
|
+
const span = opts.startSpan("D1 session batch", {
|
|
257
|
+
"db.system": "d1",
|
|
258
|
+
"db.operation.name": "batch",
|
|
259
|
+
"db.operation.batch.size": statements.length,
|
|
260
|
+
});
|
|
261
|
+
return runWithSpan(span, () => session.batch(statements.map(unwrapTarget)));
|
|
262
|
+
},
|
|
115
263
|
});
|
|
116
264
|
}
|
|
117
265
|
// ---------------------------------------------------------------------------
|
|
@@ -131,6 +279,7 @@ function wrapKV(binding, opts, namespace) {
|
|
|
131
279
|
};
|
|
132
280
|
return makeProxy(binding, {
|
|
133
281
|
get: wrapOp("get", (key) => key),
|
|
282
|
+
getWithMetadata: wrapOp("getWithMetadata", (key) => key),
|
|
134
283
|
put: wrapOp("put", (key) => key),
|
|
135
284
|
list: wrapOp("list"),
|
|
136
285
|
delete: wrapOp("delete", (key) => key),
|
|
@@ -151,11 +300,56 @@ function wrapR2(binding, opts, bucket) {
|
|
|
151
300
|
const span = opts.startSpan(`R2 ${op}`, attrs);
|
|
152
301
|
return runWithSpan(span, () => binding[op](...args));
|
|
153
302
|
};
|
|
303
|
+
const wrapMultipart = (upload, key) => makeProxy(upload, {
|
|
304
|
+
uploadPart: async (partNumber, ...args) => {
|
|
305
|
+
if (isTracingSuppressed(context.active())) {
|
|
306
|
+
return upload.uploadPart(partNumber, ...args);
|
|
307
|
+
}
|
|
308
|
+
const span = opts.startSpan("R2 uploadPart", {
|
|
309
|
+
"r2.bucket": bucket,
|
|
310
|
+
"r2.key": key,
|
|
311
|
+
"r2.multipart.part_number": partNumber,
|
|
312
|
+
});
|
|
313
|
+
return runWithSpan(span, () => upload.uploadPart(partNumber, ...args));
|
|
314
|
+
},
|
|
315
|
+
abort: wrapMultipartOp("abort", upload, key),
|
|
316
|
+
complete: wrapMultipartOp("complete", upload, key, (parts) => ({
|
|
317
|
+
"r2.multipart.part_count": parts.length,
|
|
318
|
+
})),
|
|
319
|
+
});
|
|
320
|
+
function wrapMultipartOp(op, upload, key, extraAttrs) {
|
|
321
|
+
return async (...args) => {
|
|
322
|
+
if (isTracingSuppressed(context.active()))
|
|
323
|
+
return upload[op](...args);
|
|
324
|
+
const span = opts.startSpan(`R2 ${op}`, {
|
|
325
|
+
"r2.bucket": bucket,
|
|
326
|
+
"r2.key": key,
|
|
327
|
+
...extraAttrs?.(...args),
|
|
328
|
+
});
|
|
329
|
+
return runWithSpan(span, () => upload[op](...args));
|
|
330
|
+
};
|
|
331
|
+
}
|
|
154
332
|
return makeProxy(binding, {
|
|
155
333
|
get: wrapOp("get", (key) => key),
|
|
156
334
|
put: wrapOp("put", (key) => key),
|
|
157
335
|
head: wrapOp("head", (key) => key),
|
|
158
336
|
delete: wrapOp("delete", (key) => key),
|
|
337
|
+
list: wrapOp("list"),
|
|
338
|
+
async createMultipartUpload(key, ...args) {
|
|
339
|
+
if (isTracingSuppressed(context.active())) {
|
|
340
|
+
const upload = await binding.createMultipartUpload(key, ...args);
|
|
341
|
+
return wrapMultipart(upload, key);
|
|
342
|
+
}
|
|
343
|
+
const span = opts.startSpan("R2 createMultipartUpload", {
|
|
344
|
+
"r2.bucket": bucket,
|
|
345
|
+
"r2.key": key,
|
|
346
|
+
});
|
|
347
|
+
const upload = await runWithSpan(span, () => binding.createMultipartUpload(key, ...args));
|
|
348
|
+
return wrapMultipart(upload, key);
|
|
349
|
+
},
|
|
350
|
+
resumeMultipartUpload(key, uploadId) {
|
|
351
|
+
return wrapMultipart(binding.resumeMultipartUpload(key, uploadId), key);
|
|
352
|
+
},
|
|
159
353
|
});
|
|
160
354
|
}
|
|
161
355
|
// ---------------------------------------------------------------------------
|
|
@@ -261,6 +455,66 @@ function wrapQueue(binding, opts, name) {
|
|
|
261
455
|
const span = opts.startSpan("Queue sendBatch", batchAttrs, SpanKind.PRODUCER);
|
|
262
456
|
return runWithSpan(span, () => binding.sendBatch(...args));
|
|
263
457
|
},
|
|
458
|
+
metrics: async (...args) => {
|
|
459
|
+
if (isTracingSuppressed(context.active()))
|
|
460
|
+
return binding.metrics(...args);
|
|
461
|
+
const span = opts.startSpan("Queue metrics", {
|
|
462
|
+
...baseAttrs,
|
|
463
|
+
"messaging.operation": "metrics",
|
|
464
|
+
});
|
|
465
|
+
return runWithSpan(span, () => binding.metrics(...args));
|
|
466
|
+
},
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
// ---------------------------------------------------------------------------
|
|
470
|
+
// Workflow, rate-limit, and Analytics Engine bindings
|
|
471
|
+
// ---------------------------------------------------------------------------
|
|
472
|
+
function wrapWorkflow(binding, opts, name) {
|
|
473
|
+
const wrapOp = (op, kind, getCount) => async (...args) => {
|
|
474
|
+
if (isTracingSuppressed(context.active()))
|
|
475
|
+
return binding[op](...args);
|
|
476
|
+
const attrs = {
|
|
477
|
+
"cloudflare.binding.type": "workflow",
|
|
478
|
+
"cloudflare.binding.name": name,
|
|
479
|
+
"cloudflare.workflow.operation": op,
|
|
480
|
+
};
|
|
481
|
+
const count = getCount?.(...args);
|
|
482
|
+
if (count !== undefined)
|
|
483
|
+
attrs["cloudflare.workflow.batch.size"] = count;
|
|
484
|
+
const span = opts.startSpan(`Workflow ${op}`, attrs, kind);
|
|
485
|
+
return runWithSpan(span, () => binding[op](...args));
|
|
486
|
+
};
|
|
487
|
+
return makeProxy(binding, {
|
|
488
|
+
get: wrapOp("get", SpanKind.CLIENT),
|
|
489
|
+
create: wrapOp("create", SpanKind.PRODUCER),
|
|
490
|
+
createBatch: wrapOp("createBatch", SpanKind.PRODUCER, (batch) => batch.length),
|
|
491
|
+
deleteBatch: wrapOp("deleteBatch", SpanKind.CLIENT, (ids) => ids.length),
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
function wrapRateLimit(binding, opts, name) {
|
|
495
|
+
return makeProxy(binding, {
|
|
496
|
+
limit: async (...args) => {
|
|
497
|
+
if (isTracingSuppressed(context.active()))
|
|
498
|
+
return binding.limit(...args);
|
|
499
|
+
const span = opts.startSpan("RateLimit limit", {
|
|
500
|
+
"cloudflare.binding.type": "rate_limit",
|
|
501
|
+
"cloudflare.binding.name": name,
|
|
502
|
+
});
|
|
503
|
+
return runWithSpan(span, () => binding.limit(...args));
|
|
504
|
+
},
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
function wrapAnalyticsEngine(binding, opts, name) {
|
|
508
|
+
return makeProxy(binding, {
|
|
509
|
+
writeDataPoint(...args) {
|
|
510
|
+
if (isTracingSuppressed(context.active()))
|
|
511
|
+
return binding.writeDataPoint(...args);
|
|
512
|
+
const span = opts.startSpan("AnalyticsEngine writeDataPoint", {
|
|
513
|
+
"cloudflare.binding.type": "analytics_engine",
|
|
514
|
+
"cloudflare.binding.name": name,
|
|
515
|
+
});
|
|
516
|
+
return runSyncWithSpan(span, () => binding.writeDataPoint(...args));
|
|
517
|
+
},
|
|
264
518
|
});
|
|
265
519
|
}
|
|
266
520
|
// ---------------------------------------------------------------------------
|
|
@@ -276,6 +530,14 @@ function wrapQueue(binding, opts, name) {
|
|
|
276
530
|
export function injectTraceparentArgs(args, traceparent) {
|
|
277
531
|
const [input, ...rest] = args;
|
|
278
532
|
if (input instanceof Request) {
|
|
533
|
+
const originalInit = rest[0];
|
|
534
|
+
if (originalInit?.headers !== undefined) {
|
|
535
|
+
const init = { ...originalInit };
|
|
536
|
+
const headers = new Headers(originalInit.headers);
|
|
537
|
+
headers.set("traceparent", traceparent);
|
|
538
|
+
init.headers = headers;
|
|
539
|
+
return [input, init, ...rest.slice(1)];
|
|
540
|
+
}
|
|
279
541
|
const headers = new Headers(input.headers);
|
|
280
542
|
headers.set("traceparent", traceparent);
|
|
281
543
|
return [new Request(input, { headers }), ...rest];
|
|
@@ -295,13 +557,202 @@ function wrapService(binding, opts, name) {
|
|
|
295
557
|
}
|
|
296
558
|
const span = opts.startSpan(`Service ${name} fetch`, { "peer.service": name }, SpanKind.CLIENT);
|
|
297
559
|
const sc = span.spanContext();
|
|
298
|
-
const traceparent =
|
|
560
|
+
const traceparent = makeTraceparent(sc);
|
|
299
561
|
const injectedArgs = injectTraceparentArgs(args, traceparent);
|
|
300
562
|
return runWithSpan(span, () => binding.fetch(...injectedArgs));
|
|
301
563
|
},
|
|
302
564
|
});
|
|
303
565
|
}
|
|
304
566
|
// ---------------------------------------------------------------------------
|
|
567
|
+
// Durable Object namespace binding
|
|
568
|
+
// ---------------------------------------------------------------------------
|
|
569
|
+
/** DO namespace detector using the real synchronous Cloudflare API surface. */
|
|
570
|
+
function isDurableObjectNamespaceLike(b) {
|
|
571
|
+
return (typeof b?.idFromName === "function" &&
|
|
572
|
+
typeof b?.get === "function" &&
|
|
573
|
+
typeof b?.newUniqueId === "function");
|
|
574
|
+
}
|
|
575
|
+
/** Fetcher/DO members that are platform operations or must retain host semantics. */
|
|
576
|
+
const DO_STUB_RPC_EXCLUDED = new Set([
|
|
577
|
+
"fetch",
|
|
578
|
+
"connect",
|
|
579
|
+
"alarm",
|
|
580
|
+
"webSocketMessage",
|
|
581
|
+
"webSocketClose",
|
|
582
|
+
"webSocketError",
|
|
583
|
+
"id",
|
|
584
|
+
"name",
|
|
585
|
+
"dup",
|
|
586
|
+
"constructor",
|
|
587
|
+
"then",
|
|
588
|
+
]);
|
|
589
|
+
/** Wrap a DO stub: CLIENT spans on fetch (+ traceparent injection) and RPC calls. */
|
|
590
|
+
function wrapDurableObjectStub(stub, opts, name) {
|
|
591
|
+
const fetch = (...args) => {
|
|
592
|
+
if (isTracingSuppressed(context.active()))
|
|
593
|
+
return stub.fetch(...args);
|
|
594
|
+
const span = opts.startSpan(`DO ${name} fetch`, { "peer.service": name }, SpanKind.CLIENT);
|
|
595
|
+
const sc = span.spanContext();
|
|
596
|
+
// Propagate into the DO so its own spans join this trace.
|
|
597
|
+
const injectedArgs = injectTraceparentArgs(args, makeTraceparent(sc));
|
|
598
|
+
return runWithSpanPreservingReturn(span, () => stub.fetch(...injectedArgs));
|
|
599
|
+
};
|
|
600
|
+
// workerd exposes RPC methods through a wildcard property hook, so method
|
|
601
|
+
// names are not enumerable. Resolve and wrap them lazily on first access.
|
|
602
|
+
const rpcMethods = new Map();
|
|
603
|
+
return new Proxy(stub, {
|
|
604
|
+
get(target, property) {
|
|
605
|
+
if (property === "fetch")
|
|
606
|
+
return fetch;
|
|
607
|
+
const value = Reflect.get(target, property, target);
|
|
608
|
+
if (typeof property !== "string" ||
|
|
609
|
+
typeof value !== "function" ||
|
|
610
|
+
DO_STUB_RPC_EXCLUDED.has(property) ||
|
|
611
|
+
property in Object.prototype) {
|
|
612
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
613
|
+
}
|
|
614
|
+
const cached = rpcMethods.get(property);
|
|
615
|
+
if (cached)
|
|
616
|
+
return cached;
|
|
617
|
+
// Capture the host callable returned for this property. Calling it with
|
|
618
|
+
// the original stub as receiver preserves both ordinary and workerd RPC
|
|
619
|
+
// method semantics; the wrapper itself remains stable across reads.
|
|
620
|
+
const method = value;
|
|
621
|
+
const wrapped = (...args) => {
|
|
622
|
+
if (isTracingSuppressed(context.active())) {
|
|
623
|
+
return Reflect.apply(method, target, args);
|
|
624
|
+
}
|
|
625
|
+
const span = opts.startSpan(`DO ${name} ${property}`, { "peer.service": name, "rpc.method": property }, SpanKind.CLIENT);
|
|
626
|
+
return runWithSpanPreservingReturn(span, () => Reflect.apply(method, target, args));
|
|
627
|
+
};
|
|
628
|
+
rpcMethods.set(property, wrapped);
|
|
629
|
+
return wrapped;
|
|
630
|
+
},
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
function wrapDurableObjectNamespace(ns, opts, name) {
|
|
634
|
+
const handlers = {
|
|
635
|
+
// Namespace lookup is local and synchronous. Trace the returned stub's
|
|
636
|
+
// actual fetch/RPC calls, not get() itself.
|
|
637
|
+
get: (...args) => wrapDurableObjectStub(ns.get(...args), opts, name),
|
|
638
|
+
};
|
|
639
|
+
if (typeof ns.getByName === "function") {
|
|
640
|
+
handlers.getByName = (...args) => wrapDurableObjectStub(ns.getByName(...args), opts, name);
|
|
641
|
+
}
|
|
642
|
+
if (typeof ns.jurisdiction === "function") {
|
|
643
|
+
handlers.jurisdiction = (...args) => wrapDurableObjectNamespace(ns.jurisdiction(...args), opts, name);
|
|
644
|
+
}
|
|
645
|
+
// idFromName/idFromString/newUniqueId are synchronous local operations and
|
|
646
|
+
// fall through untouched.
|
|
647
|
+
return makeProxy(ns, handlers);
|
|
648
|
+
}
|
|
649
|
+
// ---------------------------------------------------------------------------
|
|
650
|
+
// Workflows binding wrapper
|
|
651
|
+
// ---------------------------------------------------------------------------
|
|
652
|
+
/** Workflow binding: exposes create/createBatch/get and is not a DO namespace. */
|
|
653
|
+
function isWorkflowBindingLike(b) {
|
|
654
|
+
return (typeof b?.create === "function" &&
|
|
655
|
+
typeof b?.createBatch === "function" &&
|
|
656
|
+
typeof b?.get === "function" &&
|
|
657
|
+
!isDurableObjectNamespaceLike(b));
|
|
658
|
+
}
|
|
659
|
+
const WORKFLOW_INSTANCE_METHODS = [
|
|
660
|
+
"pause",
|
|
661
|
+
"resume",
|
|
662
|
+
"terminate",
|
|
663
|
+
"restart",
|
|
664
|
+
"status",
|
|
665
|
+
"sendEvent",
|
|
666
|
+
];
|
|
667
|
+
function workflowId(value) {
|
|
668
|
+
if (typeof value === "string" && value.length > 0)
|
|
669
|
+
return value;
|
|
670
|
+
if (value && typeof value === "object") {
|
|
671
|
+
const id = value.id;
|
|
672
|
+
if (typeof id === "string" && id.length > 0)
|
|
673
|
+
return id;
|
|
674
|
+
}
|
|
675
|
+
return undefined;
|
|
676
|
+
}
|
|
677
|
+
function workflowBatchIds(value) {
|
|
678
|
+
if (!Array.isArray(value))
|
|
679
|
+
return [];
|
|
680
|
+
return value.map(workflowId).filter((id) => id !== undefined);
|
|
681
|
+
}
|
|
682
|
+
function wrapWorkflowInstance(instance, opts, name) {
|
|
683
|
+
if (!instance || typeof instance !== "object")
|
|
684
|
+
return instance;
|
|
685
|
+
const target = instance;
|
|
686
|
+
const handlers = {};
|
|
687
|
+
const instanceId = workflowId(instance);
|
|
688
|
+
for (const method of WORKFLOW_INSTANCE_METHODS) {
|
|
689
|
+
if (typeof target[method] !== "function")
|
|
690
|
+
continue;
|
|
691
|
+
handlers[method] = (...args) => {
|
|
692
|
+
const underlying = target[method];
|
|
693
|
+
if (isTracingSuppressed(context.active())) {
|
|
694
|
+
return underlying.apply(instance, args);
|
|
695
|
+
}
|
|
696
|
+
const attrs = {
|
|
697
|
+
"cloudflare.workflow.namespace": name,
|
|
698
|
+
};
|
|
699
|
+
if (instanceId !== undefined) {
|
|
700
|
+
attrs["cloudflare.workflow.instance_id"] = instanceId;
|
|
701
|
+
}
|
|
702
|
+
const span = opts.startSpan(`Workflow ${method}`, attrs, SpanKind.CLIENT);
|
|
703
|
+
return runWithSpanPreservingReturn(span, () => underlying.apply(instance, args));
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
return makeProxy(instance, handlers);
|
|
707
|
+
}
|
|
708
|
+
function wrapWorkflowBindingResult(value, op, opts, name, span) {
|
|
709
|
+
if (op === "deleteBatch")
|
|
710
|
+
return value;
|
|
711
|
+
if (op === "createBatch") {
|
|
712
|
+
if (!Array.isArray(value))
|
|
713
|
+
return value;
|
|
714
|
+
const returnedIds = workflowBatchIds(value);
|
|
715
|
+
if (span && returnedIds.length > 0) {
|
|
716
|
+
span.setAttribute("cloudflare.workflow.instance_ids", returnedIds);
|
|
717
|
+
}
|
|
718
|
+
return value.map((item) => wrapWorkflowInstance(item, opts, name));
|
|
719
|
+
}
|
|
720
|
+
const returnedId = workflowId(value);
|
|
721
|
+
if (span && returnedId !== undefined) {
|
|
722
|
+
span.setAttribute("cloudflare.workflow.instance_id", returnedId);
|
|
723
|
+
}
|
|
724
|
+
return wrapWorkflowInstance(value, opts, name);
|
|
725
|
+
}
|
|
726
|
+
function wrapWorkflowBinding(binding, opts, name) {
|
|
727
|
+
const baseAttrs = {
|
|
728
|
+
"cloudflare.workflow.namespace": name,
|
|
729
|
+
"cloudflare.binding.type": "workflow",
|
|
730
|
+
"cloudflare.binding.name": name,
|
|
731
|
+
};
|
|
732
|
+
const ops = ["create", "createBatch", "get", "deleteBatch"];
|
|
733
|
+
const handlers = {};
|
|
734
|
+
for (const op of ops) {
|
|
735
|
+
handlers[op] = (...args) => {
|
|
736
|
+
if (isTracingSuppressed(context.active())) {
|
|
737
|
+
return mapReturn(binding[op](...args), (value) => wrapWorkflowBindingResult(value, op, opts, name));
|
|
738
|
+
}
|
|
739
|
+
const attrs = { ...baseAttrs };
|
|
740
|
+
const instanceId = workflowId(args[0]);
|
|
741
|
+
const batchIds = op === "createBatch" || op === "deleteBatch" ? workflowBatchIds(args[0]) : [];
|
|
742
|
+
if (instanceId !== undefined) {
|
|
743
|
+
attrs["cloudflare.workflow.instance_id"] = instanceId;
|
|
744
|
+
}
|
|
745
|
+
if (batchIds.length > 0) {
|
|
746
|
+
attrs["cloudflare.workflow.instance_ids"] = batchIds;
|
|
747
|
+
attrs["cloudflare.workflow.batch.size"] = batchIds.length;
|
|
748
|
+
}
|
|
749
|
+
const span = opts.startSpan(`Workflow ${op}`, attrs, SpanKind.CLIENT);
|
|
750
|
+
return runWithSpanPreservingReturn(span, () => mapReturn(binding[op](...args), (value) => wrapWorkflowBindingResult(value, op, opts, name, span)));
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
return makeProxy(binding, handlers);
|
|
754
|
+
}
|
|
755
|
+
// ---------------------------------------------------------------------------
|
|
305
756
|
// Main export
|
|
306
757
|
// ---------------------------------------------------------------------------
|
|
307
758
|
/**
|
|
@@ -315,6 +766,11 @@ function wrapService(binding, opts, name) {
|
|
|
315
766
|
* - Vectorize: `query`+`upsert`
|
|
316
767
|
* - Workers AI: `run` (without `prepare`)
|
|
317
768
|
* - Queue producer: `send`+`sendBatch`
|
|
769
|
+
* - Workflow: `get`+`create`+`createBatch`
|
|
770
|
+
* - Rate limit: `limit`
|
|
771
|
+
* - Analytics Engine: `writeDataPoint`
|
|
772
|
+
* - Durable Object namespace: `idFromName`+`get`+`newUniqueId` (stub fetch/RPC spans)
|
|
773
|
+
* - Workflow binding: `create`+`createBatch`+`get` and not a DO namespace
|
|
318
774
|
* - Service binding / Fetcher: `fetch` (last branch; most generic)
|
|
319
775
|
*
|
|
320
776
|
* Unrecognised bindings are passed through unchanged.
|
|
@@ -356,6 +812,21 @@ export function instrumentEnv(env, opts) {
|
|
|
356
812
|
else if (isQueueLike(binding)) {
|
|
357
813
|
result[key] = wrapQueue(binding, opts, key);
|
|
358
814
|
}
|
|
815
|
+
else if (isDurableObjectNamespaceLike(binding)) {
|
|
816
|
+
result[key] = wrapDurableObjectNamespace(binding, opts, key);
|
|
817
|
+
}
|
|
818
|
+
else if (isWorkflowBindingLike(binding)) {
|
|
819
|
+
result[key] = wrapWorkflowBinding(binding, opts, key);
|
|
820
|
+
}
|
|
821
|
+
else if (isWorkflowLike(binding)) {
|
|
822
|
+
result[key] = wrapWorkflow(binding, opts, key);
|
|
823
|
+
}
|
|
824
|
+
else if (isRateLimitLike(binding)) {
|
|
825
|
+
result[key] = wrapRateLimit(binding, opts, key);
|
|
826
|
+
}
|
|
827
|
+
else if (isAnalyticsEngineLike(binding)) {
|
|
828
|
+
result[key] = wrapAnalyticsEngine(binding, opts, key);
|
|
829
|
+
}
|
|
359
830
|
else if (isServiceLike(binding)) {
|
|
360
831
|
result[key] = wrapService(binding, opts, key);
|
|
361
832
|
}
|