spaniel
Local OpenTelemetry viewer. Postman for your traces.

You run docker compose up. You hit an endpoint. It takes 800ms. You have no idea if it's Postgres, Redis, an N+1 query, or the downstream HTTP call. So you add print() statements. There has to be a better way.
spaniel is a single binary that receives OpenTelemetry traces, logs, and metrics from your local services and shows them in a beautiful UI β with automatic N+1 detection, a semantic convention linter, and session diffing so you can see exactly what your code change made better or worse.
No Docker required. No cloud account. Nothing leaves your machine.
Install
# macOS / Linux
brew install zfogg/tap/spaniel
# Go
go install github.com/zfogg/spaniel/cmd/spaniel@latest
# Docker
docker run -p 8080:8080 -p 4317:4317 -p 4318:4318 ghcr.io/zfogg/spaniel:latest
Quickstart
1. Start spaniel (browser opens automatically)
spaniel
2. Point your app at it
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
3. Hit an endpoint. Traces appear live.
That's it. No config files, no API keys, no YAML pipelines.
Features
π Trace waterfall & flame graph
Click any trace to see a full waterfall view with parent-child span relationships, duration bars, and service color-coding. Toggle to flame graph mode to spot hot paths instantly. Click any span to inspect its attributes as a structured tree.
β‘ Automatic N+1 detection
spaniel fingerprints your DB spans and flags when the same query is called an excessive number of times within a single trace. It surfaces the offending parent span, the total wasted time, and the exact statement β without you having to count anything.
β N+1 detected in GET /api/projects
SELECT * FROM builds WHERE project_id = ? β called 47 times (220ms wasted)
Likely origin: ProjectController.list [span_id: a3f2...]
π§Ή Semantic convention linter
As spans arrive, spaniel validates them against the OpenTelemetry Semantic Conventions spec and flags violations in real time. Missing db.system on a database span? Wrong attribute name on an HTTP span? spaniel catches it before you ship.
12 spans with warnings this session:
[error] 3 DB spans missing required attribute: db.system
[warn] 8 spans with service.name = "unknown_service" β configure OTEL_SERVICE_NAME
[warn] 1 span with zero duration β likely an instrumentation bug
π Session diff
Mark any point in time as a baseline. Make your code change. Run again. spaniel diffs the two sessions and shows exactly what changed: new spans, removed spans, duration deltas per operation, attribute changes.
Session diff: "before refactor" β "after refactor"
β GET /api/builds β18% faster (820ms β 672ms)
β SELECT builds β52% fewer calls (47 β 3)
β³ POST /api/webhooks +4% slower (within noise)
spaniel session new "before refactor"
# ... make your change ...
spaniel session new "after refactor"
# open the diff view in the browser
πΊ Auto-generated service map
No config. spaniel builds a live dependency graph from your span data β which services are calling which, with call counts, average latency, and error rates on each edge.
π Log correlation
Full log viewer with severity filtering and free-text search. If a log has a trace_id, click it to jump directly to that trace in the waterfall. From any span, see the logs emitted during its execution window.
π‘ OTLP proxy mode
Already sending traces to Grafana Tempo or Datadog? Run spaniel as a transparent proxy β it stores locally and forwards to your upstream simultaneously. Zero changes to your existing OTel pipeline.
spaniel --forward http://tempo:4318
How it works
spaniel is a single self-contained binary. No external services, no sidecar processes.
- OTLP receiver β accepts traces/logs/metrics over gRPC (
:4317) and HTTP (:4318)
- DuckDB β stores everything locally in
~/.spaniel/spaniel.duckdb; persists across restarts
- Ingestion pipeline β normalizes, lints, runs detectors, publishes live updates via WebSocket
- Embedded React UI β served from the binary itself; opens in your browser at
http://localhost:8080
your app ββOTLPβββΊ spaniel :4317/:4318
β
DuckDB (~/.spaniel/)
β
React UI :8080
Data retention defaults to 7 days and 500MB. Configure with ~/.spaniel/config.yaml or flags.
CI integration
Run spaniel in GitHub Actions to catch regressions before they merge.
- name: Start spaniel
run: spaniel serve --ci &
- name: Run integration tests
run: pytest tests/integration/
- name: Check for regressions
run: spaniel ci check --baseline ./spaniel-baseline.json --threshold 20
Commit spaniel-baseline.json to your repo. spaniel fails the build if p95 latency regresses more than 20%, new N+1s are introduced, or spans with ERROR status appear that weren't in the baseline.
Export a new baseline after intentional changes:
spaniel ci export --output ./spaniel-baseline.json
Configuration
# ~/.spaniel/config.yaml
port: 8080
db_path: ~/.spaniel/spaniel.duckdb
retention_days: 7
max_db_size_mb: 500
no_browser: false
forward:
- url: http://tempo:4318
Or use a per-project .spaniel.yaml in your repo root. CLI flags always take precedence.
CLI reference
spaniel start the server (opens browser)
spaniel session new [label] create and activate a new session
spaniel session list list all sessions
spaniel session baseline [id] mark a session as the diff baseline
spaniel import <file> import a trace from OTLP/Jaeger JSON as a baseline
spaniel ci check compare current session against baseline (for CI)
spaniel ci export export current session as a baseline JSON file
spaniel prune delete sessions older than retention period
spaniel reset wipe all data
spaniel config show current effective config
Docker Compose
Drop spaniel into your existing docker-compose.yml:
services:
spaniel:
image: ghcr.io/zfogg/spaniel:latest
ports:
- "8080:8080"
- "4317:4317"
- "4318:4318"
volumes:
- spaniel-data:/data
environment:
- SPANIEL_DB_PATH=/data/spaniel.duckdb
your-api:
# ... your existing service ...
environment:
- OTEL_EXPORTER_OTLP_ENDPOINT=http://spaniel:4318
- OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
- OTEL_SERVICE_NAME=your-api
volumes:
spaniel-data:
One engineer adds this. The whole team gets local observability. No individual setup required.
Why not Grafana / Jaeger / Datadog?
|
spaniel |
Grafana LGTM |
Jaeger |
Datadog |
| Single binary |
β |
β (4+ containers) |
β |
β |
| Zero config |
β |
β |
β |
β |
| N+1 detection |
β |
β |
β |
paid |
| Semconv linter |
β |
β |
β |
β |
| Session diff |
β |
β |
β |
β |
| Local only / private |
β |
β |
β |
β |
| Cost |
free |
free |
free |
expensive |
spaniel is specifically built for the local development loop, not production monitoring. Use it on your laptop. Use Grafana or Datadog in prod.
Roadmap
- OTLP receiver (gRPC + HTTP)
- DuckDB storage
- Trace waterfall + flame graph
- N+1 query detection
- Semantic convention linter
- Session diff
- Service map
- Log correlation
- OTLP proxy mode
- Production baseline import (Tempo, Jaeger export)
- Instrumentation coverage heatmap
- CI regression detection (
spaniel ci)
- Cloud baseline sync (team feature)
Contributing
git clone https://github.com/zfogg/spaniel
cd spaniel
make dev # starts Go backend + Vite dev server with hot reload
make build # builds production binary with embedded frontend
make test # runs Go tests
Issues and PRs welcome.
License
MIT Β© Zachary Fogg