<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Yasvanth Udayakumar</title>
    <description>The latest articles on DEV Community by Yasvanth Udayakumar (@yudayakumar).</description>
    <link>https://dev.to/yudayakumar</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4047441%2F9fabbe2a-ca42-4198-a3c6-11ad40fb0284.png</url>
      <title>DEV Community: Yasvanth Udayakumar</title>
      <link>https://dev.to/yudayakumar</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yudayakumar"/>
    <language>en</language>
    <item>
      <title>I Built a Local Observability Stack for Claude Code: Traces, Tool Calls, Tokens, and Cost</title>
      <dc:creator>Yasvanth Udayakumar</dc:creator>
      <pubDate>Sun, 26 Jul 2026 05:22:50 +0000</pubDate>
      <link>https://dev.to/yudayakumar/i-built-a-local-observability-stack-for-claude-code-traces-tool-calls-tokens-and-cost-3ce8</link>
      <guid>https://dev.to/yudayakumar/i-built-a-local-observability-stack-for-claude-code-traces-tool-calls-tokens-and-cost-3ce8</guid>
      <description>&lt;p&gt;I was building a Claude Code plugin — a few skills, a couple of MCP servers, some hooks, the usual pile of scripts. It mostly worked. That was the problem: "mostly worked" was the highest resolution I had.&lt;/p&gt;

&lt;p&gt;Some prompts came back quickly. Others chewed through minutes and noticeably more money for work that looked the same size. The terminal showed me the same thing either way: text scrolling past, a result, a cursor. It could not tell me how many tools a prompt actually called, whether a skill I had written fired at all, or which of my prompts was quietly the expensive one.&lt;/p&gt;

&lt;p&gt;I have spent years insisting that services get instrumented before they ship. It felt strange to be debugging an agent workflow by squinting at scrollback.&lt;/p&gt;

&lt;p&gt;Claude Code exports OpenTelemetry data. So I pointed it at a local LGTM stack and built dashboards against what actually came out.&lt;/p&gt;

&lt;h2&gt;
  
  
  The observability problem
&lt;/h2&gt;

&lt;p&gt;Terminal output renders the conversation. It does not render the execution.&lt;/p&gt;

&lt;p&gt;Past a certain plugin size, the questions I care about stop being answerable from the transcript:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How many tool calls did this prompt actually take?&lt;/li&gt;
&lt;li&gt;Which skills and which plugin MCP servers were involved?&lt;/li&gt;
&lt;li&gt;How many tokens went into this session, split by billing class?&lt;/li&gt;
&lt;li&gt;What did that one prompt cost?&lt;/li&gt;
&lt;li&gt;How long was the session actually active?&lt;/li&gt;
&lt;li&gt;Can I correlate a log line with a span?&lt;/li&gt;
&lt;li&gt;Did Claude take a path with unnecessary steps in it?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are ordinary questions. If this were a checkout service, nobody would consider it exotic to want per-request latency, a dependency map, and cost attribution. The agent case is the same shape: a request fans out into a loop of model calls and tool executions, each with a duration and a price, and you want to see the fan-out.&lt;/p&gt;

&lt;p&gt;The one thing that differs: tokens are the bill. In a normal service, cost is an infra line item you review monthly. Here it is a per-request property that varies by an order of magnitude depending on how the agent chose to work.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;The repository is &lt;a href="https://github.com/xops-labs/claude-observability" rel="noopener noreferrer"&gt;xops-labs/claude-observability&lt;/a&gt;. It is small on purpose — a Compose file, one collector config, three dashboards, and a bootstrap script per platform.&lt;/p&gt;

&lt;p&gt;Claude Code exports OTLP over gRPC (HTTP is also accepted) to a standalone OpenTelemetry Collector running on the host. The collector limits memory, tags every record with the local environment and route, batches, and forwards all three signals to the collector embedded in Grafana's &lt;code&gt;otel-lgtm&lt;/code&gt; image. From there metrics land in Mimir, logs and events in Loki, traces in Tempo, and Grafana reads all three.&lt;/p&gt;

