Skip to content
tsirysndrPublic

About

Self-hosted OpenTelemetry viewer in a single binary — traces, metrics & logs with a beautiful web UI. OTLP in, DuckDB inside.

Topics

Resources

Stars

4 stars

Watchers

0 watching

Forks

Repository files navigation

otelview

otelview

ci FlakeHub

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 │
└─────────────────────────────────────────────────────────────────────────────────────┘

Table of Contents

Highlights

  • 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 with span./resource./bare attributes, the intrinsics (name, duration, status, kind, rootName, rootServiceName, traceDuration), =~/!~ regex and count/avg/sum/min/max aggregates;
    • Lucene (logs and traces): http.method:POST AND http.status_code:[500 TO *] — terms, phrases, ?/* wildcards, fuzzy ~n, proximity, ranges, +/-, AND/OR/NOT and 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.

  • 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 one docker 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 (/mcp on the UI port, behind a bearer token), and otelview mcp speaks 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.TraceReader plus otelview.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.

Used in production

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.

Benchmarks

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.

Install

Shell script (macOS arm64, Linux x86_64/arm64):

curl -fsSL https://raw.githubusercontent.com/tsirysndr/otelview/main/install.sh | bash

Homebrew (macOS):

brew install tsirysndr/tap/otelview          # CLI/server
brew install --cask tsirysndr/tap/otelview   # desktop app

Docker:

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 duckdb

bun / npm (downloads the same release binary):

bun install -g otelview   # or: npm install -g otelview

Debian / 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/otelview

Quickstart

otelview                      # 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:4318

Screenshots

Traces — search by attributes, TraceQL or Lucene with a latency scatter plot, then drill into the waterfall:

traces

Logs — live tail with KQL or Lucene search, fields sidebar and trace correlation:

logs

Metrics — explorer with multi-series charts for every OTLP metric type:

metrics

MCP: otelview for AI agents

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:4320

A 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.

Skills

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

Single sign-on

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 configuration

Authorization 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

Configuration

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: info

The same shape in TOML lives in examples/otelview.toml.

The remote-storage APIs

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.

Development

./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.

Releases

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.

License

MIT

About

Self-hosted OpenTelemetry viewer in a single binary — traces, metrics & logs with a beautiful web UI. OTLP in, DuckDB inside.

Topics

Resources

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages