Skip to content

[miniflare] Local dev observability: traces, spans & logs via a SQL query API#14633

Open
nickpatt wants to merge 6 commits into
cloudflare:mainfrom
nickpatt:wobs-6-ui
Open

[miniflare] Local dev observability: traces, spans & logs via a SQL query API#14633
nickpatt wants to merge 6 commits into
cloudflare:mainfrom
nickpatt:wobs-6-ui

Conversation

@nickpatt

@nickpatt nickpatt commented Jul 9, 2026

Copy link
Copy Markdown

Adds experimental local-dev observability to Miniflare: each local Worker invocation is captured as a trace (spans + logs) and surfaced in the Local Explorer, read entirely through a single read-only SQL endpoint. This is the clean, SQL-query-only rewrite of the earlier exploration in #14391, which it supersedes.

Architecture

  • Miniflare core attaches an internal trace collector as a streaming-tail consumer of every user Worker — wired once, so the dev tools don't duplicate it — folding the tail into spans/logs using the Workers Observability attribute conventions (faas.trigger, http.request.method, http.response.status_code, cloudflare.outcome, cpu_time_ms, …) so a local waterfall matches production.
  • Traces persist to an internal SQLite-backed Durable Object (TraceStore).
  • The only read surface is POST /cdn-cgi/explorer/api/observability/query — one read-only SELECT/WITH statement, params bound, rows capped. The spans/logs schema is the contract and is published in the endpoint's OpenAPI description, so the UI and coding agents read the same way. No purpose-built per-view routes — the query surface is the API, mirroring the direction of the production Observability API.
  • The Local Explorer UI ships the common views (traces list + trace-detail waterfall) as canned queries against that endpoint.

Reviewing

The 6 commits are the review units, in order:

  1. [miniflare] opt-in switch + core wiring (placeholder collector)
  2. [miniflare] internal SQLite trace store + SQL read API
  3. [miniflare] capture traces into the SQLite store
  4. [miniflare] attribute captured traces to their worker
  5. [miniflare] expose the SQL query API over the Local Explorer HTTP API
  6. [local-explorer-ui] traces list & trace-detail views — also flips the default on (see below)

⚠️ On by default

The final commit flips X_LOCAL_OBSERVABILITY default false → true, so once landed, local-dev capture is on by default under both wrangler dev and the Vite plugin (a collector DO + streaming-tail capture + SQLite writes per session). Set X_LOCAL_OBSERVABILITY=false to opt out. Happy to split this flip into its own commit, or keep it off for the initial landing, if reviewers prefer.

Scope / not included

Cross-process trace propagation (spans across separate wrangler dev processes) is not included: workerd doesn't carry trace context across the dev-registry proxy hop, so a distributed trace only stitches within a single process today. Single-process multi-worker (service bindings in one dev session) is captured and stitched.


  • 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 feature gated behind X_LOCAL_OBSERVABILITY; user-facing docs will follow when it stabilises.

nickpatt added 2 commits July 9, 2026 11:10
…lder collector)

Experimental. Add the X_LOCAL_OBSERVABILITY env var (read by both wrangler and
the Vite plugin — there is no CLI flag or plugin option), which is translated
into a single `unsafeObservability` Miniflare option. When set, Miniflare core
attaches an internal trace collector as a streaming-tail consumer of each user
worker and adds the compat flags workerd needs to emit that tail — done once in
core so the dev tools don't duplicate the wiring. It defaults to off while
experimental.

The collector is a placeholder for now (no-op tailStream); capturing spans/logs
and persisting them to an internal store land in follow-up PRs.
…ad API

Adds the internal SQLite-backed `TraceStore` Durable Object (hosted by the collector worker) that owns captured spans + logs, plus its sole read surface: a single read-only `POST /query` SQL endpoint. `query()` binds params, allows only one SELECT/WITH statement (rejecting writes, DDL, PRAGMA, ATTACH, and multi-statement), and caps returned rows. The spans/logs schema is the read contract. tailStream is still a placeholder here (capture lands next).
@changeset-bot

changeset-bot Bot commented Jul 9, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1e390e7

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

This PR includes changesets to release 6 packages
Name Type
miniflare Minor
wrangler Minor
@cloudflare/vite-plugin 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