&lt;p&gt;Each piece has one job. Claude Code produces the telemetry. The standalone collector is the ingestion and processing boundary. The LGTM image supplies storage plus preconfigured data sources. The dashboards are where I actually spent my time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not an existing tool?
&lt;/h2&gt;

&lt;p&gt;Worth answering before you spend a weekend on this, because the space is not empty.&lt;/p&gt;

&lt;p&gt;Grafana Cloud ships an official Claude Code integration with prebuilt stats, usage-breakdown and productivity dashboards; it takes metrics and logs/events, but not traces. There is a community Claude Code dashboard on Grafana's site built on Azure Monitor and Application Insights, and several good write-ups routing Claude Code metrics into Prometheus or VictoriaMetrics to watch usage against plan limits. They all answer the same question: how much is being spent, by whom, over time. If that is your question, use one of those — they are considerably less work than this.&lt;/p&gt;

&lt;p&gt;The LLM observability platforms — Langfuse, Helicone, Arize Phoenix, OpenLLMetry — solve a different problem again. They instrument an application &lt;em&gt;you&lt;/em&gt; wrote, where you own the call sites and can decide what a span means. Claude Code is not that. It is a closed agent you drive, and you get whatever it chooses to emit.&lt;/p&gt;

&lt;p&gt;What I wanted was neither org-level spend nor app instrumentation, but the execution path of one prompt I had just typed: which tools it called, in what order, how long each took, what it cost. That is why this stack is Loki-first with prompt and session drill-downs, and why traces matter enough to turn on a beta flag. It is a debugger for your own workflow, not a usage report.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Claude Code
    |  OTLP/gRPC :4317  (or OTLP/HTTP :4318)
    v
OpenTelemetry Collector
    |  memory_limiter -&amp;gt; resource -&amp;gt; batch
    |  OTLP/gRPC on the Docker network
    v
Grafana OTEL-LGTM ---&amp;gt; Mimir  (metrics)
                  |--&amp;gt; Loki   (logs &amp;amp; events)
                  '--&amp;gt; Tempo  (traces)
                          |
                          v
                       Grafana :3300
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every published port binds to &lt;code&gt;127.0.0.1&lt;/code&gt;. The LGTM image's own OTLP ports stay internal to the Docker network, so host &lt;code&gt;4317&lt;/code&gt; and &lt;code&gt;4318&lt;/code&gt; belong only to the standalone collector — deliberately, so nothing can accidentally bypass the processing layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a standalone collector?
&lt;/h2&gt;

&lt;p&gt;I could have pointed Claude Code straight at the LGTM image, which has its own OTLP receivers. I did not, because collapsing ingestion into storage means every future change to how telemetry is handled becomes a change to how telemetry is stored.&lt;/p&gt;

&lt;p&gt;What the collector config does today, and nothing more:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OTLP receivers&lt;/strong&gt; on gRPC &lt;code&gt;:4317&lt;/code&gt; and HTTP &lt;code&gt;:4318&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;memory_limiter&lt;/code&gt;&lt;/strong&gt; — 256 MiB limit with a 64 MiB spike allowance, checked every second. This runs on a laptop already hosting Docker, an editor, and the agent itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;resource&lt;/code&gt;&lt;/strong&gt; — upserts &lt;code&gt;deployment.environment.name=local&lt;/code&gt; and &lt;code&gt;telemetry.route=local-otel-collector&lt;/code&gt; onto every record, so the origin is visible from inside the data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;batch&lt;/code&gt;&lt;/strong&gt; — 1024 records or 5 seconds, whichever comes first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OTLP gRPC exporter&lt;/strong&gt; to &lt;code&gt;lgtm:4317&lt;/code&gt; with &lt;code&gt;sending_queue&lt;/code&gt; and &lt;code&gt;retry_on_failure&lt;/code&gt; enabled, so a restarting LGTM container does not silently drop a session's telemetry.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Traces, metrics, and logs each get their own pipeline, all three running the same processor chain. There is no sampling, filtering, or redaction processor in here. If I later want to strip an attribute before storage, that is a change in one file that never touches the backends.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three dashboard perspectives
&lt;/h2&gt;

