How OpticsOps works
One npm package + two env vars. No manual spans. No framework plugins. No collector to deploy. Every HTTP hop is traced automatically.
Quick start
npm install @opticsops/agent
OTEL_SERVICE_NAME=orders-api OPTICSOPS_API_KEY=oo_your_key_here OTEL_EXPORTER_OTLP_ENDPOINT=https://opticsops-ingest-551302578563.us-central1.run.app
# ESM apps node --import @opticsops/agent/register app.js # CommonJS apps node --require @opticsops/agent/register app.js # Or via NODE_OPTIONS (Docker / Kubernetes) NODE_OPTIONS="--import @opticsops/agent/register" node app.js
https://opticsops-ingest.../dashboard#apiKey=oo_your_key
What gets traced
| Traffic type | What happens | Storage cost |
|---|---|---|
| Error (status ≥ 500) | Full linked trace exported via OTLP | 1 row per span |
| Slow (duration > 500 ms) | Full linked trace exported via OTLP | 1 row per span |
| Healthy (fast + 2xx) | Counted in heartbeat aggregation | 1 row per 10 s window |
This is tail-based sampling. The sampling decision is made after the request completes — never before — so you always capture the full chain for any request that actually failed.
What the agent collects
| Collected | Not collected |
|---|---|
| ✓ HTTP method (GET, POST…) | ✗ Request body |
| ✓ URL path + host | ✗ Response body |
| ✓ HTTP status code | ✗ Authorization / Cookie headers |
| ✓ Duration (ms) | ✗ Query string values (only key names) |
| ✓ W3C traceparent for linking | ✗ Database queries or file contents |
| ✓ Service name | ✗ User PII (unless in the URL path) |
The pipeline
Agent (in your process)
Monkey-patches Node.js http/https. Reads W3C traceparent headers on inbound requests, injects them on outbound calls. Zero framework dependency.
Tail sampler
Buffers spans per trace. When the root span closes: if any span has status ≥ 500 or duration > 500 ms, the full trace is exported. Otherwise it is dropped.
Heartbeat aggregator
Healthy, fast requests are never exported as spans. The agent counts source → destination call volume and flushes aggregate counts every 10 seconds.
Ingest API (Cloud Run)
Receives OTLP spans (errors/slow) and heartbeat edge counts. Authenticates via X-Api-Key header. Writes to ClickHouse.
ClickHouse (GCE VM)
Stores spans, heartbeat_edges, and a materialised view (service_edges_mv) that pre-aggregates edge call counts per minute.
Dashboard (SSE + Cytoscape.js)
Queries the last 60 minutes of edges on load. Then opens a Server-Sent Events stream that refreshes every 5 seconds. Edges are coloured green / amber / red by error rate.
Honest limitations
- Node.js only.The agent patches Node's built-in
http/httpsmodules. Python, Go, Java — not supported yet. - Tail sampling misses silent errors. If a service crashes before the root span closes (process kill, OOM), that trace is never exported.
- No DB query tracing yet. The service map shows HTTP edges only. PostgreSQL, Redis, MongoDB calls are not traced (planned).
- No log correlation. Logs and traces are separate — you cannot click a trace and see the associated log lines (planned).
- Single ClickHouse VM. One GCE VM is a single point of failure. No replication, no automatic backup.
- Early beta. APIs and the dashboard may change between releases without a migration path.
Want the full architecture — the datastore, the isolation model, how keys and sessions are handled? Read the architecture docs →