Skip to main content
Anvil ships a self-contained observability dashboard that renders trace trees, token usage, and cost per run directly in your browser. In development, anvil dev serves it automatically at /_anvil. In production, mount it yourself as middleware so you control who can reach it. The dashboard reads from the same TraceStore you pass to your Tracer, so there is no separate sink to configure — traces written during a request are visible the moment the run ends.

What the dashboard shows

  • Trace list — every agent run, sorted by most recent, with name, status badge, duration, token count, and cost.
  • Span tree — drill into any trace to see the nested agent → model → tool → retrieval hierarchy, with per-span attributes (model, provider, token usage, tool inputs/outputs) and error details.
  • Token usage and cost — rolled-up totalInputTokens, totalOutputTokens, and totalCostUsd displayed per trace and per span.
The page polls the trace list every three seconds, so it stays current during an active development session without any WebSocket setup.

Enabling in production with dashboardMiddleware

For deployed environments, import dashboardMiddleware from anvil/trace and add it to your root _middleware.ts:
import { dashboardMiddleware } from 'anvil/trace';
import { SqliteTraceStore } from 'anvil/trace';

const store = await SqliteTraceStore.open('.anvil/traces.db');

export default dashboardMiddleware(store);
Mount it alongside your auth middleware to protect it:
import { dashboardMiddleware } from 'anvil/trace';
import { SqliteTraceStore } from 'anvil/trace';
import { requireInternalToken } from '../lib/auth.js';

const store = await SqliteTraceStore.open('.anvil/traces.db');

export default [
  requireInternalToken,
  dashboardMiddleware(store, { path: '/_anvil' }),
];
In production, always protect the dashboard with authentication middleware. The dashboard exposes full trace data including tool inputs, model outputs, and cost information that should not be publicly accessible.

dashboardMiddleware arguments

dashboardMiddleware accepts a TraceStore as its first argument and an optional DashboardOptions object as its second. The first positional argument is the trace store to read from. Wire this to the same SqliteTraceStore (or other TraceStore) instance you passed to your Tracer, so the dashboard and the tracer share one underlying database. DashboardOptions has one field:
path
string
default:"/_anvil"
The URL prefix at which the dashboard is mounted. The middleware intercepts all requests whose path starts with this value. Change it if /_anvil conflicts with an existing route.

Sharing the store with the Tracer

The dashboard and the tracer must point at the same TraceStore instance. The idiomatic pattern is to open the store once at application startup and pass it to both:
import { Tracer, SqliteTraceStore, dashboardMiddleware } from 'anvil/trace';
import { defineAgent } from 'anvil/agent';
import { openai } from 'anvil/llm';

// Open once — shared by tracer and dashboard.
const store = await SqliteTraceStore.open('.anvil/traces.db');

export const tracer = new Tracer(store);

// In your root _middleware.ts
export const middleware = dashboardMiddleware(store);

// In your agent route
export const post = defineAgent({
  client: openai({ model: 'gpt-4o' }),
  tracer,
  // ...
});

Dashboard API routes

The middleware also exposes a small JSON API used by the built-in UI:
RouteDescription
GET /_anvilServes the self-contained dashboard HTML (no external assets)
GET /_anvil/api/tracesLists traces (spans omitted for payload efficiency). Accepts ?limit=
GET /_anvil/api/traces/:idReturns a single trace with the full span tree
You can query these endpoints directly from your own tooling or scripts.
Pair the dashboard with the Cost Governor to see budget breaches in the trace tree, and with the OpenTelemetry exporter to forward the same traces to Datadog, Grafana, or Langfuse alongside the local view.