Skip to content

[DRAFT] [wrangler] Experimental local-dev observability (traces, spans, logs + MCP)#14391

Draft
nickhilpat wants to merge 40 commits into
cloudflare:mainfrom
nickpatt:tail-observability-experiment
Draft

[DRAFT] [wrangler] Experimental local-dev observability (traces, spans, logs + MCP)#14391
nickhilpat wants to merge 40 commits into
cloudflare:mainfrom
nickpatt:tail-observability-experiment

Conversation

@nickhilpat

Copy link
Copy Markdown

Experimental / draft — opening early so prerelease builds are available for folks to try. Off by default; opt-in only.

Adds local-dev observability: when enabled, wrangler dev and the Cloudflare Vite plugin automatically capture structured traces (spans + logs) for whatever Worker you're developing — no extra config — and surface them in the Local Explorer's new Observability tab.

Enable it

  • wrangler dev --experimental-observability
  • Vite: X_LOCAL_OBSERVABILITY=true vite dev
  • (both drive the same internal X_LOCAL_OBSERVABILITY gate)

Then open http://localhost:<port>/cdn-cgi/explorer/Observability.

What you get

  • A trace list + dashboard-style waterfall (Traces / Logs views), filtering, and a Clear action.
  • Multi-worker / distributed traces: invocations sharing a traceId (service bindings, Durable Objects, Vite auxiliary workers) stitch into a single trace, mirroring the production model (group by traceId, parent-less root). Invocation boundaries are delineated in the waterfall, with a toggle to hide Vite dev module-runner plumbing spans.
  • An MCP tab: connect an agent (opencode / Claude Code / Cursor) to read traces/logs via a bundled stdio MCP server, gated by a per-resource/log-level access config, with an agent activity log.

How it works

  • miniflare injects an internal streaming-tail collector (behind unsafeObservability) that persists traces/spans/logs to an internal D1 store; capture parity with the production STW (cf-to-otel) ingestion.
  • wrangler (dev) and @cloudflare/vite-plugin wire the collector + store onto the user worker(s) when the flag/env is set.
  • @cloudflare/local-explorer-ui adds the Observability + MCP UI (bundled into miniflare).

Scope / safety

  • Fully gated and off by default; zero impact unless --experimental-observability / X_LOCAL_OBSERVABILITY is set.
  • Local-dev only; no production code paths affected.

  • Tests
    • Tests included/updated
    • Automated tests not possible - manual testing has been completed as follows:
    • Additional testing not necessary because:
  • Public documentation
    • Cloudflare docs PR(s):
    • Documentation not necessary because: experimental, off-by-default feature gated behind --experimental-observability; public docs will follow if/when it graduates from experimental.

nickpatt and others added 25 commits June 18, 2026 13:39
Adds a top-level Observability tab to the local dev explorer that renders a
trace waterfall, modeled on the Workers Observability dashboard's Traces page.

- src/lib/traces.ts: read the local trace store (D1 traces/spans tables) via
  the existing D1 raw-query endpoint; discover the trace DB from worker
  bindings; build the span tree from parent_id.
- src/components/observability/TraceWaterfall.tsx: two-column waterfall
  (span-name tree + timeline) with gridlines, nice time axis, state-colored
  bars, kind dots, hover/selection, and a span attributes detail panel.
- src/routes/observability.tsx: trace list (duration gauge + status badges)
  with click-to-expand waterfall.
- Sidebar: top-level Observability entry.

Reuses the explorer's existing D1 read path - no backend/openapi changes.
Self-contained local-dev tracing prototype that feeds the Observability tab:
a demo Worker plus a tailStream collector that persists traces to a local D1
(with the trace-query helper). Lives under experiments/ (not a workspace
member) so it doesn't couple into the monorepo install/build. See
RUNNING-IN-WORKERS-SDK.md for how to run it with the locally built wrangler.
What this branch is, what's built (Observability tab + prototype), how it
works, how to run it, and the remaining TODOs.
- List view restyled to match the Workers Observability dashboard:
  Timestamp | Operation | Duration (ms) | Spans | Errors columns, left accent
  bar, stacked duration cell with gauge, dotted-underline timestamps.
- Waterfall ported faithfully (dashboard span-kind icons, two-column timeline,
  marker lines, state-colored bars, collapse).
- Filtering (a simpler query builder): free-text search over operation/span
  names/attributes, status + type (span kind) dropdowns, and tag filters
  (attribute key + value) via SQLite json_each over the persisted attributes -
  no new storage required.
Replace native selects with the app's DropdownMenu (trigger button + menu
items with check marks), matching the dashboard's filter dropdown look, for
the status / type / tag-key filters.
- Collector now persists console.log events to a new D1 'logs' table
  (level, message, operation, span) on outcome.