@nickpatt nickpatt closed this Jul 9, 2026
@nickpatt nickpatt reopened this Jul 10, 2026
@nickpatt nickpatt marked this pull request as ready for review July 11, 2026 04:07
@workers-devprod workers-devprod requested review from a team and emily-shen and removed request for a team July 11, 2026 04:07
@workers-devprod

Copy link
Copy Markdown
Contributor

Codeowners approval required for this PR:

  • @cloudflare/wrangler
Show detailed file reviewers
  • .changeset/wobs-1-enablement.md: [@cloudflare/wrangler]
  • .changeset/wobs-2-collector.md: [@cloudflare/wrangler]
  • .changeset/wobs-3-capture.md: [@cloudflare/wrangler]
  • .changeset/wobs-4-multi-worker.md: [@cloudflare/wrangler]
  • .changeset/wobs-5-api.md: [@cloudflare/wrangler]
  • .changeset/wobs-6-ui.md: [@cloudflare/wrangler]
  • packages/local-explorer-ui/src/tests/observability/observability.test.ts: [@cloudflare/wrangler]
  • packages/local-explorer-ui/src/components/Sidebar.tsx: [@cloudflare/wrangler]
  • packages/local-explorer-ui/src/routeTree.gen.ts: [@cloudflare/wrangler]
  • packages/local-explorer-ui/src/routes/observability/index.tsx: [@cloudflare/wrangler]
  • packages/local-explorer-ui/src/routes/observability/traces/$traceId.tsx: [@cloudflare/wrangler]
  • packages/local-explorer-ui/src/utils/observability.ts: [@cloudflare/wrangler]
  • packages/miniflare/scripts/openapi-filter-config.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/core/explorer.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/core/index.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/core/observability.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/core/constants.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/local-explorer/explorer.worker.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/local-explorer/generated/index.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/local-explorer/generated/types.gen.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/local-explorer/generated/zod.gen.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/local-explorer/openapi.local.json: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/local-explorer/resources/observability.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/local-explorer/route-names.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/observability/collector.worker.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/observability/tail-to-store.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/observability/trace-store.ts: [@cloudflare/wrangler]
  • packages/miniflare/test/plugins/core/observability.spec.ts: [@cloudflare/wrangler]
  • packages/miniflare/test/plugins/local-explorer/observability.spec.ts: [@cloudflare/wrangler]
  • packages/vite-plugin-cloudflare/src/miniflare-options.ts: [@cloudflare/wrangler]
  • packages/workers-utils/src/environment-variables/factory.ts: [@cloudflare/wrangler]
  • packages/workers-utils/src/environment-variables/misc-variables.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/dev/miniflare/index.ts: [@cloudflare/wrangler]
  • turbo.json: [@cloudflare/wrangler]

devin-ai-integration[bot]

This comment was marked as resolved.

nickpatt added 4 commits July 10, 2026 23:19
Wires the local observability collector to capture: it folds each user worker's streaming tail straight into spans + logs — using the Workers Observability attribute conventions (faas.trigger, http.request.method, http.response.status_code, cloudflare.outcome, cpu_time_ms, …) so a local waterfall matches production — and persists them to the internal TraceStore Durable Object. Every invocation emits a synthetic log (so silent workers still show up), console.* output is captured, and uncaught exceptions surface as both a span error and an error-level log. No OpenTelemetry SDK/transport is needed locally, so there are no added runtime dependencies.
…orker

Local observability already captures every user worker (the collector is attached to each one), and a request that crosses a service binding is stitched into a single distributed trace. This adds per-worker attribution: each captured span now records the worker (service) that produced it, so multi-worker setups — and cross-worker distributed traces — can be grouped and filtered by worker. Miniflare passes each worker's name to the collector via binding props (workerd doesn't surface it on the tail locally), and the trace store gains a service column plus a service_count per trace in the list view.
…cal Explorer HTTP API

Adds a single read endpoint to the Local Explorer API: POST /observability/query, which proxies to the collector's read-only SQL /query. The spans/logs schema and json(attributes) note are published in the endpoint's OpenAPI description so agents can discover the columns. No typed per-view routes — the query surface is the API, mirroring the direction of the production Observability API.
… views

Adds the Observability section to the Local Explorer UI: a traces list and a trace detail view (spans + logs). Data is read via canned SQL sent to POST /observability/query; the UI owns the queries and maps the {columns, rows} result into local Span/Log/TraceSummary shapes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants