# Go OTel Debugger A debug receiver for OpenTelemetry telemetry data (traces, metrics, and logs) that provides real-time visualization and historical data access. It accepts OTLP data via HTTP and gRPC protocols, stores a configurable history of received telemetry, and broadcasts updates to connected WebSocket clients for live debugging. ## Features - **OTLP Support**: Receives traces, metrics, and logs via both HTTP (JSON/Protobuf) and gRPC (Protobuf) protocols on standard OTLP ports. - **Real-time Broadcasting**: Uses WebSocket connections to broadcast incoming telemetry data to all connected clients instantly. - **Historical Data**: Maintains a configurable ring buffer of recent telemetry data, accessible via REST API endpoints. - **Web Interface**: Includes a built-in HTML interface for visualizing telemetry data in real-time with counters and clear buttons. - **Docker Support**: Containerized for easy deployment. - **Kubernetes Ready**: Includes deployment manifests for Kubernetes clusters. ## Architecture The application consists of several key components: 1. **HTTP/gRPC Servers**: Handle incoming OTLP requests on ports 8080 (HTTP) and 4317 (gRPC). 2. **Collector**: Processes and stores telemetry data in a ring buffer for history. Implements OpenTelemetry consumer interfaces. 3. **WebSocket Server**: Manages client connections and broadcasts new telemetry data as JSON messages. 4. **Web Interface**: Static HTML page served on the root path that connects to the WebSocket for real-time updates. 5. **History API**: REST endpoints to retrieve stored telemetry data by type or all combined. Data flow: ``` OTLP Client → HTTP/gRPC Server → Collector → WebSocket Broadcast → Connected Clients ↓ History Storage → REST API → Clients ``` ## Getting Started ### Prerequisites - Go 1.25+ (for local development) - Docker (optional, for containerized deployment) ### Running Locally 1. Clone the repository: ```sh git clone https://git.k6n.net/go-otel.git cd go-otel ``` 2. Run the server: ```sh go run ./cmd/server ``` The server will start on ports 8080 (HTTP/WebSocket) and 4317 (gRPC). ### Running with Docker 1. Build the Docker image: ```sh docker build -t go-otel . ``` 2. Run the container: ```sh docker run -p 8080:8080 -p 4317:4317 go-otel ``` ### Kubernetes Deployment Apply the included Kubernetes manifests: ```sh kubectl apply -f k8s/ ``` This deploys the application with a Service and Ingress for external access. ## API Endpoints ### OTLP HTTP Endpoints (Port 8080) - `POST /v1/traces` - Receive OTLP traces - `POST /v1/metrics` - Receive OTLP metrics - `POST /v1/logs` - Receive OTLP logs Supported Content-Types: `application/json`, `application/x-protobuf` ### OTLP gRPC Endpoints (Port 4317) gRPC services for traces, metrics, and logs using standard OTLP protobuf definitions. ### WebSocket Endpoint (Port 8080) - `GET /ws` - WebSocket connection for real-time telemetry streaming ### History API Endpoints (Port 8080) - `GET /api/history` - Retrieve all historical telemetry data - `GET /api/history/traces` - Retrieve only trace history - `GET /api/history/metrics` - Retrieve only metrics history - `GET /api/history/logs` - Retrieve only logs history - `GET /api/history/search` - Search history. Supports `q` (free-text substring), `type`, `since`/`until` (unix nanoseconds), and `limit`. - `GET /api/history/wait` - Block until a matching entry arrives. Same filters as `search`, plus `timeout` (seconds, default 30). Rate-limited per client IP. - `GET /api/traces/{traceId}` - Retrieve all stored entries (spans and correlated logs) referencing a given trace id. With ClickHouse this is an indexed lookup (`entries.trace_ids`) and each payload is trimmed to that trace's own spans/log records. - `GET /api/traces/summaries?ids=a,b,c` - Per-trace rollups (name, start/end, duration, span count, error count, services; max 100 ids) without reading span payloads. Maintained on insert by the `trace_summaries` view; empty with the file store. All endpoints above search the full persistent store, not just the in-memory ring buffer. ### Stats API Endpoints (Port 8080, requires ClickHouse) Aggregate time-series over the stored telemetry, computed server-side from an hourly rollup table. Available only when `OTEL_CLICKHOUSE_ADDR` is configured (see Configuration below); otherwise they respond `501` with an explanatory JSON body and the UI hides its "Trends" tab. - `GET /api/stats/errors?hours=72&service=` - Total/error entry counts per hour (per day when `hours > 336`). Response: `{"granularity":"hour","points":[{"bucket":"2026-09-17 14:00:00","total":42,"errors":3}]}`. - `GET /api/stats/releases?hours=72&service=` - Per-release error-count series: `{"series":{"v1.3":[{bucket,total,errors}...],"v1.4":[...]}}` for release-over-release comparison. Entries need a `service.release` attribute to appear here. - `GET /api/stats/services` - Distinct services seen in the rollup. - `GET /api/stats/endpoints?hours=72&host=&service=&limit=100` - HTTP span traffic grouped by normalized service, host, method, path, route, status, user-agent, and safe browser request metadata. `spanCount` exposes nested proxy/instrumentation duplication while `traceCount` approximates logical requests. Origin and referrer are returned as host-only values; query strings are stripped from paths. All accept `hours` (1..2160, default 72) and an optional `service` filter. ### Issues API Endpoints (Port 8080) Errors seen in traces (spans with an error status) and logs (severity `ERROR`/`FATAL`) are deduplicated into "issues" as they're ingested. - `GET /api/issues` - List deduplicated issues, most recently seen first. Supports `limit`. - `GET /api/issues/{id}` - Fetch a single issue, including up to 5 recent sample occurrences (timestamp, trace/span id, message). - `GET /api/issues/{id}/sources` - Return every distinct source IP recorded for the issue, with occurrence count and first/last seen timestamps. Requires the ClickHouse occurrence store; returns `501` with file/in-memory storage. - `GET /api/issues/{id}/traces` - Every trace the issue occurred in, most recently hit first, with each trace's summary and its occurrences, in one request (`limit`, `before` = `nextBefore` from the previous page). ClickHouse keeps *all* occurrences (`issue_occurrences`, 90-day TTL); without it the response is built from the 5 in-memory samples and `complete` is `false`. - `GET /api/patterns` - List deterministic groups of related exact issues. Pattern grouping is explainable and does not merge issue IDs. - `GET /api/issues/{id}/similar` - Return bounded, deterministic related-issue suggestions with a score and matching reasons. - `POST /api/issues/{id}/similar` - Record a review label with `{"issueId":"...","decision":"same|different"}` for future training data. - `GET /api/rollouts` - List read-only Kubernetes Deployment rollout state. - `GET /api/rollouts/{id}` - Fetch one normalized rollout. - `GET /api/rollouts/{id}/impact` - Return optional Prometheus error-rate points around the rollout. - `POST /api/sourcemaps` - Upload a JavaScript source map. Requires the shared `OTEL_SOURCEMAP_KEY` in an `X-Sourcemap-Key` header (uploads are refused while it is unset). Multipart upload; multipart fields are `release`, `source` (the generated bundle URL/path), and `file`. Uploaded maps are used server-side to resolve frontend stacks. ### Web Interface - `GET /` - Serves the HTML visualization interface ### Static Routes - `GET /static/otel-rum.js` - The frontend error collector script (see below). - `GET /agents_example.md` - Serves this repo's agent instructions doc directly, so an AI agent can fetch it from the base URL before making any API calls. - `GET /instrumentation.md` - Serves the frontend/backend instrumentation guide, including React error boundaries and source-map deployment. - `GET /static/instrumentation.md` - Static/public alias for the same guide. ## Usage ### Sending Telemetry Data Send OTLP data to the appropriate endpoints. The data will be stored in history and broadcast to all connected WebSocket clients. #### HTTP Example (JSON) ```sh curl -X POST http://localhost:8080/v1/traces \ -H "Content-Type: application/json" \ -d '{"resourceSpans":[{"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"test-service"}}]},"scopeSpans":[{"scope":{"name":"test-scope"},"spans":[{"traceId":"0102030405060708090a0b0c0d0e0f10","spanId":"0102030405060708","name":"test-span","startTimeUnixNano":"1640995200000000000","endTimeUnixNano":"1640995260000000000","attributes":[{"key":"test.key","value":{"stringValue":"test-value"}}]}]}]}]}' ``` #### gRPC Example Use any OTLP gRPC client library to send data to `localhost:4317`. ### Real-time Visualization 1. Open `http://localhost:8080` in your browser 2. The interface will automatically connect via WebSocket 3. Send telemetry data to see it appear in real-time 4. Use the "Clear" buttons to reset individual data sections 5. View message counters for each telemetry type ### Accessing Historical Data Use the history API endpoints to retrieve previously received data: ```sh curl http://localhost:8080/api/history/traces ``` This returns JSON arrays of historical entries with timestamps. ### Frontend Error Collector (`static/otel-rum.js`) A framework-free browser script that captures uncaught errors, unhandled promise rejections and failed resource loads (img/script/link), and ships them as OTLP log records to the existing `/v1/logs` endpoint - so they flow through the same history/search/issue pipeline as backend telemetry, tagged `source: frontend`. ```html ``` React render failures should be reported from an error boundary as well. The project UI uses this pattern in `otel-ui/src/components/ViewErrorBoundary.tsx`: ```tsx class AppBoundary extends React.Component<{ children: React.ReactNode }> { componentDidCatch(error: Error, info: React.ErrorInfo) { OtelRUM.last?.captureException(error, { "exception.type": "ReactErrorBoundary", "react.component_stack": info.componentStack, }); } render() { return this.props.children; } } ``` The boundary should also render a fallback UI; otherwise a broken component can leave the whole application blank while still being logged. `fetch` is auto-instrumented by default (same-origin requests only) to inject `traceparent` and record request/response breadcrumbs; this can be disabled via `injectTraceHeaders: false` / `autoInstrumentFetch: false`. The log ingest endpoint supports unauthenticated cross-origin POSTs; no OIDC session or `Authorization` header is needed. Source maps are kept server-side. When a captured frontend error has a matching `service.release` and generated bundle path, the Issues view displays the resolved original source frame while retaining the generated frame. #### Source-map deploy workflow Build the UI with a stable release identifier and keep the generated map next to the bundle. Do not publish the map publicly: ```sh export RELEASE="2026.09.18-$(git rev-parse --short HEAD)" npm --prefix otel-ui run build # Upload after the new UI is deployed. Use the generated URL/path exactly as # it appears in browser stack traces. curl -fS -X POST https://otel.example.com/api/sourcemaps \ -H "X-Sourcemap-Key: $OTEL_SOURCEMAP_KEY" \ -F "release=$RELEASE" \ -F "source=/assets/index-ABC123.js" \ -F "file=@otel-ui/dist/assets/index-ABC123.js.map" ``` For this project, `otel-ui/src/main.tsx` sends `VITE_APP_RELEASE` as the RUM `service.release` value; set it during the build, for example: ```sh VITE_APP_RELEASE="$RELEASE" npm --prefix otel-ui run build ``` The repository also includes `scripts/upload-sourcemap.sh`; after deploying, run it against the emitted bundle, for example: ```sh RELEASE="$RELEASE" OTEL_URL="https://otel.example.com" \ ./scripts/upload-sourcemap.sh otel-ui/dist/assets/index-ABC123.js ``` The receiver stores uploaded maps under `OTEL_SOURCEMAP_DIR` (default `./sourcemaps`, `/sourcemaps` in the Kubernetes deployment). Maps are reloaded on start, so keep the directory on persistent storage. See `instrumentation.md` (*Source maps → Requirements checklist*) for everything a project needs for stacks to resolve. `GET /api/sourcemaps` lists loaded maps. The API never returns map files themselves. `build.sh` passes the release into the Docker UI build. If `OTEL_URL` is set, it also builds the matching UI locally and uploads all generated JavaScript maps. The Dockerfile removes `.map` files from the final image by default; set `PUBLIC_SOURCEMAPS=true` for a development or staging image where Chrome DevTools should fetch the original sources: ```sh PUBLIC_SOURCEMAPS=true ./build.sh ``` Public maps can expose application source code, so keep this disabled for production unless that exposure is intentional. Credentials are never passed to Docker or stored in the image. ## Configuration - `OTELLOG_PATH` - path to the persistent telemetry log file (default `otel-agent.log`). Only used when ClickHouse is not configured. - `OTEL_SOURCEMAP_KEY` - shared key required to upload source maps. - `OTEL_ISSUE_STATUS_PATH` - file that keeps resolved/ignored issue decisions across restarts when ClickHouse is not used (default: next to `OTELLOG_PATH`). - `OTEL_TODOS_PATH` - file for todos filed via `otel-todo.js` when ClickHouse is not used (default: next to `OTELLOG_PATH`). - `OTEL_SOURCEMAP_CACHE_MB` - size budget (on-disk MB, default 64) for maps kept parsed in memory; maps are indexed at start and parsed lazily on first use. - `OTEL_SOURCEMAP_DIR` - directory for uploaded release source maps (default `./sourcemaps`; mount persistent storage there in production). - `OTEL_IP_INVESTIGATION_NMAP` - set to `true` to enable the authenticated, rate-limited active service probe. It is off by default because Nmap sends active network traffic to the investigated public IP. - `ABUSE_KEY` - optional AbuseIPDB API key. When set, the authenticated IP detail view can check a public address and submit a report without exposing the key to the browser. Keep it in a secret, never in the frontend build. - `OTEL_PPROF_ADDR` - address for the Go pprof server (default `127.0.0.1:6060`). It serves `/debug/pprof/` separately from the public UI port; use a port-forward or set this explicitly while diagnosing memory or CPU usage. - `OTEL_KUBERNETES_ENABLED` - set to `false` to disable rollout observation; otherwise it activates in-cluster or `KUBECONFIG` observation when available. - `OTEL_KUBERNETES_NAMESPACE` - optional namespace restriction; empty watches all namespaces permitted by the service account. - `OTEL_KUBERNETES_LABEL_SELECTOR` - optional Deployment label selector. - `OTEL_PROMETHEUS_URL` - optional Prometheus base URL for rollout impact. - `OTEL_PROMETHEUS_ERROR_QUERY` - optional `fmt.Sprintf`-style PromQL template; `%q` placeholders receive service and release in that order. - `OTEL_CLICKHOUSE_ADDR` - optional `host:port` (native protocol, usually `9000`). When set, telemetry is stored in ClickHouse instead of the JSONL file: history search stays fast and unbounded, and the aggregate stats endpoints (`/api/stats/*`) and the UI "Trends" tab are enabled. The schema (`otel` database, raw `entries` table + materialized hourly rollup) is created automatically on first connect with a 90-day TTL; if ClickHouse is unreachable at startup the server falls back to the file store. - `OTEL_CLICKHOUSE_DB` / `OTEL_CLICKHOUSE_USER` / `OTEL_CLICKHOUSE_PASSWORD` - optional ClickHouse connection settings (defaults: `otel` / `default` / empty). ### Issues display The standalone `issues-tui` command is a terminal dashboard for currently firing alert spikes. It reads normalized error events directly from ClickHouse; it does not replace the web UI's persisted `/api/issues` view. When a spike stops firing, its card stays on the display marked resolved until something fires again. Build and run it on the display computer (or use `go run`): ```sh go build -o issues-tui ./cmd/issues-tui OTEL_CLICKHOUSE_ADDR=clickhouse.example.net:9000 \ OTEL_CLICKHOUSE_DB=otel \ OTEL_CLICKHOUSE_USER=default \ OTEL_CLICKHOUSE_PASSWORD='…' \ OTEL_ALERT_BASE_URL=https://otel.example.net \ ./issues-tui ``` The display computer must be able to reach ClickHouse's native TCP port. For a Kubernetes-only ClickHouse service, use a private network path or tunnel; the in-cluster service name is not reachable from an ordinary workstation. The dashboard refreshes using the `OTEL_ALERT_*` interval, window, baseline, cooldown and lag settings. Press `r` to scan immediately and `q` to quit. Use `-once` for a one-shot, non-interactive output suitable for a process supervisor health check. - `OTEL_INGEST_KEY` - optional shared key required (via `?key=` or the `X-Otel-Ingest-Key` header) on `/v1/traces`, `/v1/metrics` and `/v1/logs` over HTTP. Unset by default (no key required). This is meant to scope/rate-limit a public ingest endpoint fed by a browser script, not to provide real authentication - pair it with the built-in per-IP rate limiting on those endpoints if the endpoint is public. - `OTEL_AGENT_TOKEN` - optional service token for non-interactive debugging agents. Send it as `X-Otel-Agent-Token` or `Authorization: Bearer ...` when calling protected `/api/history`, `/api/issues`, `/api/traces`, and stats endpoints. Keep it in a Kubernetes Secret and never expose it to browsers. A single-node ClickHouse instance for Kubernetes is provided in `k8s/clickhouse.yaml` (StatefulSet + Service + PVC, deployed in the `monitoring` namespace); the receiver Deployment in `k8s/deployment.yaml` already points at it. The UI's Trends tab (and live issue updates) appear automatically once the stats endpoints answer; on file-store deployments the tab hides itself. SPA routes (/issues, /traces, /logs, /metrics, /trends, /setup) are served client-side by wouter and survive the OIDC login redirect. Selection lives in the URL too, so every step can be reloaded, stepped with back/forward, and shared (each detail view has a link chip that copies its permalink): | Path | Shows | | --- | --- | | `/issues/` | one issue with its traces | | `/issues//trace/` | the same, with a trace selected | | `/patterns/[/]` | a pattern card open, optionally reviewing one member | | `/traces/[/]` | a trace, optionally with a span selected | | `/rum/session/` | a RUM session | | `/rum/errors[/]` | JS errors, optionally one error expanded (`errorId` is a hash of type + message) | Currently, the history buffer size is hardcoded to 100 entries. This can be made configurable in future versions. ## Development To modify the application: 1. Edit source files in `cmd/server/` and `internal/` 2. Run locally with `go run ./cmd/server` 3. For UI changes, edit `index.html` directly The application uses standard Go modules and OpenTelemetry collector libraries for data processing.