otel-desktop-viewer

command module
v0.7.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 7, 2026 License: Apache-2.0 Imports: 43 Imported by: 0

README

otel-desktop-viewer

Hello there. Lulu the First, a pink axolotl striking a heroic pose while gazing at a field of stars through a telescope

otel-desktop-viewer is a local OpenTelemetry viewer. It uses the OpenTelemetry Collector, DuckDB, and Svelte.

Choose from eight Madoromi seasonal Day/Night themes and 32 named inks.

Table of Contents

Screenshots

Traces

Traces view

Metrics

Metrics view

Logs

Logs view

Explore Telemetry

Home discovers services across traces, metrics and logs. Search and the selected time range scope the results. The Service map shows recorded cross-service connections; selecting a service shows its matching telemetry in Details. Traces, Metrics and Logs tabs show matching card counts, with independent pages, scroll positions and a choice of 25, 50 or 100 records per page. Open a card to inspect it in its signal workspace. Shared List and Details pane widths and open preferences carry across routes.

The appearance controls are Season · Day/Night. Defaults are Autumn and the system's Day/Night preference; saved choices take priority. Service and metric-series colours stay fixed across themes and have individual overrides. See Chart Palettes.

Getting Started

Once running, the UI is at localhost:8000. The OTLP receivers listen on localhost:4317 (gRPC) and localhost:4318 (HTTP).

Via Homebrew Cask

On macOS:

brew tap ctrlspice/otel-desktop-viewer
brew install --cask otel-desktop-viewer
Via GitHub Releases

Download a pre-built binary for your platform from Releases.

Platform Architecture File
macOS Apple Silicon (M1–M4) otel-desktop-viewer_darwin_arm64.tar.gz
Linux x86_64 otel-desktop-viewer_linux_amd64.tar.gz
Linux arm64 otel-desktop-viewer_linux_arm64.tar.gz
Windows x86_64 otel-desktop-viewer_windows_amd64.zip

On Windows, unzip the archive and run otel-desktop-viewer.exe.

Linux: release binaries require glibc 2.39 or newer (Ubuntu 24.04+, Debian 13+, Fedora 40+). This applies to the tarballs, the .deb/.rpm packages, and the Docker images.

# example: macOS Apple Silicon
curl -LO https://github.com/CtrlSpice/otel-desktop-viewer/releases/latest/download/otel-desktop-viewer_darwin_arm64.tar.gz
tar xzf otel-desktop-viewer_darwin_arm64.tar.gz
./otel-desktop-viewer
Via apt / dnf (Linux)

Stable releases include .deb and .rpm packages on GemFury.

Debian / Ubuntu:

curl -fsSL https://apt.fury.io/ctrlspice/gpg.key | sudo gpg --dearmor -o /usr/share/keyrings/fury.gpg
echo "deb [signed-by=/usr/share/keyrings/fury.gpg] https://apt.fury.io/ctrlspice/ * *" \
  | sudo tee /etc/apt/sources.list.d/fury.list
sudo apt update
sudo apt install otel-desktop-viewer

Fedora / RHEL:

sudo tee /etc/yum.repos.d/fury.repo <<EOF
[fury]
name=Gemfury Repo
baseurl=https://yum.fury.io/ctrlspice/
enabled=1
gpgcheck=0
EOF
sudo dnf install otel-desktop-viewer
Via go install

Building the current source requires Go 1.26 and CGO. DuckDB 2 is statically linked into the executable; a separate DuckDB installation is not required.

go version
go env CGO_ENABLED   # should print 1
gcc --version        # or cc --version

On Windows: Use a MinGW64 GCC toolchain compatible with DuckDB's prebuilt static library. CI uses C:\mingw64\bin. Put the compatible toolchain first in PATH and verify which compiler CGO will find. For that installation, in PowerShell:

$env:PATH = "C:\mingw64\bin;$env:PATH"
(Get-Command gcc).Source
gcc --version
g++ --version

See the build prerequisites for the supported toolchain. Prebuilt release binaries do not need a compiler.

On Linux/macOS: the checks above are sufficient.

@latest resolves to the newest stable tag on the Go module proxy, not an alpha or beta release. Pin a version such as @v0.3.0, or use a GitHub Release binary to avoid compiling locally.

# install the CLI tool
go install github.com/CtrlSpice/otel-desktop-viewer@latest

# run it!
$(go env GOPATH)/bin/otel-desktop-viewer

# if you have $GOPATH/bin added to your $PATH you can call it directly!
otel-desktop-viewer

# if not you can add it to your $PATH by running this or adding it to
# your startup script (usually ~/.bashrc or ~/.zshrc)
export PATH="$(go env GOPATH)/bin:$PATH"
Via Docker

Docker does not require a local Go installation.

Pull from GitHub Container Registry (auto-selects your architecture):

docker pull ghcr.io/ctrlspice/otel-desktop-viewer:latest
docker run -p 8000:8000 -p 4317:4317 -p 4318:4318 ghcr.io/ctrlspice/otel-desktop-viewer:latest

Or pin a specific version:

docker pull ghcr.io/ctrlspice/otel-desktop-viewer:v0.3.0

Explicit per-arch tags are also available:

docker pull ghcr.io/ctrlspice/otel-desktop-viewer:latest-amd64
docker pull ghcr.io/ctrlspice/otel-desktop-viewer:latest-arm64

Or build locally from source (compiles the frontend and Go binary inside the image):

docker build --tag otel-desktop-viewer:latest .
docker run -p 8000:8000 -p 4317:4317 -p 4318:4318 otel-desktop-viewer:latest

Docker Compose

Running your app in Compose? Add the viewer as a service and export OTLP to otel-desktop-viewer:4318 (HTTP) or otel-desktop-viewer:4317 (gRPC).

services:
  app:
    image: your-apps-image-tag
    # Add your app configuration here

  otel-desktop-viewer:
    image: ghcr.io/ctrlspice/otel-desktop-viewer:latest
    ports:
      - "8000:8000"
      - "4317:4317"
      - "4318:4318"

Command Line Options

Telemetry is stored in memory by default. Use --db to persist to a file.

The bare command runs the viewer in the foreground. Keep it running while using the UI or client commands.

Automation should reuse an existing viewer. If none is available, start otel-desktop-viewer --open-browser=false as a managed child and wait for its HTTP endpoint. Stop it only if you started it.

Flags:
      --browser-port int     Port for the web UI and JSON-RPC API (default 8000)
      --db string            DuckDB file path (default: in-memory)
      --db-max-size string   Store size cap, e.g. 512MB or 2GB; oldest telemetry
                             is pruned past it. 0 disables pruning.
                             (default: 512MB in-memory, 2GB with --db)
      --grpc int             OTLP gRPC listen port (default 4317)
      --host string          Host for OTLP receivers and the web UI (default localhost)
      --http int             OTLP HTTP listen port (default 4318)
      --open-browser         Open the browser on launch (default true)
      --self-telemetry-endpoint string
                             Export the viewer's own traces and metrics to this OTLP/gRPC endpoint
  -h, --help                 help for otel-desktop-viewer
  -v, --version              version for otel-desktop-viewer
otel-desktop-viewer --db ./telemetry.duckdb --db-max-size 4GB

Observe the Viewer

Run another viewer to receive the observed viewer's own traces and metrics:

otel-desktop-viewer --grpc 4327 --http 4328 --browser-port 8001

Then start the observed viewer in another terminal:

otel-desktop-viewer --self-telemetry-endpoint http://localhost:4327

Omitting --self-telemetry-endpoint keeps self-telemetry off. The endpoint is external to the observed viewer. The monitoring endpoint must remain running through observed viewer shutdown. The caller owns starting, stopping, and waiting for both foreground processes.

Query a Running Viewer

Start the viewer with the bare command, then query it from another terminal:

otel-desktop-viewer query 'SHOW TABLES'
otel-desktop-viewer query 'SELECT service_name, count(*) FROM spans GROUP BY service_name' --limit 50

The query command runs read-only SQL against the viewer at http://localhost:8000. It returns up to 25 rows as aligned columns by default. Use --endpoint for another viewer address, --limit for another row limit, or --json for the JSON result.

query --json returns {columns, rows, truncated}, including column names and SQL types. Attributes and log bodies are stored as native VARIANT values; select otel_value_json(value) when you need their complete typed JSON representation. Use a precision-preserving JSON parser for integer tokens.

Search Telemetry from the CLI

With the viewer running, search its trace, log, or metric summaries from another terminal:

otel-desktop-viewer traces --service checkout --since 30m
otel-desktop-viewer logs --since 1h --limit 50
otel-desktop-viewer metrics --start 2026-10-02T08:00:00Z --end 2026-10-02T09:00:00Z --json

These commands use http://localhost:8000, search the last hour, and return up to 25 summaries. Use --endpoint, --service, --since, --start, --end, --limit, or --json to change those defaults.

In metrics --json, metricRef is the exact UUID text from metrics.id. The viewer generates it; OTLP does not provide it. It is valid only for that database. Pass it unchanged and do not parse it.

CLI signal searches share the browser's database filtering, sorting and limit builders. The CLI uses JSON-RPC and renders tables or JSON; the browser reads native Arrow datasets. Summaries are compact: metric description and metadata are available through metric <metric-ref> --json, complete log bodies through log <log-ref> --json, and complete span data through the trace-qualified span command. services shares the service catalogue and returns complete labels.

For direct API use, GET /openrpc.json describes methods at POST /rpc, and GET /llms.txt serves the agent guide. GET /openapi.json describes HTTP routes, media types and statuses, and directs agents to those JSON-RPC inspection tools. The browser's POST /api/data/{method} Arrow routes are a separate HTTP transport; OpenRPC describes JSON-RPC results, not Arrow schemas. OTLP JSON downloads use GET /export/{signal}/{id}.

Inspect every compact span and trace-linked log row for one trace:

otel-desktop-viewer trace 0123456789abcdef0123456789abcdef
otel-desktop-viewer trace 0123456789abcdef0123456789abcdef --json

Table and JSON output contain the same untruncated fields. Trace start is min(spans.start_time). Trace duration is max(spans.end_time) - min(spans.start_time). Span offset and duration are calculated from received nanosecond timestamps and returned as exact decimal strings. Logs use the received timestamp unless it is zero, then use the received observed timestamp. Severity uses received text when present; otherwise it uses the display band derived from the received number. The body is the compact body_preview. Use query for complete stored log fields.

Inspect one span with full typed detail and every log associated with that exact trace and span ID:

otel-desktop-viewer span 000000000000002a
otel-desktop-viewer span 0123456789abcdef0123456789abcdef 000000000000002a --json

A standalone span ID returns a not-found result, one exact span, or stable span summaries when the ID occurs in multiple traces. The default limit is 25; use --limit to request more or fewer. Ambiguous summaries include both IDs, the exact match count, and whether more rows are available. It never chooses between duplicate span IDs from different traces. The qualified form selects only the requested trace and span pair. Both not-found forms are successful structured results.

Configuring Your OpenTelemetry SDK

Point your app's OTLP exporter at the viewer. Send to http://localhost:4318 (HTTP) or http://localhost:4317 (gRPC).

If your SDK supports configuration via environment variables, you can use:

# HTTP
export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4318"
export OTEL_TRACES_EXPORTER="otlp"
export OTEL_METRICS_EXPORTER="otlp"
export OTEL_LOGS_EXPORTER="otlp"
export OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf"

# gRPC
export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4317"
export OTEL_TRACES_EXPORTER="otlp"
export OTEL_METRICS_EXPORTER="otlp"
export OTEL_LOGS_EXPORTER="otlp"
export OTEL_EXPORTER_OTLP_PROTOCOL="grpc"
Declarative configuration

SDKs that support declarative configuration can use a YAML file instead. Save this as otel-config.yaml:

file_format: "1.1"

resource:
  attributes:
    - name: service.name
      value: my-service

tracer_provider:
  processors:
    - batch:
        exporter:
          otlp_http:
            endpoint: http://localhost:4318/v1/traces

meter_provider:
  readers:
    - periodic:
        exporter:
          otlp_http:
            endpoint: http://localhost:4318/v1/metrics

logger_provider:
  processors:
    - batch:
        exporter:
          otlp_http:
            endpoint: http://localhost:4318/v1/logs

Then point your app at it:

export OTEL_CONFIG_FILE=/path/to/otel-config.yaml

[!NOTE] When a config file is used, SDKs ignore the traditional OTEL_* environment variables entirely (aside from ${VAR} substitution inside the file itself).

Example With otel-cli

otel-cli can send test traces from shell scripts, including attributes, events, propagated context, and background spans.

Start the desktop viewer in one terminal:

otel-desktop-viewer

In another terminal, point otel-cli at the viewer:

export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf

Quick span: wrap any command:

otel-cli exec --service my-service --name "check the archive" curl -s -o /dev/null https://archive.org/

Chained spans: otel-cli propagates context automatically:

otel-cli exec --kind producer --service demo --name produce -- \
  otel-cli exec --kind consumer --service demo --name consume sleep 0.2

Rich trace: background span, events, attributes, and linked child spans:

sockdir=$(mktemp -d)
carrier=$(mktemp)

otel-cli span background \
  --service "otel-cli-example" \
  --name "script runtime" \
  --attrs "deployment.environment=local,team=platform" \
  --tp-carrier "$carrier" \
  --sockdir "$sockdir" &
sleep 0.1

otel-cli span event --name "starting work" --attrs "phase=setup,attempt=1" --sockdir "$sockdir"

otel-cli exec --service "otel-cli-example" --name "fetch example" --kind client \
  --attrs "http.url=https://example.com" \
  --tp-carrier "$carrier" \
  curl -s -o /dev/null https://example.com

otel-cli exec --kind producer --service "otel-cli-example" --name "hand off" \
  --tp-carrier "$carrier" -- \
  otel-cli exec --kind consumer --service "otel-cli-example" --name "process" sleep 0.1