- New Events route: list (timestamp / level / message / operation) with
  search + level filter; click a row to expand the full event JSON (with copy),
  mirroring the dashboard's events view.
- Extract shared FilterSelect (Kumo DropdownMenu); sidebar now has Traces +
  Events entries.
Collapse the two sidebar entries into one Observability item and add a
title dropdown to switch between the Traces and Events views, preserving
the selected worker in the URL.
Parse a simple AND-only query syntax in the Traces and Events search bars
(status:, kind:, dur:>N, <attr>:value, level:, op:) on top of the existing
free-text search, reusing the D1 filter builder.
…anel

trace-collector: TRACE_FORMAT (pretty|agent|json) for token-light trace
output and TRACE_LOG_LEVEL filtering. wobs-trace-demo: click-to-fire
control panel served at / for generating traces.
Productizes the local trace prototype into the platform: with
X_LOCAL_OBSERVABILITY=true, wrangler dev and the Vite plugin auto-inject an
internal trace collector (embedded miniflare worker) as a streaming-tail
consumer of user workers, set the required compat flags, and provision an
internal WOBS_TRACES D1 the collector persists to and the Local Explorer reads.

- miniflare: unsafeObservability option, embedded collector worker, collector
  service registration (resolved via getUserServiceName so user workers'
  streamingTails reference resolves), explicit D1 binding to the internal store.
- wrangler dev + vite plugin: gate on X_LOCAL_OBSERVABILITY; add compat flags,
  the internal D1, and the collector streamingTail to user workers.
- workers-utils: X_LOCAL_OBSERVABILITY gate + shared collector/D1 constants.

No user config needed - traces are captured for any worker on either dev server.
Tests across all packages:
- local-explorer-ui: query parser + traces SQL-builder/pure-fn unit tests
- miniflare: integration spec (capture -> persist via unsafeObservability)
- wrangler + vite-plugin: X_LOCAL_OBSERVABILITY gating unit tests

Local Explorer Observability tab:
- Multi-expand trace rows (toggle open/close, multiple waterfalls at once)
- Stable composite keys (trace_id + root_span_id) to fix collapse flicker on
  auto-refresh for distributed traces that share a trace_id
- Exact WOBS_TRACES discovery; hide the internal trace store from the D1 list

Other:
- Extract applyLocalObservability helper in the Vite plugin (testable)
- Filter the internal trace D1 from the explorer listD1Databases endpoint
- Remove the wobs-local-traces prototype + branch notes; fix stale collector comment
…W ingestion

Bring the local collector's tail-stream ingestion to parity with vega's
cf-to-otel streaming-tail worker:

- Name + classify the root span for every trigger type (fetch, scheduled,
  alarm, queue, email, trace, hibernatableWebSocket, jsrpc, custom) instead of
  only fetch
- Capture per-trigger onset attributes (faas.trigger, faas.cron,
  cloudflare.scheduled_time, cloudflare.queue.*, cloudflare.email.*,
  cloudflare.trace.count, http.request.method, url.full)
- Capture worker/invocation metadata (script_name, entrypoint, execution_model,
  dispatch_namespace, script_version.*, script_tags)
- Capture cpu_time_ms / wall_time_ms and cloudflare.outcome on the root span
- Capture exception stacktraces

Adds a spec assertion covering the enriched root-span attributes.
Add a Clear button (next to Refresh) that wipes all captured traces/spans/logs
from the local trace store, with an "are you sure?" confirmation dialog and a
"Don't show this message again" opt-out persisted in localStorage. Disabled
when there are no traces.

Adds a `clearTraces` helper + unit test.
…ility

Match the production model where a trace is identified by traceId and the root
is the parent-less span, so a request flowing through multiple worker
invocations renders as one stitched trace.

- Collector stores absolute span/log timestamps (so invocations sharing a
  traceId share a timeline) and records the root span's parent_span_id; adds a
  best-effort schema migration for existing stores
- listTraces returns one row per distributed trace (the parent-less root
  invocation) with whole-trace span counts/durations
- buildSpanTree re-bases absolute times to the trace start; the waterfall
  derives its window from the spans so sub-invocations past the root are covered

Verified working for the Vite multi-worker case: in the multi-worker playground
(worker-a -> worker-b via service binding, both in one miniflare instance via
auxiliaryWorkers) the cross-worker call stitches into a single trace (one row,
multiple invocations across both workers). Cross-process `wrangler dev` (separate
miniflare instances) does not stitch, because workerd does not propagate span
context across the dev-registry socket; prod-parity local multi-worker tracing
therefore requires the workers to share one miniflare instance, which the Vite
plugin's auxiliary workers already do.
Port the local-observability MCP feature from the local-obs-mcp branch:

- MCP page (/mcp): agent access-control config (log levels + per-resource
  allow-list, mirrored to an mcp_config table), copy-paste connect snippets for
  opencode / Claude Code / Cursor (+ Add-to-Cursor deeplink), and an agent
  activity log (reads mcp_calls)
