@heystack/otel 0.17.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 +169 -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/product.d.ts +36 -0
- package/dist/product.js +41 -3
- 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.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// `@opentelemetry/resources` and the platform `fetch` — all WinterCG-safe — and
|
|
8
8
|
// ships its own OTLP/JSON-over-fetch span exporter so it runs on Workers/Edge
|
|
9
9
|
// where the Node SDK cannot.
|
|
10
|
-
import { context, createContextKey,
|
|
10
|
+
import { context, createContextKey, trace, SpanKind, SpanStatusCode, } from "@opentelemetry/api";
|
|
11
11
|
import { suppressTracing, isTracingSuppressed } from "@opentelemetry/core";
|
|
12
12
|
import { ROOT_CONTEXT } from "@opentelemetry/api";
|
|
13
13
|
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
@@ -16,9 +16,14 @@ import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
|
|
|
16
16
|
import { buildExporterConfig, DEFAULT_ENDPOINT, releaseResourceAttributes, } from "./core.js";
|
|
17
17
|
import { isSelfSpanAttrs, safeHostname } from "./self-span.js";
|
|
18
18
|
import { llmRequestAttrs, llmResponseAttrs, llmContentAttrs, llmOperationName, resolveLLMProvider, scanSseStreamForUsage, } from "./llm-enrich.js";
|
|
19
|
-
import { instrumentEnv, makeProxy, runWithSpan } from "./workers-bindings.js";
|
|
19
|
+
import { instrumentEnv, keysOf, makeProxy, runWithSpan, runWithSpanKeyed, runWithSpanPreservingReturn, settlePendingKeyHashes, wrapSqlClient, } from "./workers-bindings.js";
|
|
20
|
+
import { NATIVELY_COVERED, NATIVE_PROPAGATES_EXTERNAL, shouldEnrichNatively } from "./workers-coverage.js";
|
|
20
21
|
import { makeSampler, loadRemoteSamplingRate } from "./workers-sampler.js";
|
|
22
|
+
import { createNativePropagation, injectOutboundHeaders, isAiGatewayHost, NATIVE_PROPAGATION_CONTEXT_KEY, propagationTargetAllowed, propagationTraceparent, } from "./workers-propagation.js";
|
|
23
|
+
import { resolveRoute } from "./workers-routes.js";
|
|
21
24
|
import { makeTraceparent } from "./traceparent.js";
|
|
25
|
+
import { NATIVE_TRACING_CONTEXT_KEY, NativeOtelSpan, getNativeTracing, nativeTracingFromContext, runNativeSpan, setNativeContextInit, } from "./workers-native.js";
|
|
26
|
+
import { applySpanPrivacy, makeUrlRedactor, } from "./redact.js";
|
|
22
27
|
// `ExportResult` / `ExportResultCode` mirror `@opentelemetry/core`. We define
|
|
23
28
|
// them inline (structurally identical) rather than import them: core is only a
|
|
24
29
|
// transitive dep of sdk-trace-base and isn't reliably resolvable, and keeping it
|
|
@@ -109,8 +114,9 @@ function toStatus(status) {
|
|
|
109
114
|
out.message = status.message;
|
|
110
115
|
return out;
|
|
111
116
|
}
|
|
112
|
-
function readableSpanToOtlp(span) {
|
|
117
|
+
function readableSpanToOtlp(span, transform) {
|
|
113
118
|
const ctx = span.spanContext();
|
|
119
|
+
const attrs = span.attributes;
|
|
114
120
|
return {
|
|
115
121
|
traceId: ctx.traceId,
|
|
116
122
|
spanId: ctx.spanId,
|
|
@@ -121,7 +127,7 @@ function readableSpanToOtlp(span) {
|
|
|
121
127
|
kind: toOtlpKind(span.kind),
|
|
122
128
|
startTimeUnixNano: hrTimeToUnixNano(span.startTime),
|
|
123
129
|
endTimeUnixNano: hrTimeToUnixNano(span.endTime),
|
|
124
|
-
attributes: toKeyValues(span
|
|
130
|
+
attributes: toKeyValues(transform ? transform(attrs, span) : attrs),
|
|
125
131
|
events: span.events.map((ev) => ({
|
|
126
132
|
timeUnixNano: hrTimeToUnixNano(ev.time),
|
|
127
133
|
name: ev.name,
|
|
@@ -136,7 +142,7 @@ function readableSpanToOtlp(span) {
|
|
|
136
142
|
* All spans in a SimpleSpanProcessor batch share the same per-request provider,
|
|
137
143
|
* hence the same resource, so we emit a single resourceSpans entry.
|
|
138
144
|
*/
|
|
139
|
-
export function serializeSpans(spans) {
|
|
145
|
+
export function serializeSpans(spans, transform) {
|
|
140
146
|
const resourceAttrs = spans[0]
|
|
141
147
|
? spans[0].resource.attributes
|
|
142
148
|
: {};
|
|
@@ -144,7 +150,7 @@ export function serializeSpans(spans) {
|
|
|
144
150
|
resourceSpans: [
|
|
145
151
|
{
|
|
146
152
|
resource: { attributes: toKeyValues(resourceAttrs) },
|
|
147
|
-
scopeSpans: [{ spans: spans.map(readableSpanToOtlp) }],
|
|
153
|
+
scopeSpans: [{ spans: spans.map((s) => readableSpanToOtlp(s, transform)) }],
|
|
148
154
|
},
|
|
149
155
|
],
|
|
150
156
|
};
|
|
@@ -228,6 +234,8 @@ export function isSelfSpanForTest(attrs, ingestHost) {
|
|
|
228
234
|
// self-span guardrail satisfied and the path WinterCG-safe (pure string logic).
|
|
229
235
|
// ---------------------------------------------------------------------------
|
|
230
236
|
let _fetchInstrumented = false;
|
|
237
|
+
/** URL redactor for outbound CLIENT spans (set from config by ensureGlobalProvider). */
|
|
238
|
+
let _fetchRedact = makeUrlRedactor();
|
|
231
239
|
let _originalFetch;
|
|
232
240
|
let _fetchWrapper;
|
|
233
241
|
/** Resolve the absolute URL of an outbound fetch arg (string | URL | Request). */
|
|
@@ -270,12 +278,6 @@ function injectTraceparent(input, init, traceparent) {
|
|
|
270
278
|
}
|
|
271
279
|
/** Max response body size to clone for gen_ai enrichment (512 KB). */
|
|
272
280
|
const LLM_RESPONSE_SIZE_CAP = 512 * 1024;
|
|
273
|
-
/**
|
|
274
|
-
* Context key under which `runNativeSpan` / `withSpan` publish the request's
|
|
275
|
-
* Cloudflare native tracer, so nested helpers (and the fetch wrapper) can join
|
|
276
|
-
* the platform trace tree instead of creating OTel-provider spans.
|
|
277
|
-
*/
|
|
278
|
-
const NATIVE_TRACING_CONTEXT_KEY = createContextKey("heystack.cloudflare.native-tracing");
|
|
279
281
|
/**
|
|
280
282
|
* Latest ingest host / ai config seen by `ensureFetchInstrumentation`. The
|
|
281
283
|
* wrapper is installed ONCE per isolate but `instrument()` may reach it first
|
|
@@ -285,6 +287,37 @@ const NATIVE_TRACING_CONTEXT_KEY = createContextKey("heystack.cloudflare.native-
|
|
|
285
287
|
*/
|
|
286
288
|
let _fetchIngestHost = "";
|
|
287
289
|
let _fetchAiCfg = {};
|
|
290
|
+
let _fetchPropagateTargets;
|
|
291
|
+
/** True when the caller already supplied header `name` on this fetch. */
|
|
292
|
+
function hasHeader(input, init, name) {
|
|
293
|
+
try {
|
|
294
|
+
return new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined)).has(name);
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
/** Bare ingest hostname for native mode (no exporter exists to derive it from). */
|
|
301
|
+
function nativeIngestHost(config) {
|
|
302
|
+
return safeHostname(config.endpoint ?? DEFAULT_ENDPOINT);
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Native root helper: record the inbound parent (only when it validates) and
|
|
306
|
+
* the invocation's synthetic propagation pair as link attributes, then run `fn`
|
|
307
|
+
* with the pair published in OTel context so outbound fetches can use it.
|
|
308
|
+
* These are SDK-minted ids, NOT Cloudflare's native ids (which don't exist).
|
|
309
|
+
*/
|
|
310
|
+
function withNativePropagation(span, inboundTraceparent, fn) {
|
|
311
|
+
const prop = createNativePropagation();
|
|
312
|
+
const parent = parseTraceparent(inboundTraceparent);
|
|
313
|
+
if (parent) {
|
|
314
|
+
span.setAttribute("heystack.parent.trace_id", parent.traceId);
|
|
315
|
+
span.setAttribute("heystack.parent.span_id", parent.spanId);
|
|
316
|
+
}
|
|
317
|
+
span.setAttribute("heystack.propagation.trace_id", prop.traceId);
|
|
318
|
+
span.setAttribute("heystack.propagation.span_id", prop.spanId);
|
|
319
|
+
return context.with(context.active().setValue(NATIVE_PROPAGATION_CONTEXT_KEY, prop), fn);
|
|
320
|
+
}
|
|
288
321
|
/** A promise whose settle handles are exposed. */
|
|
289
322
|
function deferred() {
|
|
290
323
|
let resolve;
|
|
@@ -395,7 +428,7 @@ async function enrichLLMResponse(span, provider, response, parsedReqBody, aiCfg)
|
|
|
395
428
|
* deferred — that is what lets usage tokens land on the span without buffering
|
|
396
429
|
* or delaying the stream. No OTel provider/exporter is involved.
|
|
397
430
|
*/
|
|
398
|
-
function nativeLLMFetch(nativeTracing, resolved, url, input, init, originalFetch, aiCfg) {
|
|
431
|
+
function nativeLLMFetch(nativeTracing, resolved, url, input, init, originalFetch, aiCfg, delegated = false) {
|
|
399
432
|
const ready = deferred();
|
|
400
433
|
const name = `${llmOperationName(url.pathname)} ${resolved.provider}`;
|
|
401
434
|
let run;
|
|
@@ -404,7 +437,7 @@ function nativeLLMFetch(nativeTracing, resolved, url, input, init, originalFetch
|
|
|
404
437
|
run = nativeTracing.enterSpan(name, async (nativeSpan) => {
|
|
405
438
|
invoked = true;
|
|
406
439
|
// Unsampled invocation: skip all enrichment work (no tee, no clone).
|
|
407
|
-
if (
|
|
440
|
+
if (nativeSpan.isTraced === false) {
|
|
408
441
|
try {
|
|
409
442
|
ready.resolve(await originalFetch(input, init));
|
|
410
443
|
}
|
|
@@ -413,9 +446,11 @@ function nativeLLMFetch(nativeTracing, resolved, url, input, init, originalFetch
|
|
|
413
446
|
}
|
|
414
447
|
return;
|
|
415
448
|
}
|
|
416
|
-
const adapter = new
|
|
449
|
+
const adapter = new NativeOtelSpan(nativeSpan);
|
|
417
450
|
const { attrs, parsedReqBody } = llmRequestSide(resolved, url, init);
|
|
418
451
|
adapter.setAttributes({ ...attrs, "server.address": url.hostname });
|
|
452
|
+
if (delegated)
|
|
453
|
+
adapter.setAttribute("heystack.ai.delegated", true);
|
|
419
454
|
try {
|
|
420
455
|
const response = await originalFetch(input, init);
|
|
421
456
|
adapter.setAttribute("http.response.status_code", response.status);
|
|
@@ -464,10 +499,11 @@ function nativeLLMFetch(nativeTracing, resolved, url, input, init, originalFetch
|
|
|
464
499
|
* exporter's own uploads are never traced; native callers pass "" (no exporter).
|
|
465
500
|
* `aiCfg` configures gen_ai enrichment (content capture is opt-in).
|
|
466
501
|
*/
|
|
467
|
-
function ensureFetchInstrumentation(ingestHost, aiCfg = {}) {
|
|
502
|
+
function ensureFetchInstrumentation(ingestHost, aiCfg = {}, propagateTargets) {
|
|
468
503
|
if (ingestHost)
|
|
469
504
|
_fetchIngestHost = ingestHost;
|
|
470
505
|
_fetchAiCfg = aiCfg;
|
|
506
|
+
_fetchPropagateTargets = propagateTargets;
|
|
471
507
|
if (_fetchInstrumented)
|
|
472
508
|
return;
|
|
473
509
|
_fetchInstrumented = true;
|
|
@@ -487,10 +523,27 @@ function ensureFetchInstrumentation(ingestHost, aiCfg = {}) {
|
|
|
487
523
|
const nativeTracing = active.getValue(NATIVE_TRACING_CONTEXT_KEY);
|
|
488
524
|
if (nativeTracing) {
|
|
489
525
|
const url = parseTarget(input);
|
|
490
|
-
|
|
491
|
-
if (!url || !resolved)
|
|
526
|
+
if (!url)
|
|
492
527
|
return originalFetch(input, init);
|
|
493
|
-
|
|
528
|
+
// Link headers: the invocation's synthetic ids, never to the Heystack
|
|
529
|
+
// ingest host and only to hosts allowed by `propagateTargets`.
|
|
530
|
+
let reqInput = input;
|
|
531
|
+
let reqInit = init;
|
|
532
|
+
const prop = active.getValue(NATIVE_PROPAGATION_CONTEXT_KEY);
|
|
533
|
+
const host = url.hostname.toLowerCase();
|
|
534
|
+
const gateway = isAiGatewayHost(url);
|
|
535
|
+
let injected = false;
|
|
536
|
+
if (prop && host !== _fetchIngestHost && propagationTargetAllowed(host, _fetchPropagateTargets)) {
|
|
537
|
+
injected = gateway && !hasHeader(input, init, "cf-aig-otel-trace-id");
|
|
538
|
+
[reqInput, reqInit] = injectOutboundHeaders(input, init, {
|
|
539
|
+
traceparent: propagationTraceparent(prop),
|
|
540
|
+
aiGateway: gateway ? { traceId: prop.traceId, parentSpanId: prop.spanId } : undefined,
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
const resolved = resolveLLMProvider(url, _fetchAiCfg.gateways);
|
|
544
|
+
if (!resolved)
|
|
545
|
+
return originalFetch(reqInput, reqInit);
|
|
546
|
+
return nativeLLMFetch(nativeTracing, resolved, url, reqInput, reqInit, originalFetch, _fetchAiCfg, injected && _fetchAiCfg.gatewayExport === true);
|
|
494
547
|
}
|
|
495
548
|
// Direct plane: nothing to parent to (a fetch outside a traced request).
|
|
496
549
|
if (!trace.getSpan(active)) {
|
|
@@ -507,13 +560,27 @@ function ensureFetchInstrumentation(ingestHost, aiCfg = {}) {
|
|
|
507
560
|
kind: SpanKind.CLIENT,
|
|
508
561
|
attributes: {
|
|
509
562
|
"http.request.method": method,
|
|
510
|
-
|
|
563
|
+
// Redacted: userinfo stripped + sensitive query/fragment values
|
|
564
|
+
// (e.g. Gemini `?key=`, Supabase `?apikey=`) replaced with REDACTED.
|
|
565
|
+
"url.full": _fetchRedact(target),
|
|
511
566
|
"server.address": host,
|
|
512
567
|
},
|
|
513
568
|
}, active);
|
|
514
569
|
const sc = span.spanContext();
|
|
515
570
|
const traceparent = makeTraceparent(sc);
|
|
516
|
-
|
|
571
|
+
let [reqInput, reqInit] = injectTraceparent(input, init, traceparent);
|
|
572
|
+
const directUrl = parseTarget(input);
|
|
573
|
+
const directGateway = directUrl !== null && isAiGatewayHost(directUrl);
|
|
574
|
+
if (directGateway) {
|
|
575
|
+
const ours = !hasHeader(input, init, "cf-aig-otel-trace-id");
|
|
576
|
+
// The span's REAL trace id + this CLIENT span id; existing headers win.
|
|
577
|
+
[reqInput, reqInit] = injectOutboundHeaders(reqInput, reqInit, {
|
|
578
|
+
traceparent,
|
|
579
|
+
aiGateway: { traceId: sc.traceId, parentSpanId: sc.spanId },
|
|
580
|
+
});
|
|
581
|
+
if (ours && _fetchAiCfg.gatewayExport === true)
|
|
582
|
+
span.setAttribute("heystack.ai.delegated", true);
|
|
583
|
+
}
|
|
517
584
|
// gen_ai enrichment — request side (direct host or configured gateway).
|
|
518
585
|
const url = parseTarget(input);
|
|
519
586
|
const resolved = url ? resolveLLMProvider(url, _fetchAiCfg.gateways) : null;
|
|
@@ -568,6 +635,7 @@ export function __resetFetchInstrumentation() {
|
|
|
568
635
|
_fetchWrapper = undefined;
|
|
569
636
|
_fetchIngestHost = "";
|
|
570
637
|
_fetchAiCfg = {};
|
|
638
|
+
_fetchRedact = makeUrlRedactor();
|
|
571
639
|
}
|
|
572
640
|
// ---------------------------------------------------------------------------
|
|
573
641
|
// Exporter
|
|
@@ -630,6 +698,7 @@ function markManualFlushObserved() {
|
|
|
630
698
|
* completes, letting the isolate be torn down and silently dropping the trace.
|
|
631
699
|
*/
|
|
632
700
|
async function drainProvider(provider) {
|
|
701
|
+
await settlePendingKeyHashes();
|
|
633
702
|
await provider.forceFlush().catch(() => { });
|
|
634
703
|
await provider.heystackExporter.forceFlush().catch(() => { });
|
|
635
704
|
}
|
|
@@ -681,8 +750,18 @@ export class HeystackSpanExporter {
|
|
|
681
750
|
* the `pending` set + manual `flushHeystack()` when absent/unavailable.
|
|
682
751
|
*/
|
|
683
752
|
waitUntil;
|
|
753
|
+
/**
|
|
754
|
+
* Privacy pass on every exported span — redacts URL attributes (`url.full`,
|
|
755
|
+
* `http.url`, `http.target`, `url.query`, …) so spans from host frameworks
|
|
756
|
+
* (Next/OpenNext, manual spans) are covered too, not just Heystack's own.
|
|
757
|
+
*/
|
|
758
|
+
privacy;
|
|
684
759
|
constructor(options) {
|
|
685
760
|
const cfg = buildExporterConfig(options);
|
|
761
|
+
const redact = makeUrlRedactor(options);
|
|
762
|
+
// Client IP is gated at its source (instrument()'s SERVER span), so the
|
|
763
|
+
// exporter only redacts URLs and leaves deliberate manual attributes alone.
|
|
764
|
+
this.privacy = (attrs) => applySpanPrivacy(attrs, { redact, captureClientIp: true });
|
|
686
765
|
this.url = cfg.url;
|
|
687
766
|
this.ingestHost = safeHostname(cfg.url);
|
|
688
767
|
this.headers = {
|
|
@@ -710,7 +789,7 @@ export class HeystackSpanExporter {
|
|
|
710
789
|
resultCallback({ code: ExportResultCode.SUCCESS });
|
|
711
790
|
return;
|
|
712
791
|
}
|
|
713
|
-
const body = JSON.stringify(serializeSpans(exportable));
|
|
792
|
+
const body = JSON.stringify(serializeSpans(exportable, this.privacy));
|
|
714
793
|
// Build the export chain as a promise we retain, so forceFlush() can await
|
|
715
794
|
// the actual network write — INCLUDING the bounded retry/backoff, so a
|
|
716
795
|
// waitUntil drain keeps the isolate alive until retries settle too. It
|
|
@@ -891,87 +970,31 @@ export class HeystackSpanExporter {
|
|
|
891
970
|
}
|
|
892
971
|
}
|
|
893
972
|
}
|
|
894
|
-
function nativeTracingFromContext() {
|
|
895
|
-
return context.active().getValue(NATIVE_TRACING_CONTEXT_KEY);
|
|
896
|
-
}
|
|
897
973
|
/**
|
|
898
|
-
*
|
|
899
|
-
*
|
|
900
|
-
*
|
|
974
|
+
* Native mode: wrap the env's uncovered bindings (Workers AI, Vectorize,
|
|
975
|
+
* Workflows, Analytics Engine) so their calls become native child spans. An
|
|
976
|
+
* explicit `instrumentBindings` wins over the coverage gate.
|
|
901
977
|
*/
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
this.nativeSpan.setAttribute(key, JSON.stringify(value));
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
catch {
|
|
918
|
-
// Telemetry must never surface as an application error (e.g. a platform
|
|
919
|
-
// span rejecting a value); drop the attribute instead.
|
|
920
|
-
}
|
|
921
|
-
return this;
|
|
922
|
-
}
|
|
923
|
-
setAttributes(attributes) {
|
|
924
|
-
for (const [key, value] of Object.entries(attributes)) {
|
|
925
|
-
if (value !== undefined)
|
|
926
|
-
this.setAttribute(key, value);
|
|
927
|
-
}
|
|
928
|
-
return this;
|
|
929
|
-
}
|
|
930
|
-
addEvent(name, attributesOrStartTime) {
|
|
931
|
-
const prefix = `event.${name}`;
|
|
932
|
-
this.nativeSpan.setAttribute(prefix, true);
|
|
933
|
-
if (attributesOrStartTime && !Array.isArray(attributesOrStartTime) && typeof attributesOrStartTime === "object") {
|
|
934
|
-
for (const [key, value] of Object.entries(attributesOrStartTime)) {
|
|
935
|
-
if (value !== undefined)
|
|
936
|
-
this.setAttribute(`${prefix}.${key}`, value);
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
|
-
return this;
|
|
940
|
-
}
|
|
941
|
-
addLink(_link) { return this; }
|
|
942
|
-
addLinks(_links) { return this; }
|
|
943
|
-
/** True once `recordException` stamped a specific `error.type` (the exception class). */
|
|
944
|
-
errorTyped = false;
|
|
945
|
-
setStatus(status) {
|
|
946
|
-
if (status.code === SpanStatusCode.ERROR) {
|
|
947
|
-
// Don't clobber the more specific class name a preceding recordException set.
|
|
948
|
-
if (!this.errorTyped)
|
|
949
|
-
this.nativeSpan.setAttribute("error.type", "error");
|
|
950
|
-
if (status.message)
|
|
951
|
-
this.nativeSpan.setAttribute("error.message", status.message);
|
|
952
|
-
}
|
|
953
|
-
return this;
|
|
954
|
-
}
|
|
955
|
-
updateName(_name) { return this; }
|
|
956
|
-
end(_endTime) { }
|
|
957
|
-
isRecording() { return this.nativeSpan.isTraced; }
|
|
958
|
-
recordException(exception, _time) {
|
|
959
|
-
const error = exception instanceof Error ? exception : new Error(String(exception));
|
|
960
|
-
this.nativeSpan.setAttribute("error.type", error.name || "Error");
|
|
961
|
-
this.errorTyped = true;
|
|
962
|
-
this.nativeSpan.setAttribute("error.message", error.message);
|
|
963
|
-
if (error.stack)
|
|
964
|
-
this.nativeSpan.setAttribute("exception.stacktrace", error.stack);
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
|
-
function getNativeTracing(ctx) {
|
|
968
|
-
const candidate = ctx.tracing;
|
|
969
|
-
return candidate && typeof candidate.enterSpan === "function" ? candidate : undefined;
|
|
978
|
+
function nativeInstrumentedEnv(env, tracing, config) {
|
|
979
|
+
if (!env || typeof env !== "object")
|
|
980
|
+
return env;
|
|
981
|
+
const explicit = config.instrumentBindings;
|
|
982
|
+
if (explicit === false)
|
|
983
|
+
return env;
|
|
984
|
+
return instrumentEnv(env, {
|
|
985
|
+
select: explicit ? explicit : true,
|
|
986
|
+
kindFilter: explicit ? undefined : (k) => shouldEnrichNatively(k, config.nativeEnrichment),
|
|
987
|
+
withSpan: (name, attrs, _kind, fn) => runNativeSpan(tracing, name, attrs, fn),
|
|
988
|
+
nativeNames: true,
|
|
989
|
+
});
|
|
970
990
|
}
|
|
971
991
|
function setNativeRequestAttributes(span, config, req) {
|
|
972
|
-
const url = new URL(req.url);
|
|
973
992
|
span.setAttribute("service.name", config.service);
|
|
974
|
-
|
|
993
|
+
// Labels the heystack.request child only; the fetch wrapper also sets the
|
|
994
|
+
// platform invocation span via `getActiveSpan()` when the runtime exposes it.
|
|
995
|
+
const route = resolveRoute(req, config);
|
|
996
|
+
if (route)
|
|
997
|
+
span.setAttribute("http.route", route);
|
|
975
998
|
if (config.version)
|
|
976
999
|
span.setAttribute("service.version", config.version);
|
|
977
1000
|
if (config.build)
|
|
@@ -985,26 +1008,6 @@ function setNativeRequestAttributes(span, config, req) {
|
|
|
985
1008
|
if (requestId)
|
|
986
1009
|
span.setAttribute("http.request.id", requestId);
|
|
987
1010
|
}
|
|
988
|
-
async function runNativeSpan(nativeTracing, name, attrs, fn) {
|
|
989
|
-
ensureContextManager();
|
|
990
|
-
return nativeTracing.enterSpan(name, async (nativeSpan) => {
|
|
991
|
-
const adapter = new NativeSpanAdapter(nativeSpan);
|
|
992
|
-
for (const [key, value] of Object.entries(attrs)) {
|
|
993
|
-
if (value !== undefined)
|
|
994
|
-
adapter.setAttribute(key, value);
|
|
995
|
-
}
|
|
996
|
-
const nativeContext = trace.setSpan(context.active().setValue(NATIVE_TRACING_CONTEXT_KEY, nativeTracing), adapter);
|
|
997
|
-
try {
|
|
998
|
-
return await context.with(nativeContext, () => fn(adapter));
|
|
999
|
-
}
|
|
1000
|
-
catch (error) {
|
|
1001
|
-
const err = error instanceof Error ? error : new Error(String(error));
|
|
1002
|
-
adapter.recordException(err);
|
|
1003
|
-
adapter.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
|
|
1004
|
-
throw error;
|
|
1005
|
-
}
|
|
1006
|
-
});
|
|
1007
|
-
}
|
|
1008
1011
|
/**
|
|
1009
1012
|
* Build a `BasicTracerProvider` wired to a `HeystackSpanExporter` via a
|
|
1010
1013
|
* `SimpleSpanProcessor`. Exposed for advanced users who want to add manual
|
|
@@ -1022,10 +1025,18 @@ export function createTracerProvider(config) {
|
|
|
1022
1025
|
// only when the caller supplied `version`/`build` (release attribution).
|
|
1023
1026
|
resource: resourceFromAttributes({
|
|
1024
1027
|
[ATTR_SERVICE_NAME]: config.service,
|
|
1028
|
+
// Lets ingest recognise direct-mode Workers apps as Cloudflare (route
|
|
1029
|
+
// templating, native-error and binding inference).
|
|
1030
|
+
"cloud.provider": "cloudflare",
|
|
1031
|
+
"cloud.platform": "cloudflare.workers",
|
|
1032
|
+
// Marks direct-mode SDK spans so coverage never reports a wrapped Worker as native-only.
|
|
1033
|
+
"heystack.sdk.mode": "direct",
|
|
1025
1034
|
...releaseResourceAttributes(config),
|
|
1026
1035
|
}),
|
|
1027
1036
|
spanProcessors: [new SimpleSpanProcessor(exporter)],
|
|
1028
|
-
sampler: makeSampler(config.sampling
|
|
1037
|
+
sampler: makeSampler(config.sampling, {
|
|
1038
|
+
trustRemoteParent: config.trustInboundSampling !== false,
|
|
1039
|
+
}),
|
|
1029
1040
|
});
|
|
1030
1041
|
// Attach the exporter so flush paths can await its in-flight fetches.
|
|
1031
1042
|
return Object.assign(provider, { heystackExporter: exporter });
|
|
@@ -1125,6 +1136,7 @@ function ensureContextManager() {
|
|
|
1125
1136
|
const mgr = getALS() ? new AlsContextManager() : new SyncStackContextManager();
|
|
1126
1137
|
context.setGlobalContextManager(mgr.enable());
|
|
1127
1138
|
}
|
|
1139
|
+
setNativeContextInit(ensureContextManager);
|
|
1128
1140
|
/** Reset the context-manager registration guard. Internal/testing helper. */
|
|
1129
1141
|
export function __resetContextManager() {
|
|
1130
1142
|
_contextManagerRegistered = false;
|
|
@@ -1157,7 +1169,8 @@ function ensureGlobalProvider(config) {
|
|
|
1157
1169
|
// Patch globalThis.fetch (once) so outbound subrequests get CLIENT child spans
|
|
1158
1170
|
// + `traceparent` injection (distributed tracing). The exporter's own POST uses
|
|
1159
1171
|
// the captured original fetch, so it is never re-entered by this wrapper.
|
|
1160
|
-
|
|
1172
|
+
_fetchRedact = makeUrlRedactor(config);
|
|
1173
|
+
ensureFetchInstrumentation(_provider.heystackExporter.ingestHost, config.ai ?? {}, config.propagateTargets);
|
|
1161
1174
|
return _provider;
|
|
1162
1175
|
}
|
|
1163
1176
|
/**
|
|
@@ -1190,6 +1203,13 @@ export async function flushHeystack() {
|
|
|
1190
1203
|
markManualFlushObserved();
|
|
1191
1204
|
if (!_provider)
|
|
1192
1205
|
return;
|
|
1206
|
+
// Bounded by the same budget as the export drain so a stuck hash can't pin us.
|
|
1207
|
+
let timer;
|
|
1208
|
+
await Promise.race([
|
|
1209
|
+
settlePendingKeyHashes(),
|
|
1210
|
+
new Promise((resolve) => { timer = setTimeout(resolve, DRAIN_TIMEOUT_MS); }),
|
|
1211
|
+
]).finally(() => { if (timer)
|
|
1212
|
+
clearTimeout(timer); });
|
|
1193
1213
|
if (typeof _provider.forceFlush === "function") {
|
|
1194
1214
|
await _provider.forceFlush();
|
|
1195
1215
|
}
|
|
@@ -1289,6 +1309,9 @@ export function instrument(handler, config) {
|
|
|
1289
1309
|
build: config.build,
|
|
1290
1310
|
waitUntil: config.waitUntil,
|
|
1291
1311
|
sampling: config.sampling,
|
|
1312
|
+
trustInboundSampling: config.trustInboundSampling,
|
|
1313
|
+
redactQueryParams: config.redactQueryParams,
|
|
1314
|
+
captureClientIp: config.captureClientIp,
|
|
1292
1315
|
ai: config.ai,
|
|
1293
1316
|
});
|
|
1294
1317
|
// instrument() wires a per-request drain (see `drain` below), so exports on
|
|
@@ -1296,6 +1319,9 @@ export function instrument(handler, config) {
|
|
|
1296
1319
|
requestDrainWired = true;
|
|
1297
1320
|
return { provider, tracer: trace.getTracer("heystack") };
|
|
1298
1321
|
};
|
|
1322
|
+
// Privacy: one redactor per instrument() call for the inbound SERVER span URL.
|
|
1323
|
+
const redactInboundUrl = makeUrlRedactor(config);
|
|
1324
|
+
const captureGeo = config.captureGeo ?? "country";
|
|
1299
1325
|
// Drain BOTH the provider's span processor AND the exporter's in-flight fetch
|
|
1300
1326
|
// via ctx.waitUntil, raced against an ~8s timeout (see drainWithTimeout, #25).
|
|
1301
1327
|
const drain = (provider, ctx) => {
|
|
@@ -1307,7 +1333,7 @@ export function instrument(handler, config) {
|
|
|
1307
1333
|
const originalFetch = handler.fetch?.bind(handler);
|
|
1308
1334
|
if (originalFetch) {
|
|
1309
1335
|
wrapped.fetch = async (req, env, ctx) => {
|
|
1310
|
-
const nativeTracing = config.nativeTracing === "direct" ?
|
|
1336
|
+
const nativeTracing = config.nativeTracing === "direct" ? null : getNativeTracing(ctx, config.tracing);
|
|
1311
1337
|
if (nativeTracing) {
|
|
1312
1338
|
// `withSpan()` discovers the request's native tracer through OTel
|
|
1313
1339
|
// context. Register our WinterCG-safe manager even though native mode
|
|
@@ -1316,14 +1342,25 @@ export function instrument(handler, config) {
|
|
|
1316
1342
|
// Install the (LLM-only under native) fetch wrapper so outbound calls
|
|
1317
1343
|
// to LLM providers/gateways still get gen_ai.* enrichment — native mode
|
|
1318
1344
|
// never runs `setup()`, which is where the direct plane installs it.
|
|
1319
|
-
ensureFetchInstrumentation(
|
|
1345
|
+
ensureFetchInstrumentation(nativeIngestHost(config), config.ai ?? {}, config.propagateTargets);
|
|
1346
|
+
// The platform's invocation (root fetch) span is what Service Health/RED
|
|
1347
|
+
// read. When the runtime exposes it, put the route template there too;
|
|
1348
|
+
// otherwise `routes` labels only the `heystack.request` child.
|
|
1349
|
+
try {
|
|
1350
|
+
const rootRoute = resolveRoute(req, config);
|
|
1351
|
+
if (rootRoute)
|
|
1352
|
+
nativeTracing.getActiveSpan?.()?.setAttribute("http.route", rootRoute);
|
|
1353
|
+
}
|
|
1354
|
+
catch { /* telemetry must never surface */ }
|
|
1320
1355
|
return runNativeSpan(nativeTracing, "heystack.request", {}, async (adapter) => {
|
|
1321
1356
|
setNativeRequestAttributes(adapter, config, req);
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1357
|
+
return withNativePropagation(adapter, req.headers.get("traceparent"), async () => {
|
|
1358
|
+
const response = await originalFetch(req, nativeInstrumentedEnv(env, nativeTracing, config), ctx);
|
|
1359
|
+
adapter.setAttribute("http.response.status_code", response.status);
|
|
1360
|
+
if (response.status >= 500)
|
|
1361
|
+
adapter.setAttribute("error.type", "http_error");
|
|
1362
|
+
return response;
|
|
1363
|
+
});
|
|
1327
1364
|
});
|
|
1328
1365
|
}
|
|
1329
1366
|
const s = setup(env);
|
|
@@ -1362,12 +1399,16 @@ export function instrument(handler, config) {
|
|
|
1362
1399
|
kind: SpanKind.SERVER,
|
|
1363
1400
|
attributes: {
|
|
1364
1401
|
"http.request.method": req.method,
|
|
1365
|
-
"url.full": req.url,
|
|
1402
|
+
"url.full": redactInboundUrl(req.url),
|
|
1366
1403
|
"url.path": url.pathname,
|
|
1367
1404
|
"server.address": url.host,
|
|
1405
|
+
"cloudflare.handler_type": "fetch",
|
|
1368
1406
|
},
|
|
1369
1407
|
}, startCtx);
|
|
1370
|
-
|
|
1408
|
+
const directRoute = resolveRoute(req, config);
|
|
1409
|
+
if (directRoute)
|
|
1410
|
+
span.setAttribute("http.route", directRoute);
|
|
1411
|
+
// A1 enrichment: identity, request id, (opt-in) client ip, geo.
|
|
1371
1412
|
// All attributes are set only when non-empty to keep spans lean.
|
|
1372
1413
|
const userInfo = config.getUser?.(req);
|
|
1373
1414
|
if (userInfo?.id)
|
|
@@ -1377,20 +1418,26 @@ export function instrument(handler, config) {
|
|
|
1377
1418
|
const reqId = userInfo?.requestId ?? req.headers.get("cf-ray") ?? "";
|
|
1378
1419
|
if (reqId)
|
|
1379
1420
|
span.setAttribute("http.request.id", reqId);
|
|
1380
|
-
|
|
1381
|
-
if (
|
|
1382
|
-
|
|
1421
|
+
// End-user IP is personal data: recorded only with `captureClientIp: true`.
|
|
1422
|
+
if (config.captureClientIp === true) {
|
|
1423
|
+
const clientIp = req.headers.get("CF-Connecting-IP") ?? "";
|
|
1424
|
+
if (clientIp)
|
|
1425
|
+
span.setAttribute("client.address", clientIp);
|
|
1426
|
+
}
|
|
1383
1427
|
// Cloudflare geo — only present on the real CF runtime; read defensively.
|
|
1428
|
+
// Default is country-only; region/city/ASN need `captureGeo: "full"`.
|
|
1384
1429
|
const cf = req.cf;
|
|
1385
|
-
if (cf) {
|
|
1430
|
+
if (cf && captureGeo) {
|
|
1386
1431
|
if (typeof cf.country === "string" && cf.country)
|
|
1387
1432
|
span.setAttribute("geo.country", cf.country);
|
|
1388
|
-
if (
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1433
|
+
if (captureGeo === "full") {
|
|
1434
|
+
if (typeof cf.region === "string" && cf.region)
|
|
1435
|
+
span.setAttribute("geo.region", cf.region);
|
|
1436
|
+
if (typeof cf.city === "string" && cf.city)
|
|
1437
|
+
span.setAttribute("geo.city", cf.city);
|
|
1438
|
+
if (cf.asn != null)
|
|
1439
|
+
span.setAttribute("geo.asn", String(cf.asn));
|
|
1440
|
+
}
|
|
1394
1441
|
}
|
|
1395
1442
|
// FR5: response header carrying THIS span's trace + span id.
|
|
1396
1443
|
const sc = span.spanContext();
|
|
@@ -1558,19 +1605,21 @@ export function instrument(handler, config) {
|
|
|
1558
1605
|
const originalQueue = handler.queue?.bind(handler);
|
|
1559
1606
|
if (originalQueue) {
|
|
1560
1607
|
wrapped.queue = async (batch, env, ctx) => {
|
|
1561
|
-
const nativeTracing = config.nativeTracing === "direct" ?
|
|
1608
|
+
const nativeTracing = config.nativeTracing === "direct" ? null : getNativeTracing(ctx, config.tracing);
|
|
1562
1609
|
if (nativeTracing) {
|
|
1563
|
-
ensureFetchInstrumentation(
|
|
1610
|
+
ensureFetchInstrumentation(nativeIngestHost(config), config.ai ?? {}, config.propagateTargets);
|
|
1564
1611
|
// Native mode: Cloudflare owns queue spans — pass the ORIGINAL batch
|
|
1565
1612
|
// through untouched so platform objects keep their identity.
|
|
1566
1613
|
return runNativeSpan(nativeTracing, `heystack.queue ${batch.queue}`, {
|
|
1567
1614
|
"service.name": config.service,
|
|
1568
1615
|
"messaging.batch.message_count": batch.messages.length,
|
|
1616
|
+
"cloudflare.queue.batch_size": batch.messages.length,
|
|
1617
|
+
"cloudflare.handler_type": "queue",
|
|
1569
1618
|
"messaging.destination.name": batch.queue,
|
|
1570
1619
|
"messaging.operation.type": "process",
|
|
1571
1620
|
"service.version": config.version,
|
|
1572
1621
|
"service.build": config.build,
|
|
1573
|
-
}, () => originalQueue(batch, env, ctx));
|
|
1622
|
+
}, (adapter) => withNativePropagation(adapter, null, () => originalQueue(batch, nativeInstrumentedEnv(env, nativeTracing, config), ctx)));
|
|
1574
1623
|
}
|
|
1575
1624
|
const s = setup(env);
|
|
1576
1625
|
if (!s)
|
|
@@ -1580,7 +1629,9 @@ export function instrument(handler, config) {
|
|
|
1580
1629
|
kind: SpanKind.CONSUMER,
|
|
1581
1630
|
attributes: {
|
|
1582
1631
|
"messaging.batch.message_count": batch.messages.length,
|
|
1632
|
+
"cloudflare.queue.batch_size": batch.messages.length,
|
|
1583
1633
|
"messaging.destination.name": batch.queue,
|
|
1634
|
+
"cloudflare.handler_type": "queue",
|
|
1584
1635
|
},
|
|
1585
1636
|
});
|
|
1586
1637
|
// The request's CONSUMER context, captured per-batch as an explicit parent
|
|
@@ -1620,16 +1671,18 @@ export function instrument(handler, config) {
|
|
|
1620
1671
|
const originalScheduled = handler.scheduled?.bind(handler);
|
|
1621
1672
|
if (originalScheduled) {
|
|
1622
1673
|
wrapped.scheduled = async (controller, env, ctx) => {
|
|
1623
|
-
const nativeTracing = config.nativeTracing === "direct" ?
|
|
1674
|
+
const nativeTracing = config.nativeTracing === "direct" ? null : getNativeTracing(ctx, config.tracing);
|
|
1624
1675
|
if (nativeTracing) {
|
|
1625
|
-
ensureFetchInstrumentation(
|
|
1676
|
+
ensureFetchInstrumentation(nativeIngestHost(config), config.ai ?? {}, config.propagateTargets);
|
|
1626
1677
|
return runNativeSpan(nativeTracing, `heystack.scheduled ${controller.cron}`, {
|
|
1627
1678
|
"service.name": config.service,
|
|
1628
1679
|
"faas.trigger": "timer",
|
|
1629
1680
|
"cloudflare.scheduled.cron": controller.cron,
|
|
1681
|
+
"faas.cron": controller.cron,
|
|
1682
|
+
"cloudflare.handler_type": "scheduled",
|
|
1630
1683
|
"service.version": config.version,
|
|
1631
1684
|
"service.build": config.build,
|
|
1632
|
-
}, () => originalScheduled(controller, env, ctx));
|
|
1685
|
+
}, (adapter) => withNativePropagation(adapter, null, () => originalScheduled(controller, nativeInstrumentedEnv(env, nativeTracing, config), ctx)));
|
|
1633
1686
|
}
|
|
1634
1687
|
const s = setup(env);
|
|
1635
1688
|
if (!s)
|
|
@@ -1639,6 +1692,8 @@ export function instrument(handler, config) {
|
|
|
1639
1692
|
kind: SpanKind.INTERNAL,
|
|
1640
1693
|
attributes: {
|
|
1641
1694
|
"controller.cron": controller.cron,
|
|
1695
|
+
"faas.cron": controller.cron,
|
|
1696
|
+
"cloudflare.handler_type": "scheduled",
|
|
1642
1697
|
},
|
|
1643
1698
|
});
|
|
1644
1699
|
try {
|
|
@@ -1662,7 +1717,7 @@ export function instrument(handler, config) {
|
|
|
1662
1717
|
const originalEmail = handler.email?.bind(handler);
|
|
1663
1718
|
if (originalEmail) {
|
|
1664
1719
|
wrapped.email = async (message, env, ctx) => {
|
|
1665
|
-
const nativeTracing = config.nativeTracing === "direct" ?
|
|
1720
|
+
const nativeTracing = config.nativeTracing === "direct" ? null : getNativeTracing(ctx, config.tracing);
|
|
1666
1721
|
// PRIVACY: only from/to DOMAINS + envelope size are recorded — never the
|
|
1667
1722
|
// full addresses and never any message content.
|
|
1668
1723
|
const { name: emailSpanName, attrs: emailAttrs } = emailHandlerAttrs(message);
|
|
@@ -1672,7 +1727,7 @@ export function instrument(handler, config) {
|
|
|
1672
1727
|
...emailAttrs,
|
|
1673
1728
|
"service.version": config.version,
|
|
1674
1729
|
"service.build": config.build,
|
|
1675
|
-
}, () => originalEmail(message, env, ctx));
|
|
1730
|
+
}, (adapter) => withNativePropagation(adapter, null, () => originalEmail(message, nativeInstrumentedEnv(env, nativeTracing, config), ctx)));
|
|
1676
1731
|
}
|
|
1677
1732
|
const s = setup(env);
|
|
1678
1733
|
if (!s)
|
|
@@ -1680,7 +1735,7 @@ export function instrument(handler, config) {
|
|
|
1680
1735
|
const { provider, tracer } = s;
|
|
1681
1736
|
const span = tracer.startSpan(emailSpanName, {
|
|
1682
1737
|
kind: SpanKind.SERVER,
|
|
1683
|
-
attributes: emailAttrs,
|
|
1738
|
+
attributes: { ...emailAttrs, "cloudflare.handler_type": "email" },
|
|
1684
1739
|
});
|
|
1685
1740
|
try {
|
|
1686
1741
|
await context.with(trace.setSpan(context.active(), span), () => originalEmail(message, env, wrapExecutionContextWithProvider(ctx, provider)));
|
|
@@ -1703,7 +1758,7 @@ export function instrument(handler, config) {
|
|
|
1703
1758
|
const originalTail = handler.tail?.bind(handler);
|
|
1704
1759
|
if (originalTail) {
|
|
1705
1760
|
wrapped.tail = async (events, env, ctx) => {
|
|
1706
|
-
const nativeTracing = config.nativeTracing === "direct" ?
|
|
1761
|
+
const nativeTracing = config.nativeTracing === "direct" ? null : getNativeTracing(ctx, config.tracing);
|
|
1707
1762
|
// PRIVACY: only the event COUNT is recorded — tail payloads can contain
|
|
1708
1763
|
// request URLs, headers and console output; none of it is captured.
|
|
1709
1764
|
const count = Array.isArray(events) ? events.length : 0;
|
|
@@ -1722,7 +1777,7 @@ export function instrument(handler, config) {
|
|
|
1722
1777
|
const { provider, tracer } = s;
|
|
1723
1778
|
const span = tracer.startSpan("tail", {
|
|
1724
1779
|
kind: SpanKind.INTERNAL,
|
|
1725
|
-
attributes: attrs,
|
|
1780
|
+
attributes: { ...attrs, "cloudflare.handler_type": "tail" },
|
|
1726
1781
|
});
|
|
1727
1782
|
try {
|
|
1728
1783
|
await context.with(trace.setSpan(context.active(), span), () => originalTail(events, env, wrapExecutionContextWithProvider(ctx, provider)));
|
|
@@ -1862,7 +1917,18 @@ function classStartSpan(opts) {
|
|
|
1862
1917
|
* to the invocation's DurableObjectState `waitUntil`. Best-effort no-op when no
|
|
1863
1918
|
* state/waitUntil is available (e.g. unit tests).
|
|
1864
1919
|
*/
|
|
1865
|
-
function scheduleStateFlush(state) {
|
|
1920
|
+
function scheduleStateFlush(state, invocation) {
|
|
1921
|
+
// SQL cursors the app never finished iterating: close THIS invocation's spans
|
|
1922
|
+
// (without row counts); concurrent invocations on the same DO keep theirs.
|
|
1923
|
+
if (state && typeof state === "object") {
|
|
1924
|
+
const pending = pendingSqlSpans.get(state);
|
|
1925
|
+
if (pending) {
|
|
1926
|
+
for (const p of [...pending]) {
|
|
1927
|
+
if (invocation === undefined || p.owner === undefined || p.owner === invocation)
|
|
1928
|
+
p.abandon();
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1866
1932
|
const waitUntil = state
|
|
1867
1933
|
?.waitUntil;
|
|
1868
1934
|
if (typeof waitUntil !== "function")
|
|
@@ -1874,8 +1940,152 @@ function scheduleStateFlush(state) {
|
|
|
1874
1940
|
// A failing waitUntil must never break the wrapped handler.
|
|
1875
1941
|
}
|
|
1876
1942
|
}
|
|
1877
|
-
/**
|
|
1878
|
-
|
|
1943
|
+
/** Marks the DO handler invocation in context; nested spans inherit it. */
|
|
1944
|
+
const DO_INVOCATION_KEY = createContextKey("heystack.do.invocation");
|
|
1945
|
+
/** Open `sql.exec` spans per raw DurableObjectState, closed when the invocation ends. */
|
|
1946
|
+
const pendingSqlSpans = new WeakMap();
|
|
1947
|
+
/**
|
|
1948
|
+
* `ctx.storage.sql.exec()` → `durable_object_storage_exec`. The cursor is lazy and
|
|
1949
|
+
* owned by the app: we never step it. The span ends (with rows read/written) when
|
|
1950
|
+
* the app completes it via `toArray()`, `one()` or iterator exhaustion, and is
|
|
1951
|
+
* closed without counts if the invocation ends first.
|
|
1952
|
+
*/
|
|
1953
|
+
function wrapDurableObjectSql(sql, startSpan, doId, pending) {
|
|
1954
|
+
return new Proxy(sql, {
|
|
1955
|
+
get(t, prop) {
|
|
1956
|
+
const val = Reflect.get(t, prop, t);
|
|
1957
|
+
if (typeof val !== "function")
|
|
1958
|
+
return val;
|
|
1959
|
+
if (prop !== "exec")
|
|
1960
|
+
return val.bind(t);
|
|
1961
|
+
return (query, ...bindings) => {
|
|
1962
|
+
if (isTracingSuppressed(context.active()))
|
|
1963
|
+
return val.call(t, query, ...bindings);
|
|
1964
|
+
const attrs = {};
|
|
1965
|
+
if (typeof query === "string")
|
|
1966
|
+
attrs["db.query.text"] = query;
|
|
1967
|
+
if (doId)
|
|
1968
|
+
attrs["cloudflare.durable_object.id"] = doId;
|
|
1969
|
+
const span = startSpan("durable_object_storage_exec", attrs, SpanKind.INTERNAL);
|
|
1970
|
+
let cursor;
|
|
1971
|
+
try {
|
|
1972
|
+
cursor = val.call(t, query, ...bindings);
|
|
1973
|
+
}
|
|
1974
|
+
catch (error) {
|
|
1975
|
+
span.recordException(error instanceof Error ? error : new Error(String(error)));
|
|
1976
|
+
span.setStatus({
|
|
1977
|
+
code: SpanStatusCode.ERROR,
|
|
1978
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1979
|
+
});
|
|
1980
|
+
span.end();
|
|
1981
|
+
throw error;
|
|
1982
|
+
}
|
|
1983
|
+
if (!cursor || typeof cursor !== "object") {
|
|
1984
|
+
span.end();
|
|
1985
|
+
return cursor;
|
|
1986
|
+
}
|
|
1987
|
+
let done = false;
|
|
1988
|
+
let lastActivity = Date.now();
|
|
1989
|
+
const owner = context.active().getValue(DO_INVOCATION_KEY);
|
|
1990
|
+
const finish = (counts, error) => {
|
|
1991
|
+
if (done)
|
|
1992
|
+
return;
|
|
1993
|
+
done = true;
|
|
1994
|
+
pending.delete(entry);
|
|
1995
|
+
try {
|
|
1996
|
+
if (error !== undefined) {
|
|
1997
|
+
span.recordException(error instanceof Error ? error : new Error(String(error)));
|
|
1998
|
+
span.setStatus({
|
|
1999
|
+
code: SpanStatusCode.ERROR,
|
|
2000
|
+
message: error instanceof Error ? error.message : String(error),
|
|
2001
|
+
});
|
|
2002
|
+
}
|
|
2003
|
+
if (counts) {
|
|
2004
|
+
// Side-effect-free getters: reading them never steps the cursor.
|
|
2005
|
+
if (typeof cursor.rowsRead === "number") {
|
|
2006
|
+
span.setAttribute("cloudflare.durable_object.response.rows_read", cursor.rowsRead);
|
|
2007
|
+
}
|
|
2008
|
+
if (typeof cursor.rowsWritten === "number") {
|
|
2009
|
+
span.setAttribute("cloudflare.durable_object.response.rows_written", cursor.rowsWritten);
|
|
2010
|
+
}
|
|
2011
|
+
span.end();
|
|
2012
|
+
}
|
|
2013
|
+
else {
|
|
2014
|
+
// Abandoned: the work effectively ended at the last observed activity.
|
|
2015
|
+
span.end(lastActivity);
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
catch {
|
|
2019
|
+
// Telemetry must never alter the cursor's behaviour.
|
|
2020
|
+
}
|
|
2021
|
+
};
|
|
2022
|
+
const entry = { owner, abandon: () => finish(false) };
|
|
2023
|
+
pending.add(entry);
|
|
2024
|
+
// A statement with no result columns (INSERT/UPDATE/DDL) has fully run
|
|
2025
|
+
// by the time exec() returns; nothing will ever be iterated.
|
|
2026
|
+
if (Array.isArray(cursor.columnNames) && cursor.columnNames.length === 0) {
|
|
2027
|
+
finish(true);
|
|
2028
|
+
return cursor;
|
|
2029
|
+
}
|
|
2030
|
+
const step = (fn, c, a) => {
|
|
2031
|
+
let r;
|
|
2032
|
+
try {
|
|
2033
|
+
r = fn.apply(c, a);
|
|
2034
|
+
}
|
|
2035
|
+
catch (error) {
|
|
2036
|
+
finish(false, error);
|
|
2037
|
+
throw error;
|
|
2038
|
+
}
|
|
2039
|
+
lastActivity = Date.now();
|
|
2040
|
+
if (r && r.done === true)
|
|
2041
|
+
finish(true);
|
|
2042
|
+
return r;
|
|
2043
|
+
};
|
|
2044
|
+
// One observer for the cursor and for raw() iterators: completion is
|
|
2045
|
+
// toArray()/one()/iterator exhaustion; nothing is ever stepped by us.
|
|
2046
|
+
const observe = (target) => {
|
|
2047
|
+
if (!target || typeof target !== "object")
|
|
2048
|
+
return target;
|
|
2049
|
+
const proxy = new Proxy(target, {
|
|
2050
|
+
get(c, p) {
|
|
2051
|
+
const v = Reflect.get(c, p, c);
|
|
2052
|
+
if (typeof v !== "function")
|
|
2053
|
+
return v;
|
|
2054
|
+
if (p === "toArray" || p === "one") {
|
|
2055
|
+
return (...a) => {
|
|
2056
|
+
try {
|
|
2057
|
+
const r = v.apply(c, a);
|
|
2058
|
+
finish(true);
|
|
2059
|
+
return r;
|
|
2060
|
+
}
|
|
2061
|
+
catch (error) {
|
|
2062
|
+
finish(false, error);
|
|
2063
|
+
throw error;
|
|
2064
|
+
}
|
|
2065
|
+
};
|
|
2066
|
+
}
|
|
2067
|
+
if (p === "next")
|
|
2068
|
+
return (...a) => step(v, c, a);
|
|
2069
|
+
if (p === Symbol.iterator) {
|
|
2070
|
+
return () => {
|
|
2071
|
+
const it = v.call(c);
|
|
2072
|
+
return it === c ? proxy : observe(it);
|
|
2073
|
+
};
|
|
2074
|
+
}
|
|
2075
|
+
if (p === "raw")
|
|
2076
|
+
return (...a) => observe(v.apply(c, a));
|
|
2077
|
+
return v.bind(c);
|
|
2078
|
+
},
|
|
2079
|
+
});
|
|
2080
|
+
return proxy;
|
|
2081
|
+
};
|
|
2082
|
+
return observe(cursor);
|
|
2083
|
+
};
|
|
2084
|
+
},
|
|
2085
|
+
});
|
|
2086
|
+
}
|
|
2087
|
+
/** INTERNAL child spans for DurableObjectStorage get/put/delete/list, alarms and SQL. */
|
|
2088
|
+
function wrapDurableObjectStorage(storage, startSpan, doId, pending = new Set()) {
|
|
1879
2089
|
const wrapOp = (op) => async (...args) => {
|
|
1880
2090
|
const underlying = storage[op];
|
|
1881
2091
|
if (typeof underlying !== "function")
|
|
@@ -1884,11 +2094,10 @@ function wrapDurableObjectStorage(storage, startSpan) {
|
|
|
1884
2094
|
return underlying.apply(storage, args);
|
|
1885
2095
|
}
|
|
1886
2096
|
const attrs = {};
|
|
2097
|
+
if (doId)
|
|
2098
|
+
attrs["cloudflare.durable_object.id"] = doId;
|
|
1887
2099
|
const first = args[0];
|
|
1888
|
-
if (
|
|
1889
|
-
attrs["cloudflare.do.storage.key"] = first;
|
|
1890
|
-
}
|
|
1891
|
-
else if (Array.isArray(first)) {
|
|
2100
|
+
if (Array.isArray(first)) {
|
|
1892
2101
|
attrs["cloudflare.do.storage.key_count"] = first.length;
|
|
1893
2102
|
}
|
|
1894
2103
|
else if (op === "list" &&
|
|
@@ -1898,8 +2107,17 @@ function wrapDurableObjectStorage(storage, startSpan) {
|
|
|
1898
2107
|
attrs["cloudflare.do.storage.limit"] = first.limit;
|
|
1899
2108
|
}
|
|
1900
2109
|
const span = startSpan(`DO storage ${op}`, attrs, SpanKind.INTERNAL);
|
|
2110
|
+
// Keys are user data: only the hash is ever emitted (omitted if hashing fails).
|
|
2111
|
+
return runWithSpanKeyed(span, "cloudflare.do.storage.key", keysOf(first), () => Promise.resolve(underlying.apply(storage, args)));
|
|
2112
|
+
};
|
|
2113
|
+
const wrapAlarm = (op) => async (...args) => {
|
|
2114
|
+
const underlying = storage[op];
|
|
2115
|
+
if (isTracingSuppressed(context.active()))
|
|
2116
|
+
return underlying.apply(storage, args);
|
|
2117
|
+
const span = startSpan(`durable_object_alarms_${op}`, doId ? { "cloudflare.durable_object.id": doId } : {}, SpanKind.INTERNAL);
|
|
1901
2118
|
return runWithSpan(span, () => Promise.resolve(underlying.apply(storage, args)));
|
|
1902
2119
|
};
|
|
2120
|
+
let tracedSql;
|
|
1903
2121
|
// A dedicated proxy (not makeProxy) because `storage` is a VALUE property on
|
|
1904
2122
|
// the state object, not a method — handlers there must be getters.
|
|
1905
2123
|
return new Proxy(storage, {
|
|
@@ -1908,10 +2126,31 @@ function wrapDurableObjectStorage(storage, startSpan) {
|
|
|
1908
2126
|
if ((prop === "get" || prop === "put" || prop === "delete" || prop === "list") && typeof val === "function") {
|
|
1909
2127
|
return wrapOp(prop);
|
|
1910
2128
|
}
|
|
2129
|
+
if ((prop === "getAlarm" || prop === "setAlarm" || prop === "deleteAlarm") &&
|
|
2130
|
+
typeof val === "function") {
|
|
2131
|
+
return wrapAlarm(prop);
|
|
2132
|
+
}
|
|
2133
|
+
if (prop === "sql" && val && typeof val === "object") {
|
|
2134
|
+
return (tracedSql ??= wrapDurableObjectSql(val, startSpan, doId, pending));
|
|
2135
|
+
}
|
|
1911
2136
|
return typeof val === "function" ? val.bind(t) : val;
|
|
1912
2137
|
},
|
|
1913
2138
|
});
|
|
1914
2139
|
}
|
|
2140
|
+
/** The DO id string from a raw DurableObjectState; undefined when unavailable. */
|
|
2141
|
+
function durableObjectIdOf(state) {
|
|
2142
|
+
try {
|
|
2143
|
+
const raw = state?.id?.toString?.();
|
|
2144
|
+
return typeof raw === "string" && raw ? raw : undefined;
|
|
2145
|
+
}
|
|
2146
|
+
catch {
|
|
2147
|
+
return undefined;
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
const doIdAttrs = (state) => {
|
|
2151
|
+
const id = durableObjectIdOf(state);
|
|
2152
|
+
return id ? { "cloudflare.durable_object.id": id } : {};
|
|
2153
|
+
};
|
|
1915
2154
|
/** Proxy a DurableObjectState so `state.storage` resolves to the traced proxy. */
|
|
1916
2155
|
function wrapDurableObjectState(state, startSpan) {
|
|
1917
2156
|
if (!state || typeof state !== "object")
|
|
@@ -1919,7 +2158,18 @@ function wrapDurableObjectState(state, startSpan) {
|
|
|
1919
2158
|
const s = state;
|
|
1920
2159
|
if (!s.storage || typeof s.storage !== "object")
|
|
1921
2160
|
return state;
|
|
1922
|
-
|
|
2161
|
+
let doId;
|
|
2162
|
+
try {
|
|
2163
|
+
const raw = s.id?.toString?.();
|
|
2164
|
+
if (typeof raw === "string" && raw)
|
|
2165
|
+
doId = raw;
|
|
2166
|
+
}
|
|
2167
|
+
catch {
|
|
2168
|
+
// An id without a usable toString() just omits the attribute.
|
|
2169
|
+
}
|
|
2170
|
+
const pending = new Set();
|
|
2171
|
+
pendingSqlSpans.set(s, pending);
|
|
2172
|
+
const tracedStorage = wrapDurableObjectStorage(s.storage, startSpan, doId, pending);
|
|
1923
2173
|
return new Proxy(s, {
|
|
1924
2174
|
get(t, prop, _receiver) {
|
|
1925
2175
|
if (prop === "storage")
|
|
@@ -1931,6 +2181,10 @@ function wrapDurableObjectState(state, startSpan) {
|
|
|
1931
2181
|
}
|
|
1932
2182
|
/** Prototype methods that are wrapped specially (never treated as RPC). */
|
|
1933
2183
|
const DO_LIFECYCLE_METHODS = new Set(["fetch", "alarm", "constructor"]);
|
|
2184
|
+
/** WebSocket hibernation handlers: wrapped as named lifecycle spans, never as RPC. */
|
|
2185
|
+
const DO_WEBSOCKET_METHODS = ["webSocketMessage", "webSocketClose", "webSocketError"];
|
|
2186
|
+
for (const m of DO_WEBSOCKET_METHODS)
|
|
2187
|
+
DO_LIFECYCLE_METHODS.add(m);
|
|
1934
2188
|
const MAX_DO_RPC_METHODS = 32;
|
|
1935
2189
|
/**
|
|
1936
2190
|
* Decorate a Durable Object class:
|
|
@@ -2011,9 +2265,11 @@ export function wrapDurableObjectClass(cls, opts = {}) {
|
|
|
2011
2265
|
"http.request.method": request.method,
|
|
2012
2266
|
"url.path": url.pathname,
|
|
2013
2267
|
"server.address": url.host,
|
|
2268
|
+
"cloudflare.handler_type": "fetch",
|
|
2269
|
+
...doIdAttrs(this.__heystackState),
|
|
2014
2270
|
}, SpanKind.SERVER, startCtx);
|
|
2015
2271
|
try {
|
|
2016
|
-
const response = (await context.with(trace.setSpan(startCtx, span), () => origFetch.call(this, request)));
|
|
2272
|
+
const response = (await context.with(trace.setSpan(startCtx, span).setValue(DO_INVOCATION_KEY, span), () => origFetch.call(this, request)));
|
|
2017
2273
|
span.setAttribute("http.response.status_code", response.status);
|
|
2018
2274
|
span.setStatus({
|
|
2019
2275
|
code: response.status >= 500 ? SpanStatusCode.ERROR : SpanStatusCode.UNSET,
|
|
@@ -2030,7 +2286,7 @@ export function wrapDurableObjectClass(cls, opts = {}) {
|
|
|
2030
2286
|
}
|
|
2031
2287
|
finally {
|
|
2032
2288
|
span.end();
|
|
2033
|
-
scheduleStateFlush(this.__heystackState);
|
|
2289
|
+
scheduleStateFlush(this.__heystackState, span);
|
|
2034
2290
|
}
|
|
2035
2291
|
};
|
|
2036
2292
|
}
|
|
@@ -2039,9 +2295,9 @@ export function wrapDurableObjectClass(cls, opts = {}) {
|
|
|
2039
2295
|
if (isTracingSuppressed(context.active())) {
|
|
2040
2296
|
return origAlarm.apply(this, args);
|
|
2041
2297
|
}
|
|
2042
|
-
const span = startSpan("DO alarm", {}, SpanKind.INTERNAL);
|
|
2298
|
+
const span = startSpan("DO alarm", { "cloudflare.handler_type": "alarm", ...doIdAttrs(this.__heystackState) }, SpanKind.INTERNAL);
|
|
2043
2299
|
try {
|
|
2044
|
-
return await context.with(trace.setSpan(context.active(), span), () => origAlarm.apply(this, args));
|
|
2300
|
+
return await context.with(trace.setSpan(context.active(), span).setValue(DO_INVOCATION_KEY, span), () => origAlarm.apply(this, args));
|
|
2045
2301
|
}
|
|
2046
2302
|
catch (error) {
|
|
2047
2303
|
span.recordException(error instanceof Error ? error : new Error(String(error)));
|
|
@@ -2053,22 +2309,20 @@ export function wrapDurableObjectClass(cls, opts = {}) {
|
|
|
2053
2309
|
}
|
|
2054
2310
|
finally {
|
|
2055
2311
|
span.end();
|
|
2056
|
-
scheduleStateFlush(this.__heystackState);
|
|
2312
|
+
scheduleStateFlush(this.__heystackState, span);
|
|
2057
2313
|
}
|
|
2058
2314
|
};
|
|
2059
2315
|
}
|
|
2060
|
-
|
|
2316
|
+
const className = Base.name || "DurableObject";
|
|
2317
|
+
const wrapMethod = (method, spanName, attrs) => {
|
|
2061
2318
|
const orig = proto[method];
|
|
2062
2319
|
Wrapped.prototype[method] = async function (...args) {
|
|
2063
2320
|
if (isTracingSuppressed(context.active())) {
|
|
2064
2321
|
return orig.apply(this, args);
|
|
2065
2322
|
}
|
|
2066
|
-
const span = startSpan(
|
|
2067
|
-
"rpc.system": "cloudflare.durable_object",
|
|
2068
|
-
"rpc.method": method,
|
|
2069
|
-
}, SpanKind.SERVER);
|
|
2323
|
+
const span = startSpan(spanName, { ...attrs, ...doIdAttrs(this.__heystackState) }, SpanKind.SERVER);
|
|
2070
2324
|
try {
|
|
2071
|
-
return await context.with(trace.setSpan(context.active(), span), () => orig.apply(this, args));
|
|
2325
|
+
return await context.with(trace.setSpan(context.active(), span).setValue(DO_INVOCATION_KEY, span), () => orig.apply(this, args));
|
|
2072
2326
|
}
|
|
2073
2327
|
catch (error) {
|
|
2074
2328
|
span.recordException(error instanceof Error ? error : new Error(String(error)));
|
|
@@ -2080,9 +2334,21 @@ export function wrapDurableObjectClass(cls, opts = {}) {
|
|
|
2080
2334
|
}
|
|
2081
2335
|
finally {
|
|
2082
2336
|
span.end();
|
|
2083
|
-
scheduleStateFlush(this.__heystackState);
|
|
2337
|
+
scheduleStateFlush(this.__heystackState, span);
|
|
2084
2338
|
}
|
|
2085
2339
|
};
|
|
2340
|
+
};
|
|
2341
|
+
for (const method of DO_WEBSOCKET_METHODS) {
|
|
2342
|
+
if (typeof proto[method] === "function") {
|
|
2343
|
+
wrapMethod(method, `DO ${className} ${method}`, { "cloudflare.handler_type": method });
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
for (const method of rpcMethods) {
|
|
2347
|
+
wrapMethod(method, `DO rpc ${method}`, {
|
|
2348
|
+
"rpc.system": "cloudflare.durable_object",
|
|
2349
|
+
"rpc.method": method,
|
|
2350
|
+
"cloudflare.handler_type": "rpc",
|
|
2351
|
+
});
|
|
2086
2352
|
}
|
|
2087
2353
|
return Wrapped;
|
|
2088
2354
|
}
|
|
@@ -2225,7 +2491,7 @@ export function wrapWorkflowEntrypointClass(cls, opts = {}) {
|
|
|
2225
2491
|
return origRun.call(this, event, step, ...rest);
|
|
2226
2492
|
}
|
|
2227
2493
|
const attrs = workflowRunAttrs(event);
|
|
2228
|
-
const span = startSpan("workflow run", attrs, SpanKind.INTERNAL);
|
|
2494
|
+
const span = startSpan("workflow run", { ...attrs, "cloudflare.handler_type": "workflow" }, SpanKind.INTERNAL);
|
|
2229
2495
|
try {
|
|
2230
2496
|
return await context.with(trace.setSpan(context.active(), span), () => origRun.call(this, event, step && typeof step === "object"
|
|
2231
2497
|
? wrapWorkflowStep(step, startSpan, this.__heystackState)
|
|
@@ -2246,25 +2512,155 @@ export function wrapWorkflowEntrypointClass(cls, opts = {}) {
|
|
|
2246
2512
|
};
|
|
2247
2513
|
return Wrapped;
|
|
2248
2514
|
}
|
|
2515
|
+
// ---------------------------------------------------------------------------
|
|
2516
|
+
// WorkerEntrypoint CLASS instrumentation
|
|
2517
|
+
// ---------------------------------------------------------------------------
|
|
2518
|
+
/** Handler names and RpcTarget plumbing that are never treated as RPC methods. */
|
|
2519
|
+
const ENTRYPOINT_NON_RPC = new Set([
|
|
2520
|
+
"constructor",
|
|
2521
|
+
"fetch",
|
|
2522
|
+
"tail",
|
|
2523
|
+
"trace",
|
|
2524
|
+
"scheduled",
|
|
2525
|
+
"queue",
|
|
2526
|
+
"email",
|
|
2527
|
+
"test",
|
|
2528
|
+
"then",
|
|
2529
|
+
"toJSON",
|
|
2530
|
+
"connect",
|
|
2531
|
+
"tailStream",
|
|
2532
|
+
]);
|
|
2533
|
+
const ENTRYPOINT_BASES = new Set(["WorkerEntrypoint", "RpcTarget", "DurableObject"]);
|
|
2534
|
+
const MAX_ENTRYPOINT_RPC_METHODS = 64;
|
|
2535
|
+
/**
|
|
2536
|
+
* Decorate a `WorkerEntrypoint` class (the target of a Service binding):
|
|
2537
|
+
* - `fetch()` is traced exactly like an `instrument()`ed handler (SERVER span,
|
|
2538
|
+
* inbound `traceparent` continued, export drained via `ctx.waitUntil`).
|
|
2539
|
+
* - every public RPC method gets a SERVER span `rpc <method>` with
|
|
2540
|
+
* `jsrpc.method` / `rpc.method`. Return values (including stubs, RpcTargets
|
|
2541
|
+
* and custom thenables), `this` and errors pass through untouched.
|
|
2542
|
+
*
|
|
2543
|
+
* RPC spans exist in DIRECT mode only. When Cloudflare native tracing is active
|
|
2544
|
+
* it already emits `jsrpc` spans, so RPC calls pass straight through and `fetch`
|
|
2545
|
+
* takes `instrument()`'s native path. Constructor, `#private`, `_underscore` and
|
|
2546
|
+
* Symbol-keyed members are never wrapped.
|
|
2547
|
+
*
|
|
2548
|
+
* export default wrapWorkerEntrypointClass(class Api extends WorkerEntrypoint { ... }, { service: "api" });
|
|
2549
|
+
*/
|
|
2550
|
+
export function wrapWorkerEntrypointClass(cls, cfg) {
|
|
2551
|
+
const Base = cls;
|
|
2552
|
+
const proto = Base.prototype;
|
|
2553
|
+
const instances = new WeakMap();
|
|
2554
|
+
// Enumerate public RPC methods without invoking any getter.
|
|
2555
|
+
const rpcMethods = [];
|
|
2556
|
+
try {
|
|
2557
|
+
let p = Base.prototype;
|
|
2558
|
+
for (let depth = 0; depth < 6 && p && p !== Object.prototype; depth++) {
|
|
2559
|
+
const ctorName = p.constructor?.name;
|
|
2560
|
+
if (depth > 0 && ctorName && ENTRYPOINT_BASES.has(ctorName))
|
|
2561
|
+
break;
|
|
2562
|
+
for (const key of Object.getOwnPropertyNames(p)) {
|
|
2563
|
+
if (ENTRYPOINT_NON_RPC.has(key) || key.startsWith("_") || key.startsWith("#"))
|
|
2564
|
+
continue;
|
|
2565
|
+
if (rpcMethods.includes(key) || rpcMethods.length >= MAX_ENTRYPOINT_RPC_METHODS)
|
|
2566
|
+
continue;
|
|
2567
|
+
if (typeof Object.getOwnPropertyDescriptor(p, key)?.value === "function")
|
|
2568
|
+
rpcMethods.push(key);
|
|
2569
|
+
}
|
|
2570
|
+
p = Object.getPrototypeOf(p);
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
catch {
|
|
2574
|
+
// Best-effort enumeration — a hostile prototype must never break decoration.
|
|
2575
|
+
}
|
|
2576
|
+
class Wrapped extends Base {
|
|
2577
|
+
constructor(...args) {
|
|
2578
|
+
// The entrypoint's own `this.env` bindings get traced too, parented to
|
|
2579
|
+
// whichever RPC/fetch span is active when they are used.
|
|
2580
|
+
const env = args[1];
|
|
2581
|
+
const rawEnv = env;
|
|
2582
|
+
if (cfg.instrumentBindings !== false && env && typeof env === "object") {
|
|
2583
|
+
const native = cfg.nativeTracing === "direct" ? null : getNativeTracing(args[0], cfg.tracing);
|
|
2584
|
+
if (native) {
|
|
2585
|
+
args[1] = nativeInstrumentedEnv(env, native, cfg);
|
|
2586
|
+
}
|
|
2587
|
+
else if (cfg.instrumentBindings) {
|
|
2588
|
+
args[1] = instrumentEnv(env, {
|
|
2589
|
+
startSpan: (name, attrs, kind) => trace.getTracer("heystack").startSpan(name, { attributes: attrs, kind }, context.active()),
|
|
2590
|
+
select: cfg.instrumentBindings,
|
|
2591
|
+
});
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
super(...args);
|
|
2595
|
+
instances.set(this, { ctx: args[0], env: args[1], rawEnv });
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
try {
|
|
2599
|
+
Object.defineProperty(Wrapped, "name", { value: Base.name, configurable: true });
|
|
2600
|
+
}
|
|
2601
|
+
catch {
|
|
2602
|
+
// Non-configurable name: harmless.
|
|
2603
|
+
}
|
|
2604
|
+
const origFetch = proto.fetch;
|
|
2605
|
+
if (typeof origFetch === "function") {
|
|
2606
|
+
// Built once per instance (WorkerEntrypoint.fetch takes only the request).
|
|
2607
|
+
const handlers = new WeakMap();
|
|
2608
|
+
Wrapped.prototype.fetch = function (request) {
|
|
2609
|
+
const inst = instances.get(this);
|
|
2610
|
+
let handler = handlers.get(this);
|
|
2611
|
+
if (!handler) {
|
|
2612
|
+
handler = instrument({ fetch: (req) => origFetch.call(this, req) }, cfg);
|
|
2613
|
+
handlers.set(this, handler);
|
|
2614
|
+
}
|
|
2615
|
+
return handler.fetch(request, inst?.rawEnv ?? this.env, inst?.ctx ?? this.ctx);
|
|
2616
|
+
};
|
|
2617
|
+
}
|
|
2618
|
+
for (const method of rpcMethods) {
|
|
2619
|
+
const orig = proto[method];
|
|
2620
|
+
Wrapped.prototype[method] = function (...args) {
|
|
2621
|
+
if (isTracingSuppressed(context.active()))
|
|
2622
|
+
return orig.apply(this, args);
|
|
2623
|
+
const inst = instances.get(this) ?? {
|
|
2624
|
+
ctx: this.ctx,
|
|
2625
|
+
env: this.env,
|
|
2626
|
+
};
|
|
2627
|
+
// Native tracing owns jsrpc spans: pass straight through.
|
|
2628
|
+
if (cfg.nativeTracing !== "direct" && getNativeTracing(inst.ctx, cfg.tracing)) {
|
|
2629
|
+
return orig.apply(this, args);
|
|
2630
|
+
}
|
|
2631
|
+
const apiKey = cfg.apiKey ?? inst.env?.HEYSTACK_API_KEY;
|
|
2632
|
+
if (!apiKey) {
|
|
2633
|
+
warnOnceNoKey();
|
|
2634
|
+
return orig.apply(this, args);
|
|
2635
|
+
}
|
|
2636
|
+
const provider = ensureGlobalProvider({ ...cfg, apiKey });
|
|
2637
|
+
requestDrainWired = true;
|
|
2638
|
+
const span = trace.getTracer("heystack").startSpan(`rpc ${method}`, { kind: SpanKind.SERVER, attributes: { "jsrpc.method": method, "rpc.method": method, "cloudflare.handler_type": "rpc" } }, context.active());
|
|
2639
|
+
// Only the three members the shared tracer helper uses; `end` also drains.
|
|
2640
|
+
const tracked = {
|
|
2641
|
+
recordException: (e) => span.recordException(e),
|
|
2642
|
+
setStatus: (st) => span.setStatus(st),
|
|
2643
|
+
end: () => {
|
|
2644
|
+
span.end();
|
|
2645
|
+
try {
|
|
2646
|
+
inst.ctx?.waitUntil?.(drainWithTimeout(provider));
|
|
2647
|
+
}
|
|
2648
|
+
catch {
|
|
2649
|
+
// A rejected host waitUntil must not break the RPC call.
|
|
2650
|
+
}
|
|
2651
|
+
},
|
|
2652
|
+
};
|
|
2653
|
+
return runWithSpanPreservingReturn(tracked, () => context.with(trace.setSpan(context.active(), span), () => orig.apply(this, args)));
|
|
2654
|
+
};
|
|
2655
|
+
}
|
|
2656
|
+
return Wrapped;
|
|
2657
|
+
}
|
|
2249
2658
|
export async function withSpan(name, attrsOrFn, maybeFn) {
|
|
2250
2659
|
const attrs = typeof attrsOrFn === "function" ? undefined : attrsOrFn;
|
|
2251
2660
|
const fn = (typeof attrsOrFn === "function" ? attrsOrFn : maybeFn);
|
|
2252
2661
|
const nativeTracing = nativeTracingFromContext();
|
|
2253
2662
|
if (nativeTracing) {
|
|
2254
|
-
return nativeTracing
|
|
2255
|
-
const adapter = new NativeSpanAdapter(nativeSpan);
|
|
2256
|
-
if (attrs)
|
|
2257
|
-
adapter.setAttributes(attrs);
|
|
2258
|
-
const nativeContext = trace.setSpan(context.active().setValue(NATIVE_TRACING_CONTEXT_KEY, nativeTracing), adapter);
|
|
2259
|
-
try {
|
|
2260
|
-
return await context.with(nativeContext, () => fn(adapter));
|
|
2261
|
-
}
|
|
2262
|
-
catch (err) {
|
|
2263
|
-
adapter.recordException(err instanceof Error ? err : new Error(String(err)));
|
|
2264
|
-
adapter.setStatus({ code: SpanStatusCode.ERROR, message: err instanceof Error ? err.message : String(err) });
|
|
2265
|
-
throw err;
|
|
2266
|
-
}
|
|
2267
|
-
});
|
|
2663
|
+
return runNativeSpan(nativeTracing, name, attrs ?? {}, fn);
|
|
2268
2664
|
}
|
|
2269
2665
|
const tracer = trace.getTracer("heystack");
|
|
2270
2666
|
const span = tracer.startSpan(name, attrs ? { attributes: attrs } : undefined);
|
|
@@ -2313,3 +2709,5 @@ export { llmRequestAttrs as genAiRequestAttrs } from "./llm-enrich.js";
|
|
|
2313
2709
|
* Workers AI results (`usage.prompt_tokens`/`completion_tokens`) included.
|
|
2314
2710
|
*/
|
|
2315
2711
|
export { llmResponseAttrs as genAiResponseAttrs } from "./llm-enrich.js";
|
|
2712
|
+
export { NATIVELY_COVERED, NATIVE_PROPAGATES_EXTERNAL, shouldEnrichNatively, wrapSqlClient };
|
|
2713
|
+
export { matchRoute } from "./workers-routes.js";
|