&lt;p&gt;The dashboards are provisioned from &lt;code&gt;grafana/dashboards/*.json&lt;/code&gt; into a &lt;strong&gt;Claude Observability&lt;/strong&gt; folder, with UI edits disabled so the JSON stays authoritative.&lt;/p&gt;

&lt;p&gt;One thing surprised me while building them, and it shaped everything: &lt;strong&gt;almost all of the useful detail arrives as logs, not metrics.&lt;/strong&gt; Session IDs, prompt IDs, per-request cost, token splits, tool names, tool inputs, hook executions — these come through as Loki events with structured metadata. The Prometheus counters exist, but they are coarse, and (observed on Claude Code 2.1.218) the VS Code extension and &lt;code&gt;claude -p&lt;/code&gt; do not emit them at all. So the dashboards are Loki-first, with the native metrics parked in a clearly-labelled collapsed row.&lt;/p&gt;

&lt;h3&gt;
  
  
  Claude Code Telemetry (overview)
&lt;/h3&gt;

&lt;p&gt;The entry point. Stats over the selected range — total cost, distinct sessions, input, output, cache read and cache creation tokens — then time series for cost by model, token usage by billing class, API requests by model, request duration at p50 and p95, and errors split into API errors and failed tool executions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvv99e60dju4yfpopfus6.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvv99e60dju4yfpopfus6.jpg" alt="Claude Code Telemetry overview: cost, sessions and token totals" width="800" height="366"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Top of the overview: total cost, distinct sessions, and the four token classes over the selected range.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Then the part I use most: &lt;strong&gt;Tool calls by tool&lt;/strong&gt;, &lt;strong&gt;Skill &amp;amp; plugin activity&lt;/strong&gt; (skill invocations, plugin MCP tool calls, hook executions, plugins loaded), and &lt;strong&gt;MCP tool calls by server&lt;/strong&gt;. Plus a &lt;strong&gt;Cost by prompt&lt;/strong&gt; table for the top 25 prompts in range, a &lt;strong&gt;Top MCP tools&lt;/strong&gt; bar gauge, a &lt;strong&gt;Sessions breakdown&lt;/strong&gt; table, &lt;strong&gt;Context size by session&lt;/strong&gt;, and a raw event log.&lt;/p&gt;

&lt;p&gt;One wrinkle worth knowing. The raw &lt;code&gt;tool_result&lt;/code&gt; events carry generic tool names — every MCP call is &lt;code&gt;mcp_tool&lt;/code&gt; and every skill invocation is &lt;code&gt;Skill&lt;/code&gt;. The real names live inside &lt;code&gt;tool_parameters&lt;/code&gt;. So the dashboards use LogQL &lt;code&gt;label_format&lt;/code&gt; with &lt;code&gt;regexReplaceAll&lt;/code&gt; to pull them back out, rendering MCP calls as &lt;code&gt;server: tool&lt;/code&gt; and skills as &lt;code&gt;skill: name&lt;/code&gt;. Without that, the most interesting panel for plugin work is one enormous bar labelled &lt;code&gt;mcp_tool&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn8bstopwe4hvezspgkpq.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn8bstopwe4hvezspgkpq.jpg" alt="Tool calls by tool, with MCP calls resolved to server and tool" width="800" height="366"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Tool calls by tool. Note &lt;code&gt;claude_ai_Vercel: list_deployments&lt;/code&gt; — that name is reconstructed from &lt;code&gt;tool_parameters&lt;/code&gt;, not emitted directly.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpbi0avf9lspmbz48i7k4.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpbi0avf9lspmbz48i7k4.jpg" alt="Skill and plugin activity: hook executions and plugins loaded" width="800" height="366"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Skill &amp;amp; plugin activity: plugin hook executions and plugins loaded at session start.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The collapsed &lt;strong&gt;Native metrics (Prometheus)&lt;/strong&gt; row holds &lt;code&gt;claude_code_cost_usage_USD_total&lt;/code&gt;, &lt;code&gt;claude_code_session_count_total&lt;/code&gt;, and &lt;code&gt;claude_code_token_usage_tokens_total&lt;/code&gt;. Active time (&lt;code&gt;claude_code_active_time_seconds_total&lt;/code&gt;) is available for ad hoc queries but I have not put it on a panel yet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Claude Code — Session Drill-down
&lt;/h3&gt;

&lt;p&gt;A session ID textbox variable (regex, &lt;code&gt;.*&lt;/code&gt; shows everything) scopes the whole dashboard. You do not usually type it — you click a row in the overview's Sessions breakdown and land here.&lt;/p&gt;

&lt;p&gt;It gives session cost, prompt count, API requests and tool calls, peak context, token split, and errors. Then a &lt;strong&gt;Prompts in this session&lt;/strong&gt; table where each row links through to the prompt view, context size over time, token usage by type, a ranked &lt;strong&gt;Tool, MCP &amp;amp; skill calls&lt;/strong&gt; gauge, a &lt;strong&gt;Tool spans (Tempo)&lt;/strong&gt; table, the full tool call log, and the session's prompts in order.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6vs579s2uec42js11sa3.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6vs579s2uec42js11sa3.jpg" alt="Sessions breakdown table with cost, prompts and peak context per session" width="800" height="366"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Sessions breakdown. Each &lt;code&gt;session_id&lt;/code&gt; links straight through to the session drill-down; note the $6.03 session with 865K peak context next to seven sessions under $1.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Two details worth being precise about. Sessions have no name in telemetry — only a UUID — so the first prompt is the closest thing to a title, which is why the prompt list is prominent. And peak context is derived, not exported: &lt;code&gt;input + cache_read + cache_creation&lt;/code&gt; on the largest single API request. It tracks the context meter you see in VS Code, but the per-category breakdown behind that meter is not exported.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feltx8hbuxbw42zup3ibc.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feltx8hbuxbw42zup3ibc.jpg" alt="Tool spans from Tempo with real tool names and durations" width="800" height="366"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Tool spans read from Tempo. Every span is named &lt;code&gt;claude_code.tool&lt;/code&gt;; the real name lives in the &lt;code&gt;tool_name&lt;/code&gt; attribute, selected here as a column.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Claude Code — Prompt Drill-down
&lt;/h3&gt;

&lt;p&gt;One prompt, end to end. Cost, API requests and tool calls, token split, cache tokens, errors, hook executions. An event timeline by type where the agent loop shows up as bursts of &lt;code&gt;tool_result&lt;/code&gt; between &lt;code&gt;api_request&lt;/code&gt; bars. The prompt text itself. A ranked tool call gauge. A chronological tool/MCP/skill log with inputs, success flags, and durations. The full event trail.&lt;/p&gt;

&lt;p&gt;Then the honest bit. Spans carry &lt;code&gt;session.id&lt;/code&gt; but &lt;strong&gt;not&lt;/strong&gt; &lt;code&gt;prompt_id&lt;/code&gt;. So the Tempo panel here is range-scoped rather than prompt-scoped — it lists every &lt;code&gt;claude_code.interaction&lt;/code&gt; trace in the window, not just this prompt's. The workaround is a small Loki panel above it, &lt;strong&gt;Traces for THIS prompt&lt;/strong&gt;, which pulls the distinct &lt;code&gt;trace_id&lt;/code&gt; values off that prompt's &lt;code&gt;api_request&lt;/code&gt; events. Expand a line, click through, and you are in the right waterfall.&lt;/p&gt;

&lt;p&gt;Inside that waterfall every tool span is named &lt;code&gt;claude_code.tool&lt;/code&gt;; the actual tool name sits in the &lt;code&gt;tool_name&lt;/code&gt; span attribute, which is why the session dashboard's Tempo table selects it as an explicit column.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxwna8w19062klyqxo1ys.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxwna8w19062klyqxo1ys.jpg" alt="Event timeline by type for one prompt, showing the agent loop" width="800" height="366"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Event timeline by type. The bursts are agent-loop iterations: &lt;code&gt;api_request&lt;/code&gt;, then &lt;code&gt;tool_result&lt;/code&gt;, then another &lt;code&gt;api_request&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwv430xj2tg9nomx4roor.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwv430xj2tg9nomx4roor.jpg" alt="Ranked tool calls for a single prompt" width="800" height="366"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The ranked tool gauge, here scoped to the whole window rather than one prompt — Bash at two thirds of everything, and the Vercel MCP call resolved to a real name.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Running it locally
&lt;/h2&gt;

&lt;p&gt;You need Docker with Compose v2, Claude Code installed and authenticated, and PowerShell or a POSIX shell.&lt;/p&gt;

&lt;p&gt;Windows PowerShell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;\start.ps1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;macOS, Linux, or WSL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sh ./start.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bootstrap checks Docker, generates an ignored &lt;code&gt;.env&lt;/code&gt; with a random local Grafana password on first run, merges the telemetry environment from &lt;code&gt;claude-settings.example.json&lt;/code&gt; into your user-level &lt;code&gt;~/.claude/settings.json&lt;/code&gt; (preserving whatever else is in there), starts both services, waits for the LGTM healthcheck and the collector health endpoint, then verifies all three dashboard UIDs are present in Grafana before declaring success. Re-running is safe. Because the settings go to the user-level file, telemetry applies to every project, not just this one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Restart Claude Code — CLI or editor extension — after the first bootstrap.&lt;/strong&gt; Then use it normally. Grafana is at &lt;code&gt;http://localhost:3300&lt;/code&gt;, and the export intervals are short (10s metrics, 5s logs and traces), so give it a few seconds.&lt;/p&gt;

&lt;p&gt;To check health, and to stop or wipe everything including stored telemetry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsS&lt;/span&gt; http://localhost:13133/
docker compose ps
docker compose down
docker compose down &lt;span class="nt"&gt;--volumes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What the telemetry actually shows you
&lt;/h2&gt;

&lt;p&gt;Rather than describe this in the abstract, here is my own last 24 hours — the same window as the screenshots above.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost is wildly uneven, and a daily total hides it.&lt;/strong&gt; Eight sessions, $8.19. One session accounts for $6.03 of that, about three quarters of the day, while five others come in under eleven cents each. The expensive one is not expensive because it ran longer or sent more prompts — it sent two. It is expensive because its peak context reached 865K tokens against roughly 30K for the cheap ones. Cost and peak context sitting in the same row make that legible immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cache reads dominate, by a margin I did not expect.&lt;/strong&gt; Over the same window: 12K uncached input tokens against 5 million cache reads, plus 525K of cache creation. That ratio is the system working as intended, history and tool schemas arriving at roughly a tenth of the input rate. It also reframes what "reduce token usage" means — the lever is not the input number, it is whatever invalidates the cache and forces re-creation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The tool mix is more lopsided than it feels from the transcript.&lt;/strong&gt; 45 tool calls across 40 API requests: 31 Bash, 9 Read, 2 Write, 1 Edit, 1 ToolSearch, and a single MCP call out to Vercel. Bash at two thirds of everything is the sort of fact that changes where you put a hook, or what you bother to cache.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Durations live in the spans, not the metrics.&lt;/strong&gt; The Tempo table puts numbers on it: Bash spans from 384ms up to 7.84s, &lt;code&gt;ToolSearch&lt;/code&gt; at 5.57ms, the Vercel MCP call at 621ms. When something feels slow, this is where you find out whether it was the model or one shell command.&lt;/p&gt;

&lt;p&gt;What this does &lt;strong&gt;not&lt;/strong&gt; do: tell you whether Claude's answer was correct. There is no evaluation, accuracy scoring, hallucination detection, or intent matching anywhere in this repository. It shows the execution path and what it cost. Judging the output is still your job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security and privacy — read this part
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;This configuration deliberately captures sensitive data.&lt;/strong&gt; It enables &lt;code&gt;OTEL_LOG_USER_PROMPTS&lt;/code&gt;, &lt;code&gt;OTEL_LOG_ASSISTANT_RESPONSES&lt;/code&gt;, &lt;code&gt;OTEL_LOG_TOOL_DETAILS&lt;/code&gt;, &lt;code&gt;OTEL_LOG_TOOL_CONTENT&lt;/code&gt;, and &lt;code&gt;OTEL_LOG_RAW_API_BODIES&lt;/code&gt;. That means prompts, assistant responses, source file contents, command output, tool inputs, API conversation bodies, and OAuth account identity attributes can end up in the Docker volume &lt;code&gt;claude-observability_lgtm-data&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Local is not the same as harmless. That volume is a plaintext record of your work sitting on your disk. I found this out concretely while picking screenshots for this article: half the panels I captured had real prompt text, an email address, and private project paths in them, and could not be published without redaction.&lt;/p&gt;

&lt;p&gt;The precautions the repo takes, and the ones it asks of you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All published ports bind to &lt;code&gt;127.0.0.1&lt;/code&gt;. Keep them there.&lt;/li&gt;
&lt;li&gt;Grafana anonymous auth is disabled; the bootstrap generates a random password rather than shipping a default.&lt;/li&gt;
&lt;li&gt;Use synthetic test data. Do not point this at work involving customer data or production secrets.&lt;/li&gt;
&lt;li&gt;Delete the volume after a sensitive test run: &lt;code&gt;docker compose down --volumes&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Know which flags you have on. Turning the logging flags off is a one-line edit in &lt;code&gt;~/.claude/settings.json&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two limits worth knowing: inline bodies are truncated at Claude's content limit, and extended thinking stays redacted. Prompt logging is also not retroactive — anything sent before you enabled it stays &lt;code&gt;&amp;lt;REDACTED&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;p&gt;Being straight about what this is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Local development and testing only. Not hardened, not multi-user, no auth story beyond a local Grafana password.&lt;/li&gt;
&lt;li&gt;Trace export is a beta capability gated behind &lt;code&gt;CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1&lt;/code&gt;. If your build or account does not support it, metrics and events still export fine — you lose the trace views.&lt;/li&gt;
&lt;li&gt;Detailed hook tracing has additional gates and is left disabled.&lt;/li&gt;
&lt;li&gt;Everything is subject to export intervals. Data arrives in seconds, not instantly.&lt;/li&gt;
&lt;li&gt;Native Prometheus metrics did not appear from VS Code extension or &lt;code&gt;claude -p&lt;/code&gt; sessions in my testing (Claude Code 2.1.218). The Loki panels cover all session types, which is why they carry the dashboards.&lt;/li&gt;
&lt;li&gt;The dashboards are only as good as the fields Claude Code emits. The MCP and skill name extraction in particular depends on the current shape of &lt;code&gt;tool_parameters&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The dashboards query &lt;code&gt;service_name="claude-code"&lt;/code&gt;. Other service names need their own queries.&lt;/li&gt;
&lt;li&gt;This is not an evaluation platform.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Lessons learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Output does not reveal execution.&lt;/strong&gt; A good answer can come from an efficient path or a wasteful one, and looks identical either way. Only the telemetry distinguishes them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost is actionable only when correlated.&lt;/strong&gt; "You spent $8.19 today" is trivia. "One session took $6.03 of that across two prompts, with peak context at 865K while the cheap sessions sat near 30K" points at something you can change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;More telemetry is more responsibility.&lt;/strong&gt; The moment I turned on prompt and response logging, I created a new place where sensitive content lives — one holding my source files, my command output, and my account identity. That is a trade-off, not a checkbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;This started as a way to stop guessing about my own plugin, and became something I now open reflexively when a session feels off. It is a small stack: a collector, an LGTM image, three dashboards, one bootstrap script.&lt;/p&gt;

&lt;p&gt;The repository is at &lt;strong&gt;&lt;a href="https://github.com/xops-labs/claude-observability" rel="noopener noreferrer"&gt;github.com/xops-labs/claude-observability&lt;/a&gt;&lt;/strong&gt;. Clone it, run one script, and your own sessions will be on screen in about a minute.&lt;/p&gt;

&lt;p&gt;I would like to know how other people are approaching this. Plenty of setups track Claude Code spend; far fewer try to reconstruct what happened inside a single prompt, and anyone who has tried has almost certainly hit problems I have not — particularly around trace correlation and evaluation signals. If you are running something similar, or you try this and find a panel that is wrong or a query that could be better, open an issue or send a PR. I am more interested in comparing approaches than defending mine.&lt;/p&gt;

</description>
      <category>opentelemetry</category>
      <category>observability</category>
      <category>devops</category>
      <category>grafana</category>
    </item>
  </channel>
</rss>
