@heystack/otel 0.18.0 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +151 -27
- package/dist/core.d.ts +1 -0
- package/dist/core.js +4 -0
- package/dist/hash.d.ts +19 -0
- package/dist/hash.js +47 -0
- package/dist/next.d.ts +2 -1
- package/dist/next.js +5 -0
- package/dist/node.d.ts +14 -2
- package/dist/node.js +49 -4
- package/dist/redact.d.ts +57 -0
- package/dist/redact.js +190 -0
- package/dist/web.d.ts +42 -0
- package/dist/web.js +91 -24
- package/dist/workers-bindings.d.ts +39 -2
- package/dist/workers-bindings.js +388 -78
- package/dist/workers-coverage.d.ts +11 -0
- package/dist/workers-coverage.js +22 -0
- package/dist/workers-native.d.ts +66 -0
- package/dist/workers-native.js +147 -0
- package/dist/workers-propagation.d.ts +24 -0
- package/dist/workers-propagation.js +72 -0
- package/dist/workers-routes.d.ts +11 -0
- package/dist/workers-routes.js +45 -0
- package/dist/workers-sampler.d.ts +10 -1
- package/dist/workers-sampler.js +7 -3
- package/dist/workers.d.ts +108 -19
- package/dist/workers.js +639 -190
- package/package.json +9 -4
package/dist/workers-bindings.js
CHANGED
|
@@ -7,9 +7,11 @@
|
|
|
7
7
|
// WinterCG-safe: no Node-builtin imports. Span factory is injected so the logic
|
|
8
8
|
// is pure and unit-testable without a global provider.
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
10
|
-
import { context, SpanKind, SpanStatusCode } from "@opentelemetry/api";
|
|
10
|
+
import { context, INVALID_SPAN_CONTEXT, isSpanContextValid, SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
|
|
11
11
|
import { isTracingSuppressed } from "@opentelemetry/core";
|
|
12
|
+
import { scrubKeysAsync } from "./hash.js";
|
|
12
13
|
import { makeTraceparent } from "./traceparent.js";
|
|
14
|
+
import { nativeTracingFromContext, runNativeSpan } from "./workers-native.js";
|
|
13
15
|
// ---------------------------------------------------------------------------
|
|
14
16
|
// Duck-type detectors — conservative; require the distinctive method set
|
|
15
17
|
// exactly as documented in the task brief.
|
|
@@ -61,6 +63,78 @@ function isAnalyticsEngineLike(b) {
|
|
|
61
63
|
function isServiceLike(b) {
|
|
62
64
|
return typeof b?.fetch === "function";
|
|
63
65
|
}
|
|
66
|
+
/** A service binding to a WorkerEntrypoint answers EVERY property name with a callable
|
|
67
|
+
* JS RPC stub, so it would pass every duck-typed check above (`prepare` looks like D1).
|
|
68
|
+
* No real binding has a function under this name: one that does is an RPC stub. */
|
|
69
|
+
const RPC_PROBE = "__heystackRpcProbe__";
|
|
70
|
+
function isRpcServiceLike(b) {
|
|
71
|
+
try {
|
|
72
|
+
return typeof b?.fetch === "function" && typeof b?.[RPC_PROBE] === "function";
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Adapt a callback-scoped `withSpan` to the immediate-return `startSpan` shape
|
|
80
|
+
* the legacy wrappers use. The callback stays open (pending promise) until the
|
|
81
|
+
* wrapper calls `end()`; calls made before the callback has run are buffered.
|
|
82
|
+
*/
|
|
83
|
+
function startSpanFromWithSpan(withSpan) {
|
|
84
|
+
return (name, attrs, kind) => {
|
|
85
|
+
let real;
|
|
86
|
+
const queue = [];
|
|
87
|
+
let release = () => undefined;
|
|
88
|
+
const done = new Promise((resolve) => { release = resolve; });
|
|
89
|
+
let ended = false;
|
|
90
|
+
const apply = (f) => {
|
|
91
|
+
if (real)
|
|
92
|
+
f(real);
|
|
93
|
+
else
|
|
94
|
+
queue.push(f);
|
|
95
|
+
};
|
|
96
|
+
void withSpan(name, attrs, kind, (s) => {
|
|
97
|
+
real = s;
|
|
98
|
+
for (const f of queue.splice(0))
|
|
99
|
+
f(s);
|
|
100
|
+
return done;
|
|
101
|
+
}).catch(() => undefined);
|
|
102
|
+
const deferred = {
|
|
103
|
+
spanContext: () => real?.spanContext() ?? INVALID_SPAN_CONTEXT,
|
|
104
|
+
setAttribute(k, v) { apply((s) => s.setAttribute(k, v)); return deferred; },
|
|
105
|
+
setAttributes(a) { apply((s) => s.setAttributes(a)); return deferred; },
|
|
106
|
+
addEvent(n, a) { apply((s) => s.addEvent(n, a)); return deferred; },
|
|
107
|
+
addLink() { return deferred; },
|
|
108
|
+
addLinks() { return deferred; },
|
|
109
|
+
setStatus(st) { apply((s) => s.setStatus(st)); return deferred; },
|
|
110
|
+
updateName() { return deferred; },
|
|
111
|
+
end() {
|
|
112
|
+
if (ended)
|
|
113
|
+
return;
|
|
114
|
+
ended = true;
|
|
115
|
+
apply((s) => s.end());
|
|
116
|
+
release();
|
|
117
|
+
},
|
|
118
|
+
isRecording: () => real?.isRecording() ?? true,
|
|
119
|
+
recordException(e) { apply((s) => s.recordException(e)); },
|
|
120
|
+
};
|
|
121
|
+
return deferred;
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Run an async operation inside a span: `withSpan` when native, else
|
|
126
|
+
* `startSpan` + {@link runWithSpan}.
|
|
127
|
+
*/
|
|
128
|
+
function traced(opts, name, attrs, kind, fn) {
|
|
129
|
+
if (opts.withSpan)
|
|
130
|
+
return opts.withSpan(name, attrs, kind, fn);
|
|
131
|
+
const span = opts.startSpan(name, attrs, kind);
|
|
132
|
+
return runWithSpan(span, () => fn(span));
|
|
133
|
+
}
|
|
134
|
+
/** Native name when `opts.nativeNames`, else the direct-mode name. */
|
|
135
|
+
function nm(opts, direct, native) {
|
|
136
|
+
return opts.nativeNames ? native : direct;
|
|
137
|
+
}
|
|
64
138
|
// ---------------------------------------------------------------------------
|
|
65
139
|
// Span lifecycle helper
|
|
66
140
|
// ---------------------------------------------------------------------------
|
|
@@ -85,6 +159,68 @@ export async function runWithSpan(span, fn) {
|
|
|
85
159
|
span.end();
|
|
86
160
|
}
|
|
87
161
|
}
|
|
162
|
+
/** Plain string keys from a string / string[] argument (anything else yields none). */
|
|
163
|
+
export function keysOf(value) {
|
|
164
|
+
if (typeof value === "string")
|
|
165
|
+
return [value];
|
|
166
|
+
if (Array.isArray(value))
|
|
167
|
+
return value.filter((v) => typeof v === "string");
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
/** At most this many keys of a multi-key call are hashed and recorded. */
|
|
171
|
+
const MAX_HASHED_KEYS = 10;
|
|
172
|
+
/** Key hashes still in flight; exports drain these first so no span is flushed half-built. */
|
|
173
|
+
const pendingKeyHashes = new Set();
|
|
174
|
+
/** Resolves once every in-flight key hash has attached its attribute and ended its span. */
|
|
175
|
+
export async function settlePendingKeyHashes() {
|
|
176
|
+
while (pendingKeyHashes.size > 0)
|
|
177
|
+
await Promise.allSettled([...pendingKeyHashes]);
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Like {@link runWithSpan} for operations that carry user-controlled keys. The
|
|
181
|
+
* hash is computed concurrently with the call and attached under `attr` before
|
|
182
|
+
* the span ends; it can neither delay the call's result nor change its error.
|
|
183
|
+
* If hashing is impossible the attribute is omitted (never the raw key).
|
|
184
|
+
*/
|
|
185
|
+
export async function runWithSpanKeyed(span, attr, keys, fn, countAttr) {
|
|
186
|
+
// Bounded work per call: hash only the first MAX_HASHED_KEYS; the count is exact.
|
|
187
|
+
if (countAttr && keys.length > 0)
|
|
188
|
+
span.setAttribute(countAttr, keys.length);
|
|
189
|
+
const hashed = keys.length > 0 ? scrubKeysAsync(keys.slice(0, MAX_HASHED_KEYS)) : undefined;
|
|
190
|
+
try {
|
|
191
|
+
return await fn();
|
|
192
|
+
}
|
|
193
|
+
catch (err) {
|
|
194
|
+
recordSpanError(span, err);
|
|
195
|
+
throw err;
|
|
196
|
+
}
|
|
197
|
+
finally {
|
|
198
|
+
const end = () => {
|
|
199
|
+
try {
|
|
200
|
+
span.end();
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
// Telemetry must never surface errors.
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
if (hashed) {
|
|
207
|
+
const done = hashed.then((h) => {
|
|
208
|
+
try {
|
|
209
|
+
if (h !== undefined)
|
|
210
|
+
span.setAttribute(attr, h);
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
// ignore
|
|
214
|
+
}
|
|
215
|
+
end();
|
|
216
|
+
}, end).finally(() => pendingKeyHashes.delete(done));
|
|
217
|
+
pendingKeyHashes.add(done);
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
end();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
88
224
|
function runSyncWithSpan(span, fn) {
|
|
89
225
|
try {
|
|
90
226
|
return fn();
|
|
@@ -113,7 +249,7 @@ function recordSpanError(span, error) {
|
|
|
113
249
|
});
|
|
114
250
|
}
|
|
115
251
|
/** Trace a call without replacing synchronous, Promise, or custom thenable results. */
|
|
116
|
-
function runWithSpanPreservingReturn(span, fn) {
|
|
252
|
+
export function runWithSpanPreservingReturn(span, fn) {
|
|
117
253
|
try {
|
|
118
254
|
const result = fn();
|
|
119
255
|
if (isPromiseLike(result)) {
|
|
@@ -200,6 +336,8 @@ function wrapD1Statement(stmt, sql, opts) {
|
|
|
200
336
|
const span = opts.startSpan(`D1 ${op}`, {
|
|
201
337
|
"db.system": "d1",
|
|
202
338
|
"db.statement": sql,
|
|
339
|
+
"db.system.name": "cloudflare-d1",
|
|
340
|
+
"db.query.text": sql,
|
|
203
341
|
});
|
|
204
342
|
return runWithSpan(span, () => stmt[op](...args));
|
|
205
343
|
};
|
|
@@ -225,6 +363,7 @@ function wrapD1(binding, opts) {
|
|
|
225
363
|
}
|
|
226
364
|
const span = opts.startSpan("D1 batch", {
|
|
227
365
|
"db.system": "d1",
|
|
366
|
+
"db.system.name": "cloudflare-d1",
|
|
228
367
|
"db.operation.name": "batch",
|
|
229
368
|
"db.operation.batch.size": statements.length,
|
|
230
369
|
});
|
|
@@ -235,6 +374,7 @@ function wrapD1(binding, opts) {
|
|
|
235
374
|
return binding.exec(query);
|
|
236
375
|
const span = opts.startSpan("D1 exec", {
|
|
237
376
|
"db.system": "d1",
|
|
377
|
+
"db.system.name": "cloudflare-d1",
|
|
238
378
|
"db.operation.name": "exec",
|
|
239
379
|
});
|
|
240
380
|
return runWithSpan(span, () => binding.exec(query));
|
|
@@ -255,6 +395,7 @@ function wrapD1Session(session, opts) {
|
|
|
255
395
|
}
|
|
256
396
|
const span = opts.startSpan("D1 session batch", {
|
|
257
397
|
"db.system": "d1",
|
|
398
|
+
"db.system.name": "cloudflare-d1",
|
|
258
399
|
"db.operation.name": "batch",
|
|
259
400
|
"db.operation.batch.size": statements.length,
|
|
260
401
|
});
|
|
@@ -266,39 +407,33 @@ function wrapD1Session(session, opts) {
|
|
|
266
407
|
// KV wrappers
|
|
267
408
|
// ---------------------------------------------------------------------------
|
|
268
409
|
function wrapKV(binding, opts, namespace) {
|
|
269
|
-
const wrapOp = (op,
|
|
410
|
+
const wrapOp = (op, keyed = false) => async (...args) => {
|
|
270
411
|
if (isTracingSuppressed(context.active())) {
|
|
271
412
|
return binding[op](...args);
|
|
272
413
|
}
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
if (key !== undefined)
|
|
276
|
-
attrs["kv.key"] = key;
|
|
277
|
-
const span = opts.startSpan(`KV ${op}`, attrs);
|
|
278
|
-
return runWithSpan(span, () => binding[op](...args));
|
|
414
|
+
const span = opts.startSpan(`KV ${op}`, { "kv.namespace": namespace });
|
|
415
|
+
return runWithSpanKeyed(span, "cloudflare.kv.query.keys", keyed ? keysOf(args[0]) : [], () => binding[op](...args), "cloudflare.kv.query.key_count");
|
|
279
416
|
};
|
|
280
417
|
return makeProxy(binding, {
|
|
281
|
-
get: wrapOp("get",
|
|
282
|
-
getWithMetadata: wrapOp("getWithMetadata",
|
|
283
|
-
put: wrapOp("put",
|
|
418
|
+
get: wrapOp("get", true),
|
|
419
|
+
getWithMetadata: wrapOp("getWithMetadata", true),
|
|
420
|
+
put: wrapOp("put", true),
|
|
284
421
|
list: wrapOp("list"),
|
|
285
|
-
delete: wrapOp("delete",
|
|
422
|
+
delete: wrapOp("delete", true),
|
|
286
423
|
});
|
|
287
424
|
}
|
|
288
425
|
// ---------------------------------------------------------------------------
|
|
289
426
|
// R2 wrappers
|
|
290
427
|
// ---------------------------------------------------------------------------
|
|
428
|
+
/** R2 object keys are user data: only the hashed form is ever emitted. */
|
|
429
|
+
const R2_KEY_ATTR = "cloudflare.r2.request.key";
|
|
291
430
|
function wrapR2(binding, opts, bucket) {
|
|
292
|
-
const wrapOp = (op,
|
|
431
|
+
const wrapOp = (op, keyed = false) => async (...args) => {
|
|
293
432
|
if (isTracingSuppressed(context.active())) {
|
|
294
433
|
return binding[op](...args);
|
|
295
434
|
}
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
if (key !== undefined)
|
|
299
|
-
attrs["r2.key"] = key;
|
|
300
|
-
const span = opts.startSpan(`R2 ${op}`, attrs);
|
|
301
|
-
return runWithSpan(span, () => binding[op](...args));
|
|
435
|
+
const span = opts.startSpan(`R2 ${op}`, { "r2.bucket": bucket });
|
|
436
|
+
return runWithSpanKeyed(span, R2_KEY_ATTR, keyed ? keysOf(args[0]) : [], () => binding[op](...args), "cloudflare.r2.request.key_count");
|
|
302
437
|
};
|
|
303
438
|
const wrapMultipart = (upload, key) => makeProxy(upload, {
|
|
304
439
|
uploadPart: async (partNumber, ...args) => {
|
|
@@ -307,10 +442,9 @@ function wrapR2(binding, opts, bucket) {
|
|
|
307
442
|
}
|
|
308
443
|
const span = opts.startSpan("R2 uploadPart", {
|
|
309
444
|
"r2.bucket": bucket,
|
|
310
|
-
"r2.key": key,
|
|
311
445
|
"r2.multipart.part_number": partNumber,
|
|
312
446
|
});
|
|
313
|
-
return
|
|
447
|
+
return runWithSpanKeyed(span, R2_KEY_ATTR, [key], () => upload.uploadPart(partNumber, ...args));
|
|
314
448
|
},
|
|
315
449
|
abort: wrapMultipartOp("abort", upload, key),
|
|
316
450
|
complete: wrapMultipartOp("complete", upload, key, (parts) => ({
|
|
@@ -323,28 +457,24 @@ function wrapR2(binding, opts, bucket) {
|
|
|
323
457
|
return upload[op](...args);
|
|
324
458
|
const span = opts.startSpan(`R2 ${op}`, {
|
|
325
459
|
"r2.bucket": bucket,
|
|
326
|
-
"r2.key": key,
|
|
327
460
|
...extraAttrs?.(...args),
|
|
328
461
|
});
|
|
329
|
-
return
|
|
462
|
+
return runWithSpanKeyed(span, R2_KEY_ATTR, [key], () => upload[op](...args));
|
|
330
463
|
};
|
|
331
464
|
}
|
|
332
465
|
return makeProxy(binding, {
|
|
333
|
-
get: wrapOp("get",
|
|
334
|
-
put: wrapOp("put",
|
|
335
|
-
head: wrapOp("head",
|
|
336
|
-
delete: wrapOp("delete",
|
|
466
|
+
get: wrapOp("get", true),
|
|
467
|
+
put: wrapOp("put", true),
|
|
468
|
+
head: wrapOp("head", true),
|
|
469
|
+
delete: wrapOp("delete", true),
|
|
337
470
|
list: wrapOp("list"),
|
|
338
471
|
async createMultipartUpload(key, ...args) {
|
|
339
472
|
if (isTracingSuppressed(context.active())) {
|
|
340
473
|
const upload = await binding.createMultipartUpload(key, ...args);
|
|
341
474
|
return wrapMultipart(upload, key);
|
|
342
475
|
}
|
|
343
|
-
const span = opts.startSpan("R2 createMultipartUpload", {
|
|
344
|
-
|
|
345
|
-
"r2.key": key,
|
|
346
|
-
});
|
|
347
|
-
const upload = await runWithSpan(span, () => binding.createMultipartUpload(key, ...args));
|
|
476
|
+
const span = opts.startSpan("R2 createMultipartUpload", { "r2.bucket": bucket });
|
|
477
|
+
const upload = await runWithSpanKeyed(span, R2_KEY_ATTR, [key], () => binding.createMultipartUpload(key, ...args));
|
|
348
478
|
return wrapMultipart(upload, key);
|
|
349
479
|
},
|
|
350
480
|
resumeMultipartUpload(key, uploadId) {
|
|
@@ -360,18 +490,23 @@ function wrapVectorize(binding, opts, indexName) {
|
|
|
360
490
|
if (isTracingSuppressed(context.active())) {
|
|
361
491
|
return binding[op](...args);
|
|
362
492
|
}
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
493
|
+
const attrs = { "vectorize.index": indexName };
|
|
494
|
+
if (opts.nativeNames) {
|
|
495
|
+
attrs["cloudflare.binding.type"] = "vectorize";
|
|
496
|
+
attrs["cloudflare.binding.name"] = indexName;
|
|
497
|
+
}
|
|
498
|
+
return traced(opts, nm(opts, `Vectorize ${op}`, `vectorize_${op}`), attrs, undefined, () => binding[op](...args));
|
|
367
499
|
};
|
|
368
|
-
|
|
500
|
+
const handlers = {
|
|
369
501
|
query: wrapOp("query"),
|
|
370
502
|
upsert: wrapOp("upsert"),
|
|
371
503
|
insert: wrapOp("insert"),
|
|
372
504
|
deleteByIds: wrapOp("deleteByIds"),
|
|
373
505
|
getByIds: wrapOp("getByIds"),
|
|
374
|
-
}
|
|
506
|
+
};
|
|
507
|
+
if (opts.nativeNames && typeof binding.describe === "function")
|
|
508
|
+
handlers.describe = wrapOp("describe");
|
|
509
|
+
return makeProxy(binding, handlers);
|
|
375
510
|
}
|
|
376
511
|
// ---------------------------------------------------------------------------
|
|
377
512
|
// Workers AI wrapper
|
|
@@ -385,11 +520,17 @@ function wrapWorkersAI(binding, opts, _name) {
|
|
|
385
520
|
const attrs = {
|
|
386
521
|
"gen_ai.system": "cloudflare.workers_ai",
|
|
387
522
|
};
|
|
523
|
+
if (opts.nativeNames) {
|
|
524
|
+
attrs["cloudflare.binding.type"] = "ai";
|
|
525
|
+
attrs["cloudflare.binding.name"] = _name;
|
|
526
|
+
attrs["gen_ai.provider.name"] = "cloudflare";
|
|
527
|
+
attrs["gen_ai.operation.name"] = "chat";
|
|
528
|
+
}
|
|
388
529
|
if (typeof args[0] === "string") {
|
|
389
530
|
attrs["gen_ai.request.model"] = args[0];
|
|
390
531
|
}
|
|
391
|
-
|
|
392
|
-
|
|
532
|
+
// PRIVACY: never records prompts/completions — only model + token usage.
|
|
533
|
+
return traced(opts, nm(opts, "AI run", "ai_run"), attrs, SpanKind.CLIENT, async (span) => {
|
|
393
534
|
const result = await binding.run(...args);
|
|
394
535
|
// Best-effort usage extraction — never read/await/tee the stream.
|
|
395
536
|
try {
|
|
@@ -410,18 +551,7 @@ function wrapWorkersAI(binding, opts, _name) {
|
|
|
410
551
|
// Swallow — a weird shape must never throw.
|
|
411
552
|
}
|
|
412
553
|
return result;
|
|
413
|
-
}
|
|
414
|
-
catch (err) {
|
|
415
|
-
span.recordException(err instanceof Error ? err : new Error(String(err)));
|
|
416
|
-
span.setStatus({
|
|
417
|
-
code: SpanStatusCode.ERROR,
|
|
418
|
-
message: err instanceof Error ? err.message : String(err),
|
|
419
|
-
});
|
|
420
|
-
throw err;
|
|
421
|
-
}
|
|
422
|
-
finally {
|
|
423
|
-
span.end();
|
|
424
|
-
}
|
|
554
|
+
});
|
|
425
555
|
},
|
|
426
556
|
});
|
|
427
557
|
}
|
|
@@ -432,6 +562,7 @@ function wrapQueue(binding, opts, name) {
|
|
|
432
562
|
const baseAttrs = {
|
|
433
563
|
"messaging.system": "cloudflare_queues",
|
|
434
564
|
"messaging.destination.name": name,
|
|
565
|
+
"cloudflare.queue.name": name,
|
|
435
566
|
};
|
|
436
567
|
return makeProxy(binding, {
|
|
437
568
|
send: async (...args) => {
|
|
@@ -481,8 +612,7 @@ function wrapWorkflow(binding, opts, name) {
|
|
|
481
612
|
const count = getCount?.(...args);
|
|
482
613
|
if (count !== undefined)
|
|
483
614
|
attrs["cloudflare.workflow.batch.size"] = count;
|
|
484
|
-
|
|
485
|
-
return runWithSpan(span, () => binding[op](...args));
|
|
615
|
+
return traced(opts, nm(opts, `Workflow ${op}`, `workflow_${op}`), attrs, kind, () => binding[op](...args));
|
|
486
616
|
};
|
|
487
617
|
return makeProxy(binding, {
|
|
488
618
|
get: wrapOp("get", SpanKind.CLIENT),
|
|
@@ -497,7 +627,7 @@ function wrapRateLimit(binding, opts, name) {
|
|
|
497
627
|
if (isTracingSuppressed(context.active()))
|
|
498
628
|
return binding.limit(...args);
|
|
499
629
|
const span = opts.startSpan("RateLimit limit", {
|
|
500
|
-
"cloudflare.binding.type": "
|
|
630
|
+
"cloudflare.binding.type": "ratelimit",
|
|
501
631
|
"cloudflare.binding.name": name,
|
|
502
632
|
});
|
|
503
633
|
return runWithSpan(span, () => binding.limit(...args));
|
|
@@ -509,7 +639,7 @@ function wrapAnalyticsEngine(binding, opts, name) {
|
|
|
509
639
|
writeDataPoint(...args) {
|
|
510
640
|
if (isTracingSuppressed(context.active()))
|
|
511
641
|
return binding.writeDataPoint(...args);
|
|
512
|
-
const span = opts.startSpan("AnalyticsEngine writeDataPoint", {
|
|
642
|
+
const span = opts.startSpan(nm(opts, "AnalyticsEngine writeDataPoint", "analytics_engine_writeDataPoint"), {
|
|
513
643
|
"cloudflare.binding.type": "analytics_engine",
|
|
514
644
|
"cloudflare.binding.name": name,
|
|
515
645
|
});
|
|
@@ -550,17 +680,37 @@ export function injectTraceparentArgs(args, traceparent) {
|
|
|
550
680
|
return [input, init];
|
|
551
681
|
}
|
|
552
682
|
function wrapService(binding, opts, name) {
|
|
553
|
-
|
|
683
|
+
const handlers = {
|
|
554
684
|
fetch: async (...args) => {
|
|
555
685
|
if (isTracingSuppressed(context.active())) {
|
|
556
686
|
return binding.fetch(...args);
|
|
557
687
|
}
|
|
558
688
|
const span = opts.startSpan(`Service ${name} fetch`, { "peer.service": name }, SpanKind.CLIENT);
|
|
559
689
|
const sc = span.spanContext();
|
|
560
|
-
|
|
561
|
-
const injectedArgs =
|
|
690
|
+
// Native spans expose no ids (INVALID context): never inject an all-zero traceparent.
|
|
691
|
+
const injectedArgs = isSpanContextValid(sc)
|
|
692
|
+
? injectTraceparentArgs(args, makeTraceparent(sc))
|
|
693
|
+
: args;
|
|
562
694
|
return runWithSpan(span, () => binding.fetch(...injectedArgs));
|
|
563
695
|
},
|
|
696
|
+
};
|
|
697
|
+
if (!isRpcServiceLike(binding))
|
|
698
|
+
return makeProxy(binding, handlers);
|
|
699
|
+
// JS RPC binding: every other property is a callable stub that must reach the caller
|
|
700
|
+
// untouched. Reading `.bind` on it would itself be an RPC call (to a method named
|
|
701
|
+
// "bind"), and a bound copy would lose nested property chains (`env.SVC.users.get()`).
|
|
702
|
+
return new Proxy(binding, {
|
|
703
|
+
get(target, prop) {
|
|
704
|
+
if (prop === rawTarget)
|
|
705
|
+
return target;
|
|
706
|
+
if (prop === "fetch")
|
|
707
|
+
return handlers.fetch;
|
|
708
|
+
const value = Reflect.get(target, prop, target);
|
|
709
|
+
// connect() is a host method on the Fetcher and needs its receiver.
|
|
710
|
+
if (prop === "connect" && typeof value === "function")
|
|
711
|
+
return Function.prototype.bind.call(value, target);
|
|
712
|
+
return value;
|
|
713
|
+
},
|
|
564
714
|
});
|
|
565
715
|
}
|
|
566
716
|
// ---------------------------------------------------------------------------
|
|
@@ -594,7 +744,9 @@ function wrapDurableObjectStub(stub, opts, name) {
|
|
|
594
744
|
const span = opts.startSpan(`DO ${name} fetch`, { "peer.service": name }, SpanKind.CLIENT);
|
|
595
745
|
const sc = span.spanContext();
|
|
596
746
|
// Propagate into the DO so its own spans join this trace.
|
|
597
|
-
const injectedArgs =
|
|
747
|
+
const injectedArgs = isSpanContextValid(sc)
|
|
748
|
+
? injectTraceparentArgs(args, makeTraceparent(sc))
|
|
749
|
+
: args;
|
|
598
750
|
return runWithSpanPreservingReturn(span, () => stub.fetch(...injectedArgs));
|
|
599
751
|
};
|
|
600
752
|
// workerd exposes RPC methods through a wildcard property hook, so method
|
|
@@ -699,7 +851,11 @@ function wrapWorkflowInstance(instance, opts, name) {
|
|
|
699
851
|
if (instanceId !== undefined) {
|
|
700
852
|
attrs["cloudflare.workflow.instance_id"] = instanceId;
|
|
701
853
|
}
|
|
702
|
-
|
|
854
|
+
if (opts.nativeNames) {
|
|
855
|
+
attrs["cloudflare.binding.type"] = "workflow";
|
|
856
|
+
attrs["cloudflare.binding.name"] = name;
|
|
857
|
+
}
|
|
858
|
+
const span = opts.startSpan(nm(opts, `Workflow ${method}`, `workflow_${method}`), attrs, SpanKind.CLIENT);
|
|
703
859
|
return runWithSpanPreservingReturn(span, () => underlying.apply(instance, args));
|
|
704
860
|
};
|
|
705
861
|
}
|
|
@@ -746,7 +902,7 @@ function wrapWorkflowBinding(binding, opts, name) {
|
|
|
746
902
|
attrs["cloudflare.workflow.instance_ids"] = batchIds;
|
|
747
903
|
attrs["cloudflare.workflow.batch.size"] = batchIds.length;
|
|
748
904
|
}
|
|
749
|
-
const span = opts.startSpan(`Workflow ${op}`, attrs, SpanKind.CLIENT);
|
|
905
|
+
const span = opts.startSpan(nm(opts, `Workflow ${op}`, `workflow_${op}`), attrs, SpanKind.CLIENT);
|
|
750
906
|
return runWithSpanPreservingReturn(span, () => mapReturn(binding[op](...args), (value) => wrapWorkflowBindingResult(value, op, opts, name, span)));
|
|
751
907
|
};
|
|
752
908
|
}
|
|
@@ -785,52 +941,206 @@ function wrapWorkflowBinding(binding, opts, name) {
|
|
|
785
941
|
* @param opts - `startSpan` factory (now accepts optional `SpanKind`) + `select` filter.
|
|
786
942
|
* @returns A shallow copy of `env` with selected bindings replaced by proxies.
|
|
787
943
|
*/
|
|
788
|
-
export function instrumentEnv(env,
|
|
944
|
+
export function instrumentEnv(env, rawOpts) {
|
|
945
|
+
if (typeof rawOpts.startSpan !== "function" && typeof rawOpts.withSpan !== "function") {
|
|
946
|
+
throw new TypeError("instrumentEnv requires opts.startSpan or opts.withSpan");
|
|
947
|
+
}
|
|
948
|
+
// withSpan wins: the legacy wrappers get an adapter so startSpan is never called.
|
|
949
|
+
const opts = rawOpts.withSpan
|
|
950
|
+
? { ...rawOpts, startSpan: startSpanFromWithSpan(rawOpts.withSpan) }
|
|
951
|
+
: rawOpts;
|
|
789
952
|
const result = { ...env };
|
|
790
|
-
const { select } = opts;
|
|
953
|
+
const { select, kindFilter } = opts;
|
|
954
|
+
const allow = (kind) => !kindFilter || kindFilter(kind);
|
|
955
|
+
// Direct mode: stamp binding type/name on every span a binding emits. Native
|
|
956
|
+
// mode (`withSpan`) already sets them where it enriches, and stays unchanged.
|
|
957
|
+
const stamped = (type, key) => {
|
|
958
|
+
if (rawOpts.withSpan)
|
|
959
|
+
return opts;
|
|
960
|
+
const base = opts.startSpan;
|
|
961
|
+
return {
|
|
962
|
+
...opts,
|
|
963
|
+
startSpan: (name, attrs, kind) => base(name, { "cloudflare.binding.type": type, "cloudflare.binding.name": key, ...attrs }, kind),
|
|
964
|
+
};
|
|
965
|
+
};
|
|
791
966
|
for (const key of Object.keys(env)) {
|
|
792
967
|
// Filter: if select is an array, only wrap keys in the list.
|
|
793
968
|
if (select !== true && !select.includes(key))
|
|
794
969
|
continue;
|
|
795
970
|
const binding = env[key];
|
|
796
|
-
if (
|
|
797
|
-
|
|
971
|
+
if (isRpcServiceLike(binding)) {
|
|
972
|
+
// First: an RPC stub matches every duck-typed check below.
|
|
973
|
+
if (allow("service"))
|
|
974
|
+
result[key] = wrapService(binding, stamped("service", key), key);
|
|
975
|
+
}
|
|
976
|
+
else if (isD1Like(binding)) {
|
|
977
|
+
if (allow("d1"))
|
|
978
|
+
result[key] = wrapD1(binding, stamped("d1", key));
|
|
798
979
|
}
|
|
799
980
|
else if (isR2Like(binding)) {
|
|
800
981
|
// R2 before KV — R2 also has `list`, so checking `head` first avoids mis-classifying.
|
|
801
|
-
|
|
982
|
+
if (allow("r2"))
|
|
983
|
+
result[key] = wrapR2(binding, stamped("r2", key), key);
|
|
802
984
|
}
|
|
803
985
|
else if (isKVLike(binding)) {
|
|
804
|
-
|
|
986
|
+
if (allow("kv"))
|
|
987
|
+
result[key] = wrapKV(binding, stamped("kv", key), key);
|
|
805
988
|
}
|
|
806
989
|
else if (isVectorizeLike(binding)) {
|
|
807
|
-
|
|
990
|
+
if (allow("vectorize"))
|
|
991
|
+
result[key] = wrapVectorize(binding, stamped("vectorize", key), key);
|
|
808
992
|
}
|
|
809
993
|
else if (isWorkersAILike(binding)) {
|
|
810
|
-
|
|
994
|
+
if (allow("ai"))
|
|
995
|
+
result[key] = wrapWorkersAI(binding, stamped("ai", key), key);
|
|
811
996
|
}
|
|
812
997
|
else if (isQueueLike(binding)) {
|
|
813
|
-
|
|
998
|
+
if (allow("queue"))
|
|
999
|
+
result[key] = wrapQueue(binding, stamped("queue", key), key);
|
|
814
1000
|
}
|
|
815
1001
|
else if (isDurableObjectNamespaceLike(binding)) {
|
|
816
|
-
|
|
1002
|
+
if (allow("durable_object"))
|
|
1003
|
+
result[key] = wrapDurableObjectNamespace(binding, stamped("durable_object", key), key);
|
|
817
1004
|
}
|
|
818
1005
|
else if (isWorkflowBindingLike(binding)) {
|
|
819
|
-
|
|
1006
|
+
if (allow("workflow"))
|
|
1007
|
+
result[key] = wrapWorkflowBinding(binding, stamped("workflow", key), key);
|
|
820
1008
|
}
|
|
821
1009
|
else if (isWorkflowLike(binding)) {
|
|
822
|
-
|
|
1010
|
+
if (allow("workflow"))
|
|
1011
|
+
result[key] = wrapWorkflow(binding, stamped("workflow", key), key);
|
|
823
1012
|
}
|
|
824
1013
|
else if (isRateLimitLike(binding)) {
|
|
825
|
-
|
|
1014
|
+
if (allow("ratelimit"))
|
|
1015
|
+
result[key] = wrapRateLimit(binding, stamped("ratelimit", key), key);
|
|
826
1016
|
}
|
|
827
1017
|
else if (isAnalyticsEngineLike(binding)) {
|
|
828
|
-
|
|
1018
|
+
if (allow("analytics_engine"))
|
|
1019
|
+
result[key] = wrapAnalyticsEngine(binding, stamped("analytics_engine", key), key);
|
|
829
1020
|
}
|
|
830
1021
|
else if (isServiceLike(binding)) {
|
|
831
|
-
|
|
1022
|
+
if (allow("service"))
|
|
1023
|
+
result[key] = wrapService(binding, stamped("service", key), key);
|
|
832
1024
|
}
|
|
833
1025
|
// Unrecognised bindings are left as-is.
|
|
834
1026
|
}
|
|
835
1027
|
return result;
|
|
836
1028
|
}
|
|
1029
|
+
// ---------------------------------------------------------------------------
|
|
1030
|
+
// Hyperdrive: wrap the SQL client the app builds on top of the binding
|
|
1031
|
+
// ---------------------------------------------------------------------------
|
|
1032
|
+
/**
|
|
1033
|
+
* Wrap a Postgres/MySQL client (postgres.js, pg, mysql2, ...) created from a
|
|
1034
|
+
* Hyperdrive binding so each `query` / `unsafe` / `execute` call, and each
|
|
1035
|
+
* tagged-template call when the client itself is callable (postgres.js
|
|
1036
|
+
* `sql\`select 1\``), emits a `hyperdrive_query` span. `db.query.text` is
|
|
1037
|
+
* recorded raw (template strings are joined with `?`; ingest sanitises). The
|
|
1038
|
+
* ORIGINAL return value is passed through untouched; the span ends when it
|
|
1039
|
+
* settles. Uses the request's native tracer when present, else the global one.
|
|
1040
|
+
*/
|
|
1041
|
+
export function wrapSqlClient(client, opts = {}) {
|
|
1042
|
+
const traceCall = (args, run) => {
|
|
1043
|
+
if (isTracingSuppressed(context.active()))
|
|
1044
|
+
return run();
|
|
1045
|
+
const attrs = { "cloudflare.binding.type": "hyperdrive" };
|
|
1046
|
+
if (opts.name)
|
|
1047
|
+
attrs["cloudflare.binding.name"] = opts.name;
|
|
1048
|
+
if (opts.dbSystem)
|
|
1049
|
+
attrs["db.system.name"] = opts.dbSystem;
|
|
1050
|
+
const first = args[0];
|
|
1051
|
+
if (Array.isArray(first) && "raw" in first)
|
|
1052
|
+
attrs["db.query.text"] = first.join("?");
|
|
1053
|
+
else if (typeof first === "string")
|
|
1054
|
+
attrs["db.query.text"] = first;
|
|
1055
|
+
else if (typeof first?.text === "string") {
|
|
1056
|
+
attrs["db.query.text"] = first.text;
|
|
1057
|
+
}
|
|
1058
|
+
const native = nativeTracingFromContext();
|
|
1059
|
+
const startSpan = () => native
|
|
1060
|
+
? startSpanFromWithSpan((n, at, _k, fn) => runNativeSpan(native, n, at, fn))("hyperdrive_query", attrs, SpanKind.CLIENT)
|
|
1061
|
+
: trace.getTracer("heystack").startSpan("hyperdrive_query", {
|
|
1062
|
+
attributes: attrs,
|
|
1063
|
+
kind: SpanKind.CLIENT,
|
|
1064
|
+
});
|
|
1065
|
+
let result;
|
|
1066
|
+
let span;
|
|
1067
|
+
try {
|
|
1068
|
+
result = run();
|
|
1069
|
+
}
|
|
1070
|
+
catch (error) {
|
|
1071
|
+
span = startSpan();
|
|
1072
|
+
recordSpanError(span, error);
|
|
1073
|
+
span.end();
|
|
1074
|
+
throw error;
|
|
1075
|
+
}
|
|
1076
|
+
if (!isPromiseLike(result)) {
|
|
1077
|
+
// Synchronous result: nothing lazy to protect.
|
|
1078
|
+
span = startSpan();
|
|
1079
|
+
span.end();
|
|
1080
|
+
return result;
|
|
1081
|
+
}
|
|
1082
|
+
// A plain native Promise is already running and `await` bypasses an own
|
|
1083
|
+
// `then`, so observe it eagerly (safe: observing cannot start anything).
|
|
1084
|
+
if (Object.getPrototypeOf(result) === Promise.prototype) {
|
|
1085
|
+
return runWithSpanPreservingReturn(startSpan(), () => result);
|
|
1086
|
+
}
|
|
1087
|
+
// LAZY thenables (postgres.js `Query`) start executing inside `then()`, so we
|
|
1088
|
+
// must never call `then` ourselves: a fragment nested in another query and
|
|
1089
|
+
// never awaited would otherwise run standalone. Instead shadow `then` on the
|
|
1090
|
+
// instance and start the span on the first consumer-driven call.
|
|
1091
|
+
const origThen = result.then;
|
|
1092
|
+
let started = false;
|
|
1093
|
+
try {
|
|
1094
|
+
Object.defineProperty(result, "then", {
|
|
1095
|
+
configurable: true,
|
|
1096
|
+
writable: true,
|
|
1097
|
+
value: function (onOk, onErr) {
|
|
1098
|
+
if (started)
|
|
1099
|
+
return origThen.call(this, onOk, onErr);
|
|
1100
|
+
started = true;
|
|
1101
|
+
const sp = startSpan();
|
|
1102
|
+
return origThen.call(this, (v) => {
|
|
1103
|
+
try {
|
|
1104
|
+
sp.end();
|
|
1105
|
+
}
|
|
1106
|
+
catch { /* never alter the result */ }
|
|
1107
|
+
return onOk ? onOk(v) : v;
|
|
1108
|
+
}, (e) => {
|
|
1109
|
+
try {
|
|
1110
|
+
recordSpanError(sp, e);
|
|
1111
|
+
sp.end();
|
|
1112
|
+
}
|
|
1113
|
+
catch { /* never alter the result */ }
|
|
1114
|
+
if (onErr)
|
|
1115
|
+
return onErr(e);
|
|
1116
|
+
throw e;
|
|
1117
|
+
});
|
|
1118
|
+
},
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
catch {
|
|
1122
|
+
// Frozen / non-extensible: return untraced.
|
|
1123
|
+
}
|
|
1124
|
+
return result;
|
|
1125
|
+
};
|
|
1126
|
+
const methods = new Map();
|
|
1127
|
+
return new Proxy(client, {
|
|
1128
|
+
get(target, prop, receiver) {
|
|
1129
|
+
const value = Reflect.get(target, prop, receiver);
|
|
1130
|
+
if (typeof value !== "function")
|
|
1131
|
+
return value;
|
|
1132
|
+
if (prop === "query" || prop === "unsafe" || prop === "execute") {
|
|
1133
|
+
let wrapped = methods.get(prop);
|
|
1134
|
+
if (!wrapped) {
|
|
1135
|
+
wrapped = (...args) => traceCall(args, () => value.apply(target, args));
|
|
1136
|
+
methods.set(prop, wrapped);
|
|
1137
|
+
}
|
|
1138
|
+
return wrapped;
|
|
1139
|
+
}
|
|
1140
|
+
return value.bind(target);
|
|
1141
|
+
},
|
|
1142
|
+
apply(target, thisArg, args) {
|
|
1143
|
+
return traceCall(args, () => Reflect.apply(target, thisArg, args));
|
|
1144
|
+
},
|
|
1145
|
+
});
|
|
1146
|
+
}
|