OpenObserve + Claude Code: End-to-End AI Observability
You can ask Claude Code what it did, but telemetry shows what it actually did — every tool call, every token, every dollar. Claude Code…
OpenObserve + Claude Code: End-to-End AI Observability

You can ask Claude Code what it did, but telemetry shows what it actually did — every tool call, every token, every dollar. Claude Code ships with native OpenTelemetry support. OpenObserve is a Rust-based observability platform that runs as a single binary. Together, they give you full visibility into AI-assisted development sessions in under 5 minutes of setup.
The Problem: AI-Assisted Development Is a Black Box
When you work with Claude Code, you see the output — the refactored function, the passing tests, the new feature. What you don’t see is the process: how many API calls it made, which tools it reached for, how long each operation took, and what it cost you.
Token costs accumulate invisibly in the background. One session might cost 18. Without instrumentation, you have no way to compare session efficiency, identify wasteful patterns, or answer the most basic question: how much did that refactor actually cost?
That’s the gap telemetry fills.
The Stack: Claude Code OTel + OpenObserve
Claude Code emits OpenTelemetry signals natively — logs, metrics, and traces — without any plugins or patches required. You just need something to receive them.
OpenObserve is that something. It’s a self-hosted observability platform written in Rust that ingests OTLP (OpenTelemetry Protocol) over HTTP with zero configuration. No JVM, no cluster, no per-GB pricing. It runs as a single binary on a laptop and achieves roughly 140x lower storage cost than Elasticsearch through columnar Parquet compression and S3-native architecture, meaning months of session history fits comfortably on local disk.
Total setup time: one Docker command and eight environment variables.
Setup in 5 Minutes
1. Start OpenObserve
docker run -d \
--name openobserve \
-v $PWD/data:/data \
-e ZO_DATA_DIR="/data" \
-p 5080:5080 \
-e ZO_ROOT_USER_EMAIL=admin@example.com \
-e ZO_ROOT_USER_PASSWORD=yourpassword \
public.ecr.aws/zinclabs/openobserve:latest
The -v mount persists data across container restarts. Credentials are set on first startup only — subsequent launches use the stored config. OpenObserve's UI is now available at [http://localhost:5080.](http://localhost:5080.)
If you prefer running the binary directly (no Docker):
# Download from https://openobserve.ai/downloads
chmod +x openobserve
ZO_ROOT_USER_EMAIL="admin@example.com"
ZO_ROOT_USER_PASSWORD="yourpassword"
./openobserve
2. Configure Claude Code’s OTel Export
Add these to your shell profile (.zshrc, .bashrc, etc.):
# Protocol and endpoint
export OTEL_EXPORTER_OTLP_PROTOCOL="http/json"
export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:5080/api/default"
# Enable all three signal types
export OTEL_METRICS_EXPORTER="otlp"
export OTEL_LOGS_EXPORTER="otlp"
export OTEL_TRACES_EXPORTER="otlp"
# Auth header (Base64 of "email:password")
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic $(echo -n 'admin@example.com:yourpassword' | base64)"
# Export intervals (milliseconds)
export OTEL_METRIC_EXPORT_INTERVAL="10000" # 10 seconds
export OTEL_LOGS_EXPORT_INTERVAL="5000" # 5 seconds
# Delta temporality is required for counters to work correctly
export OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE="delta"
A few things to note:
http/jsonis the simplest protocol to get working. OpenObserve also supportshttp/protobufand gRPC, buthttp/jsonrequires no additional dependencies and is easy to debug withcurl.- All three exporters must be explicitly enabled. Claude Code won’t emit metrics, logs, or traces unless the corresponding
OTEL_*_EXPORTERvariable is set tootlp. - Delta temporality is critical. Without it, counters like
token_usageandcost_usagewill report cumulative values instead of per-interval deltas, making aggregation queries return wildly inflated numbers. - The auth header is a standard HTTP Basic auth string. The Base64 value encodes
email:password— replace both to match your OpenObserve credentials.
3. Launch Claude Code
Open a new terminal (to pick up the environment variables) and start Claude Code normally. Data begins flowing to OpenObserve immediately. You can verify ingestion by checking the Streams page in the OpenObserve UI — you should see default log and trace streams appear within seconds of your first prompt.
4. Verify It’s Working
A quick health check with curl confirms data is landing:
# Check OpenObserve is running
curl -s -u "admin@example.com:yourpassword" http://localhost:5080/api/default/streams \
| python3 -c "
import json, sys
for s in json.load(sys.stdin)['list']:
print(f\"{s['stream_type']:10s} {s['name']:45s} docs={s['stats']['doc_num']}\")
"
After a few prompts you should see streams like:
| Stream Type | Name | Description |
| ----------- | ------------------------------------- | ------------------------------ |
| `default` | `logs` | Claude Code event logs |
| `default` | `traces` | Claude Code trace spans |
| `metrics` | `claude_code_token_usage` | Token consumption per request |
| `metrics` | `claude_code_cost_usage` | Cost tracking in USD |
| `metrics` | `claude_code_session_count` | Session counter |
| `metrics` | `claude_code_active_time_total` | Active usage time |
| `metrics` | `claude_code_lines_of_code_count` | Lines added/removed |
| `metrics` | `claude_code_code_edit_tool_decision` | Edit accept/reject by language |
5. Optional: Auto-Start on Login
You don’t want to remember to start OpenObserve every morning. On macOS, a launchd LaunchAgent handles this:
Create ~/Library/LaunchAgents/com.openobserve.server.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.openobserve.server</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/openobserve</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>ZO_DATA_DIR</key>
<string>/Users/you/data/openobserve</string>
<key>ZO_ROOT_USER_EMAIL</key>
<string>admin@example.com</string>
<key>ZO_ROOT_USER_PASSWORD</key>
<string>yourpassword</string>
</dict>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/Users/you/Library/Logs/openobserve.stdout.log</string>
<key>StandardErrorPath</key>
<string>/Users/you/Library/Logs/openobserve.stderr.log</string>
</dict>
</plist>
# Load and start
launchctl load ~/Library/LaunchAgents/com.openobserve.server.plist
# Check status
launchctl list | grep openobserve
# Stop
launchctl unload ~/Library/LaunchAgents/com.openobserve.server.plist
On Linux, a systemd user unit does the same job. On Windows, NSSM wraps the binary as a proper service — sc.exe won't work since OpenObserve doesn't implement the Win32 Service API.
What Claude Code Actually Emits
Once connected, you’ll see five log event types flowing in:
Event TypeWhat It Capturestool_resultOutput of each tool call (Bash, Read, Write, etc.)tool_decisionWhether a tool was auto-accepted or required approvalapi_requestEach call to the Anthropic API, with model and durationuser_promptThe prompts you typedapi_errorAny failed API calls
On the metrics side, six streams are emitted continuously (these are the actual stream names as they appear in OpenObserve):
claude_code_token_usage— input, output, and cache tokens per requestclaude_code_cost_usage— cost in USD per API callclaude_code_session_count— number of active sessionsclaude_code_active_time_total— time spent with Claude Code runningclaude_code_lines_of_code_count— lines added or removedclaude_code_code_edit_tool_decision— accept/reject counts for suggested edits, by language
Every event carries a rich set of attributes that make filtering and correlation possible:
| Attribute | Example | Present On |
| ------------------------------------------- | ----------------------------------------- | ----------------------------------- |
| `session_id` | 0cdd8777-08eb-... | All events |
| `prompt_id` | 4f61c431-778f-... | All events |
| `service_name` | claude-code | All events |
| `service_version` | 2.1.96 | All events |
| `organization_id` | ce392699-792c-... | All events |
| `user_email` | [you@company.com](you@company.com) | All events |
| `terminal_type` | zed, vscode, iterm2 | All events |
| `host_arch` | arm64 | All events |
| `os_type / os_version` | darwin / 24.6.0 | All events |
| `event_sequence` | 82 | logs |
| `tool_name` | Bash, Read, Edit | tool_result, tool_decision |
| `duration_ms` | 730 | tool_result, api_request, api_error |
| `success` | true / false | tool_result |
| `decision_type / decision_source` | accept / config | tool_result |
| `decision / source` | accept / config | tool_decision |
| `model` | claude-opus-4-6 | api_request, api_error, metrics |
| `cost_usd` | 0.05392925 | api_request |
| `input_tokens / output_tokens` | 1 / 749 | api_request |
| `cache_read_tokens / cache_creation_tokens` | 59411 / 879 | api_request |
| `speed` | normal | api_request, api_error |
| `error` | Request was aborted. | api_error |
| `attempt` | 1 | api_error |
| `status_code` | undefined | api_error |
| `prompt_length` | 175 | user_prompt |
| `type (on metrics)` | input, output, cacheRead, added, removed | metrics |
The session_id and prompt_id pair lets you correlate across all three signal types — logs, metrics, and spans — for any given interaction. The event_sequence field provides ordering within a session when timestamps alone aren't granular enough.
Note that prompt content is redacted by default (<REDACTED>) — only prompt_length is visible. This is a privacy-conscious design choice by the Claude Code team.
What the Data Reveals
Here’s a snapshot from eight real Claude Code sessions (numbers are point-in-time — they grow as sessions continue):
| Metric | Value |
| ----------------------------- | ----------- |
| Total cost | $39.06 |
| Total API calls | 582 |
| User prompts | 43 |
| API calls per prompt | ~13.5 |
| Avg cost per API call (Opus) | $0.067 |
| Cache reads vs. input tokens | 61:1 |
| Bash as % of tool calls | 59% |
| Max single tool call duration | 7.7 minutes |
| Top 2 sessions as % of spend | 72% |
A few things stand out.
The 13.5x amplification. Each user prompt triggered an average of 13.5 API calls. Agentic loops are real — Claude Code doesn’t just call the API once and return. It plans, executes, reads results, adjusts, and calls again. Knowing this changes how you think about prompt granularity.
Cache is everything. Across 8 sessions, there were 40 million cache read tokens versus 653,000 direct input tokens — a 61:1 ratio. Without caching, costs would be dramatically higher. Claude Code’s aggressive caching of context is doing heavy lifting that’s invisible at the surface level.
Bash dominates tool usage. At 59% of all tool calls, Bash is far and away the most-used tool. The “AI pair programmer” is really closer to an “AI shell operator” — it’s spending most of its time running commands, not writing code.
Model usage is tiered. Opus handled 93% of requests at $0.067 per call on average. Haiku was used for subagent work — 6% of calls but less than 2% of cost. The model routing is doing exactly what you’d hope.
Spend is highly concentrated. The top two sessions account for 72% of total cost. If you’re trying to reduce spend, there’s probably one or two session patterns (long agentic loops, large context files) responsible for most of the bill.
Useful Queries
OpenObserve’s query interface supports SQL-style queries against your log and metric streams. Here are some that surface real insight:
All queries below use POST /api/default/_search with a JSON body. For metric streams, append ?type=metrics to the URL. The start_time and end_time fields are required in the request body despite being marked optional in the OpenAPI spec — omitting them returns invalid time range. Timestamps are in microseconds (Unix epoch seconds × 1,000,000). The OpenObserve web UI injects these automatically, but when using curl you'll need to calculate them:
# Last 7 days in microseconds
START=$(python3 -c "import time; print(int((time.time() - 7*86400) * 1000000))")
END=$(python3 -c "import time; print(int((time.time() + 86400) * 1000000))")
Cost per session (from api_request log events, which carry cost_usd):
SELECT session_id,
SUM(CAST(cost_usd AS DOUBLE)) as total_cost,
COUNT(*) as api_calls
FROM default
WHERE event_name = 'api_request'
GROUP BY session_id
ORDER BY total_cost DESC
Tool failure rates:
SELECT tool_name,
COUNT(*) as total_calls,
SUM(CASE WHEN success = 'false' THEN 1 ELSE 0 END) as failures
FROM default
WHERE event_name = 'tool_result'
GROUP BY tool_name
ORDER BY failures DESC
Note that success is a string field ('true' / 'false'), not a boolean — quote accordingly. From the real data: Bash fails 7.3% of the time (expected — exploratory commands error), while WebFetch fails 11.4% (network timeouts, blocked URLs). The API itself was 100% reliable during this window — all 3 API errors were user-aborted.
Slowest operations:
SELECT tool_name,
AVG(CAST(duration_ms AS BIGINT)) as avg_ms,
MAX(CAST(duration_ms AS BIGINT)) as max_ms,
COUNT(*) as calls
FROM default
WHERE event_name = 'tool_result'
GROUP BY tool_name
ORDER BY avg_ms DESC
duration_ms is stored as a string, so cast it for aggregation. WebSearch averages 46 seconds per call. Agent subagent calls averaged 2.3 minutes with a max of 7.7 minutes. Both were used sparingly, which makes sense — at that latency, you want Claude Code to reach for them only when necessary.
Token usage by model and type (uses the metrics stream):
-- Query with ?type=metrics on the endpoint
SELECT model, type, SUM(value) as total_tokens
FROM claude_code_token_usage
GROUP BY model, type
ORDER BY total_tokens DESC
The type field distinguishes input, output, cacheRead, and cacheCreation — giving you a full breakdown of where tokens are going.
Lines of code by session:
-- Query with ?type=metrics on the endpoint
SELECT session_id, type, SUM(value) as total_lines
FROM claude_code_lines_of_code_count
GROUP BY session_id, type
ORDER BY total_lines DESC
The type field is added or removed, so you can compute net lines per session.
A Note on Stream Types
OpenObserve separates logs, metrics, and traces into different stream types. This matters when querying:
- Logs (
defaultstream): Contains all five event types. Query withPOST /api/default/_search. Most fields are strings — useCAST()for numeric aggregation. - Metrics (e.g.,
claude_code_token_usage): Query withPOST /api/default/_search?type=metrics. Thevaluefield is a Float64 on streams that have it (token_usage,lines_of_code_count). Some metric streams (cost_usage,active_time_total) don't expose avaluefield via SQL — use the log-sidecost_usdfield fromapi_requestevents instead. - Traces (
defaultstream): Currently mirrors the log data with the same schema. Browse traces through the OpenObserve web UI for the best experience — the API's trace search endpoints return "Organization not found" for some query patterns, though the data is there.
Full-Stack Observability in the Agentic Loop
So far we’ve talked about observing Claude Code. But there’s a more powerful pattern: giving Claude Code access to OpenObserve as a debugging tool — closing the loop between the code it writes and the errors that code produces.
OpenObserve isn’t limited to Claude Code telemetry. It ingests anything that speaks OTLP, syslog, or the Elasticsearch bulk API. If your application sends RUM (Real User Monitoring) data, backend logs, or infrastructure metrics to the same OpenObserve instance, Claude Code can query all of it through the same API. Your AI assistant doesn’t just write code and hope — it can observe the running application, see what broke, and fix it.
Example: Debugging a Frontend Error by ID
Here’s a real scenario. Your application’s RUM SDK captures an unhandled error in the browser. You hand Claude Code the error ID:
“Debug RUM error
d7681005-04bf-4084-842d-35c6eb040f87"
Claude Code queries OpenObserve:
SELECT error_id, error_type, error_message, error_stack,
error_handling, session_id, view_url
FROM _rumdata
WHERE error_id = 'd7681005-04bf-4084-842d-35c6eb040f87'
And gets back:
error_type: TypeError
error_message: this._fn is not a function
error_handling: unhandled
error_source: source (browser)
view_url: http://localhost:3333/
session_id: 2f8f37d7-3454-4072-85dc-06b9b859897c
Stack trace:
TypeError: this._fn is not a function
at Animation.tick @ chunk-QL7BZQER.js:3206:31
at <anonymous> @ chunk-QL7BZQER.js:3022:16
at Map.forEach @ <anonymous>
at Animator._update @ chunk-QL7BZQER.js:3008:18
Now Claude Code has something to work with. But it doesn’t stop there — it queries the session timeline for context:
SELECT type, error_message, resource_url, _timestamp
FROM _rumdata
WHERE session_id = '2f8f37d7-3454-4072-85dc-06b9b859897c'
ORDER BY _timestamp
This reveals the full user journey: the page loaded, API calls went out to OpenObserve for dashboard data, chart libraries were loaded (chart.js, @arrow-js/core), and then an animation callback in a Vite-bundled dependency threw a TypeError. Claude Code can now trace the bundled chunk back to the source dependency, read the relevant code, and propose a fix — all from a single error ID.
Why This Matters
The typical debugging workflow is: a user reports an error, an engineer finds it in a dashboard, copies the stack trace, opens the codebase, searches for the relevant file, reads surrounding code, forms a hypothesis, makes a fix. That’s a multi-tool, multi-context-switch process.
With OpenObserve in the loop, Claude Code collapses that into a single conversation. You give it an error ID (or a time range, or a session, or a status code spike), and it has direct SQL access to the same observability data your team would use. The difference is that it can also read the code, correlate the stack trace to source files, and write the fix — all without leaving the terminal.
This works for any signal OpenObserve collects:
- RUM errors → query
_rumdatafor stack traces, session replays, user context - Backend logs → query application log streams for request traces, error patterns
- API metrics → query for latency spikes, error rates, throughput changes
- Infrastructure → query for resource exhaustion, connectivity issues
The pattern is always the same: tell Claude Code what to look for, point it at the OpenObserve API, and let it correlate observability data with source code.
Building on This
Once your data is flowing, there are several directions worth exploring.
Dashboards. OpenObserve has a built-in dashboard builder. A cost burn rate panel (spend over time, by session) and a tool usage breakdown (pie chart of Bash vs. Read vs. Write vs. WebSearch) are high-value starting points.
Alerting. Set a cost threshold alert — if any single session exceeds $10, you get a notification. Error rate spikes (e.g., Bash failure rate above 15%) are another useful signal.
Enrichment. The OTel SDK lets you add custom attributes. Tagging spans with git.branch and git.repository connects telemetry to the actual work being done. If multiple developers share a workspace, adding a developer.id attribute makes per-person cost attribution trivial.
Scaling. Local disk works fine for individual use. For team deployments or longer retention windows, OpenObserve supports S3 as a backend — swap the storage config and your history scales without changing anything else.
Why OpenObserve Over the Alternatives
If you’ve looked at observability tooling before, you’ve probably encountered the usual tradeoffs:
- Datadog / New Relic — excellent products, but you’re paying per GB ingested and per host. Routing your personal coding telemetry through a SaaS adds cost and raises data sovereignty questions.
- Grafana + Loki + Prometheus — powerful, but that’s four or more components to run and keep in sync. Doable for a team, overkill for a single developer.
- Just reading the terminal — fine for the moment, useless for anything historical, and impossible to aggregate or query.
OpenObserve is a single binary, runs local, achieves 140x lower storage cost, and queries with SQL. For Claude Code telemetry, it’s a proportionate tool for the job.
Closing Thought
There’s a certain recursion in how this post came together: it was researched and drafted by Claude Code, running under the observation of the OpenObserve instance described here. The numbers in the data table above are real telemetry from those actual sessions. The RUM error it debugged was a real bug in the dashboard it was building to visualize its own usage. The tool was watching itself, and fixing what it saw.
That’s the point of observability — not just to catch problems, but to understand behavior you couldn’t otherwise see. Claude Code is doing a lot of work on your behalf. Now you can see exactly what that work looks like — and so can it.
Resources
메타데이터
- post_id
- 984afcaeba36
- slug
- openobserve-claude-code-end-to-end-ai-observability-984afcaeba36
- url
- https://medium.com/devops-ai/openobserve-claude-code-end-to-end-ai-observability-984afcaeba36
- canonical_url
- https://medium.com/devops-ai/openobserve-claude-code-end-to-end-ai-observability-984afcaeba36
- author_url
- https://medium.com/@ozanzal_81488
- status
- ok
- fetched_at
- 2026-08-18 07:58:06