@heystack/otel 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/traceparent.d.ts +8 -0
- package/dist/traceparent.js +5 -0
- package/dist/workers-agents.d.ts +1 -1
- package/dist/workers-agents.js +1 -1
- package/dist/workers-bindings.d.ts +11 -1
- package/dist/workers-bindings.js +286 -6
- package/dist/workers.d.ts +91 -20
- package/dist/workers.js +962 -114
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ Always read your key from the environment — never paste it into source:
|
|
|
13
13
|
HEYSTACK_API_KEY=sk_live_…
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
> **Use `@heystack/otel` `>=0.
|
|
16
|
+
> **Use `@heystack/otel` `>=0.16.0`.** 0.16 extends direct Workers instrumentation to Durable Objects, Workflows, and the remaining Worker lifecycle entry points while preserving host-object return semantics. Cloudflare native OTel remains the preferred path for Workers; direct SDK export is a compatibility fallback when native tracing is unavailable. See [Migration](#migration--versioning) below.
|
|
17
17
|
|
|
18
18
|
## Runtime matrix
|
|
19
19
|
|
|
@@ -615,6 +615,7 @@ As belt-and-suspenders the exporter also drops any span whose HTTP target points
|
|
|
615
615
|
|
|
616
616
|
## Migration / versioning
|
|
617
617
|
|
|
618
|
+
- **`0.16.0`** — **complete direct Workers lifecycle and binding coverage.** Durable Object namespace stubs now trace `fetch` and RPC calls with W3C trace propagation; Workflow bindings trace creation, batch creation, lookup, instance lifecycle methods, and deletion operations. Direct instrumentation also covers scheduled, queue, email, Tail Worker, and related Worker entry points, while preserving synchronous values, native promises, custom thenables, upgrade responses, host-object identity, and callback-scoped native span ownership. Service-binding propagation now respects caller-supplied request headers. No breaking changes.
|
|
618
619
|
- **`0.15.0`** — **direct Workers D1 session and batch coverage.** With `nativeTracing: "direct"` and `instrumentBindings` enabled, prepared D1 statements, database/session `batch`, database `exec`, and statements created through `withSession` now receive child spans. Native Cloudflare OTel remains the recommended Workers path because it preserves Cloudflare's platform trace/log tree; use direct export only where `ctx.tracing` is unavailable. No breaking changes.
|
|
619
620
|
- **`0.14.0`** — **gen_ai enrichment under Cloudflare native tracing, Mistral + AI-gateway detection, streaming usage; OpenTelemetry JS SDK 2.x.** (1) `/workers`: under `nativeTracing: "auto" | "native"` `instrument()` never installed the fetch wrapper, so native-mode Workers emitted **no** `gen_ai.*` telemetry. It now installs a lean LLM-only wrapper that opens a native `<operation> <provider>` span (e.g. `chat mistral`) with request/response `gen_ai.*` for detected providers and passes every other subrequest through untouched. (2) New provider **Mistral** (`api.mistral.ai`) and a new `ai.gateways` option (`[{ hostSuffix, pathPattern }]`, e.g. `{ hostSuffix: "wafersecurity.ai", pathPattern: "/p/:tenant/:provider" }`) that resolves the provider from an AI-gateway URL and stamps `heystack.ai.gateway`; `gemini` → `google`, `workers-ai` → `cloudflare`. New `gen_ai.operation.name`. (3) **Streaming (`text/event-stream`) responses now yield usage**: the body is `tee()`d and scanned line-by-line for `usage`/`usageMetadata`, model, id and finish reason only (never content); the span stays open until the stream closes. Google (`usageMetadata`) and Mistral response shapes added to the JSON path too. (4) New `/workers` exports `genAiRequestAttrs` / `genAiResponseAttrs` for manual `withSpan` spans around `env.AI.run` or any non-`fetch` LLM call. Native `error.type` now keeps the exception class instead of being overwritten with `error`. (5) All `@opentelemetry/*` dependencies move to the 2.x SDK line (`core`/`sdk-trace-base`/`resources` ^2.11, `sdk-node`/`exporter-trace-otlp-http` ^0.222, `auto-instrumentations-node` ^0.80, `semantic-conventions` ^1.43), clearing the downstream Dependabot alerts the 1.x line surfaced through this package (GHSA-8988-4f7v-96qf in `@opentelemetry/core`, GHSA-q7rr-3cgh-j5r3 in `sdk-node` / `auto-instrumentations-node` / `exporter-prometheus`, GHSA-45rx-2jwx-cxfr in `propagator-jaeger`). **Node ≥ 18.19 (or ≥ 20.6) is now required on `/node` and `/next`** — the 2.x SDK dropped Node 14/16. If you pass your own `instrumentations` array to `initHeystack`, upgrade those `@opentelemetry/instrumentation-*` packages to a release that peers on `@opentelemetry/core@^2` (the auto-instrumentations 0.80 set already does). `/workers` is still WinterCG-safe (no `node:*` imports). No other API changes; all new options are optional.
|
|
620
621
|
- **`0.13.0`** — **Cloudflare-native tracing and complete Workers platform coverage.** On runtimes exposing `ctx.tracing`, `/workers` now joins Cloudflare's authoritative invocation trace instead of creating and exporting a duplicate tree. `nativeTracing: "auto"` is the default; `withCloudflareSpan(ctx, ...)` adds async-safe business spans, while fetch, queue, and scheduled handlers are enriched in the native tree. The new `/workers/agents` entry maps Cloudflare Agents observability callbacks and Tail Worker diagnostics to structured, redacted telemetry. Direct OTLP remains available with `nativeTracing: "direct"` for runtimes without the native API. Heystack ingest now preserves full OTLP trace/log fidelity, accepts OTLP metrics, and can import Workers Analytics metrics through the app-scoped Cloudflare connector. Native mode needs no Worker API-key secret and must not be combined with direct export.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
interface TraceparentSpanContext {
|
|
2
|
+
traceId: string;
|
|
3
|
+
spanId: string;
|
|
4
|
+
traceFlags: number;
|
|
5
|
+
}
|
|
6
|
+
/** Encode a span context as W3C traceparent, preserving the low flags byte. */
|
|
7
|
+
export declare function makeTraceparent(context: TraceparentSpanContext): string;
|
|
8
|
+
export {};
|
package/dist/workers-agents.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Span } from "@opentelemetry/api";
|
|
2
|
-
import { type CloudflareNativeTracing } from "./workers";
|
|
2
|
+
import { type CloudflareNativeTracing } from "./workers.js";
|
|
3
3
|
/** Structural equivalent of the Agents SDK event; no `agents` peer required. */
|
|
4
4
|
export interface CloudflareAgentEvent {
|
|
5
5
|
type: string;
|
package/dist/workers-agents.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { trace } from "@opentelemetry/api";
|
|
2
|
-
import { withCloudflareSpan, } from "./workers";
|
|
2
|
+
import { withCloudflareSpan, } from "./workers.js";
|
|
3
3
|
const SENSITIVE_KEY = /(?:authorization|cookie|password|secret|token|api[_-]?key|prompt|completion|content|message)/i;
|
|
4
4
|
const ERROR_EVENT = /(?::error|:failed|:stalled|:interrupted|:exhausted)$/;
|
|
5
5
|
function sanitize(value, extraKeys, depth = 0) {
|
|
@@ -10,11 +10,19 @@ export interface InstrumentBindingsOpts {
|
|
|
10
10
|
*/
|
|
11
11
|
startSpan: (name: string, attrs: Record<string, unknown>, kind?: SpanKind) => Span;
|
|
12
12
|
/**
|
|
13
|
-
* `true` → auto-detect and wrap
|
|
13
|
+
* `true` → auto-detect and wrap every supported Cloudflare binding.
|
|
14
14
|
* `string[]` → only wrap bindings whose env key is listed.
|
|
15
15
|
*/
|
|
16
16
|
select: boolean | string[];
|
|
17
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Run `fn`, ending `span` in `finally` and recording exceptions + ERROR status
|
|
20
|
+
* on failure (re-throwing). Internal helper shared with the class-level
|
|
21
|
+
* wrappers in workers.ts.
|
|
22
|
+
*/
|
|
23
|
+
export declare function runWithSpan<T>(span: Span, fn: () => Promise<T>): Promise<T>;
|
|
24
|
+
/** Internal proxy builder shared with the class-level wrappers in workers.ts. */
|
|
25
|
+
export declare function makeProxy(target: any, handlers: Record<string, Function>): any;
|
|
18
26
|
/**
|
|
19
27
|
* Build a new args array for `fetch(input, init?)` with a `traceparent` header
|
|
20
28
|
* injected WITHOUT mutating the caller's original Request or init object.
|
|
@@ -37,6 +45,8 @@ export declare function injectTraceparentArgs(args: any[], traceparent: string):
|
|
|
37
45
|
* - Workflow: `get`+`create`+`createBatch`
|
|
38
46
|
* - Rate limit: `limit`
|
|
39
47
|
* - Analytics Engine: `writeDataPoint`
|
|
48
|
+
* - Durable Object namespace: `idFromName`+`get`+`newUniqueId` (stub fetch/RPC spans)
|
|
49
|
+
* - Workflow binding: `create`+`createBatch`+`get` and not a DO namespace
|
|
40
50
|
* - Service binding / Fetcher: `fetch` (last branch; most generic)
|
|
41
51
|
*
|
|
42
52
|
* Unrecognised bindings are passed through unchanged.
|
package/dist/workers-bindings.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
2
|
// Cloudflare binding instrumentation for @heystack/otel/workers.
|
|
3
3
|
//
|
|
4
|
-
// Wraps
|
|
5
|
-
//
|
|
4
|
+
// Wraps supported Cloudflare data, AI, messaging, Durable Object, Workflow,
|
|
5
|
+
// and Service binding operations with OTel child spans.
|
|
6
6
|
//
|
|
7
|
-
// WinterCG-safe: no
|
|
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
10
|
import { context, SpanKind, SpanStatusCode } from "@opentelemetry/api";
|
|
11
11
|
import { isTracingSuppressed } from "@opentelemetry/core";
|
|
12
|
+
import { makeTraceparent } from "./traceparent.js";
|
|
12
13
|
// ---------------------------------------------------------------------------
|
|
13
14
|
// Duck-type detectors — conservative; require the distinctive method set
|
|
14
15
|
// exactly as documented in the task brief.
|
|
@@ -63,7 +64,12 @@ function isServiceLike(b) {
|
|
|
63
64
|
// ---------------------------------------------------------------------------
|
|
64
65
|
// Span lifecycle helper
|
|
65
66
|
// ---------------------------------------------------------------------------
|
|
66
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Run `fn`, ending `span` in `finally` and recording exceptions + ERROR status
|
|
69
|
+
* on failure (re-throwing). Internal helper shared with the class-level
|
|
70
|
+
* wrappers in workers.ts.
|
|
71
|
+
*/
|
|
72
|
+
export async function runWithSpan(span, fn) {
|
|
67
73
|
try {
|
|
68
74
|
return await fn();
|
|
69
75
|
}
|
|
@@ -95,11 +101,80 @@ function runSyncWithSpan(span, fn) {
|
|
|
95
101
|
span.end();
|
|
96
102
|
}
|
|
97
103
|
}
|
|
104
|
+
function isPromiseLike(value) {
|
|
105
|
+
return (((typeof value === "object" && value !== null) || typeof value === "function") &&
|
|
106
|
+
typeof value.then === "function");
|
|
107
|
+
}
|
|
108
|
+
function recordSpanError(span, error) {
|
|
109
|
+
span.recordException(error instanceof Error ? error : new Error(String(error)));
|
|
110
|
+
span.setStatus({
|
|
111
|
+
code: SpanStatusCode.ERROR,
|
|
112
|
+
message: error instanceof Error ? error.message : String(error),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
/** Trace a call without replacing synchronous, Promise, or custom thenable results. */
|
|
116
|
+
function runWithSpanPreservingReturn(span, fn) {
|
|
117
|
+
try {
|
|
118
|
+
const result = fn();
|
|
119
|
+
if (isPromiseLike(result)) {
|
|
120
|
+
let settled = false;
|
|
121
|
+
const finish = (error, failed = false) => {
|
|
122
|
+
if (settled)
|
|
123
|
+
return;
|
|
124
|
+
settled = true;
|
|
125
|
+
if (failed) {
|
|
126
|
+
try {
|
|
127
|
+
recordSpanError(span, error);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// Instrumentation must not alter the observed thenable's behaviour.
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
span.end();
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
// Instrumentation must not alter the observed thenable's behaviour.
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
try {
|
|
141
|
+
const observer = result.then(() => finish(), (error) => finish(error, true));
|
|
142
|
+
// A non-compliant custom thenable may reject the observer despite the
|
|
143
|
+
// rejection callback above. Consume that rejection without replacing
|
|
144
|
+
// or awaiting the original RPC/Promise object returned to the caller.
|
|
145
|
+
if (isPromiseLike(observer)) {
|
|
146
|
+
try {
|
|
147
|
+
observer.then(undefined, () => undefined);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// Calling then() is best-effort for exotic host thenables.
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
finish(error, true);
|
|
156
|
+
}
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
span.end();
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
recordSpanError(span, error);
|
|
164
|
+
span.end();
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/** Transform either a synchronous result or a promise without changing its category. */
|
|
169
|
+
function mapReturn(result, map) {
|
|
170
|
+
return isPromiseLike(result) ? Promise.resolve(result).then(map) : map(result);
|
|
171
|
+
}
|
|
98
172
|
// ---------------------------------------------------------------------------
|
|
99
173
|
// Proxy helper — intercepts listed handlers, falls through to prototype for rest
|
|
100
174
|
// ---------------------------------------------------------------------------
|
|
101
175
|
const rawTarget = Symbol("heystack.rawBindingTarget");
|
|
102
|
-
|
|
176
|
+
/** Internal proxy builder shared with the class-level wrappers in workers.ts. */
|
|
177
|
+
export function makeProxy(target, handlers) {
|
|
103
178
|
return new Proxy(target, {
|
|
104
179
|
get(t, prop, receiver) {
|
|
105
180
|
if (prop === rawTarget)
|
|
@@ -455,6 +530,14 @@ function wrapAnalyticsEngine(binding, opts, name) {
|
|
|
455
530
|
export function injectTraceparentArgs(args, traceparent) {
|
|
456
531
|
const [input, ...rest] = args;
|
|
457
532
|
if (input instanceof Request) {
|
|
533
|
+
const originalInit = rest[0];
|
|
534
|
+
if (originalInit?.headers !== undefined) {
|
|
535
|
+
const init = { ...originalInit };
|
|
536
|
+
const headers = new Headers(originalInit.headers);
|
|
537
|
+
headers.set("traceparent", traceparent);
|
|
538
|
+
init.headers = headers;
|
|
539
|
+
return [input, init, ...rest.slice(1)];
|
|
540
|
+
}
|
|
458
541
|
const headers = new Headers(input.headers);
|
|
459
542
|
headers.set("traceparent", traceparent);
|
|
460
543
|
return [new Request(input, { headers }), ...rest];
|
|
@@ -474,13 +557,202 @@ function wrapService(binding, opts, name) {
|
|
|
474
557
|
}
|
|
475
558
|
const span = opts.startSpan(`Service ${name} fetch`, { "peer.service": name }, SpanKind.CLIENT);
|
|
476
559
|
const sc = span.spanContext();
|
|
477
|
-
const traceparent =
|
|
560
|
+
const traceparent = makeTraceparent(sc);
|
|
478
561
|
const injectedArgs = injectTraceparentArgs(args, traceparent);
|
|
479
562
|
return runWithSpan(span, () => binding.fetch(...injectedArgs));
|
|
480
563
|
},
|
|
481
564
|
});
|
|
482
565
|
}
|
|
483
566
|
// ---------------------------------------------------------------------------
|
|
567
|
+
// Durable Object namespace binding
|
|
568
|
+
// ---------------------------------------------------------------------------
|
|
569
|
+
/** DO namespace detector using the real synchronous Cloudflare API surface. */
|
|
570
|
+
function isDurableObjectNamespaceLike(b) {
|
|
571
|
+
return (typeof b?.idFromName === "function" &&
|
|
572
|
+
typeof b?.get === "function" &&
|
|
573
|
+
typeof b?.newUniqueId === "function");
|
|
574
|
+
}
|
|
575
|
+
/** Fetcher/DO members that are platform operations or must retain host semantics. */
|
|
576
|
+
const DO_STUB_RPC_EXCLUDED = new Set([
|
|
577
|
+
"fetch",
|
|
578
|
+
"connect",
|
|
579
|
+
"alarm",
|
|
580
|
+
"webSocketMessage",
|
|
581
|
+
"webSocketClose",
|
|
582
|
+
"webSocketError",
|
|
583
|
+
"id",
|
|
584
|
+
"name",
|
|
585
|
+
"dup",
|
|
586
|
+
"constructor",
|
|
587
|
+
"then",
|
|
588
|
+
]);
|
|
589
|
+
/** Wrap a DO stub: CLIENT spans on fetch (+ traceparent injection) and RPC calls. */
|
|
590
|
+
function wrapDurableObjectStub(stub, opts, name) {
|
|
591
|
+
const fetch = (...args) => {
|
|
592
|
+
if (isTracingSuppressed(context.active()))
|
|
593
|
+
return stub.fetch(...args);
|
|
594
|
+
const span = opts.startSpan(`DO ${name} fetch`, { "peer.service": name }, SpanKind.CLIENT);
|
|
595
|
+
const sc = span.spanContext();
|
|
596
|
+
// Propagate into the DO so its own spans join this trace.
|
|
597
|
+
const injectedArgs = injectTraceparentArgs(args, makeTraceparent(sc));
|
|
598
|
+
return runWithSpanPreservingReturn(span, () => stub.fetch(...injectedArgs));
|
|
599
|
+
};
|
|
600
|
+
// workerd exposes RPC methods through a wildcard property hook, so method
|
|
601
|
+
// names are not enumerable. Resolve and wrap them lazily on first access.
|
|
602
|
+
const rpcMethods = new Map();
|
|
603
|
+
return new Proxy(stub, {
|
|
604
|
+
get(target, property) {
|
|
605
|
+
if (property === "fetch")
|
|
606
|
+
return fetch;
|
|
607
|
+
const value = Reflect.get(target, property, target);
|
|
608
|
+
if (typeof property !== "string" ||
|
|
609
|
+
typeof value !== "function" ||
|
|
610
|
+
DO_STUB_RPC_EXCLUDED.has(property) ||
|
|
611
|
+
property in Object.prototype) {
|
|
612
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
613
|
+
}
|
|
614
|
+
const cached = rpcMethods.get(property);
|
|
615
|
+
if (cached)
|
|
616
|
+
return cached;
|
|
617
|
+
// Capture the host callable returned for this property. Calling it with
|
|
618
|
+
// the original stub as receiver preserves both ordinary and workerd RPC
|
|
619
|
+
// method semantics; the wrapper itself remains stable across reads.
|
|
620
|
+
const method = value;
|
|
621
|
+
const wrapped = (...args) => {
|
|
622
|
+
if (isTracingSuppressed(context.active())) {
|
|
623
|
+
return Reflect.apply(method, target, args);
|
|
624
|
+
}
|
|
625
|
+
const span = opts.startSpan(`DO ${name} ${property}`, { "peer.service": name, "rpc.method": property }, SpanKind.CLIENT);
|
|
626
|
+
return runWithSpanPreservingReturn(span, () => Reflect.apply(method, target, args));
|
|
627
|
+
};
|
|
628
|
+
rpcMethods.set(property, wrapped);
|
|
629
|
+
return wrapped;
|
|
630
|
+
},
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
function wrapDurableObjectNamespace(ns, opts, name) {
|
|
634
|
+
const handlers = {
|
|
635
|
+
// Namespace lookup is local and synchronous. Trace the returned stub's
|
|
636
|
+
// actual fetch/RPC calls, not get() itself.
|
|
637
|
+
get: (...args) => wrapDurableObjectStub(ns.get(...args), opts, name),
|
|
638
|
+
};
|
|
639
|
+
if (typeof ns.getByName === "function") {
|
|
640
|
+
handlers.getByName = (...args) => wrapDurableObjectStub(ns.getByName(...args), opts, name);
|
|
641
|
+
}
|
|
642
|
+
if (typeof ns.jurisdiction === "function") {
|
|
643
|
+
handlers.jurisdiction = (...args) => wrapDurableObjectNamespace(ns.jurisdiction(...args), opts, name);
|
|
644
|
+
}
|
|
645
|
+
// idFromName/idFromString/newUniqueId are synchronous local operations and
|
|
646
|
+
// fall through untouched.
|
|
647
|
+
return makeProxy(ns, handlers);
|
|
648
|
+
}
|
|
649
|
+
// ---------------------------------------------------------------------------
|
|
650
|
+
// Workflows binding wrapper
|
|
651
|
+
// ---------------------------------------------------------------------------
|
|
652
|
+
/** Workflow binding: exposes create/createBatch/get and is not a DO namespace. */
|
|
653
|
+
function isWorkflowBindingLike(b) {
|
|
654
|
+
return (typeof b?.create === "function" &&
|
|
655
|
+
typeof b?.createBatch === "function" &&
|
|
656
|
+
typeof b?.get === "function" &&
|
|
657
|
+
!isDurableObjectNamespaceLike(b));
|
|
658
|
+
}
|
|
659
|
+
const WORKFLOW_INSTANCE_METHODS = [
|
|
660
|
+
"pause",
|
|
661
|
+
"resume",
|
|
662
|
+
"terminate",
|
|
663
|
+
"restart",
|
|
664
|
+
"status",
|
|
665
|
+
"sendEvent",
|
|
666
|
+
];
|
|
667
|
+
function workflowId(value) {
|
|
668
|
+
if (typeof value === "string" && value.length > 0)
|
|
669
|
+
return value;
|
|
670
|
+
if (value && typeof value === "object") {
|
|
671
|
+
const id = value.id;
|
|
672
|
+
if (typeof id === "string" && id.length > 0)
|
|
673
|
+
return id;
|
|
674
|
+
}
|
|
675
|
+
return undefined;
|
|
676
|
+
}
|
|
677
|
+
function workflowBatchIds(value) {
|
|
678
|
+
if (!Array.isArray(value))
|
|
679
|
+
return [];
|
|
680
|
+
return value.map(workflowId).filter((id) => id !== undefined);
|
|
681
|
+
}
|
|
682
|
+
function wrapWorkflowInstance(instance, opts, name) {
|
|
683
|
+
if (!instance || typeof instance !== "object")
|
|
684
|
+
return instance;
|
|
685
|
+
const target = instance;
|
|
686
|
+
const handlers = {};
|
|
687
|
+
const instanceId = workflowId(instance);
|
|
688
|
+
for (const method of WORKFLOW_INSTANCE_METHODS) {
|
|
689
|
+
if (typeof target[method] !== "function")
|
|
690
|
+
continue;
|
|
691
|
+
handlers[method] = (...args) => {
|
|
692
|
+
const underlying = target[method];
|
|
693
|
+
if (isTracingSuppressed(context.active())) {
|
|
694
|
+
return underlying.apply(instance, args);
|
|
695
|
+
}
|
|
696
|
+
const attrs = {
|
|
697
|
+
"cloudflare.workflow.namespace": name,
|
|
698
|
+
};
|
|
699
|
+
if (instanceId !== undefined) {
|
|
700
|
+
attrs["cloudflare.workflow.instance_id"] = instanceId;
|
|
701
|
+
}
|
|
702
|
+
const span = opts.startSpan(`Workflow ${method}`, attrs, SpanKind.CLIENT);
|
|
703
|
+
return runWithSpanPreservingReturn(span, () => underlying.apply(instance, args));
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
return makeProxy(instance, handlers);
|
|
707
|
+
}
|
|
708
|
+
function wrapWorkflowBindingResult(value, op, opts, name, span) {
|
|
709
|
+
if (op === "deleteBatch")
|
|
710
|
+
return value;
|
|
711
|
+
if (op === "createBatch") {
|
|
712
|
+
if (!Array.isArray(value))
|
|
713
|
+
return value;
|
|
714
|
+
const returnedIds = workflowBatchIds(value);
|
|
715
|
+
if (span && returnedIds.length > 0) {
|
|
716
|
+
span.setAttribute("cloudflare.workflow.instance_ids", returnedIds);
|
|
717
|
+
}
|
|
718
|
+
return value.map((item) => wrapWorkflowInstance(item, opts, name));
|
|
719
|
+
}
|
|
720
|
+
const returnedId = workflowId(value);
|
|
721
|
+
if (span && returnedId !== undefined) {
|
|
722
|
+
span.setAttribute("cloudflare.workflow.instance_id", returnedId);
|
|
723
|
+
}
|
|
724
|
+
return wrapWorkflowInstance(value, opts, name);
|
|
725
|
+
}
|
|
726
|
+
function wrapWorkflowBinding(binding, opts, name) {
|
|
727
|
+
const baseAttrs = {
|
|
728
|
+
"cloudflare.workflow.namespace": name,
|
|
729
|
+
"cloudflare.binding.type": "workflow",
|
|
730
|
+
"cloudflare.binding.name": name,
|
|
731
|
+
};
|
|
732
|
+
const ops = ["create", "createBatch", "get", "deleteBatch"];
|
|
733
|
+
const handlers = {};
|
|
734
|
+
for (const op of ops) {
|
|
735
|
+
handlers[op] = (...args) => {
|
|
736
|
+
if (isTracingSuppressed(context.active())) {
|
|
737
|
+
return mapReturn(binding[op](...args), (value) => wrapWorkflowBindingResult(value, op, opts, name));
|
|
738
|
+
}
|
|
739
|
+
const attrs = { ...baseAttrs };
|
|
740
|
+
const instanceId = workflowId(args[0]);
|
|
741
|
+
const batchIds = op === "createBatch" || op === "deleteBatch" ? workflowBatchIds(args[0]) : [];
|
|
742
|
+
if (instanceId !== undefined) {
|
|
743
|
+
attrs["cloudflare.workflow.instance_id"] = instanceId;
|
|
744
|
+
}
|
|
745
|
+
if (batchIds.length > 0) {
|
|
746
|
+
attrs["cloudflare.workflow.instance_ids"] = batchIds;
|
|
747
|
+
attrs["cloudflare.workflow.batch.size"] = batchIds.length;
|
|
748
|
+
}
|
|
749
|
+
const span = opts.startSpan(`Workflow ${op}`, attrs, SpanKind.CLIENT);
|
|
750
|
+
return runWithSpanPreservingReturn(span, () => mapReturn(binding[op](...args), (value) => wrapWorkflowBindingResult(value, op, opts, name, span)));
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
return makeProxy(binding, handlers);
|
|
754
|
+
}
|
|
755
|
+
// ---------------------------------------------------------------------------
|
|
484
756
|
// Main export
|
|
485
757
|
// ---------------------------------------------------------------------------
|
|
486
758
|
/**
|
|
@@ -497,6 +769,8 @@ function wrapService(binding, opts, name) {
|
|
|
497
769
|
* - Workflow: `get`+`create`+`createBatch`
|
|
498
770
|
* - Rate limit: `limit`
|
|
499
771
|
* - Analytics Engine: `writeDataPoint`
|
|
772
|
+
* - Durable Object namespace: `idFromName`+`get`+`newUniqueId` (stub fetch/RPC spans)
|
|
773
|
+
* - Workflow binding: `create`+`createBatch`+`get` and not a DO namespace
|
|
500
774
|
* - Service binding / Fetcher: `fetch` (last branch; most generic)
|
|
501
775
|
*
|
|
502
776
|
* Unrecognised bindings are passed through unchanged.
|
|
@@ -538,6 +812,12 @@ export function instrumentEnv(env, opts) {
|
|
|
538
812
|
else if (isQueueLike(binding)) {
|
|
539
813
|
result[key] = wrapQueue(binding, opts, key);
|
|
540
814
|
}
|
|
815
|
+
else if (isDurableObjectNamespaceLike(binding)) {
|
|
816
|
+
result[key] = wrapDurableObjectNamespace(binding, opts, key);
|
|
817
|
+
}
|
|
818
|
+
else if (isWorkflowBindingLike(binding)) {
|
|
819
|
+
result[key] = wrapWorkflowBinding(binding, opts, key);
|
|
820
|
+
}
|
|
541
821
|
else if (isWorkflowLike(binding)) {
|
|
542
822
|
result[key] = wrapWorkflow(binding, opts, key);
|
|
543
823
|
}
|
package/dist/workers.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Span } from "@opentelemetry/api";
|
|
1
|
+
import { SpanKind, type Span } from "@opentelemetry/api";
|
|
2
2
|
import { type Context, type ContextManager } from "@opentelemetry/api";
|
|
3
3
|
import { BasicTracerProvider, type ReadableSpan, type SpanExporter } from "@opentelemetry/sdk-trace-base";
|
|
4
4
|
import { type HeystackOptions } from "./core.js";
|
|
@@ -92,6 +92,8 @@ export declare class HeystackSpanExporter implements SpanExporter {
|
|
|
92
92
|
readonly ingestHost: string;
|
|
93
93
|
private readonly headers;
|
|
94
94
|
private shutdownState;
|
|
95
|
+
/** Exports rejected before network work because the in-flight cap was hit. */
|
|
96
|
+
droppedPending: number;
|
|
95
97
|
/**
|
|
96
98
|
* In-flight export fetches. Each `export()` adds its settled-when-the-POST-
|
|
97
99
|
* completes promise here and removes it on `finally`. `forceFlush()` awaits
|
|
@@ -112,10 +114,21 @@ export declare class HeystackSpanExporter implements SpanExporter {
|
|
|
112
114
|
waitUntil?: (p: Promise<unknown>) => void;
|
|
113
115
|
constructor(options: HeystackOptions);
|
|
114
116
|
export(spans: ReadableSpan[], resultCallback: (result: ExportResult) => void): void;
|
|
117
|
+
/**
|
|
118
|
+
* POST the batch with ONE bounded automatic retry for network errors / 429 /
|
|
119
|
+
* 5xx. Honours `Retry-After` (seconds form) on 429 within the total backoff
|
|
120
|
+
* cap; after retries are exhausted it preserves plain FAILED semantics via
|
|
121
|
+
* `resultCallback`. The returned promise never rejects.
|
|
122
|
+
*/
|
|
123
|
+
private postWithRetry;
|
|
124
|
+
/** Reserve a tracked slot synchronously before starting any network work. */
|
|
125
|
+
private reservePending;
|
|
115
126
|
/**
|
|
116
127
|
* Hand `p` to the OpenNext Cloudflare request context's `ctx.waitUntil`.
|
|
117
128
|
* Resolves the accessor first (awaiting its in-flight load if needed) so an
|
|
118
129
|
* export that fired before the dynamic import settled is still covered.
|
|
130
|
+
* Sets `outcome.borrowed` when (and only when) a real `waitUntil` received
|
|
131
|
+
* `p` — the signal the no-handoff advisory keys off.
|
|
119
132
|
*/
|
|
120
133
|
private handOffToCloudflareContext;
|
|
121
134
|
private warnedWaitUntilFailure;
|
|
@@ -235,13 +248,11 @@ export interface WorkersConfig {
|
|
|
235
248
|
/** Minimal public surface exposed by Cloudflare's Workers custom-spans API. */
|
|
236
249
|
export interface CloudflareNativeSpan {
|
|
237
250
|
readonly isTraced: boolean;
|
|
238
|
-
setAttribute(key: string, value
|
|
239
|
-
end(): void;
|
|
251
|
+
setAttribute(key: string, value?: string | number | boolean): void;
|
|
240
252
|
}
|
|
241
253
|
/** `ExecutionContext.tracing` without importing the `cloudflare:workers` module. */
|
|
242
254
|
export interface CloudflareNativeTracing {
|
|
243
|
-
enterSpan<T>(name: string, callback: (span: CloudflareNativeSpan) => T): T;
|
|
244
|
-
startActiveSpan<T>(name: string, callback: (span: CloudflareNativeSpan) => T): T;
|
|
255
|
+
enterSpan<T, A extends unknown[]>(name: string, callback: (span: CloudflareNativeSpan, ...args: A) => T, ...args: A): T;
|
|
245
256
|
}
|
|
246
257
|
/**
|
|
247
258
|
* A `BasicTracerProvider` with the underlying `HeystackSpanExporter` attached so
|
|
@@ -273,9 +284,9 @@ export declare function createTracerProvider(config: HeystackOptions & {
|
|
|
273
284
|
* context across `await` boundaries — child spans created after an `await`
|
|
274
285
|
* inside a handler are correctly parented to the request's root span.
|
|
275
286
|
*
|
|
276
|
-
* WinterCG-safe: it does NOT import
|
|
277
|
-
* ALS global that the runtime exposes. Falls back to
|
|
278
|
-
* when the global is absent (Deno, Bun without globals, etc.).
|
|
287
|
+
* WinterCG-safe: it does NOT import the Node-only async_hooks builtin; instead
|
|
288
|
+
* it uses the ALS global that the runtime exposes. Falls back to
|
|
289
|
+
* `SyncStackContextManager` when the global is absent (Deno, Bun without globals, etc.).
|
|
279
290
|
*/
|
|
280
291
|
export declare class AlsContextManager implements ContextManager {
|
|
281
292
|
private _als;
|
|
@@ -311,10 +322,12 @@ export declare function __resetContextManager(): void;
|
|
|
311
322
|
* FLUSH: on Next-on-OpenNext (where `@opennextjs/cloudflare` is present) the
|
|
312
323
|
* exporter automatically borrows the Cloudflare request context's
|
|
313
324
|
* `ctx.waitUntil` so the export POST completes before the isolate is torn down
|
|
314
|
-
* — no app hook needed. For other workerd setups without it,
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
* the
|
|
325
|
+
* — no app hook needed. For other workerd setups without it, wrap your
|
|
326
|
+
* handler's ExecutionContext with {@link wrapExecutionContext} (every
|
|
327
|
+
* `ctx.waitUntil` then also schedules the export drain), call `flushHeystack()`
|
|
328
|
+
* (which awaits the export fetch) from a response hook, hand it to
|
|
329
|
+
* `ctx.waitUntil(flushHeystack())`, or pass an explicit `waitUntil` in the
|
|
330
|
+
* config (highest priority). Returns the provider.
|
|
318
331
|
*/
|
|
319
332
|
export declare function initHeystackWorkers(config: WorkersConfig & {
|
|
320
333
|
apiKey: string;
|
|
@@ -328,21 +341,25 @@ export declare function initHeystackWorkers(config: WorkersConfig & {
|
|
|
328
341
|
* isolate is torn down. Best-effort no-op if no provider is registered.
|
|
329
342
|
*/
|
|
330
343
|
export declare function flushHeystack(): Promise<void>;
|
|
344
|
+
export declare function wrapExecutionContext<Props>(ctx: ExecutionContext<Props>): ExecutionContext<Props>;
|
|
331
345
|
/** Reset the singleton global provider. Internal/testing helper. */
|
|
332
346
|
export declare function __resetProvider(): void;
|
|
333
|
-
/** Reset the once-only
|
|
347
|
+
/** Reset the once-only warnings. Internal/testing helper. */
|
|
334
348
|
export declare function __resetWarnings(): void;
|
|
335
349
|
/**
|
|
336
350
|
* The shape of a Worker's default export. `fetch` is the common entrypoint, but
|
|
337
|
-
* a Worker may also export `queue` / `scheduled` / `tail` (and
|
|
338
|
-
* siblings). `instrument()`
|
|
339
|
-
*
|
|
340
|
-
*
|
|
351
|
+
* a Worker may also export `queue` / `scheduled` / `tail` / `email` (and
|
|
352
|
+
* arbitrary other siblings). `instrument()` traces `fetch`, `queue`,
|
|
353
|
+
* `scheduled`, `email` and `tail` when present — every other sibling is
|
|
354
|
+
* forwarded untouched, so wrapping never drops a handler Cloudflare requires
|
|
355
|
+
* for deploy.
|
|
341
356
|
*/
|
|
342
357
|
interface WorkerHandler<E> {
|
|
343
358
|
fetch?(req: Request, env: E, ctx: ExecutionContext): Promise<Response> | Response;
|
|
344
359
|
queue?(batch: MessageBatch<any>, env: E, ctx: ExecutionContext): Promise<void> | void;
|
|
345
360
|
scheduled?(controller: ScheduledController, env: E, ctx: ExecutionContext): Promise<void> | void;
|
|
361
|
+
email?(message: ForwardableEmailMessage, env: E, ctx: ExecutionContext): Promise<void> | void;
|
|
362
|
+
tail?(events: TraceItem[], env: E, ctx: ExecutionContext): Promise<void> | void;
|
|
346
363
|
[key: string]: unknown;
|
|
347
364
|
}
|
|
348
365
|
/**
|
|
@@ -370,8 +387,8 @@ interface WorkerHandler<E> {
|
|
|
370
387
|
* ALL HANDLERS FORWARDED: the returned object spreads `handler`, so sibling
|
|
371
388
|
* handlers a Worker exports (`tail`, `email`, etc.) are preserved — Cloudflare
|
|
372
389
|
* won't reject the deploy for a missing handler. `fetch` is always replaced with
|
|
373
|
-
* a traced wrapper; `queue` and `
|
|
374
|
-
* present; everything else is forwarded untouched.
|
|
390
|
+
* a traced wrapper; `queue`, `scheduled`, `email` and `tail` are wrapped (and
|
|
391
|
+
* traced) when present; everything else is forwarded untouched.
|
|
375
392
|
*
|
|
376
393
|
* If no API key is available (neither `config.apiKey` nor
|
|
377
394
|
* `env.HEYSTACK_API_KEY`), the handler runs untraced.
|
|
@@ -383,7 +400,7 @@ interface WorkerHandler<E> {
|
|
|
383
400
|
* traced key is present (and non-optional) exactly when it exists on the input
|
|
384
401
|
* handler, so a `{ fetch }` Worker yields a non-optional `fetch`.
|
|
385
402
|
*/
|
|
386
|
-
type Instrumented<E, H> = Omit<H, "fetch" | "queue" | "scheduled"> & (H extends {
|
|
403
|
+
type Instrumented<E, H> = Omit<H, "fetch" | "queue" | "scheduled" | "email" | "tail"> & (H extends {
|
|
387
404
|
fetch: unknown;
|
|
388
405
|
} ? {
|
|
389
406
|
fetch: (req: Request, env: E, ctx: ExecutionContext) => Promise<Response>;
|
|
@@ -395,6 +412,14 @@ type Instrumented<E, H> = Omit<H, "fetch" | "queue" | "scheduled"> & (H extends
|
|
|
395
412
|
scheduled: unknown;
|
|
396
413
|
} ? {
|
|
397
414
|
scheduled: (controller: ScheduledController, env: E, ctx: ExecutionContext) => Promise<void>;
|
|
415
|
+
} : unknown) & (H extends {
|
|
416
|
+
email: unknown;
|
|
417
|
+
} ? {
|
|
418
|
+
email: (message: ForwardableEmailMessage, env: E, ctx: ExecutionContext) => Promise<void>;
|
|
419
|
+
} : unknown) & (H extends {
|
|
420
|
+
tail: unknown;
|
|
421
|
+
} ? {
|
|
422
|
+
tail: (events: TraceItem[], env: E, ctx: ExecutionContext) => Promise<void>;
|
|
398
423
|
} : unknown);
|
|
399
424
|
/**
|
|
400
425
|
* Recover a handler's concrete env type from whichever traced entrypoint it
|
|
@@ -412,6 +437,52 @@ type EnvOf<H> = H extends {
|
|
|
412
437
|
scheduled: (controller: ScheduledController, env: infer E, ctx: ExecutionContext) => unknown;
|
|
413
438
|
} ? E : unknown;
|
|
414
439
|
export declare function instrument<H extends WorkerHandler<any>>(handler: H, config: WorkersConfig): Instrumented<EnvOf<H>, H>;
|
|
440
|
+
export interface InstrumentClassesOpts {
|
|
441
|
+
/**
|
|
442
|
+
* Factory that creates and starts a new span. Called at method-invocation
|
|
443
|
+
* time. For integration: omit it — the global `heystack` tracer is used with
|
|
444
|
+
* the active context (or the explicit `parentCtx`). For unit tests inject a
|
|
445
|
+
* fake so no global provider is required.
|
|
446
|
+
*/
|
|
447
|
+
startSpan?: (name: string, attrs: Record<string, unknown>, kind?: SpanKind, parentCtx?: Context) => Span;
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Decorate a Durable Object class:
|
|
451
|
+
* - constructor: wraps the incoming `state` so `state.storage` operations emit
|
|
452
|
+
* INTERNAL child spans (`DO storage get|put|delete|list`) with key/count attrs.
|
|
453
|
+
* - `fetch()`: SERVER span named `<METHOD> <path>` (like instrument()'s fetch
|
|
454
|
+
* wrapper), continuing an inbound W3C traceparent, recording status/duration.
|
|
455
|
+
* - `alarm()`: INTERNAL span.
|
|
456
|
+
* - RPC prototype methods: SERVER span `DO rpc <method>` with rpc.method attrs.
|
|
457
|
+
*
|
|
458
|
+
* Every root span schedules flushHeystack() via `state.waitUntil` so exports
|
|
459
|
+
* complete before isolate teardown. Instance arrow-function methods assigned in
|
|
460
|
+
* the constructor are not enumerable on the prototype and stay unwrapped.
|
|
461
|
+
*
|
|
462
|
+
* export class Counter extends DurableObject {
|
|
463
|
+
* // ...
|
|
464
|
+
* }
|
|
465
|
+
* export const Counter = wrapDurableObjectClass(BaseCounter); // or decorate in place
|
|
466
|
+
*/
|
|
467
|
+
export declare function wrapDurableObjectClass<C extends {
|
|
468
|
+
readonly prototype: object;
|
|
469
|
+
}>(cls: C, opts?: InstrumentClassesOpts): C;
|
|
470
|
+
/**
|
|
471
|
+
* Decorate a Cloudflare Workflows entrypoint class so its `run(event, step)`
|
|
472
|
+
* emits a `workflow run` INTERNAL span with workflow identity/schedule metadata
|
|
473
|
+
* (never payload content), and the handed-out
|
|
474
|
+
* step object's methods become traced versions (see {@link wrapWorkflowStep}):
|
|
475
|
+
* `step.do(name, fn)` → INTERNAL span named `step <name>` with success/error
|
|
476
|
+
* status (step retries manifest as whole-run replays by the engine and are not
|
|
477
|
+
* observable at this layer, so no retry attribute is emitted), and
|
|
478
|
+
* sleep/sleepUntil/waitForEvent/waitForName → short marker spans.
|
|
479
|
+
*
|
|
480
|
+
* Every completed run schedules flushHeystack() via the entrypoint state's
|
|
481
|
+
* `waitUntil` so exports survive isolate teardown.
|
|
482
|
+
*/
|
|
483
|
+
export declare function wrapWorkflowEntrypointClass<C extends {
|
|
484
|
+
readonly prototype: object;
|
|
485
|
+
}>(cls: C, opts?: InstrumentClassesOpts): C;
|
|
415
486
|
/**
|
|
416
487
|
* Run `fn` inside a new child span named `name`. The span is automatically
|
|
417
488
|
* parented to the currently-active span (via `context.active()`), started
|