Distributed Tracing with OpenTelemetry Distributed Tracing with OpenTelemetry Distributed tracing is active across the full stack: Bun monolith → Go backend → PostgreSQL. Every request creates a single trace in Jaeger or Tempo spanning all three layers.

1. What is Distributed Tracing?

When a user makes a request, it touches multiple components: HTTP handlers, business logic, database queries, and potentially multiple services. Distributed Tracing tracks the entire journey as a single timeline. Without tracing, diagnosing “Why is this API slow?” requires manually correlating logs. With tracing, you get a visual breakdown of exactly where time was spent.

2. Architecture

End to end request flow with OpenTelemetry End to end request flow with OpenTelemetry

3. E2E Trace Example

A request to list org permissions produces a single trace:
The 48ms team_memberships query is immediately visible in Jaeger without any manual instrumentation.

4. Feature Flag & Runtime Toggle

Tracing has two control layers:
The admin endpoint is protected by AuthMiddleware and only registered when TELEMETRY_ENABLED=true at startup.

Admin Endpoint


5. Sampling Strategy

ParentBased ensures DB spans (created by otelgorm) inherit the HTTP span’s sampling decision. No per-query configuration needed.

6. Go Backend

Key files

Dependencies

Environment variables


7. Bun Backend

Key files

Dependencies

traceparent propagation

Due to Bun’s runtime differences with Node.js, we avoid NodeSDK and Node-specific instrumentations like UndiciInstrumentation. Instead, HTTPClient manually injects the W3C traceparent context into outgoing request headers using propagation.inject(). This ensures traces continue unbroken into the Go backend.

Environment variables


8. Local Development

The infrastructure services (MongoDB, Redis, NATS, and Telemetry) have been consolidated into the root infra/ directory.

Tracing Backends

You can choose your preferred tracing backend via TELEMETRY_BACKEND in your .env. Option A: Jaeger (Default)
  • Exposes OTLP HTTP on :4318
  • Start it: docker compose -f infra/docker-compose.yml up -d jaeger
  • UI: http://localhost:16686
Option B: Grafana Tempo + OTel Collector
  • The otel-collector receives traces on :4319 and forwards them to Tempo.
  • Tempo stores the traces, and Grafana visualizes them.
  • Start them: docker compose -f infra/docker-compose.yml up -d otel-collector tempo grafana
  • UI: http://localhost:3001 (Navigate to Explore → Data source: Tempo)

Verify tracing

  1. Start your preferred backend (see above)
  2. Start Go/Bun backends with TELEMETRY_ENABLED=true
  3. Make a request: curl http://localhost:8080/api/organizations
  4. Open your backend’s UI and search for traces from go-core or bun-monolith.

Disable tracing at runtime


9. Core Concepts

Span

A single unit of work: span_id, parent_span_id, name, start/end timestamps, attributes, status. Spans form a tree matching the call hierarchy.

Context Propagation

The W3C traceparent header carries the trace_id and span_id across service boundaries:

What to look for in Jaeger or Tempo


10. Production Considerations

  • Sampling: Default is 100% locally. Set TELEMETRY_COLLECTOR_URL to your collector and reduce rate via POST /admin/telemetry {"rate": 0.1} or restart with a lower default.
  • TLS: The OTLP exporter uses WithInsecure() for local dev. In production, configure TLS via the collector.
  • Span attributes: Avoid logging PII (emails, tokens) as span attributes.
  • Performance: Typical overhead is 1–3% CPU with batching. Setting TELEMETRY_ENABLED=false gives zero overhead.