A lightweight, zero-config OpenTelemetry trace viewer for local development.
Drop-in replacement for a collector endpoint — point your OTLP exporter at it and see traces immediately. No database required.
- Zero config — listens on port 4318, the standard OTLP/HTTP port. Most exporters work without changing a single setting
- OTLP JSON & Protobuf — accepts both
application/jsonandapplication/x-protobufpayloads - Real-time updates — new traces appear instantly via SSE (Server-Sent Events), no polling
- Waterfall timeline — Honeycomb-style span waterfall with resizable name column and sidebar
- Service map — auto-generated graph of cross-service calls with error rates and latency (p50/p99)
- Search & filter — filter trace list by text, service, status, and duration range; search spans inside a trace based on attributes, events, and span name and id
- Import/export traces — export one trace, filtered traces, or selected traces as OTLP JSON envelope; import from OTLP JSON or otel-gui export files with metadata preview before confirmation
- Bulk list actions — trace list supports multi-select export and split delete actions (
Clear All+Delete Selected (n)) - Keyboard navigation — rich keyboard control: arrow keys for the span tree,
/to search,t/lto jump to Traces/Logs tabs,mto toggle Traces/Service Map, escape key to clear search and go back to the trace list,?for shortcuts help - Error navigation — jump between error spans with one key
- Span details — attributes, events with timeline markers, resource attributes, instrumentation scope, span links, correlated logs
- Global logs workflow — browse all logs in a dedicated tab, open full log details, and jump from logs to the owning trace/span
- Trace and log counts — the trace list shows both span and correlated log counts, and trace detail summarizes correlated logs alongside spans/services/depth
- Collapse/expand — hide subtrees in the waterfall for cleaner viewing
- Resizable panels — drag splitters to resize the waterfall name column and the span details sidebar
- Dark mode — toggle between light and dark themes
- Incremental ingestion — spans from the same trace can arrive in separate requests and out of order; the store merges them correctly
- In-memory with optional local persistence — default is in-memory only; opt into PGlite-backed restart recovery with bounded retention
Requires: Node.js ≥ 20, pnpm
git clone https://github.com/metafab/otel-gui
cd otel-gui
pnpm install
pnpm devpnpm run dev # Start dev server on port 4318
pnpm run lint # Lint TypeScript, JavaScript, and Svelte files
pnpm run format # Format files with Prettier
pnpm run format:check # Check formatting without writing changes
pnpm run check # TypeScript type-check
pnpm run test # Run unit tests (Vitest)
pnpm run test:watch # Tests in watch mode
pnpm run build # Production buildOpen http://localhost:4318 — the OTLP endpoint is live at the same address.
otel-gui can be installed from a custom Homebrew tap:
brew install metafab/tap/otel-guiThen run:
otel-gui
# or override default port (4318)
PORT=55681 otel-guiNotes:
- Homebrew formula updates are automated from tagged releases (
v*) by the release workflow.
To update to the latest version, you'll use:
brew update
brew upgrade metafab/tap/otel-guiPull and run the published GHCR image:
docker pull ghcr.io/metafab/otel-gui:latest
docker run --rm --name otel-gui -p 4318:4318 ghcr.io/metafab/otel-gui:latestContainer tags are published from Git refs:
latestfor the default branchv*tags (for example,v1.1.0)sha-<commit>immutable tags
Build and run locally with Docker:
docker build -t otel-gui .
docker run --rm -p 4318:4318 otel-guiThen use the standard OTLP endpoint:
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318The container reads PORT (default 4318). You can override it at runtime:
docker run --rm -e PORT=55681 -p 55681:55681 otel-guiUsing port 4318 is recommended for zero-config OTLP exporters.
Run with Docker Compose:
docker compose up --buildRun in background:
docker compose up -d --buildStop:
docker compose downTo use a different port:
PORT=55681 docker compose up --buildPoint any OpenTelemetry SDK exporter at the viewer:
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318No other configuration needed. The viewer accepts the standard POST /v1/traces endpoint.
Requests with Content-Encoding: gzip are also supported.
The viewer also accepts OTLP logs at:
POST /v1/logsUse the same traceId/spanId values as your spans to get correlated logs in trace detail sidebar.
Run the bundled e-commerce demo to see all features immediately:
./demo-ecommerce-trace.shOn Windows (PowerShell):
.\demo-ecommerce-trace.ps1If script execution is blocked, run it for the current shell session only:
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
.\demo-ecommerce-trace.ps1This sends a realistic multi-service trace (frontend → backend-api → auth-service + database) with errors, retries, and incremental span arrival across two requests.
# Simple 3-span trace
curl -X POST http://localhost:4318/v1/traces \
-H "Content-Type: application/json" \
-d @samples/sample-trace.json
# Correlated logs for the simple trace
curl -X POST http://localhost:4318/v1/logs \
-H "Content-Type: application/json" \
-d @samples/sample-log.json
# E-commerce trace — part 1 (frontend + backend-api)
curl -X POST http://localhost:4318/v1/traces \
-H "Content-Type: application/json" \
-d @samples/sample-trace-ecommerce-part1.json
# E-commerce correlated logs — part 1
curl -X POST http://localhost:4318/v1/logs \
-H "Content-Type: application/json" \
-d @samples/sample-log-ecommerce-part1.json
# E-commerce trace — part 2 (auth-service + database with errors)
curl -X POST http://localhost:4318/v1/traces \
-H "Content-Type: application/json" \
-d @samples/sample-trace-ecommerce-part2.json
# E-commerce correlated logs — part 2
curl -X POST http://localhost:4318/v1/logs \
-H "Content-Type: application/json" \
-d @samples/sample-log-ecommerce-part2.json
# Trace with error spans (status.code = 2)
curl -X POST http://localhost:4318/v1/traces \
-H "Content-Type: application/json" \
-d @samples/sample-trace-error.json
# Trace with span links
curl -X POST http://localhost:4318/v1/traces \
-H "Content-Type: application/json" \
-d @samples/sample-trace-links.jsonSee SAMPLE_TRACES.md for a full feature exploration guide.
| Variable | Default | Description |
|---|---|---|
PORT |
4318 |
HTTP port the server listens on |
OTEL_GUI_MAX_TRACES |
1000 |
Maximum number of traces kept in memory (1–10 000). Oldest traces are evicted first when the limit is reached. Requires a restart. |
OTEL_GUI_MAX_LOGS |
1000 |
Maximum number of log records kept in memory (1–10 000). Oldest records are evicted first when the limit is reached. Requires a restart. |
OTEL_GUI_PERSISTENCE_MODE |
memory |
Persistence backend mode. Use memory (default, no disk writes) or pglite (requires an external backend module, typically enterprise). |
OTEL_GUI_PERSISTENCE_PATH |
.otel-gui/pglite |
Directory path for local PGlite data when persistence mode is pglite. |
OTEL_GUI_PERSISTENCE_FLUSH_MS |
750 |
Debounce interval for batched persistence flushes in milliseconds (50–60000). |
OTEL_GUI_PERSISTENCE_BACKEND_MODULE |
(empty) | Optional module id/path dynamically loaded at startup to register persistence backends. Relative file paths resolve from the otel-gui project root (examples: @otel-gui/enterprise-persistence/register, ../otel-gui-enterprise/enterprise-persistence/dist/register.js). |
OTEL_GUI_LICENSE_KEY |
(empty) | Optional enterprise license key consumed by private persistence backend modules. |
OTEL_GUI_LICENSE_PUBLIC_KEY_PATH |
(empty) | Optional filesystem path to the PEM-encoded public key used by enterprise modules for offline license verification. |
Copy .env.example to .env to customize:
cp .env.example .env
# then edit .envFor external backend registration details, see docs/enterprise-persistence-module.md.
When OTEL_GUI_PERSISTENCE_MODE=pglite falls back to memory, check GET /api/config -> persistence.unavailableReason for a precise cause.
pnpm build
PORT=4318 node buildThe production build uses @sveltejs/adapter-node. In-memory state is kept alive by the Node.js process — no external store required for local use.
In Docker, traces are still in-memory only and are lost when the container stops.
otel-gui can be packaged as a self-contained executable using Node 22 SEA (Single Executable Applications).
Requirements:
- Node.js 22.x (for SEA blob generation)
pnpm
Build for your current OS/arch:
pnpm run build
pnpm run sea:packageGenerate for a specific target platform:
pnpm run sea:bundle
pnpm run sea:package:target -- --target linux-x64Supported targets:
linux-x64linux-arm64macos-x64macos-arm64win-x64win-arm64
Cross-target note:
- If target differs from your host (for example building
linux-x64on macOS), provide a matching Node 22 target binary:
pnpm run sea:package:target -- \
--target linux-x64 \
--node-binary /absolute/path/to/node-linux-x64Output directory:
dist/binaries/otel-gui-<platform>/
otel-gui[.exe]
build/
proto/
Run from that directory:
./otel-gui
# or override default port (4318)
PORT=55681 ./otel-guiNotes:
- Keep
otel-gui[.exe],build/, andproto/together in the same output folder. - Current OSS executable packaging targets memory mode. Optional enterprise persistence remains an external module workflow.
| Key | Where | Action |
|---|---|---|
/ |
Everywhere | Focus search |
Esc |
Everywhere | Clear search / go back |
m |
Everywhere | Toggle Traces / Service Map tab |
Alt+Backspace |
Trace list | Clear all traces |
↑↓←→ / Enter |
Trace detail | Navigate span tree |
n / N |
Trace detail | Next / prev search match |
e / E |
Trace detail | Next / prev error span |
? |
Everywhere | Toggle shortcuts overlay |
POST /v1/traces ← OTLP receiver (JSON + Protobuf)
POST /v1/logs ← OTLP logs receiver (JSON + Protobuf)
GET /api/traces ← trace list for the UI
DELETE /api/traces ← clear all traces or delete selected traceIds
GET /api/traces/:id ← single trace
GET /api/traces/:id/logs ← trace-scoped correlated logs
GET /api/traces/:id/export ← export a single trace envelope
POST /api/traces/export ← export filtered/selected traceIds
POST /api/traces/import/preview ← validate + preview import metadata
POST /api/traces/import ← import otel-gui envelope or raw OTLP JSON
GET /api/traces/stream ← SSE stream (real-time push)
GET /api/service-map ← aggregated service graph
Server-only state lives in src/lib/server/traceStore.ts with swappable backends behind the TraceStore interface. In default memory mode, runtime state is kept in memory with FIFO eviction. The retention limit defaults to 1000 traces (OTEL_GUI_MAX_TRACES) and 1000 log records (OTEL_GUI_MAX_LOGS).
SSE subscribers are notified on every write and receive a debounced event: traces message.
Additional persistence backends (including pglite) are loaded via OTEL_GUI_PERSISTENCE_BACKEND_MODULE and can be distributed separately (for example in an enterprise package).
- SvelteKit 5 with Svelte 5 runes (
$state,$derived,$effect) @sveltejs/adapter-nodefor persistent in-memory stateprotobufjsfor Protobuf decoding- No UI library — custom waterfall, service map SVG, and all components from scratch
- TypeScript throughout
You can submit a new idea.
And of course, you can develop an existing or a new idea 😀:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Make your changes and add tests if applicable
- Run
pnpm run lint && pnpm run format:check && pnpm run check && pnpm run testto validate - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
This project is open source. See the LICENSE file for details.