DEV Community

Juan Mesaglio
Juan Mesaglio

Posted on

I built a lightweight OpenTelemetry viewer for local development

The problem

Every time I wanted to test OpenTelemetry instrumentation locally, I had to spin up Jaeger or a full collector stack. docker-compose up, wait, configure exporters, hope nothing clashes on port 4317... It felt like too much ceremony for a "does my trace look right?" check.

So I built otel-front: a single binary that receives OTLP data and shows it in a web UI. No Docker, no external database, no config files.

brew tap mesaglio/otel-front
brew install mesaglio/otel-front/otel-front
otel-front
# → opens http://localhost:8000
Enter fullscreen mode Exit fullscreen mode

Point your app at localhost:4318 (HTTP) or localhost:4317 (gRPC) and that's it.


What it does

Dashboard

  • Traces — waterfall view, flame graph, side-by-side trace comparison, search by operation or trace ID
  • Logs — full-text search, severity/service filters, correlation with traces via trace_id
  • Metrics — query builder, time series charts, aggregations (avg, sum, min, max, count)

Traces

Traces

Side-by-side comparison to spot regressions between two runs:

Traces comparation

Logs correlated by trace_id directly from a trace view:

Traces + Logs

Logs

Logs

Metrics

Metrics


How it's built

The interesting constraint was: single binary, zero external dependencies.

Go + embedded frontend

The backend is Go (Gin). The React frontend is built with Vite and then embedded directly into the binary using Go's embed.FS:

//go:embed static/*
var staticFiles embed.FS
Enter fullscreen mode Exit fullscreen mode

One go build, one binary that serves everything.

DuckDB as in-memory store

I needed SQL (for flexible filtering and aggregations) without shipping a database. DuckDB fits perfectly — it runs entirely in-process, zero setup, and handles analytical queries well.

Traces, spans, logs, and metrics all land in DuckDB tables with JSON columns for attributes:

CREATE TABLE spans (
    trace_id TEXT,
    span_id  TEXT,
    name     TEXT,
    start_time_unix_nano BIGINT,
    end_time_unix_nano   BIGINT,
    attributes           JSON,
    ...
)
Enter fullscreen mode Exit fullscreen mode

Instead of writing protobuf parsers from scratch, I reused the OpenTelemetry Collector's pdata library. It handles deserialization; I just transform the result into my internal models.


Quick start

Homebrew

brew tap mesaglio/otel-front
brew install otel-front
otel-front
Enter fullscreen mode Exit fullscreen mode

Docker

docker run -p 8000:8000 -p 4317:4317 -p 4318:4318 \
  ghcr.io/mesaglio/otel-front:latest
Enter fullscreen mode Exit fullscreen mode

Binary (macOS / Linux, x86_64 & ARM64)

https://github.com/mesaglio/otel-front/releases/latest

Configure your app:

export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4318"
export OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf"
export OTEL_LOGS_EXPORTER="otlp"
export OTEL_TRACES_EXPORTER="otlp"
export OTEL_METRICS_EXPORTER="otlp"
Enter fullscreen mode Exit fullscreen mode

The UI includes a copy-paste helper for these env vars.


Source

GitHub: mesaglio/otel-front

PRs and feedback welcome. If you're working with OpenTelemetry locally and
find it useful (or broken), let me know in the comments.

Top comments (0)