The open-source, self-hosted OpenTelemetry viewer — traces, metrics and logs in one fast, beautiful, single binary.
The simplest way to inspect OpenTelemetry data on your own infrastructure: one static binary embeds the OTLP receivers, the storage engine (DuckDB) and the web UI. No cluster, no JVM, no SaaS bill. Point your apps' OTLP exporters at it and open your browser.
┌─────────────────────────────── otelview (one binary) ───────────────────────────────┐
│ │
│ OTLP gRPC :4317 ──┐ ┌── web UI + REST :4319 │
│ OTLP HTTP :4318 ──┼──► receivers ──► storage backend ◄────┼── MCP /mcp :4319 │
│ (proto & JSON, │ (optional memory │ duckdb │ (AI agents, tokened) │
│ gzip, header │ header jaeger │ remote └── remote-storage gRPC │
│ auth) │ auth) reader APIs on :4317 │
└─────────────────────────────────────────────────────────────────────────────────────┘
- Highlights
- Used in production
- Benchmarks
- Install
- Quickstart
- Screenshots
- MCP: otelview for AI agents
- Skills
- Single sign-on
- Configuration
- The remote-storage APIs
- Development
- Releases
- License
-
All three signals: trace search + waterfall, live log tail with a severity histogram, metrics explorer with multi-series charts and query functions (rate, increase, sum/avg/min/max across series).
-
APM built in: a service dependency map and per-service RED metrics (request rate, error rate, p50/p95/p99 latency) derived live from your traces.
-
Three query languages, all parsed in Rust and evaluated as predicates over records — so they work identically on every storage backend, including the remote ones that can't express rich filters themselves:
- KQL (Kibana-style, logs):
http.method:POST and status_code:>=500— quoted phrases, wildcards, numeric comparisons,and/or/not; - TraceQL (Grafana Tempo-style, traces):
{ status = error && duration > 100ms },{ name = "charge" } && { .http.method = "GET" }— spanset selectors withspan./resource./bare attributes, the intrinsics (name,duration,status,kind,rootName,rootServiceName,traceDuration),=~/!~regex andcount/avg/sum/min/maxaggregates; - Lucene (logs and traces):
http.method:POST AND http.status_code:[500 TO *]— terms, phrases,?/*wildcards, fuzzy~n, proximity, ranges,+/-,AND/OR/NOTand grouping. A trace matches when any one of its spans does.
Every mode gets syntax highlighting and context-aware autocomplete (field names, then live top values after the
:), and each keeps its own text so toggling between languages is lossless. Logs also get a discovered-fields sidebar. - KQL (Kibana-style, logs):
-
Single sign-on, when you want it: OIDC with PKCE, roles mapped to viewer/admin RBAC, and sessions in an HttpOnly cookie — so SAML federation, MFA and passkeys come from your identity provider rather than from otelview. Off by default; a laptop needs no identity provider.
examples/zitadel/is a working setup in onedocker compose up. -
An MCP server built in: every query above is also a tool an AI agent can call — 17 of them, plus the query-language references and ready-made investigations. A running otelview is an MCP server (
/mcpon the UI port, behind a bearer token), andotelview mcpspeaks stdio for desktop clients. Details below. -
OTLP in, both transports: gRPC (
:4317) and HTTP (:4318), protobuf and JSON, gzip supported, optional header-token auth. -
Storage your way:
memory— bounded ring buffers, zero setup;duckdb— embedded analytical store, persisted to a single file (linked statically from the official pre-built GitHub release binaries — DuckDB is never compiled from source);jaeger— external trace storage via the Jaeger v2 remote-storage gRPC API (jaeger.storage.v2.TraceReader+ OTLP export writes);remote— another otelview instance as full storage for traces and logs and metrics.
-
It is a storage server too: every otelview serves
jaeger.storage.v2.TraceReaderplusotelview.storage.v1.{LogReader, MetricReader, Diagnostics}(a logs/metrics read API modeled on the Jaeger v2 spec) on its gRPC port — so otelview can back Jaeger v2, and otelviews compose. -
Single binary: the React UI is embedded; the whole thing is one self-contained executable.
-
Desktop app: a Tauri shell pointing at any remote otelview API.
-
Config: YAML or TOML, every field optional, CLI overrides for the common knobs.
otelview runs in production at Rocksky, collecting all
three signals from its full fleet of Rust, Node and Go services — millions of
spans, log records and metric points a day into a single DuckDB-backed
instance, with storage.retention keeping the database bounded.
One seeded run of every storage path against the embedded DuckDB backend, on
an Apple M-series laptop (cargo bench -p otelview-storage --bench duck_queries -- 1000000). Dataset: 1M spans across 250k traces, 1M logs, 2M
metric points — 4M rows, with JSON attributes on every row.
| operation | time | rate |
|---|---|---|
| insert 1M spans | 2.4s | 416k rows/s |
| insert 1M logs | 1.6s | 613k rows/s |
| insert 2M metric points | 3.2s | 632k rows/s |
| find_traces, newest 20 | 7.2ms | |
| find_traces, service + errors only | 3.2ms | |
| find_traces, attribute key=value | 32.6ms | |
| find_traces, attribute substring | 15.3ms | |
| get_trace | 0.6ms | |
| query_logs, newest 300 | 4.8ms | |
| query_logs, body substring | 23.6ms | |
| query_logs, errors only | 4.4ms | |
| metric series (8 series, 4k points) | 42.7ms | |
| list_services / operations / stats | < 9ms | |
| retention sweep, 2.46M expired rows | 1.2s | 2.1M rows/s |
Reads never queue behind ingest: writes serialize on one connection and
queries run on a pool of their own, against a consistent MVCC snapshot.
cargo bench -p otelview-storage --bench duck_ingest times the write path on
its own; both benches take a row count as their first argument.
Shell script (macOS arm64, Linux x86_64/arm64):
curl -fsSL https://raw.githubusercontent.com/tsirysndr/otelview/main/install.sh | bashHomebrew (macOS):
brew install tsirysndr/tap/otelview # CLI/server
brew install --cask tsirysndr/tap/otelview # desktop appDocker:
docker run -p 4317:4317 -p 4318:4318 -p 4319:4319 ghcr.io/tsirysndr/otelview
# persist DuckDB data:
docker run -p 4317:4317 -p 4318:4318 -p 4319:4319 \
-v otelview-data:/data ghcr.io/tsirysndr/otelview --storage duckdbbun / npm (downloads the same release binary):
bun install -g otelview # or: npm install -g otelviewDebian / Ubuntu (APT, amd64 and arm64):
echo "deb [trusted=yes] https://apt.fury.io/tsiry/ /" \\
| sudo tee /etc/apt/sources.list.d/otelview.list
sudo apt-get update
sudo apt-get install otelview # CLI/server
sudo apt-get install otelview-desktop # desktop app (optional)Fedora / RHEL / Rocky / Alma (DNF, x86_64 and aarch64):
sudo tee /etc/yum.repos.d/otelview.repo <<'EOF'
[otelview]
name=otelview
baseurl=https://yum.fury.io/tsiry/
enabled=1
gpgcheck=0
EOF
sudo dnf install otelview # CLI/server
sudo dnf install otelview-desktop # desktop app (optional)Nix (flakes; aarch64-darwin, x86_64-linux, aarch64-linux — binaries come
from the otelview cachix cache, no compilation):
# accept the flake's cache settings when prompted, or enable it globally:
cachix use otelview
# try it
nix run github:tsirysndr/otelview
# install into your profile
nix profile install github:tsirysndr/otelview
Pre-built binaries: grab a tarball from the
releases page; the desktop
app ships there too (.dmg, .AppImage, .deb).
From source:
git clone https://github.com/tsirysndr/otelview && cd otelview
./scripts/fetch-duckdb.sh
(cd ui && bun install && bun run build)
cargo build --release # → target/release/otelviewotelview # in-memory storage, UI on http://127.0.0.1:4319
otelview --storage duckdb # persist to ./otelview.duckdb
otelview -c otelview.yaml # full config
# send something to it
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318Traces — search by attributes, TraceQL or Lucene with a latency scatter plot, then drill into the waterfall:
Logs — live tail with KQL or Lucene search, fields sidebar and trace correlation:
Metrics — explorer with multi-series charts for every OTLP metric type:
otelview speaks the Model Context Protocol, so an AI agent can read your telemetry with the same queries the UI makes — through the same code path, so the two can never disagree about what a search means.
Two ways in. A running otelview already serves MCP at /mcp on the UI
port. For desktop clients that launch a process and talk over pipes, the
mcp subcommand speaks JSON-RPC on stdin/stdout:
# Query a running otelview over its API (works from anywhere)
otelview mcp --endpoint http://127.0.0.1:4319
# ...or open the configured storage directly, with no server running
otelview mcp --storage duckdb --duckdb-path otelview.duckdb
# ...or serve MCP over HTTP on its own port
otelview mcp --endpoint http://127.0.0.1:4319 --http 127.0.0.1:4320A DuckDB file can only be opened by one process, so use --endpoint while a
server has it.
Claude Desktop / Claude Code / any MCP client:
{
"mcpServers": {
"otelview": {
"command": "otelview",
"args": ["mcp", "--endpoint", "http://127.0.0.1:4319"]
}
}
}For the HTTP endpoint instead, point the client at http://127.0.0.1:4319/mcp
and send the token as Authorization: Bearer ….
17 tools, covering everything the UI can ask:
| Services | list_services, list_operations, service_stats (RED metrics), service_graph |
| Traces | search_traces (TraceQL/Lucene/filters), get_trace (text waterfall), list_trace_fields |
| Logs | search_logs (KQL/Lucene/filters), log_histogram, list_log_fields |
| Metrics | list_metrics, query_metric (rate/increase, sum/avg/min/max), find_exemplars |
| Cross-signal | investigate_trace — waterfall + error spans + correlated logs + exemplars in one call |
| Meta | storage_stats, get_config, query_syntax (KQL/TraceQL/Lucene references) |
Results come back twice: as structuredContent for the client to parse, and
as text built for a reader — tables for lists, and a real waterfall for a
trace:
3 spans over 800.0ms from 2026-09-22T16:39:46.973Z
████████████████████████████████ 800.0ms gateway POST /checkout [00f067aa0ba902b7]
████████████████████████ 600.0ms payments charge [00f067aa0ba902b8] ERROR: upstream timeout
█████ 120.0ms db SELECT accounts [00f067aa0ba902b9]
There are also resources (the service list, the metric catalog, storage
stats, config, and the three query-language references) and prompts —
investigate_errors, diagnose_latency, explain_trace, health_report —
each a plan the agent follows with the tools above.
Security. Every tool is read-only; nothing in the protocol can modify or
delete telemetry. The HTTP endpoint takes a bearer token from mcp.token,
falling back to ui.token and then to auth.token when protect_api is on,
so locking the UI locks MCP with the same key. OTELVIEW_MCP_TOKEN overrides
all of them, keeping the secret out of the config file. The token is accepted
as Authorization: Bearer <token> or in the auth.header header, and is
compared in constant time. Serving MCP on a non-loopback address without a
token is refused rather than done quietly, and requests carrying a browser
Origin from anywhere but loopback are rejected — the DNS-rebinding
mitigation the MCP spec asks for, which is what protects an open local
endpoint from a web page. Set mcp.enabled: false to turn the endpoint off
entirely.
Agent skills for otelview live in their own repo, tsirysndr/otelview-skills, in the format skills.sh and Claude Code read:
npx skills add tsirysndr/otelview-skills| Skill | For |
|---|---|
otelview |
Investigating traces, logs and metrics through the MCP server — the query languages, reading a waterfall, correlating the three signals |
otelview-instrument |
Pointing an application's OpenTelemetry SDK (or an existing Collector) at otelview, and verifying the data arrived |
otelview-operate |
Running an instance: storage backends, retention, tokens, deployment, composition |
otelview can sit behind an OpenID Connect provider. It is a relying party: it verifies tokens and enforces roles, while passwords, second factors, passkeys, SAML federation and user management stay with the provider — so turning on MFA there turns it on here, with no otelview change.
[auth.oidc]
enabled = true
issuer = "https://auth.example.com"
client_id = "otelview-web"
redirect_url = "https://otelview.example.com/auth/callback"
viewer_roles = ["otelview.viewer"] # read telemetry
admin_roles = ["otelview.admin"] # and read the configurationAuthorization code with PKCE, the session in an HttpOnly cookie that never carries the token, roles read from the provider's claim, and the same bearer tokens accepted on the API and on MCP. Agents discover where to get one through RFC 9728 metadata on a 401. Disabled by default, so local development needs nothing.
Three ways in, depending on where you are starting:
examples/zitadel/ |
Self-hosted Zitadel, otelview behind it, in one docker compose up |
| Zitadel Cloud guide | The hosted version, click by click and through the API |
| Deployment guide | TLS, roles, agents, backups — production in general |
YAML or TOML — the extension decides. Print all defaults with otelview --print-config.
# otelview.yaml — every field optional
receivers:
grpc: { enabled: true, listen: "0.0.0.0:4317" }
http: { enabled: true, listen: "0.0.0.0:4318" }
auth:
header: x-otelview-token # metadata key / HTTP header
token: sekret # unset = auth disabled
protect_api: false # also require the token on the query API
oidc: # single sign-on; see docs/deployment.md
enabled: false
# issuer: https://auth.example.com
# client_id: otelview-web
# redirect_url: https://otelview.example.com/auth/callback
# viewer_roles: [otelview.viewer]
# admin_roles: [otelview.admin]
storage:
backend: duckdb # memory | duckdb | jaeger | remote
retention: 7d # delete older telemetry (36h/7d/2w/1mo); unset = keep forever
retention_sweep_interval: 1h # how often the sweep runs
memory:
max_spans: 200000
max_logs: 200000
max_metric_points: 500000
duckdb:
path: otelview.duckdb # or ":memory:"
jaeger: # external Jaeger v2 remote-storage backend
endpoint: "http://127.0.0.1:17271"
fallback: memory # logs/metrics live here (not in the Jaeger API)
remote: # another otelview as full 3-signal storage
endpoint: "http://other-host:4317"
auth_header: x-otelview-token
auth_token: sekret
ui:
listen: "127.0.0.1:4319"
cors: true # allow the desktop app / other origins
# token: ui-sekret # optional: require a token to use the web UI
mcp:
enabled: true # serve MCP for AI agents on the UI port
path: /mcp
# token: agent-sekret # bearer token; falls back to ui.token, then
# auth.token when protect_api is on.
# OTELVIEW_MCP_TOKEN overrides it.
log_level: infoThe same shape in TOML lives in examples/otelview.toml.
Reads and writes both speak open protocols on the gRPC port:
| Signal | Write (ingest) | Read |
|---|---|---|
| traces | OTLP TraceService/Export |
jaeger.storage.v2.TraceReader |
| logs | OTLP LogsService/Export |
otelview.storage.v1.LogReader |
| metrics | OTLP MetricsService/Export |
otelview.storage.v1.MetricReader |
| stats | — | otelview.storage.v1.Diagnostics |
otelview.storage.v1 (proto) deliberately mirrors the Jaeger v2 design: reader services streaming standard OTLP payloads. Implement it (plus TraceReader) and anything can be an otelview backend.
./scripts/fetch-duckdb.sh # once: download the static libduckdb release
(cd ui && bun install && bun run build)
cargo build --release # single binary at target/release/otelview
cargo test --release
# or with nix (aarch64-darwin, x86_64-linux, aarch64-linux):
nix develop # toolchain + bun + protoc + static duckdb env
nix build .#otelview # crane build with the web UI embedded
cd ui
bun run dev # Vite dev server proxying /api → :4319
bun run test # vitest + testing-library + msw
bun run storybook # component workbench
bun run tauri dev # desktop shell (point Settings at a remote API)Crate layout: crates/model (records), crates/config, crates/storage (backends + protos), crates/receiver (OTLP in + reader servers), crates/api (REST + embedded UI, with the query layer both it and MCP run on), crates/mcp (the MCP server: jsonrpsee dispatch, tools, stdio + HTTP transports), crates/otelview (binary). UI: React + Tailwind + HeroUI + Tabler icons, jotai state, VS Code-style layout, Night Rider dark theme.
Tagging v* builds aarch64-apple-darwin, x86_64-unknown-linux-gnu and aarch64-unknown-linux-gnu binaries plus the Tauri desktop bundles and uploads everything to the GitHub release; the docker workflow publishes the multi-arch ghcr.io/tsirysndr/otelview image (also runnable on demand via workflow dispatch); the otelview npm package installs the matching binary via postinstall.
MIT



