Files
telemetry/CLAUDE.md
Rene Nochebuena 00d15bee8d feat(telemetry): add NewConsole — logz-backed OTel exporters for local dev (v1.1.0)
NewConsole bootstraps the OTel SDK with three logz-backed exporters:
- Trace: WithSyncer, one log line per closed span (immediate, no batch)
- Metric: PeriodicReader (60s), flushed on shutdown
- OTel log: BatchProcessor, for third-party libs using OTel log API

ConsoleConfig requires only ServiceName — no OTLP endpoint needed.
Adds logz v1.0.1 as direct dependency; module tier bumped 1 → 2.
2026-05-12 19:19:50 +00:00

5.1 KiB

telemetry

Bootstraps the full OpenTelemetry SDK (traces, metrics, logs) with OTLP gRPC exporters targeting Grafana Alloy.

Purpose

Sets the three OTel global providers so that all micro-libs using the OTel global API auto-instrument without any code changes. Returns a shutdown function that flushes all exporters on process exit. This module is the single place in an application where the OTel SDK is wired up.

Tier & Dependencies

Tier 2 — depends on Tier 1 logz (required by NewConsole). Must never be imported by framework libraries.

Depends on:

  • code.nochebuena.dev/go/logz (Tier 1) — used by NewConsole exporters
  • go.opentelemetry.io/otel and sub-packages — API and SDK
  • go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc
  • go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc
  • go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc
  • go.opentelemetry.io/otel/sdk/trace, .../metric, .../log

No launcher dependency — telemetry has no Component lifecycle.

Key Design Decisions

  • Tier 2 / app-only (ADR-001): Libraries use only the OTel API (no-op default). This module activates the real SDK. Importing it from a library is a mistake.
  • Three-signal OTLP bootstrap (ADR-002): New(ctx, cfg) sets up traces → Tempo, metrics → Mimir, logs → Loki, all over a single OTLP gRPC endpoint. W3C TraceContext + Baggage propagation is set globally.
  • Global provider strategy (ADR-003): Libraries call otel.Tracer(...) / otel.Meter(...) / global.Logger(...). After telemetry.New, those calls route to the real SDK with no library changes required.
  • No launcher.Component: Telemetry is not a lifecycle component. The caller defers the returned shutdown function directly in main. This keeps the module dependency graph minimal and the interface simple.
  • Sequential error rollback: If any exporter fails to initialize, all previously created providers are shut down before the error is returned. The process never runs with a partial telemetry state.

Patterns

Standard application usage:

func main() {
    ctx := context.Background()
    shutdown, err := telemetry.New(ctx, telemetry.Config{
        ServiceName:    "order-service",
        ServiceVersion: "1.4.2",
        Environment:    "production",
        OTLPEndpoint:   "alloy:4317",
        OTLPInsecure:   false,
    })
    if err != nil {
        log.Fatalf("telemetry: %v", err)
    }
    defer shutdown(ctx)

    // Rest of application wiring...
}

Local development — console mode (no collector required):

func main() {
    ctx := context.Background()
    logger := logz.New(logz.Options{})
    shutdown, err := telemetry.NewConsole(ctx, logger, telemetry.ConsoleConfig{
        ServiceName: "order-service",
    })
    if err != nil {
        log.Fatalf("telemetry: %v", err)
    }
    defer shutdown(ctx)
    // Traces, metrics, and OTel log records appear as logz log lines.
}

With launcher (wire shutdown into lifecycle):

shutdown, err := telemetry.New(ctx, cfg)
if err != nil {
    return err
}
lc.BeforeStop(func() error { return shutdown(ctx) })

Config env vars:

Variable Required Default Description
OTEL_SERVICE_NAME yes Service name in all signals
OTEL_SERVICE_VERSION no unknown Deployed version
OTEL_ENVIRONMENT no development Deployment environment
OTEL_EXPORTER_OTLP_ENDPOINT yes OTLP gRPC collector address (e.g. alloy:4317)
OTEL_EXPORTER_OTLP_INSECURE no false Disable TLS (set true for local dev)

What to Avoid

  • Do not import this module from any non-main package. Libraries must use only OTel API packages.
  • Do not call telemetry.New or telemetry.NewConsole more than once per process. Each call overwrites the global providers.
  • Do not omit the defer shutdown(ctx). Without it, buffered spans and metrics are lost on exit.
  • Do not use a zero-value Config. Both ServiceName and OTLPEndpoint are required; New will return an error if the OTLP connection cannot be established.
  • Do not wrap this in a launcher.Component. The shutdown function pattern is simpler and avoids adding a launcher dependency to this module.
  • Do not wire the OTel slog bridge alongside NewConsole. The bridge routes logz/slog output through the OTel log API, which logLogExporter forwards back to logz — creating a feedback loop.

Testing Notes

  • telemetry_test.go — uses a fakeCollector TCP listener to avoid connection-refused; tests New and the global provider assignments. Shutdown timeout 200ms is intentional since the fake server cannot complete a gRPC flush.
  • console_test.go — uses a stubLogger that captures log calls. TestNewConsole_ExportsSpan uses WithSyncer behavior: span export is synchronous, so the log is recorded immediately on span.End(). TestNewConsole_ExportsMetric triggers export via shutdown flush (PeriodicReader flushes on Shutdown).
  • newResource is tested separately as a pure function with no I/O.
  • Do not test against a real Alloy or Tempo instance in unit tests.