overengineeringstudio / overengineeringstudio/effect-utils

Add @overeng/otel-dev: Local OpenTelemetry backend for development

Open
#21 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

type:feature
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 (--json flag 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-inspector integration 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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.