overengineeringstudio / overengineeringstudio/effect-utils
Add @overeng/otel-dev: Local OpenTelemetry backend for development
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 82
- Forks
- 2
- Avg merge
- 1d 8h
- Merged PRs (30d)
- 121
Description
Summary
A lightweight, local OpenTelemetry backend for development. Similar to Grafana/Tempo/Jaeger but simpler and designed for local dev environments. Provides an Effect-first API and CLI.
Motivation
Running Grafana/Tempo/Jaeger locally for development is heavyweight. Developers need a simple way to:
- View traces during local development
- Debug Effect instrumentation without external services
- Have zero-config auto-wiring for Effect projects
- Enable coding agents to easily access and analyze trace data
Design
Architecture (Option C: Standalone Server)
┌─────────────────────────────────────────────────────────────┐
│ local-otel server │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ OTLP Receiver │ │
│ │ - HTTP :4318 (JSON) - initial │ │
│ │ - gRPC :4317 - later │ │
│ └───────────────────────┬────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Trace Processor │ │
│ │ - Batch writes (configurable flush interval) │ │
│ │ - Compute trace metadata (duration, status, etc) │ │
│ └───────────────────────┬────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ SQLite Storage (.otel/traces.db) │ │
│ │ - WAL mode for concurrent access │ │
│ │ - Auto-cleanup (TTL / max size) │ │
│ └───────────────────────┬────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Query API (HTTP :4200) │ │
│ │ GET /api/traces │ │
│ │ GET /api/traces/:id │ │
│ │ GET /api/traces/:id/spans │ │
│ │ GET /api/health │ │
│ ├────────────────────────────────────────────────────────┤ │
│ │ Web UI (served from same port) │ │
│ │ - Trace list with filtering │ │
│ │ - Timeline/waterfall view │ │
│ │ - Span detail inspector │ │
│ │ - Real-time updates (SSE) │ │
│ └────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
▲ ▲
│ OTLP │ HTTP
│ │
┌──────────┴───────────┐ ┌─────────────┴─────────────┐
│ Your App │ │ CLI / Browser / Agent │
│ │ │ │
│ OTEL_EXPORTER_* │ │ otel-dev traces list │
│ env vars point here │ │ http://localhost:4200 │
└──────────────────────┘ └───────────────────────────┘
Key Design Decisions
| Decision | Choice | Rationale |
|---|---|---|
| Process model | Standalone server | Clean client/server separation, multiple viewers (CLI, web) can access same data, app and UI have independent lifecycles |
| Storage | SQLite with WAL mode | Fast writes/queries, indexed, handles concurrency, single file |
| OTLP protocol | HTTP/JSON first, gRPC later | Simpler to implement initially |
| Real-time updates | SSE | Simpler than WebSocket, sufficient for unidirectional trace streaming |
| Web UI | Custom minimal React + Storybook | No good embeddable trace viewer exists; leverage @overeng/react-inspector for span attributes |
| Data model | Hybrid (OTLP wire → Effect Schema internal) | OTLP compatibility at ingestion, ergonomic types for storage/queries/UI |
Agent-Friendly CLI Design
The CLI is specifically designed to be easily consumable by coding agents (Claude, Copilot, etc.):
Design principles:
- Structured output by default: JSON output for programmatic parsing (
--jsonflag or auto-detect non-TTY) - Concise defaults: Show most relevant info first, details on demand
- Predictable format: Consistent output structure across commands
- Error context: Include actionable error messages with trace IDs for debugging
Agent-optimized commands:
# Get recent traces with errors (agent debugging workflow)
otel-dev traces --status error --limit 5 --json
# Get full trace detail for a specific error
otel-dev trace <id> --json
# Get spans matching a pattern (find slow operations)
otel-dev spans --name "db.*" --min-duration 100ms --json
# Watch for new traces in real-time (agent can tail)
otel-dev traces --follow --json
# Quick health check
otel-dev status --json
Example agent workflow:
# Agent runs tests, gets trace ID from output
$ pnpm test
# Test failed, trace: abc123def456...
# Agent queries trace to understand failure
$ otel-dev trace abc123def456 --json
{
"traceId": "abc123def456...",
"status": "error",
"duration": 1234,
"spans": [
{ "name": "test.run", "status": "ok", ... },
{ "name": "db.query", "status": "error", "statusMessage": "connection refused", ... }
]
}
# Agent now knows: DB connection issue caused test failure
Output format considerations:
- JSON output includes all relevant context (no need for follow-up queries)
- Span hierarchy preserved in output (parent-child relationships clear)
- Timestamps in ISO 8601 for easy parsing
- Duration in milliseconds (numeric, not human-formatted)
Data Model (Effect Schema)
Hybrid approach: Accept OTLP on the wire, transform to ergonomic Effect Schema types for storage/queries/UI.
// Core ID types (hex strings, branded)
export const TraceId = Schema.String.pipe(
Schema.pattern(/^[a-f0-9]{32}$/),
Schema.brand("TraceId")
)
export const SpanId = Schema.String.pipe(
Schema.pattern(/^[a-f0-9]{16}$/),
Schema.brand("SpanId")
)
// Span (stored in SQLite, used in UI)
export const Span = Schema.Struct({
traceId: TraceId,
spanId: SpanId,
parentSpanId: Schema.NullOr(SpanId),
name: Schema.String,
kind: SpanKind, // "unset" | "internal" | "server" | "client" | "producer" | "consumer"
startedAt: Schema.DateTimeUtc,
endedAt: Schema.NullOr(Schema.DateTimeUtc),
durationMs: Schema.NullOr(Schema.Number),
statusCode: SpanStatusCode, // "unset" | "ok" | "error"
statusMessage: Schema.NullOr(Schema.String),
serviceName: Schema.String,
serviceVersion: Schema.NullOr(Schema.String),
attributes: Schema.Record({ key: Schema.String, value: Schema.Unknown }),
resourceAttributes: Schema.Record({ key: Schema.String, value: Schema.Unknown }),
events: Schema.Array(SpanEvent),
links: Schema.Array(SpanLink),
})
// Trace summary (for list views)
export const TraceSummary = Schema.Struct({
traceId: TraceId,
rootSpanName: Schema.NullOr(Schema.String),
serviceName: Schema.String,
startedAt: Schema.DateTimeUtc,
endedAt: Schema.NullOr(Schema.DateTimeUtc),
durationMs: Schema.NullOr(Schema.Number),
spanCount: Schema.Number,
errorCount: Schema.Number,
status: SpanStatusCode,
})
SQLite Schema
CREATE TABLE traces (
trace_id TEXT PRIMARY KEY,
root_span_name TEXT,
service_name TEXT,
started_at INTEGER NOT NULL, -- unix ms
ended_at INTEGER,
duration_ms INTEGER,
span_count INTEGER DEFAULT 0,
error_count INTEGER DEFAULT 0,
status TEXT -- 'unset' | 'ok' | 'error'
);
CREATE TABLE spans (
span_id TEXT PRIMARY KEY,
trace_id TEXT NOT NULL REFERENCES traces(trace_id),
parent_span_id TEXT,
name TEXT NOT NULL,
kind TEXT,
started_at INTEGER NOT NULL,
ended_at INTEGER,
duration_ms INTEGER,
status_code TEXT,
status_message TEXT,
service_name TEXT,
service_version TEXT,
attributes JSON,
resource_attributes JSON,
events JSON,
links JSON
);
CREATE INDEX idx_traces_started_at ON traces(started_at DESC);
CREATE INDEX idx_traces_service ON traces(service_name);
CREATE INDEX idx_spans_trace_id ON spans(trace_id);
CREATE INDEX idx_spans_parent ON spans(parent_span_id);
Effect Layer API
import { OtelDev } from "@overeng/otel-dev"
// For your Effect app - configures OTLP exporter to point to local server
const program = myApp.pipe(
Effect.provide(OtelDev.Exporter.layer({
endpoint: "http://localhost:4318", // default
}))
)
CLI
# Server lifecycle
otel-dev start # start server (foreground)
otel-dev start -d # start server (daemon/background)
otel-dev stop # stop server
otel-dev status # show server status + stats
# Trace inspection
otel-dev traces # list recent traces
otel-dev traces --service myapp # filter by service
otel-dev traces --since 1h # filter by time
otel-dev traces --status error # filter by status
otel-dev trace <id> # show single trace detail
otel-dev trace <id> --json # export as JSON
# Span queries (agent-friendly)
otel-dev spans --name "pattern" # find spans by name pattern
otel-dev spans --min-duration 100ms # find slow spans
# Utilities
otel-dev ui # open web UI in browser
otel-dev clean # delete old traces
otel-dev clean --all # delete everything
Configuration
OtelDev.Server.layer({
dataDir: ".otel", // default
otlp: {
httpPort: 4318, // default
// grpcPort: 4317, // future
},
ui: {
port: 4200, // default
},
retention: {
maxAge: "7d", // default
maxTraces: 10000, // default
maxSizeMb: 500, // default
},
})
File Structure
.otel/
├── traces.db # SQLite database (WAL mode)
├── traces.db-wal # WAL file (auto-managed)
├── traces.db-shm # Shared memory (auto-managed)
└── server.json # Server metadata for discovery
# { "pid": 1234, "ports": { "http": 4318, "ui": 4200 } }
Scope
Phase 1: Core (MVP)
- OTLP HTTP/JSON receiver
- SQLite storage with WAL
- REST query API
- CLI (
start,stop,status,traces,trace <id>) - Minimal web UI (trace list, trace detail with timeline)
- JSON output mode for agent consumption
Phase 2: Polish
- Real-time trace streaming (SSE)
- Trace filtering/search in UI
- Auto-cleanup (TTL, max size)
-
@overeng/react-inspectorintegration for span attributes - Span query commands (
otel-dev spans)
Phase 3: Extended
- OTLP gRPC support
- Metrics support
- Logs support
Related Projects / Inspiration
- otel-desktop-viewer - Similar concept, Go-based, DuckDB storage, React UI. Active project with 727 stars. Our version is Effect-native with SQLite.
- otel-tui - Terminal-based OTLP viewer. Inspiration for CLI trace viewing.
- Jaeger - Full-featured distributed tracing. Our target is much simpler for local dev.
- Tempo - Grafana's trace backend. Production-grade, heavyweight for local dev.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
No implementation files or tests are named. Start by reviewing the Phase 1 entry points: the OTLP HTTP/JSON receiver, SQLite storage in .otel/traces.db, REST query API, and CLI commands such as start, status, traces, and trace. Done means the defined MVP scope is implemented, including JSON output and the minimal trace-list and trace-detail UI.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- react, sqlite, storybook, typescript
- Domain
- api, backend, cli, databases, frontend
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Needs clarification
- Newbie friendliness
- 18/100