otel-cli span event --name "finished" --attrs "phase=teardown,status=ok" --sockdir "$sockdir"
otel-cli span end --sockdir "$sockdir"

Open http://localhost:8000/traces to explore the result. For more otel-cli features (custom span times, {{traceparent}} in command args, config files, and a built-in TUI server), see the otel-cli README.

otel-cli example trace

Agent Usage Skill

Print the guide bundled with your installed viewer:

otel-desktop-viewer skills

While the viewer is running, the same guide is available at http://localhost:8000/llms.txt.

Install the otel-desktop-viewer skill from this repository:

npx skills add CtrlSpice/otel-desktop-viewer --skill otel-desktop-viewer

The OTel Desktop Viewer skill gives coding agents focused read-only SQL examples for inspecting telemetry in a running viewer. It requires a build where otel-desktop-viewer --help lists query.

Implementation

The CLI is a custom OpenTelemetry Collector distribution. Its desktop exporter ingests telemetry through Arrow batches into DuckDB 2.0.0. The duckdb extension owns the store, HTTP server and retention loop. It:

  • serves browser datasets as native Arrow IPC at POST /api/data/{method};
  • exposes CLI queries and control operations through JSON-RPC at POST /rpc;
  • provides store-backed OTLP JSON downloads at GET /export/{signal}/{id};
  • serves a Svelte web UI embedded in the binary via go:embed.

Attributes, log bodies and Metric metadata use native VARIANT values, retaining nested types and exact 64-bit integers. Resource and Scope belong to their received records; events, links, datapoints and exemplars are views of their stored owners. Browser datasets retain typed Arrow columns. JSON rendering and complete Copy values are produced when requested, independently of bounded display previews.

DuckDB runs in memory by default. Use --db for file-backed storage. The current application schema is 27. Incompatible databases are rejected without modification; choose a fresh database and reload source telemetry. See schema compatibility.

Public builds use EVALS=0. EVALS=1 includes Evals in both Go and the frontend at build time; shared viewer storage remains available in either build. See experimental features.

See ARCHITECTURE.md for a full system overview.

What's With the Axolotl??

Her name is Lulu Axol'Otel. She is very pink, and I love her.

More seriously, I like to give my side projects an animal theme to add a little aesthetic interest on what otherwise might be fairly plain applications.

Contributing

See CONTRIBUTING.md. Please read our Code of Conduct before participating.

License

Apache 2.0, see LICENSE

Documentation

The Go Gopher

There is no documentation for this package.

Directories

Path Synopsis
duckdbextension
Package duckdbextension owns the shared telemetry store and the viewer that serves it: the DuckDB database, the frontend HTTP server, and the retention loop that keeps the store under its size cap.
Package duckdbextension owns the shared telemetry store and the viewer that serves it: the DuckDB database, the frontend HTTP server, and the retention loop that keeps the store under its size cap.
internal/evaldecode
Package evaldecode converts producer files into historical observations.
Package evaldecode converts producer files into historical observations.
internal/store/arrowbridge
Package arrowbridge hands C-allocated Arrow batches to the stock DuckDB engine through a statically linked table function on the caller's existing connection.
Package arrowbridge hands C-allocated Arrow batches to the stock DuckDB engine through a statically linked table function on the caller's existing connection.
internal/store/arrowingest
Package arrowingest builds whole received batches with shared Arrow contexts.
Package arrowingest builds whole received batches with shared Arrow contexts.
internal/store/arrowread
Package arrowread projects native query results into browser-readable Arrow.
Package arrowread projects native query results into browser-readable Arrow.
internal/store/attributes
Package attributes provides scoped keys-first discovery from the shared structured search catalogue.
Package attributes provides scoped keys-first discovery from the shared structured search catalogue.
internal/store/queries
Package queries holds every piece of SQL the store runs, as files.
Package queries holds every piece of SQL the store runs, as files.
internal/store/query
Package query executes read-only SQL against the viewer store.
Package query executes read-only SQL against the viewer store.
internal/store/searchindex
Package searchindex maintains a derived structured discovery/search catalogue.
Package searchindex maintains a derived structured discovery/search catalogue.
internal/store/storetest
Package storetest provides shared support for store tests.
Package storetest provides shared support for store tests.
internal/telemetry
Package telemetry carries the exporter's self-observability: the spans and instruments it emits about its own ingest, queries, and retention.
Package telemetry carries the exporter's self-observability: the spans and instruments it emits about its own ingest, queries, and retention.
internal
discovery
Package discovery builds the read-only discovery queries shared by the CLI and UI.
Package discovery builds the read-only discovery queries shared by the CLI and UI.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL
⌂ 换网址