- Surfaced as a third view in the Observability switcher (Traces / Logs / MCP)
  rather than a separate sidebar item, so it lives under Observability
- lib/mcp.ts: config + call-history data layer over the explorer's D1 endpoint
- Relocated the dependency-free stdio MCP server into the repo at
  packages/local-explorer-ui/mcp-server/ (was the deleted prototype dir) and made
  its path user-configurable on the Connect card (persisted in localStorage)
  instead of a hardcoded absolute path

The server exposes list_recent_errors, explain_trace, and search_logs.
Ship the stdio MCP server with the explorer and surface its absolute path to
the Observability MCP page, so the connect snippets (opencode/Claude/Cursor)
are copy-paste-ready with no manual path entry.

- miniflare build copies mcp-server.mjs into dist/local-explorer-ui
- the core explorer plugin injects the bundled server's absolute path as a
  binding, and the explorer worker exposes it at GET /api/local/mcp
- the MCP page fetches it and uses it as the default server path; the field is
  still editable and an explicit override persists in localStorage
…erfall

Multi-worker / multi-invocation distributed traces stitch into a single
waterfall, but without per-worker labels (workerd doesn't surface scriptName
locally) it was hard to see where one worker hands off to another. Mark each
invocation root other than the trace's top-level root with a dashed divider and
an "inv" badge ("Start of a new worker invocation"), so the boundaries between
invocations/workers are visually obvious.

- getInvocationRootIds(): root span ids of every invocation sharing a trace_id
  (from the per-invocation trace rows)
- the Observability tab fetches them per expanded trace and the waterfall marks
  spans that begin a new invocation
…er boundaries

In Vite dev, user workers run inside Vite's module-runner Durable Object
(cloudflare.entrypoint = "__VITE_RUNNER_OBJECT__"), so each invocation is
wrapped by runner plumbing that showed up as spurious invocation-boundary
markers. Exclude those runner invocations from getInvocationRootIds so the "inv"
markers only flag real worker handoffs.
In `vite dev`, user code runs inside Vite's module-runner Durable Object, so
every invocation is wrapped with a durable_object_subrequest -> jsrpc
(executeCallback on __VITE_RUNNER_OBJECT__) chain that isn't the user's code.

Add a "Hide runner spans" toggle to the Observability tab (default on, persisted
in localStorage) that strips those runner spans plus the DO-subrequest that
dispatches into them and re-parents the real children up, so the waterfall shows
only the worker's actual spans. No-op outside Vite dev.

- stripDevRunnerSpans() in lib/traces + a hideDevRunner param on buildSpanTree
- TraceWaterfall threads hideDevRunner through
- unit tests for the strip/re-parent logic
Move the MCP page out of the Observability view switcher (Traces/Logs/MCP) back
into its own left-hand sidebar entry, so Observability stays focused on
traces/logs and MCP is a sibling tab.
Add a user-facing `wrangler dev --experimental-observability` opt-in for local
observability capture, as a cleaner alternative to the internal
X_LOCAL_OBSERVABILITY env var (the flag sets that env var, which the miniflare
dev wiring already reads). Declares X_LOCAL_OBSERVABILITY in turbo.json.
@changeset-bot

changeset-bot Bot commented Jun 22, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: cf840ce

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 6 packages
Name Type
@cloudflare/vite-plugin Minor
miniflare Minor
wrangler Minor
@cloudflare/deploy-helpers Patch
@cloudflare/pages-shared Patch
@cloudflare/vitest-pool-workers Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

…periment

Clone-and-build helper + usage doc (wrangler dev flag and Vite env-var paths) so
people can try the experimental local-dev observability feature from this fork
without an npm publish/prerelease.
@pkg-pr-new

pkg-pr-new Bot commented Jun 22, 2026

Copy link
Copy Markdown
@cloudflare/autoconfig

npm i https://pkg.pr.new/@cloudflare/autoconfig@14391

create-cloudflare

npm i https://pkg.pr.new/create-cloudflare@14391

@cloudflare/deploy-helpers

npm i https://pkg.pr.new/@cloudflare/deploy-helpers@14391

@cloudflare/kv-asset-handler

npm i https://pkg.pr.new/@cloudflare/kv-asset-handler@14391

miniflare

npm i https://pkg.pr.new/miniflare@14391

@cloudflare/pages-shared

npm i https://pkg.pr.new/@cloudflare/pages-shared@14391

@cloudflare/unenv-preset

npm i https://pkg.pr.new/@cloudflare/unenv-preset@14391

@cloudflare/vite-plugin

npm i https://pkg.pr.new/@cloudflare/vite-plugin@14391

@cloudflare/vitest-pool-workers

