@heystack/otel 0.14.0 → 0.15.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 +39 -5
- package/dist/workers-bindings.d.ts +4 -1
- package/dist/workers-bindings.js +194 -3
- 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.15.0`.** 0.15 extends direct Workers instrumentation to D1 sessions and batches. 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
|
|
|
@@ -21,6 +21,8 @@ HEYSTACK_API_KEY=sk_live_…
|
|
|
21
21
|
| --- | --- | --- |
|
|
22
22
|
| Next.js — **any** deploy target (Vercel/Node **and** Cloudflare/OpenNext) | `@heystack/otel/next` | `registerHeystack` in `instrumentation.ts`. Auto-detects Node vs Cloudflare workerd and picks the right exporter. No-op on Edge. |
|
|
23
23
|
| Standalone Cloudflare Workers (hand-written `export default { fetch }`) | `@heystack/otel/workers` | Native-first `instrument()` wrapper; Cloudflare owns OTLP export and automatic platform spans. |
|
|
24
|
+
| Cloudflare Python Workers | No `@heystack/otel` import | Enable Cloudflare native traces/logs and an OTLP destination. Python runs inside workerd, so platform tracing covers handler and supported binding operations without a JavaScript SDK. |
|
|
25
|
+
| WebAssembly modules used by a Worker | Instrument the JavaScript/TypeScript host, if present | Enable Cloudflare native traces/logs for the Worker. Wasm execution is part of its host invocation; this package does not inject language-level spans inside Rust/Go/C Wasm code. |
|
|
24
26
|
| Cloudflare Agents SDK / Agents Tail Worker | `@heystack/otel/workers/agents` | Structured Agents lifecycle events, trace correlation, redaction, and Tail Worker adapter. |
|
|
25
27
|
| Node / Express / Fastify / NestJS (long-running server) | `@heystack/otel/node` | `initHeystack`: auto-instrumentations + graceful shutdown. |
|
|
26
28
|
| Browser (SPA / any web frontend) | `@heystack/otel/web` | `instrumentWeb`: session replay + opt-in browser distributed tracing (`tracing: true`) that emits CLIENT spans + propagates W3C `traceparent` (browser→API shows as one trace). No-op on the server (SSR-safe). |
|
|
@@ -147,6 +149,12 @@ export default instrument(worker, {
|
|
|
147
149
|
});
|
|
148
150
|
```
|
|
149
151
|
|
|
152
|
+
Native export is the portable path across Workers languages. The dashboard destination sends OTLP/HTTP traces and logs, so the same configuration applies to JavaScript/TypeScript, [Python Workers](https://developers.cloudflare.com/workers/languages/python/), and Workers whose handlers call [WebAssembly modules](https://developers.cloudflare.com/workers/runtime-apis/webassembly/). Cloudflare's [OTel export documentation](https://developers.cloudflare.com/workers/observability/exporting-opentelemetry-data/) currently says infrastructure and custom metrics export is unavailable.
|
|
153
|
+
|
|
154
|
+
For a Python Worker, configure the same `observability.traces` and `observability.logs` blocks and emit structured application logs through the runtime's `console`. There is no Python entry point in this npm package and no claim of Python function-level auto-instrumentation. For WebAssembly, native tracing observes the Worker invocation and Cloudflare operations around the module. Add explicit spans in the JavaScript/TypeScript host with `withCloudflareSpan` when you need timings around a Wasm call; spans inside the compiled module require that language's own workerd-compatible instrumentation.
|
|
155
|
+
|
|
156
|
+
Cloudflare native tracing automatically covers handler invocations, fetch subrequests, and supported platform operations such as KV, R2, and Durable Objects. The direct JavaScript binding proxies below are a compatibility and enrichment layer; they are not required when native tracing is active.
|
|
157
|
+
|
|
150
158
|
The wrapper also enters native spans for default-export queue and scheduled handlers. `withCloudflareSpan(ctx, ...)` remains native across arbitrary `await` boundaries and does not require `nodejs_compat`. `withSpan(...)` routes to the native tracer whenever its active context is available.
|
|
151
159
|
|
|
152
160
|
### Cloudflare Agents SDK
|
|
@@ -184,7 +192,7 @@ export default instrument(
|
|
|
184
192
|
getUser: (req) => ({
|
|
185
193
|
id: req.headers.get("x-user-id") ?? undefined,
|
|
186
194
|
}),
|
|
187
|
-
instrumentBindings: true, //
|
|
195
|
+
instrumentBindings: true, // direct-mode spans for supported binding operations
|
|
188
196
|
},
|
|
189
197
|
);
|
|
190
198
|
```
|
|
@@ -201,7 +209,7 @@ Set the key as a secret: `wrangler secret put HEYSTACK_API_KEY`, and set `native
|
|
|
201
209
|
> ```toml
|
|
202
210
|
> compatibility_flags = ["nodejs_compat"]
|
|
203
211
|
> ```
|
|
204
|
-
> Without it the SDK falls back to a synchronous stack-based context manager — suppression still works, and **binding spans
|
|
212
|
+
> Without it the SDK falls back to a synchronous stack-based context manager — suppression still works, and **instrumented binding spans are still parented to the request's SERVER span** (they fall back to the request context explicitly, so they never orphan into their own single-span traces). What still degrades to best-effort without `nodejs_compat` is cross-`await` parenting for *outbound-fetch CLIENT spans* and *manual `withSpan` spans* — so `nodejs_compat` remains recommended for a fully-nested trace tree.
|
|
205
213
|
|
|
206
214
|
### `WorkersConfig` options
|
|
207
215
|
|
|
@@ -211,7 +219,7 @@ Set the key as a secret: `wrangler secret put HEYSTACK_API_KEY`, and set `native
|
|
|
211
219
|
| `nativeTracing` | `"auto" \| "native" \| "direct"` | `auto` (default) uses Cloudflare's native tree when `ctx.tracing` exists. `direct` forces the compatibility exporter. |
|
|
212
220
|
| `apiKey` | `string?` | Defaults to `env.HEYSTACK_API_KEY`. |
|
|
213
221
|
| `getUser` | `(req: Request) => { id?, session?, requestId? } \| undefined` | Called per request. `id` → `enduser.id`, `session` → `session.id`, `requestId` → `http.request.id` (falls back to the `cf-ray` header). |
|
|
214
|
-
| `instrumentBindings` | `boolean \| string[]` | `true` =
|
|
222
|
+
| `instrumentBindings` | `boolean \| string[]` | Direct mode only. `true` = child spans for all detected supported bindings; `string[]` = only named bindings. Default `false`. See the operation matrix below. |
|
|
215
223
|
| `sampling` | `{ rate?: number } \| { remote: true }` | Head-sampling configuration. `{ rate }`: keep a deterministic fraction of fresh root traces (0–1; default `1` = keep all). `{ remote: true }`: fetch the rate from the Heystack config endpoint instead — lets you change it centrally without redeploying. Cold isolates keep all traffic until the first config fetch resolves; fails open if the config can't be reached. Parent-respecting in both modes: a request arriving with a sampled `traceparent` is always recorded. See [Head sampling](#head-sampling) below. |
|
|
216
224
|
| `ai` | `{ captureContent?: boolean; redact?: (text: string) => string; maxContentChars?: number; gateways?: Array<{ hostSuffix: string; pathPattern: string }> }` | LLM/gen_ai capture for outbound calls to known providers (works under native tracing and the direct exporter). `gateways` declares AI gateways/proxies you call instead of the provider host (e.g. Wafer). See [AI / LLM observability](#ai--llm-observability) below. |
|
|
217
225
|
| `waitUntil` | `(p: Promise<unknown>) => void` | Override the isolate keep-alive hook; defaults to the auto-detected `ctx.waitUntil`. |
|
|
@@ -257,7 +265,22 @@ On startup the worker fetches its configured rate from the Heystack config endpo
|
|
|
257
265
|
- **Outbound `fetch`** — each outbound subrequest while a request span is active gets a CLIENT child span (`http.request.method`, `url.full`, `server.address`, `http.response.status_code`). A W3C `traceparent` header is injected into the subrequest so a downstream Heystack-instrumented service continues the same trace (distributed tracing across services). The exporter's own ingest POST is never traced.
|
|
258
266
|
- **Queue consumers (`queue`)** — a CONSUMER span per batch, with `messaging.destination.name` (queue name) and `messaging.batch.message_count`.
|
|
259
267
|
- **Scheduled handlers (`scheduled`)** — an INTERNAL span per invocation, with `controller.cron`.
|
|
260
|
-
- **Binding calls** (when `instrumentBindings` is set) —
|
|
268
|
+
- **Binding calls** (direct mode, when `instrumentBindings` is set) — child spans for the stable operations listed below. Service binding `.fetch` calls receive a W3C `traceparent` header so another instrumented Worker can continue the trace.
|
|
269
|
+
|
|
270
|
+
| Binding | Direct-mode operations covered |
|
|
271
|
+
| --- | --- |
|
|
272
|
+
| D1 | prepared statement `all`, `first`, `run`, `raw`; database/session `batch`; database `exec`; statements created through `withSession` |
|
|
273
|
+
| KV | `get`, `getWithMetadata`, `put`, `list`, `delete` |
|
|
274
|
+
| R2 | `get`, `put`, `head`, `delete`, `list`; multipart create/resume plus `uploadPart`, `complete`, `abort` |
|
|
275
|
+
| Vectorize | `query`, `upsert`, `insert`, `deleteByIds`, `getByIds` |
|
|
276
|
+
| Workers AI | `run`, including model and token usage when returned without consuming streams |
|
|
277
|
+
| Queues producer | `send`, `sendBatch`, `metrics` |
|
|
278
|
+
| Workflows | `get`, `create`, `createBatch`, `deleteBatch` |
|
|
279
|
+
| Rate Limiting | `limit`; the caller's rate-limit key is deliberately not attached |
|
|
280
|
+
| Analytics Engine | synchronous `writeDataPoint`; datapoint values are deliberately not attached |
|
|
281
|
+
| Fetch-compatible bindings | `fetch` on Service, Browser Rendering, Assets, and other Fetcher-shaped bindings |
|
|
282
|
+
|
|
283
|
+
Some bindings intentionally remain outside generic direct-mode proxying. Durable Object namespaces return arbitrary RPC stubs whose method surface is application-defined; Hyperdrive exposes a connection string and the actual calls belong to the selected database driver. Email and Pipelines both expose only a generic `send` method; Analytics SQL exposes only `query`; Media begins with a generic `input`; blindly identifying those shapes could wrap an unrelated application object. Images' chained transform handles and hosted-image handles are also not proxied. Secrets, version metadata, and static-data bindings do not perform observable operations. Use [Cloudflare native tracing](https://developers.cloudflare.com/workers/observability/traces/) where it supports the binding, instrument the database/client library, or wrap a known application operation with `withSpan`. RPC methods beyond `.fetch()` need explicit application spans because automatically proxying arbitrary properties can change RPC stub semantics.
|
|
261
284
|
|
|
262
285
|
### Client enrichment
|
|
263
286
|
|
|
@@ -592,6 +615,7 @@ As belt-and-suspenders the exporter also drops any span whose HTTP target points
|
|
|
592
615
|
|
|
593
616
|
## Migration / versioning
|
|
594
617
|
|
|
618
|
+
- **`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.
|
|
595
619
|
- **`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.
|
|
596
620
|
- **`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.
|
|
597
621
|
- **`0.11.0`** — **`/web`: browser error + console collection, in-app bug reports, and seekable replay keyframes.** `instrumentWeb` now captures uncaught errors (`window.onerror` + `unhandledrejection`) and `console.error` as OTLP logs by default (`event.name=browser.error` / `browser.console`, with `exception.*` semconv, `url.full`, `session_id`, and the active trace/span ids when `tracing` is on). New options: `errors?: boolean` (default `true`), `captureConsole?: 'error' | 'warn' | false` (default `'error'`). New **`reportBug({ message, email?, context? })`** API — a headless in-app bug reporter that POSTs to `/v1/bug-reports` and auto-attaches the URL, user agent, `session_id`, active `trace_id`, `release`/`build`, and the last ≤20 captured browser errors (also emitted as a `user.bug_report` log); reports triage in the console **Bugs** tab. The `version` / `build` options are now stamped as `service.version` / `service.build` on exported browser logs (release attribution). Console capture is recursion-guarded + rate-capped (no self-export loop, no flood). The recorder also checkpoints a keyframe (~60s) so the console player seeks from the nearest snapshot instead of loading the whole session. No breaking changes; all new options are optional.
|
|
@@ -624,3 +648,13 @@ As belt-and-suspenders the exporter also drops any span whose HTTP target points
|
|
|
624
648
|
- Short-lived processes: `initHeystack` registers `SIGTERM`/`SIGINT` handlers automatically, so the SDK flushes and shuts down before exit and you don't lose the last batch.
|
|
625
649
|
|
|
626
650
|
Then run your app and make a request — traces appear in your Heystack dashboard within seconds.
|
|
651
|
+
|
|
652
|
+
## Investigate with your coding agent
|
|
653
|
+
|
|
654
|
+
Once telemetry is flowing, connect your coding agent to the hosted **Heystack MCP server** and it can verify the setup (`heystack_verify_setup`), investigate incidents from your traces, sessions and releases, and propose the fix in your code:
|
|
655
|
+
|
|
656
|
+
```bash
|
|
657
|
+
claude mcp add --transport http heystack https://mcp.heystack.dev/mcp
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
One-click links for Cursor and VS Code, commands for Codex/Gemini CLI, personal access tokens for CI, and the full tool reference are in the docs: **https://heystack.dev/docs/mcp**.
|
|
@@ -10,7 +10,7 @@ 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 all
|
|
13
|
+
* `true` → auto-detect and wrap all supported Cloudflare bindings.
|
|
14
14
|
* `string[]` → only wrap bindings whose env key is listed.
|
|
15
15
|
*/
|
|
16
16
|
select: boolean | string[];
|
|
@@ -34,6 +34,9 @@ export declare function injectTraceparentArgs(args: any[], traceparent: string):
|
|
|
34
34
|
* - Vectorize: `query`+`upsert`
|
|
35
35
|
* - Workers AI: `run` (without `prepare`)
|
|
36
36
|
* - Queue producer: `send`+`sendBatch`
|
|
37
|
+
* - Workflow: `get`+`create`+`createBatch`
|
|
38
|
+
* - Rate limit: `limit`
|
|
39
|
+
* - Analytics Engine: `writeDataPoint`
|
|
37
40
|
* - Service binding / Fetcher: `fetch` (last branch; most generic)
|
|
38
41
|
*
|
|
39
42
|
* Unrecognised bindings are passed through unchanged.
|
package/dist/workers-bindings.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
2
|
// Cloudflare binding instrumentation for @heystack/otel/workers.
|
|
3
3
|
//
|
|
4
|
-
// Wraps
|
|
5
|
-
//
|
|
6
|
-
// as a child of the active request span.
|
|
4
|
+
// Wraps stable Cloudflare binding APIs with OTel child spans so that binding
|
|
5
|
+
// operations are visible as children of the active request span.
|
|
7
6
|
//
|
|
8
7
|
// WinterCG-safe: no `node:*` imports. Span factory is injected so the logic
|
|
9
8
|
// is pure and unit-testable without a global provider.
|
|
@@ -45,6 +44,17 @@ function isQueueLike(b) {
|
|
|
45
44
|
return (typeof b?.send === "function" &&
|
|
46
45
|
typeof b?.sendBatch === "function");
|
|
47
46
|
}
|
|
47
|
+
function isWorkflowLike(b) {
|
|
48
|
+
return (typeof b?.get === "function" &&
|
|
49
|
+
typeof b?.create === "function" &&
|
|
50
|
+
typeof b?.createBatch === "function");
|
|
51
|
+
}
|
|
52
|
+
function isRateLimitLike(b) {
|
|
53
|
+
return typeof b?.limit === "function";
|
|
54
|
+
}
|
|
55
|
+
function isAnalyticsEngineLike(b) {
|
|
56
|
+
return typeof b?.writeDataPoint === "function";
|
|
57
|
+
}
|
|
48
58
|
/** Service binding / Fetcher: last branch — anything with `.fetch`. Prior branches
|
|
49
59
|
* exclude D1/R2/KV/Vectorize/AI/Queue, so order alone is sufficient. */
|
|
50
60
|
function isServiceLike(b) {
|
|
@@ -69,12 +79,31 @@ async function runWithSpan(span, fn) {
|
|
|
69
79
|
span.end();
|
|
70
80
|
}
|
|
71
81
|
}
|
|
82
|
+
function runSyncWithSpan(span, fn) {
|
|
83
|
+
try {
|
|
84
|
+
return fn();
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
span.recordException(err instanceof Error ? err : new Error(String(err)));
|
|
88
|
+
span.setStatus({
|
|
89
|
+
code: SpanStatusCode.ERROR,
|
|
90
|
+
message: err instanceof Error ? err.message : String(err),
|
|
91
|
+
});
|
|
92
|
+
throw err;
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
span.end();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
72
98
|
// ---------------------------------------------------------------------------
|
|
73
99
|
// Proxy helper — intercepts listed handlers, falls through to prototype for rest
|
|
74
100
|
// ---------------------------------------------------------------------------
|
|
101
|
+
const rawTarget = Symbol("heystack.rawBindingTarget");
|
|
75
102
|
function makeProxy(target, handlers) {
|
|
76
103
|
return new Proxy(target, {
|
|
77
104
|
get(t, prop, receiver) {
|
|
105
|
+
if (prop === rawTarget)
|
|
106
|
+
return t;
|
|
78
107
|
if (typeof prop === "string" && prop in handlers)
|
|
79
108
|
return handlers[prop];
|
|
80
109
|
const val = Reflect.get(t, prop, receiver);
|
|
@@ -82,6 +111,9 @@ function makeProxy(target, handlers) {
|
|
|
82
111
|
},
|
|
83
112
|
});
|
|
84
113
|
}
|
|
114
|
+
function unwrapTarget(value) {
|
|
115
|
+
return (value?.[rawTarget] ?? value);
|
|
116
|
+
}
|
|
85
117
|
// ---------------------------------------------------------------------------
|
|
86
118
|
// D1 wrappers
|
|
87
119
|
// ---------------------------------------------------------------------------
|
|
@@ -112,6 +144,47 @@ function wrapD1(binding, opts) {
|
|
|
112
144
|
prepare(sql) {
|
|
113
145
|
return wrapD1Statement(binding.prepare(sql), sql, opts);
|
|
114
146
|
},
|
|
147
|
+
batch(statements) {
|
|
148
|
+
if (isTracingSuppressed(context.active())) {
|
|
149
|
+
return binding.batch(statements.map(unwrapTarget));
|
|
150
|
+
}
|
|
151
|
+
const span = opts.startSpan("D1 batch", {
|
|
152
|
+
"db.system": "d1",
|
|
153
|
+
"db.operation.name": "batch",
|
|
154
|
+
"db.operation.batch.size": statements.length,
|
|
155
|
+
});
|
|
156
|
+
return runWithSpan(span, () => binding.batch(statements.map(unwrapTarget)));
|
|
157
|
+
},
|
|
158
|
+
exec(query) {
|
|
159
|
+
if (isTracingSuppressed(context.active()))
|
|
160
|
+
return binding.exec(query);
|
|
161
|
+
const span = opts.startSpan("D1 exec", {
|
|
162
|
+
"db.system": "d1",
|
|
163
|
+
"db.operation.name": "exec",
|
|
164
|
+
});
|
|
165
|
+
return runWithSpan(span, () => binding.exec(query));
|
|
166
|
+
},
|
|
167
|
+
withSession(...args) {
|
|
168
|
+
return wrapD1Session(binding.withSession(...args), opts);
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
function wrapD1Session(session, opts) {
|
|
173
|
+
return makeProxy(session, {
|
|
174
|
+
prepare(sql) {
|
|
175
|
+
return wrapD1Statement(session.prepare(sql), sql, opts);
|
|
176
|
+
},
|
|
177
|
+
batch(statements) {
|
|
178
|
+
if (isTracingSuppressed(context.active())) {
|
|
179
|
+
return session.batch(statements.map(unwrapTarget));
|
|
180
|
+
}
|
|
181
|
+
const span = opts.startSpan("D1 session batch", {
|
|
182
|
+
"db.system": "d1",
|
|
183
|
+
"db.operation.name": "batch",
|
|
184
|
+
"db.operation.batch.size": statements.length,
|
|
185
|
+
});
|
|
186
|
+
return runWithSpan(span, () => session.batch(statements.map(unwrapTarget)));
|
|
187
|
+
},
|
|
115
188
|
});
|
|
116
189
|
}
|
|
117
190
|
// ---------------------------------------------------------------------------
|
|
@@ -131,6 +204,7 @@ function wrapKV(binding, opts, namespace) {
|
|
|
131
204
|
};
|
|
132
205
|
return makeProxy(binding, {
|
|
133
206
|
get: wrapOp("get", (key) => key),
|
|
207
|
+
getWithMetadata: wrapOp("getWithMetadata", (key) => key),
|
|
134
208
|
put: wrapOp("put", (key) => key),
|
|
135
209
|
list: wrapOp("list"),
|
|
136
210
|
delete: wrapOp("delete", (key) => key),
|
|
@@ -151,11 +225,56 @@ function wrapR2(binding, opts, bucket) {
|
|
|
151
225
|
const span = opts.startSpan(`R2 ${op}`, attrs);
|
|
152
226
|
return runWithSpan(span, () => binding[op](...args));
|
|
153
227
|
};
|
|
228
|
+
const wrapMultipart = (upload, key) => makeProxy(upload, {
|
|
229
|
+
uploadPart: async (partNumber, ...args) => {
|
|
230
|
+
if (isTracingSuppressed(context.active())) {
|
|
231
|
+
return upload.uploadPart(partNumber, ...args);
|
|
232
|
+
}
|
|
233
|
+
const span = opts.startSpan("R2 uploadPart", {
|
|
234
|
+
"r2.bucket": bucket,
|
|
235
|
+
"r2.key": key,
|
|
236
|
+
"r2.multipart.part_number": partNumber,
|
|
237
|
+
});
|
|
238
|
+
return runWithSpan(span, () => upload.uploadPart(partNumber, ...args));
|
|
239
|
+
},
|
|
240
|
+
abort: wrapMultipartOp("abort", upload, key),
|
|
241
|
+
complete: wrapMultipartOp("complete", upload, key, (parts) => ({
|
|
242
|
+
"r2.multipart.part_count": parts.length,
|
|
243
|
+
})),
|
|
244
|
+
});
|
|
245
|
+
function wrapMultipartOp(op, upload, key, extraAttrs) {
|
|
246
|
+
return async (...args) => {
|
|
247
|
+
if (isTracingSuppressed(context.active()))
|
|
248
|
+
return upload[op](...args);
|
|
249
|
+
const span = opts.startSpan(`R2 ${op}`, {
|
|
250
|
+
"r2.bucket": bucket,
|
|
251
|
+
"r2.key": key,
|
|
252
|
+
...extraAttrs?.(...args),
|
|
253
|
+
});
|
|
254
|
+
return runWithSpan(span, () => upload[op](...args));
|
|
255
|
+
};
|
|
256
|
+
}
|
|
154
257
|
return makeProxy(binding, {
|
|
155
258
|
get: wrapOp("get", (key) => key),
|
|
156
259
|
put: wrapOp("put", (key) => key),
|
|
157
260
|
head: wrapOp("head", (key) => key),
|
|
158
261
|
delete: wrapOp("delete", (key) => key),
|
|
262
|
+
list: wrapOp("list"),
|
|
263
|
+
async createMultipartUpload(key, ...args) {
|
|
264
|
+
if (isTracingSuppressed(context.active())) {
|
|
265
|
+
const upload = await binding.createMultipartUpload(key, ...args);
|
|
266
|
+
return wrapMultipart(upload, key);
|
|
267
|
+
}
|
|
268
|
+
const span = opts.startSpan("R2 createMultipartUpload", {
|
|
269
|
+
"r2.bucket": bucket,
|
|
270
|
+
"r2.key": key,
|
|
271
|
+
});
|
|
272
|
+
const upload = await runWithSpan(span, () => binding.createMultipartUpload(key, ...args));
|
|
273
|
+
return wrapMultipart(upload, key);
|
|
274
|
+
},
|
|
275
|
+
resumeMultipartUpload(key, uploadId) {
|
|
276
|
+
return wrapMultipart(binding.resumeMultipartUpload(key, uploadId), key);
|
|
277
|
+
},
|
|
159
278
|
});
|
|
160
279
|
}
|
|
161
280
|
// ---------------------------------------------------------------------------
|
|
@@ -261,6 +380,66 @@ function wrapQueue(binding, opts, name) {
|
|
|
261
380
|
const span = opts.startSpan("Queue sendBatch", batchAttrs, SpanKind.PRODUCER);
|
|
262
381
|
return runWithSpan(span, () => binding.sendBatch(...args));
|
|
263
382
|
},
|
|
383
|
+
metrics: async (...args) => {
|
|
384
|
+
if (isTracingSuppressed(context.active()))
|
|
385
|
+
return binding.metrics(...args);
|
|
386
|
+
const span = opts.startSpan("Queue metrics", {
|
|
387
|
+
...baseAttrs,
|
|
388
|
+
"messaging.operation": "metrics",
|
|
389
|
+
});
|
|
390
|
+
return runWithSpan(span, () => binding.metrics(...args));
|
|
391
|
+
},
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
// ---------------------------------------------------------------------------
|
|
395
|
+
// Workflow, rate-limit, and Analytics Engine bindings
|
|
396
|
+
// ---------------------------------------------------------------------------
|
|
397
|
+
function wrapWorkflow(binding, opts, name) {
|
|
398
|
+
const wrapOp = (op, kind, getCount) => async (...args) => {
|
|
399
|
+
if (isTracingSuppressed(context.active()))
|
|
400
|
+
return binding[op](...args);
|
|
401
|
+
const attrs = {
|
|
402
|
+
"cloudflare.binding.type": "workflow",
|
|
403
|
+
"cloudflare.binding.name": name,
|
|
404
|
+
"cloudflare.workflow.operation": op,
|
|
405
|
+
};
|
|
406
|
+
const count = getCount?.(...args);
|
|
407
|
+
if (count !== undefined)
|
|
408
|
+
attrs["cloudflare.workflow.batch.size"] = count;
|
|
409
|
+
const span = opts.startSpan(`Workflow ${op}`, attrs, kind);
|
|
410
|
+
return runWithSpan(span, () => binding[op](...args));
|
|
411
|
+
};
|
|
412
|
+
return makeProxy(binding, {
|
|
413
|
+
get: wrapOp("get", SpanKind.CLIENT),
|
|
414
|
+
create: wrapOp("create", SpanKind.PRODUCER),
|
|
415
|
+
createBatch: wrapOp("createBatch", SpanKind.PRODUCER, (batch) => batch.length),
|
|
416
|
+
deleteBatch: wrapOp("deleteBatch", SpanKind.CLIENT, (ids) => ids.length),
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
function wrapRateLimit(binding, opts, name) {
|
|
420
|
+
return makeProxy(binding, {
|
|
421
|
+
limit: async (...args) => {
|
|
422
|
+
if (isTracingSuppressed(context.active()))
|
|
423
|
+
return binding.limit(...args);
|
|
424
|
+
const span = opts.startSpan("RateLimit limit", {
|
|
425
|
+
"cloudflare.binding.type": "rate_limit",
|
|
426
|
+
"cloudflare.binding.name": name,
|
|
427
|
+
});
|
|
428
|
+
return runWithSpan(span, () => binding.limit(...args));
|
|
429
|
+
},
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
function wrapAnalyticsEngine(binding, opts, name) {
|
|
433
|
+
return makeProxy(binding, {
|
|
434
|
+
writeDataPoint(...args) {
|
|
435
|
+
if (isTracingSuppressed(context.active()))
|
|
436
|
+
return binding.writeDataPoint(...args);
|
|
437
|
+
const span = opts.startSpan("AnalyticsEngine writeDataPoint", {
|
|
438
|
+
"cloudflare.binding.type": "analytics_engine",
|
|
439
|
+
"cloudflare.binding.name": name,
|
|
440
|
+
});
|
|
441
|
+
return runSyncWithSpan(span, () => binding.writeDataPoint(...args));
|
|
442
|
+
},
|
|
264
443
|
});
|
|
265
444
|
}
|
|
266
445
|
// ---------------------------------------------------------------------------
|
|
@@ -315,6 +494,9 @@ function wrapService(binding, opts, name) {
|
|
|
315
494
|
* - Vectorize: `query`+`upsert`
|
|
316
495
|
* - Workers AI: `run` (without `prepare`)
|
|
317
496
|
* - Queue producer: `send`+`sendBatch`
|
|
497
|
+
* - Workflow: `get`+`create`+`createBatch`
|
|
498
|
+
* - Rate limit: `limit`
|
|
499
|
+
* - Analytics Engine: `writeDataPoint`
|
|
318
500
|
* - Service binding / Fetcher: `fetch` (last branch; most generic)
|
|
319
501
|
*
|
|
320
502
|
* Unrecognised bindings are passed through unchanged.
|
|
@@ -356,6 +538,15 @@ export function instrumentEnv(env, opts) {
|
|
|
356
538
|
else if (isQueueLike(binding)) {
|
|
357
539
|
result[key] = wrapQueue(binding, opts, key);
|
|
358
540
|
}
|
|
541
|
+
else if (isWorkflowLike(binding)) {
|
|
542
|
+
result[key] = wrapWorkflow(binding, opts, key);
|
|
543
|
+
}
|
|
544
|
+
else if (isRateLimitLike(binding)) {
|
|
545
|
+
result[key] = wrapRateLimit(binding, opts, key);
|
|
546
|
+
}
|
|
547
|
+
else if (isAnalyticsEngineLike(binding)) {
|
|
548
|
+
result[key] = wrapAnalyticsEngine(binding, opts, key);
|
|
549
|
+
}
|
|
359
550
|
else if (isServiceLike(binding)) {
|
|
360
551
|
result[key] = wrapService(binding, opts, key);
|
|
361
552
|
}
|