@heystack/otel 0.18.0 → 0.19.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 +149 -26
- 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 +351 -76
- 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 +583 -185
- 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,66 @@ function isAnalyticsEngineLike(b) {
|
|
|
61
63
|
function isServiceLike(b) {
|
|
62
64
|
return typeof b?.fetch === "function";
|
|
63
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Adapt a callback-scoped `withSpan` to the immediate-return `startSpan` shape
|
|
68
|
+
* the legacy wrappers use. The callback stays open (pending promise) until the
|
|
69
|
+
* wrapper calls `end()`; calls made before the callback has run are buffered.
|
|
70
|
+
*/
|
|
71
|
+
function startSpanFromWithSpan(withSpan) {
|
|
72
|
+
return (name, attrs, kind) => {
|
|
73
|
+
let real;
|
|
74
|
+
const queue = [];
|
|
75
|
+
let release = () => undefined;
|
|
76
|
+
const done = new Promise((resolve) => { release = resolve; });
|
|
77
|
+
let ended = false;
|
|
78
|
+
const apply = (f) => {
|
|
79
|
+
if (real)
|
|
80
|
+
f(real);
|
|
81
|
+
else
|
|
82
|
+
queue.push(f);
|
|
83
|
+
};
|
|
84
|
+
void withSpan(name, attrs, kind, (s) => {
|
|
85
|
+
real = s;
|
|
86
|
+
for (const f of queue.splice(0))
|
|
87
|
+
f(s);
|
|
88
|
+
return done;
|
|
89
|
+
}).catch(() => undefined);
|
|
90
|
+
const deferred = {
|
|
91
|
+
spanContext: () => real?.spanContext() ?? INVALID_SPAN_CONTEXT,
|
|
92
|
+
setAttribute(k, v) { apply((s) => s.setAttribute(k, v)); return deferred; },
|
|
93
|
+
setAttributes(a) { apply((s) => s.setAttributes(a)); return deferred; },
|
|
94
|
+
addEvent(n, a) { apply((s) => s.addEvent(n, a)); return deferred; },
|
|
95
|
+
addLink() { return deferred; },
|
|
96
|
+
addLinks() { return deferred; },
|
|
97
|
+
setStatus(st) { apply((s) => s.setStatus(st)); return deferred; },
|
|
98
|
+
updateName() { return deferred; },
|
|
99
|
+
end() {
|
|
100
|
+
if (ended)
|
|
101
|
+
return;
|
|
102
|
+
ended = true;
|
|
103
|
+
apply((s) => s.end());
|
|
104
|
+
release();
|
|
105
|
+
},
|
|
106
|
+
isRecording: () => real?.isRecording() ?? true,
|
|
107
|
+
recordException(e) { apply((s) => s.recordException(e)); },
|
|
108
|
+
};
|
|
109
|
+
return deferred;
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Run an async operation inside a span: `withSpan` when native, else
|
|
114
|
+
* `startSpan` + {@link runWithSpan}.
|
|
115
|
+
*/
|
|
116
|
+
function traced(opts, name, attrs, kind, fn) {
|
|
117
|
+
if (opts.withSpan)
|
|
118
|
+
return opts.withSpan(name, attrs, kind, fn);
|
|
119
|
+
const span = opts.startSpan(name, attrs, kind);
|
|
120
|
+
return runWithSpan(span, () => fn(span));
|
|
121
|
+
}
|
|
122
|
+
/** Native name when `opts.nativeNames`, else the direct-mode name. */
|
|
123
|
+
function nm(opts, direct, native) {
|
|
124
|
+
return opts.nativeNames ? native : direct;
|
|
125
|
+
}
|
|
64
126
|
// ---------------------------------------------------------------------------
|
|
65
127
|
// Span lifecycle helper
|
|
66
128
|
// ---------------------------------------------------------------------------
|
|
@@ -85,6 +147,68 @@ export async function runWithSpan(span, fn) {
|
|
|
85
147
|
span.end();
|
|
86
148
|
}
|
|
87
149
|
}
|
|
150
|
+
/** Plain string keys from a string / string[] argument (anything else yields none). */
|
|
151
|
+
export function keysOf(value) {
|
|
152
|
+
if (typeof value === "string")
|
|
153
|
+
return [value];
|
|
154
|
+
if (Array.isArray(value))
|
|
155
|
+
return value.filter((v) => typeof v === "string");
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
/** At most this many keys of a multi-key call are hashed and recorded. */
|
|
159
|
+
const MAX_HASHED_KEYS = 10;
|
|
160
|
+
/** Key hashes still in flight; exports drain these first so no span is flushed half-built. */
|
|
161
|
+
const pendingKeyHashes = new Set();
|
|
162
|
+
/** Resolves once every in-flight key hash has attached its attribute and ended its span. */
|
|
163
|
+
export async function settlePendingKeyHashes() {
|
|
164
|
+
while (pendingKeyHashes.size > 0)
|
|
165
|
+
await Promise.allSettled([...pendingKeyHashes]);
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Like {@link runWithSpan} for operations that carry user-controlled keys. The
|
|
169
|
+
* hash is computed concurrently with the call and attached under `attr` before
|
|
170
|
+
* the span ends; it can neither delay the call's result nor change its error.
|
|
171
|
+
* If hashing is impossible the attribute is omitted (never the raw key).
|
|
172
|
+
*/
|
|
173
|
+
export async function runWithSpanKeyed(span, attr, keys, fn, countAttr) {
|
|
174
|
+
// Bounded work per call: hash only the first MAX_HASHED_KEYS; the count is exact.
|
|
175
|
+
if (countAttr && keys.length > 0)
|
|
176
|
+
span.setAttribute(countAttr, keys.length);
|
|
177
|
+
const hashed = keys.length > 0 ? scrubKeysAsync(keys.slice(0, MAX_HASHED_KEYS)) : undefined;
|
|
178
|
+
try {
|
|
179
|
+
return await fn();
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
recordSpanError(span, err);
|
|
183
|
+
throw err;
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
const end = () => {
|
|
187
|
+
try {
|
|
188
|
+
span.end();
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
// Telemetry must never surface errors.
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
if (hashed) {
|
|
195
|
+
const done = hashed.then((h) => {
|
|
196
|
+
try {
|
|
197
|
+
if (h !== undefined)
|
|
198
|
+
span.setAttribute(attr, h);
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
// ignore
|
|
202
|
+
}
|
|
203
|
+
end();
|
|
204
|
+
}, end).finally(() => pendingKeyHashes.delete(done));
|
|
205
|
+
pendingKeyHashes.add(done);
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
end();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
88
212
|
function runSyncWithSpan(span, fn) {
|
|
89
213
|
try {
|
|
90
214
|
return fn();
|
|
@@ -113,7 +237,7 @@ function recordSpanError(span, error) {
|
|
|
113
237
|
});
|
|
114
238
|
}
|
|
115
239
|
/** Trace a call without replacing synchronous, Promise, or custom thenable results. */
|
|
116
|
-
function runWithSpanPreservingReturn(span, fn) {
|
|
240
|
+
export function runWithSpanPreservingReturn(span, fn) {
|
|
117
241
|
try {
|
|
118
242
|
const result = fn();
|
|
119
243
|
if (isPromiseLike(result)) {
|
|
@@ -200,6 +324,8 @@ function wrapD1Statement(stmt, sql, opts) {
|
|
|
200
324
|
const span = opts.startSpan(`D1 ${op}`, {
|
|
201
325
|
"db.system": "d1",
|
|
202
326
|
"db.statement": sql,
|
|
327
|
+
"db.system.name": "cloudflare-d1",
|
|
328
|
+
"db.query.text": sql,
|
|
203
329
|
});
|
|
204
330
|
return runWithSpan(span, () => stmt[op](...args));
|
|
205
331
|
};
|
|
@@ -225,6 +351,7 @@ function wrapD1(binding, opts) {
|
|
|
225
351
|
}
|
|
226
352
|
const span = opts.startSpan("D1 batch", {
|
|
227
353
|
"db.system": "d1",
|
|
354
|
+
"db.system.name": "cloudflare-d1",
|
|
228
355
|
"db.operation.name": "batch",
|
|
229
356
|
"db.operation.batch.size": statements.length,
|
|
230
357
|
});
|
|
@@ -235,6 +362,7 @@ function wrapD1(binding, opts) {
|
|
|
235
362
|
return binding.exec(query);
|
|
236
363
|
const span = opts.startSpan("D1 exec", {
|
|
237
364
|
"db.system": "d1",
|
|
365
|
+
"db.system.name": "cloudflare-d1",
|
|
238
366
|
"db.operation.name": "exec",
|
|
239
367
|
});
|
|
240
368
|
return runWithSpan(span, () => binding.exec(query));
|
|
@@ -255,6 +383,7 @@ function wrapD1Session(session, opts) {
|
|
|
255
383
|
}
|
|
256
384
|
const span = opts.startSpan("D1 session batch", {
|
|
257
385
|
"db.system": "d1",
|
|
386
|
+
"db.system.name": "cloudflare-d1",
|
|
258
387
|
"db.operation.name": "batch",
|
|
259
388
|
"db.operation.batch.size": statements.length,
|
|
260
389
|
});
|
|
@@ -266,39 +395,33 @@ function wrapD1Session(session, opts) {
|
|
|
266
395
|
// KV wrappers
|
|
267
396
|
// ---------------------------------------------------------------------------
|
|
268
397
|
function wrapKV(binding, opts, namespace) {
|
|
269
|
-
const wrapOp = (op,
|
|
398
|
+
const wrapOp = (op, keyed = false) => async (...args) => {
|
|
270
399
|
if (isTracingSuppressed(context.active())) {
|
|
271
400
|
return binding[op](...args);
|
|
272
401
|
}
|
|
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));
|
|
402
|
+
const span = opts.startSpan(`KV ${op}`, { "kv.namespace": namespace });
|
|
403
|
+
return runWithSpanKeyed(span, "cloudflare.kv.query.keys", keyed ? keysOf(args[0]) : [], () => binding[op](...args), "cloudflare.kv.query.key_count");
|
|
279
404
|
};
|
|
280
405
|
return makeProxy(binding, {
|
|
281
|
-
get: wrapOp("get",
|
|
282
|
-
getWithMetadata: wrapOp("getWithMetadata",
|
|
283
|
-
put: wrapOp("put",
|
|
406
|
+
get: wrapOp("get", true),
|
|
407
|
+
getWithMetadata: wrapOp("getWithMetadata", true),
|
|
408
|
+
put: wrapOp("put", true),
|
|
284
409
|
list: wrapOp("list"),
|
|
285
|
-
delete: wrapOp("delete",
|
|
410
|
+
delete: wrapOp("delete", true),
|
|
286
411
|
});
|
|
287
412
|
}
|
|
288
413
|
// ---------------------------------------------------------------------------
|
|
289
414
|
// R2 wrappers
|
|
290
415
|
// ---------------------------------------------------------------------------
|
|
416
|
+
/** R2 object keys are user data: only the hashed form is ever emitted. */
|
|
417
|
+
const R2_KEY_ATTR = "cloudflare.r2.request.key";
|
|
291
418
|
function wrapR2(binding, opts, bucket) {
|
|
292
|
-
const wrapOp = (op,
|
|
419
|
+
const wrapOp = (op, keyed = false) => async (...args) => {
|
|
293
420
|
if (isTracingSuppressed(context.active())) {
|
|
294
421
|
return binding[op](...args);
|
|
295
422
|
}
|
|
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));
|
|
423
|
+
const span = opts.startSpan(`R2 ${op}`, { "r2.bucket": bucket });
|
|
424
|
+
return runWithSpanKeyed(span, R2_KEY_ATTR, keyed ? keysOf(args[0]) : [], () => binding[op](...args), "cloudflare.r2.request.key_count");
|
|
302
425
|
};
|
|
303
426
|
const wrapMultipart = (upload, key) => makeProxy(upload, {
|
|
304
427
|
uploadPart: async (partNumber, ...args) => {
|
|
@@ -307,10 +430,9 @@ function wrapR2(binding, opts, bucket) {
|
|
|
307
430
|
}
|
|
308
431
|
const span = opts.startSpan("R2 uploadPart", {
|
|
309
432
|
"r2.bucket": bucket,
|
|
310
|
-
"r2.key": key,
|
|
311
433
|
"r2.multipart.part_number": partNumber,
|
|
312
434
|
});
|
|
313
|
-
return
|
|
435
|
+
return runWithSpanKeyed(span, R2_KEY_ATTR, [key], () => upload.uploadPart(partNumber, ...args));
|
|
314
436
|
},
|
|
315
437
|
abort: wrapMultipartOp("abort", upload, key),
|
|
316
438
|
complete: wrapMultipartOp("complete", upload, key, (parts) => ({
|
|
@@ -323,28 +445,24 @@ function wrapR2(binding, opts, bucket) {
|
|
|
323
445
|
return upload[op](...args);
|
|
324
446
|
const span = opts.startSpan(`R2 ${op}`, {
|
|
325
447
|
"r2.bucket": bucket,
|
|
326
|
-
"r2.key": key,
|
|
327
448
|
...extraAttrs?.(...args),
|
|
328
449
|
});
|
|
329
|
-
return
|
|
450
|
+
return runWithSpanKeyed(span, R2_KEY_ATTR, [key], () => upload[op](...args));
|
|
330
451
|
};
|
|
331
452
|
}
|
|
332
453
|
return makeProxy(binding, {
|
|
333
|
-
get: wrapOp("get",
|
|
334
|
-
put: wrapOp("put",
|
|
335
|
-
head: wrapOp("head",
|
|
336
|
-
delete: wrapOp("delete",
|
|
454
|
+
get: wrapOp("get", true),
|
|
455
|
+
put: wrapOp("put", true),
|
|
456
|
+
head: wrapOp("head", true),
|
|
457
|
+
delete: wrapOp("delete", true),
|
|
337
458
|
list: wrapOp("list"),
|
|
338
459
|
async createMultipartUpload(key, ...args) {
|
|
339
460
|
if (isTracingSuppressed(context.active())) {
|
|
340
461
|
const upload = await binding.createMultipartUpload(key, ...args);
|
|
341
462
|
return wrapMultipart(upload, key);
|
|
342
463
|
}
|
|
343
|
-
const span = opts.startSpan("R2 createMultipartUpload", {
|
|
344
|
-
|
|
345
|
-
"r2.key": key,
|
|
346
|
-
});
|
|
347
|
-
const upload = await runWithSpan(span, () => binding.createMultipartUpload(key, ...args));
|
|
464
|
+
const span = opts.startSpan("R2 createMultipartUpload", { "r2.bucket": bucket });
|
|
465
|
+
const upload = await runWithSpanKeyed(span, R2_KEY_ATTR, [key], () => binding.createMultipartUpload(key, ...args));
|
|
348
466
|
return wrapMultipart(upload, key);
|
|
349
467
|
},
|
|
350
468
|
resumeMultipartUpload(key, uploadId) {
|
|
@@ -360,18 +478,23 @@ function wrapVectorize(binding, opts, indexName) {
|
|
|
360
478
|
if (isTracingSuppressed(context.active())) {
|
|
361
479
|
return binding[op](...args);
|
|
362
480
|
}
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
481
|
+
const attrs = { "vectorize.index": indexName };
|
|
482
|
+
if (opts.nativeNames) {
|
|
483
|
+
attrs["cloudflare.binding.type"] = "vectorize";
|
|
484
|
+
attrs["cloudflare.binding.name"] = indexName;
|
|
485
|
+
}
|
|
486
|
+
return traced(opts, nm(opts, `Vectorize ${op}`, `vectorize_${op}`), attrs, undefined, () => binding[op](...args));
|
|
367
487
|
};
|
|
368
|
-
|
|
488
|
+
const handlers = {
|
|
369
489
|
query: wrapOp("query"),
|
|
370
490
|
upsert: wrapOp("upsert"),
|
|
371
491
|
insert: wrapOp("insert"),
|
|
372
492
|
deleteByIds: wrapOp("deleteByIds"),
|
|
373
493
|
getByIds: wrapOp("getByIds"),
|
|
374
|
-
}
|
|
494
|
+
};
|
|
495
|
+
if (opts.nativeNames && typeof binding.describe === "function")
|
|
496
|
+
handlers.describe = wrapOp("describe");
|
|
497
|
+
return makeProxy(binding, handlers);
|
|
375
498
|
}
|
|
376
499
|
// ---------------------------------------------------------------------------
|
|
377
500
|
// Workers AI wrapper
|
|
@@ -385,11 +508,17 @@ function wrapWorkersAI(binding, opts, _name) {
|
|
|
385
508
|
const attrs = {
|
|
386
509
|
"gen_ai.system": "cloudflare.workers_ai",
|
|
387
510
|
};
|
|
511
|
+
if (opts.nativeNames) {
|
|
512
|
+
attrs["cloudflare.binding.type"] = "ai";
|
|
513
|
+
attrs["cloudflare.binding.name"] = _name;
|
|
514
|
+
attrs["gen_ai.provider.name"] = "cloudflare";
|
|
515
|
+
attrs["gen_ai.operation.name"] = "chat";
|
|
516
|
+
}
|
|
388
517
|
if (typeof args[0] === "string") {
|
|
389
518
|
attrs["gen_ai.request.model"] = args[0];
|
|
390
519
|
}
|
|
391
|
-
|
|
392
|
-
|
|
520
|
+
// PRIVACY: never records prompts/completions — only model + token usage.
|
|
521
|
+
return traced(opts, nm(opts, "AI run", "ai_run"), attrs, SpanKind.CLIENT, async (span) => {
|
|
393
522
|
const result = await binding.run(...args);
|
|
394
523
|
// Best-effort usage extraction — never read/await/tee the stream.
|
|
395
524
|
try {
|
|
@@ -410,18 +539,7 @@ function wrapWorkersAI(binding, opts, _name) {
|
|
|
410
539
|
// Swallow — a weird shape must never throw.
|
|
411
540
|
}
|
|
412
541
|
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
|
-
}
|
|
542
|
+
});
|
|
425
543
|
},
|
|
426
544
|
});
|
|
427
545
|
}
|
|
@@ -432,6 +550,7 @@ function wrapQueue(binding, opts, name) {
|
|
|
432
550
|
const baseAttrs = {
|
|
433
551
|
"messaging.system": "cloudflare_queues",
|
|
434
552
|
"messaging.destination.name": name,
|
|
553
|
+
"cloudflare.queue.name": name,
|
|
435
554
|
};
|
|
436
555
|
return makeProxy(binding, {
|
|
437
556
|
send: async (...args) => {
|
|
@@ -481,8 +600,7 @@ function wrapWorkflow(binding, opts, name) {
|
|
|
481
600
|
const count = getCount?.(...args);
|
|
482
601
|
if (count !== undefined)
|
|
483
602
|
attrs["cloudflare.workflow.batch.size"] = count;
|
|
484
|
-
|
|
485
|
-
return runWithSpan(span, () => binding[op](...args));
|
|
603
|
+
return traced(opts, nm(opts, `Workflow ${op}`, `workflow_${op}`), attrs, kind, () => binding[op](...args));
|
|
486
604
|
};
|
|
487
605
|
return makeProxy(binding, {
|
|
488
606
|
get: wrapOp("get", SpanKind.CLIENT),
|
|
@@ -497,7 +615,7 @@ function wrapRateLimit(binding, opts, name) {
|
|
|
497
615
|
if (isTracingSuppressed(context.active()))
|
|
498
616
|
return binding.limit(...args);
|
|
499
617
|
const span = opts.startSpan("RateLimit limit", {
|
|
500
|
-
"cloudflare.binding.type": "
|
|
618
|
+
"cloudflare.binding.type": "ratelimit",
|
|
501
619
|
"cloudflare.binding.name": name,
|
|
502
620
|
});
|
|
503
621
|
return runWithSpan(span, () => binding.limit(...args));
|
|
@@ -509,7 +627,7 @@ function wrapAnalyticsEngine(binding, opts, name) {
|
|
|
509
627
|
writeDataPoint(...args) {
|
|
510
628
|
if (isTracingSuppressed(context.active()))
|
|
511
629
|
return binding.writeDataPoint(...args);
|
|
512
|
-
const span = opts.startSpan("AnalyticsEngine writeDataPoint", {
|
|
630
|
+
const span = opts.startSpan(nm(opts, "AnalyticsEngine writeDataPoint", "analytics_engine_writeDataPoint"), {
|
|
513
631
|
"cloudflare.binding.type": "analytics_engine",
|
|
514
632
|
"cloudflare.binding.name": name,
|
|
515
633
|
});
|
|
@@ -557,8 +675,10 @@ function wrapService(binding, opts, name) {
|
|
|
557
675
|
}
|
|
558
676
|
const span = opts.startSpan(`Service ${name} fetch`, { "peer.service": name }, SpanKind.CLIENT);
|
|
559
677
|
const sc = span.spanContext();
|
|
560
|
-
|
|
561
|
-
const injectedArgs =
|
|
678
|
+
// Native spans expose no ids (INVALID context): never inject an all-zero traceparent.
|
|
679
|
+
const injectedArgs = isSpanContextValid(sc)
|
|
680
|
+
? injectTraceparentArgs(args, makeTraceparent(sc))
|
|
681
|
+
: args;
|
|
562
682
|
return runWithSpan(span, () => binding.fetch(...injectedArgs));
|
|
563
683
|
},
|
|
564
684
|
});
|
|
@@ -594,7 +714,9 @@ function wrapDurableObjectStub(stub, opts, name) {
|
|
|
594
714
|
const span = opts.startSpan(`DO ${name} fetch`, { "peer.service": name }, SpanKind.CLIENT);
|
|
595
715
|
const sc = span.spanContext();
|
|
596
716
|
// Propagate into the DO so its own spans join this trace.
|
|
597
|
-
const injectedArgs =
|
|
717
|
+
const injectedArgs = isSpanContextValid(sc)
|
|
718
|
+
? injectTraceparentArgs(args, makeTraceparent(sc))
|
|
719
|
+
: args;
|
|
598
720
|
return runWithSpanPreservingReturn(span, () => stub.fetch(...injectedArgs));
|
|
599
721
|
};
|
|
600
722
|
// workerd exposes RPC methods through a wildcard property hook, so method
|
|
@@ -699,7 +821,11 @@ function wrapWorkflowInstance(instance, opts, name) {
|
|
|
699
821
|
if (instanceId !== undefined) {
|
|
700
822
|
attrs["cloudflare.workflow.instance_id"] = instanceId;
|
|
701
823
|
}
|
|
702
|
-
|
|
824
|
+
if (opts.nativeNames) {
|
|
825
|
+
attrs["cloudflare.binding.type"] = "workflow";
|
|
826
|
+
attrs["cloudflare.binding.name"] = name;
|
|
827
|
+
}
|
|
828
|
+
const span = opts.startSpan(nm(opts, `Workflow ${method}`, `workflow_${method}`), attrs, SpanKind.CLIENT);
|
|
703
829
|
return runWithSpanPreservingReturn(span, () => underlying.apply(instance, args));
|
|
704
830
|
};
|
|
705
831
|
}
|
|
@@ -746,7 +872,7 @@ function wrapWorkflowBinding(binding, opts, name) {
|
|
|
746
872
|
attrs["cloudflare.workflow.instance_ids"] = batchIds;
|
|
747
873
|
attrs["cloudflare.workflow.batch.size"] = batchIds.length;
|
|
748
874
|
}
|
|
749
|
-
const span = opts.startSpan(`Workflow ${op}`, attrs, SpanKind.CLIENT);
|
|
875
|
+
const span = opts.startSpan(nm(opts, `Workflow ${op}`, `workflow_${op}`), attrs, SpanKind.CLIENT);
|
|
750
876
|
return runWithSpanPreservingReturn(span, () => mapReturn(binding[op](...args), (value) => wrapWorkflowBindingResult(value, op, opts, name, span)));
|
|
751
877
|
};
|
|
752
878
|
}
|
|
@@ -785,52 +911,201 @@ function wrapWorkflowBinding(binding, opts, name) {
|
|
|
785
911
|
* @param opts - `startSpan` factory (now accepts optional `SpanKind`) + `select` filter.
|
|
786
912
|
* @returns A shallow copy of `env` with selected bindings replaced by proxies.
|
|
787
913
|
*/
|
|
788
|
-
export function instrumentEnv(env,
|
|
914
|
+
export function instrumentEnv(env, rawOpts) {
|
|
915
|
+
if (typeof rawOpts.startSpan !== "function" && typeof rawOpts.withSpan !== "function") {
|
|
916
|
+
throw new TypeError("instrumentEnv requires opts.startSpan or opts.withSpan");
|
|
917
|
+
}
|
|
918
|
+
// withSpan wins: the legacy wrappers get an adapter so startSpan is never called.
|
|
919
|
+
const opts = rawOpts.withSpan
|
|
920
|
+
? { ...rawOpts, startSpan: startSpanFromWithSpan(rawOpts.withSpan) }
|
|
921
|
+
: rawOpts;
|
|
789
922
|
const result = { ...env };
|
|
790
|
-
const { select } = opts;
|
|
923
|
+
const { select, kindFilter } = opts;
|
|
924
|
+
const allow = (kind) => !kindFilter || kindFilter(kind);
|
|
925
|
+
// Direct mode: stamp binding type/name on every span a binding emits. Native
|
|
926
|
+
// mode (`withSpan`) already sets them where it enriches, and stays unchanged.
|
|
927
|
+
const stamped = (type, key) => {
|
|
928
|
+
if (rawOpts.withSpan)
|
|
929
|
+
return opts;
|
|
930
|
+
const base = opts.startSpan;
|
|
931
|
+
return {
|
|
932
|
+
...opts,
|
|
933
|
+
startSpan: (name, attrs, kind) => base(name, { "cloudflare.binding.type": type, "cloudflare.binding.name": key, ...attrs }, kind),
|
|
934
|
+
};
|
|
935
|
+
};
|
|
791
936
|
for (const key of Object.keys(env)) {
|
|
792
937
|
// Filter: if select is an array, only wrap keys in the list.
|
|
793
938
|
if (select !== true && !select.includes(key))
|
|
794
939
|
continue;
|
|
795
940
|
const binding = env[key];
|
|
796
941
|
if (isD1Like(binding)) {
|
|
797
|
-
|
|
942
|
+
if (allow("d1"))
|
|
943
|
+
result[key] = wrapD1(binding, stamped("d1", key));
|
|
798
944
|
}
|
|
799
945
|
else if (isR2Like(binding)) {
|
|
800
946
|
// R2 before KV — R2 also has `list`, so checking `head` first avoids mis-classifying.
|
|
801
|
-
|
|
947
|
+
if (allow("r2"))
|
|
948
|
+
result[key] = wrapR2(binding, stamped("r2", key), key);
|
|
802
949
|
}
|
|
803
950
|
else if (isKVLike(binding)) {
|
|
804
|
-
|
|
951
|
+
if (allow("kv"))
|
|
952
|
+
result[key] = wrapKV(binding, stamped("kv", key), key);
|
|
805
953
|
}
|
|
806
954
|
else if (isVectorizeLike(binding)) {
|
|
807
|
-
|
|
955
|
+
if (allow("vectorize"))
|
|
956
|
+
result[key] = wrapVectorize(binding, stamped("vectorize", key), key);
|
|
808
957
|
}
|
|
809
958
|
else if (isWorkersAILike(binding)) {
|
|
810
|
-
|
|
959
|
+
if (allow("ai"))
|
|
960
|
+
result[key] = wrapWorkersAI(binding, stamped("ai", key), key);
|
|
811
961
|
}
|
|
812
962
|
else if (isQueueLike(binding)) {
|
|
813
|
-
|
|
963
|
+
if (allow("queue"))
|
|
964
|
+
result[key] = wrapQueue(binding, stamped("queue", key), key);
|
|
814
965
|
}
|
|
815
966
|
else if (isDurableObjectNamespaceLike(binding)) {
|
|
816
|
-
|
|
967
|
+
if (allow("durable_object"))
|
|
968
|
+
result[key] = wrapDurableObjectNamespace(binding, stamped("durable_object", key), key);
|
|
817
969
|
}
|
|
818
970
|
else if (isWorkflowBindingLike(binding)) {
|
|
819
|
-
|
|
971
|
+
if (allow("workflow"))
|
|
972
|
+
result[key] = wrapWorkflowBinding(binding, stamped("workflow", key), key);
|
|
820
973
|
}
|
|
821
974
|
else if (isWorkflowLike(binding)) {
|
|
822
|
-
|
|
975
|
+
if (allow("workflow"))
|
|
976
|
+
result[key] = wrapWorkflow(binding, stamped("workflow", key), key);
|
|
823
977
|
}
|
|
824
978
|
else if (isRateLimitLike(binding)) {
|
|
825
|
-
|
|
979
|
+
if (allow("ratelimit"))
|
|
980
|
+
result[key] = wrapRateLimit(binding, stamped("ratelimit", key), key);
|
|
826
981
|
}
|
|
827
982
|
else if (isAnalyticsEngineLike(binding)) {
|
|
828
|
-
|
|
983
|
+
if (allow("analytics_engine"))
|
|
984
|
+
result[key] = wrapAnalyticsEngine(binding, stamped("analytics_engine", key), key);
|
|
829
985
|
}
|
|
830
986
|
else if (isServiceLike(binding)) {
|
|
831
|
-
|
|
987
|
+
if (allow("service"))
|
|
988
|
+
result[key] = wrapService(binding, stamped("service", key), key);
|
|
832
989
|
}
|
|
833
990
|
// Unrecognised bindings are left as-is.
|
|
834
991
|
}
|
|
835
992
|
return result;
|
|
836
993
|
}
|
|
994
|
+
// ---------------------------------------------------------------------------
|
|
995
|
+
// Hyperdrive: wrap the SQL client the app builds on top of the binding
|
|
996
|
+
// ---------------------------------------------------------------------------
|
|
997
|
+
/**
|
|
998
|
+
* Wrap a Postgres/MySQL client (postgres.js, pg, mysql2, ...) created from a
|
|
999
|
+
* Hyperdrive binding so each `query` / `unsafe` / `execute` call, and each
|
|
1000
|
+
* tagged-template call when the client itself is callable (postgres.js
|
|
1001
|
+
* `sql\`select 1\``), emits a `hyperdrive_query` span. `db.query.text` is
|
|
1002
|
+
* recorded raw (template strings are joined with `?`; ingest sanitises). The
|
|
1003
|
+
* ORIGINAL return value is passed through untouched; the span ends when it
|
|
1004
|
+
* settles. Uses the request's native tracer when present, else the global one.
|
|
1005
|
+
*/
|
|
1006
|
+
export function wrapSqlClient(client, opts = {}) {
|
|
1007
|
+
const traceCall = (args, run) => {
|
|
1008
|
+
if (isTracingSuppressed(context.active()))
|
|
1009
|
+
return run();
|
|
1010
|
+
const attrs = { "cloudflare.binding.type": "hyperdrive" };
|
|
1011
|
+
if (opts.name)
|
|
1012
|
+
attrs["cloudflare.binding.name"] = opts.name;
|
|
1013
|
+
if (opts.dbSystem)
|
|
1014
|
+
attrs["db.system.name"] = opts.dbSystem;
|
|
1015
|
+
const first = args[0];
|
|
1016
|
+
if (Array.isArray(first) && "raw" in first)
|
|
1017
|
+
attrs["db.query.text"] = first.join("?");
|
|
1018
|
+
else if (typeof first === "string")
|
|
1019
|
+
attrs["db.query.text"] = first;
|
|
1020
|
+
else if (typeof first?.text === "string") {
|
|
1021
|
+
attrs["db.query.text"] = first.text;
|
|
1022
|
+
}
|
|
1023
|
+
const native = nativeTracingFromContext();
|
|
1024
|
+
const startSpan = () => native
|
|
1025
|
+
? startSpanFromWithSpan((n, at, _k, fn) => runNativeSpan(native, n, at, fn))("hyperdrive_query", attrs, SpanKind.CLIENT)
|
|
1026
|
+
: trace.getTracer("heystack").startSpan("hyperdrive_query", {
|
|
1027
|
+
attributes: attrs,
|
|
1028
|
+
kind: SpanKind.CLIENT,
|
|
1029
|
+
});
|
|
1030
|
+
let result;
|
|
1031
|
+
let span;
|
|
1032
|
+
try {
|
|
1033
|
+
result = run();
|
|
1034
|
+
}
|
|
1035
|
+
catch (error) {
|
|
1036
|
+
span = startSpan();
|
|
1037
|
+
recordSpanError(span, error);
|
|
1038
|
+
span.end();
|
|
1039
|
+
throw error;
|
|
1040
|
+
}
|
|
1041
|
+
if (!isPromiseLike(result)) {
|
|
1042
|
+
// Synchronous result: nothing lazy to protect.
|
|
1043
|
+
span = startSpan();
|
|
1044
|
+
span.end();
|
|
1045
|
+
return result;
|
|
1046
|
+
}
|
|
1047
|
+
// A plain native Promise is already running and `await` bypasses an own
|
|
1048
|
+
// `then`, so observe it eagerly (safe: observing cannot start anything).
|
|
1049
|
+
if (Object.getPrototypeOf(result) === Promise.prototype) {
|
|
1050
|
+
return runWithSpanPreservingReturn(startSpan(), () => result);
|
|
1051
|
+
}
|
|
1052
|
+
// LAZY thenables (postgres.js `Query`) start executing inside `then()`, so we
|
|
1053
|
+
// must never call `then` ourselves: a fragment nested in another query and
|
|
1054
|
+
// never awaited would otherwise run standalone. Instead shadow `then` on the
|
|
1055
|
+
// instance and start the span on the first consumer-driven call.
|
|
1056
|
+
const origThen = result.then;
|
|
1057
|
+
let started = false;
|
|
1058
|
+
try {
|
|
1059
|
+
Object.defineProperty(result, "then", {
|
|
1060
|
+
configurable: true,
|
|
1061
|
+
writable: true,
|
|
1062
|
+
value: function (onOk, onErr) {
|
|
1063
|
+
if (started)
|
|
1064
|
+
return origThen.call(this, onOk, onErr);
|
|
1065
|
+
started = true;
|
|
1066
|
+
const sp = startSpan();
|
|
1067
|
+
return origThen.call(this, (v) => {
|
|
1068
|
+
try {
|
|
1069
|
+
sp.end();
|
|
1070
|
+
}
|
|
1071
|
+
catch { /* never alter the result */ }
|
|
1072
|
+
return onOk ? onOk(v) : v;
|
|
1073
|
+
}, (e) => {
|
|
1074
|
+
try {
|
|
1075
|
+
recordSpanError(sp, e);
|
|
1076
|
+
sp.end();
|
|
1077
|
+
}
|
|
1078
|
+
catch { /* never alter the result */ }
|
|
1079
|
+
if (onErr)
|
|
1080
|
+
return onErr(e);
|
|
1081
|
+
throw e;
|
|
1082
|
+
});
|
|
1083
|
+
},
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
catch {
|
|
1087
|
+
// Frozen / non-extensible: return untraced.
|
|
1088
|
+
}
|
|
1089
|
+
return result;
|
|
1090
|
+
};
|
|
1091
|
+
const methods = new Map();
|
|
1092
|
+
return new Proxy(client, {
|
|
1093
|
+
get(target, prop, receiver) {
|
|
1094
|
+
const value = Reflect.get(target, prop, receiver);
|
|
1095
|
+
if (typeof value !== "function")
|
|
1096
|
+
return value;
|
|
1097
|
+
if (prop === "query" || prop === "unsafe" || prop === "execute") {
|
|
1098
|
+
let wrapped = methods.get(prop);
|
|
1099
|
+
if (!wrapped) {
|
|
1100
|
+
wrapped = (...args) => traceCall(args, () => value.apply(target, args));
|
|
1101
|
+
methods.set(prop, wrapped);
|
|
1102
|
+
}
|
|
1103
|
+
return wrapped;
|
|
1104
|
+
}
|
|
1105
|
+
return value.bind(target);
|
|
1106
|
+
},
|
|
1107
|
+
apply(target, thisArg, args) {
|
|
1108
|
+
return traceCall(args, () => Reflect.apply(target, thisArg, args));
|
|
1109
|
+
},
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which Cloudflare binding kinds Cloudflare's native tracing already spans.
|
|
3
|
+
* In native mode the SDK enriches ONLY the uncovered kinds so a D1/KV/R2/... call
|
|
4
|
+
* is never reported twice. WinterCG-safe: pure data, no imports.
|
|
5
|
+
*/
|
|
6
|
+
export type BindingKind = "d1" | "kv" | "r2" | "durable_object" | "queue" | "ai" | "vectorize" | "workflow" | "hyperdrive" | "analytics_engine" | "ratelimit" | "service" | "fetch";
|
|
7
|
+
export declare const NATIVELY_COVERED: Readonly<Record<BindingKind, boolean>>;
|
|
8
|
+
/** Native tracing does not propagate trace context to external (non-Cloudflare) services. */
|
|
9
|
+
export declare const NATIVE_PROPAGATES_EXTERNAL = false;
|
|
10
|
+
/** `enrich?.[kind] ?? !table[kind]` — `table` is a test seam. */
|
|
11
|
+
export declare function shouldEnrichNatively(kind: BindingKind, enrich?: Partial<Record<BindingKind, boolean>>, table?: Readonly<Record<BindingKind, boolean>>): boolean;
|