npm i https://pkg.pr.new/@cloudflare/vitest-pool-workers@14391

@cloudflare/workers-auth

npm i https://pkg.pr.new/@cloudflare/workers-auth@14391

@cloudflare/workers-editor-shared

npm i https://pkg.pr.new/@cloudflare/workers-editor-shared@14391

@cloudflare/workers-utils

npm i https://pkg.pr.new/@cloudflare/workers-utils@14391

wrangler

npm i https://pkg.pr.new/wrangler@14391

commit: cf840ce

nickpatt and others added 14 commits June 23, 2026 10:49
…aude/Cursor

Adds Install buttons on the MCP tab that write project-local agent config (.opencode/opencode.json, .mcp.json, .cursor/mcp.json) via a new explorer endpoint (POST /api/local/mcp/install) that forwards to a miniflare loopback handler. The bundled mcp-server.mjs path comes from the trusted LOCAL_EXPLORER_MCP_SERVER_PATH binding; existing config is merged non-destructively.
The draft PR's pkg.pr.new prereleases (npm i https://pkg.pr.new/cloudflare/workers-sdk/wrangler@14391) are now the way to try this, so the root OBSERVABILITY.md + setup.sh fork-clone helpers are no longer needed.
The install buttons wrote the agent config correctly but only surfaced a toast on success, which wasn't reliably visible — so it looked like nothing happened. Add an inline status line (green check + written path, or red error) under the button, matching the copy-button pattern and independent of the toast portal. Clears when switching agents.
New CLI commands (logs / traces / trace / query / skill) that read the local-dev trace store captured by `wrangler dev --experimental-observability`. They read the persisted SQLite database directly and read-only via Node's built-in node:sqlite, so they work whether or not the dev server is running, without starting a second workerd. The `query` command lets an agent run arbitrary read-only SQL (codemode-style) and `skill` prints schema + example queries. Also fixes a latent type error in `pages dev` exposed by the new typecheck.
The `wrangler observability` CLI is now the primary way to inspect captured traces and logs. The bundled MCP server is gated behind X_LOCAL_OBSERVABILITY_MCP=true (off by default) across wrangler, miniflare, and the Vite plugin; when disabled, the server path isn't exposed, the install endpoint refuses, and the Local Explorer hides the MCP tab. The MCP page is reframed as an optional alternative for agents that prefer connecting over MCP rather than running CLI commands.
…detected agents

Reuses the same agent detection as `wrangler --install-skills` (rosie) to write the observability guidance as a SKILL.md (with frontmatter) into each detected AI agent's global skills directory, so agents auto-discover the `wrangler observability` CLI when debugging a local Worker. Without --install the command still prints the guidance to stdout.
…output

Ports the Local Explorer's stripDevRunnerSpans logic to the CLI: `observability traces` and `observability trace` now hide the Vite module-runner plumbing (the durable_object_subrequest -> jsrpc into __VITE_RUNNER_OBJECT__ chain) by default and re-parent the real spans, with --include-runner-spans to show everything. No-op for wrangler dev (no marker). Also aligns the traces list span_count with the Explorer UI (whole-trace COUNT of spans rather than the stored column).
…n the skill

Adds a first-class cloudflare({ experimental: { observability: true } }) option to enable local-dev observability capture in Vite (equivalent to X_LOCAL_OBSERVABILITY=true), and updates the wrangler observability skill to tell agents how to enable capture under Vite and to run the project-local wrangler.
Hosts an MCP endpoint at /cdn-cgi/explorer/mcp (Streamable HTTP) when MCP is opted in (X_LOCAL_OBSERVABILITY_MCP). Instead of feeding the agent the full OpenAPI spec, it exposes a 'run' tool: the agent submits a JS snippet that calls a 'cf' client (D1/KV/R2/DO/Workflows/traces) and returns only what it needs. Snippets execute in-process via a workerd UnsafeEval binding (injected only when MCP is enabled), and the cf client dispatches to the explorer's own API routes in-process via Hono app.request — no network round-trips, no separate install. An explorer_api tool returns a compact cheatsheet + the OpenAPI URL.
Enforce the Agent Access config inside the Miniflare-hosted Codemode MCP cf client: log-level policy for trace logs, per-resource allowlists for D1/KV/DO/R2, and raw Explorer API access disabled by default. Audit run calls with the submitted code, access attempts, and result/error summary.
Point agent setup at the Miniflare-hosted /cdn-cgi/explorer/mcp endpoint instead of a local stdio server path. Keep Agent Access controls, add the raw Explorer API access toggle, and detect the hosted MCP endpoint for showing the optional MCP view.
Drop the packaged stdio MCP server, Miniflare build copy, one-click install backend, and server-path binding now that the supported MCP path is the hosted Codemode endpoint in Local Explorer.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Untriaged

Development

Successfully merging this pull request may close these issues.

4 participants