feat(observability): add OpenTelemetry tracing and Prometheus metrics - #33
Open
kiro-agent[bot] wants to merge 1 commit into
Open
Conversation
Tracing: - internal/observability/tracing.go: OTLP HTTP exporter, resource attrs (service.name/namespace/deployment.environment), ParentBased ratio sampler, W3C propagation. No-op when disabled so dev/tests need no collector; never fails the app on telemetry errors - instrument the HTTP server via otelhttp, naming spans after the matched mux route instead of the raw path to avoid span-name explosion, and excluding /sse - instrument pgx with otelpgx and all outbound clients with an otelhttp transport via utils.NewHTTPClient - RequestID now prefers the active span's trace/span ids so log lines correlate with the traces Tempo actually stores Metrics: - internal/observability: dedicated registry with Go runtime + process collectors and app metrics (sse_active_clients, cache_operations_total, upstream_request_duration_seconds, http_requests_total/duration) - serve /metrics and /healthz on a separate internal-only port, never on the public router - record per-upstream latency and outcome in the SSE fetch path, where failures were previously silent Wiring: - make init order explicit in main: loaders, then tracing, then the pgx pool and handlers, because otelpgx/otelhttp capture the global tracer provider at construction time - flush spans last during graceful shutdown - compose: join the shared observability network, set GOMEMLIMIT with a memory limit, keep the metrics port unpublished
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This pull request was created by @kiro-agent on behalf of @ccrsxx 👻
Comment with /kiro fix to address specific feedback or /kiro all to address everything.
Learn about Kiro Web
Roadmap Steps 5 & 6. This is the part that gives you the Cloud Trace-style waterfall and the CPU/RAM/goroutine visibility.
Tracing
internal/observability/tracing.go— OTLP HTTP exporter, resource attributes (service.name/service.namespace/deployment.environment.name),ParentBased(TraceIDRatioBased)sampler, W3C trace context + baggage propagation.TRACING_ENABLED=falseor empty endpoint) so local dev and tests need zero infrastructure.otel.ErrorHandler.otelhttp. Spans are named after the matched mux route, not the raw path — the default formatter would create one span name per blog slug and blow up Tempo's span-name index./sseis excluded (multi-hour spans).http.ServeMuxonly populatesr.Patternwhile dispatching, aTraceRoutemiddleware sits innermost and renames the span + setshttp.routeonce the route is known.otelpgxon the pool → a span per SQL query.utils.NewHTTPClientadds anotelhttptransport; applied to all 7 HTTP clients (cloudflare, github, jellyfin, navidrome, pixiv, pushover, spotify). A slow Spotify now shows up as a labelled span instead of unexplained latency.RequestIDnow prefers the active span's trace/span IDs, sotrace_idin the logs is the same one Tempo stores (this is what makes the log → trace jump work). Falls back totraceparentparsing when tracing is off.Metrics
go_goroutinesis the key leak signal here, since every SSE client parks a goroutine and cache writes are fire-and-forget.http_requests_totalandhttp_request_duration_seconds, labelled by matched route only.sse_active_clients,cache_operations_total{result}, andupstream_request_duration_seconds{platform,result}.getSSEDatasilently falls back to a default payload on error, so an outage was indistinguishable from "nothing is playing". The three duplicated fetch closures are now one instrumented helper./metricsand/healthzon a separate internal-only port (METRICS_PORT, default 9090), deliberately not on the public router and not published in compose — the collector scrapes it over theobservabilitynetwork./healthzpings the pool.Wiring / ops
main.go: loaders → tracing → pool → handlers. This matters:otelpgxandotelhttpcapture the global tracer provider at construction time, so initializing tracing later would silently lose every span.observabilitynetwork, setsGOMEMLIMIT=450MiBalongside a 512M memory limit (a container limit withoutGOMEMLIMITmeans Go gets OOM-killed instead of GCing harder).Testing
go build✅ ·go vet✅ ·gofmt✅ ·go test ./...✅ 31/31 packages pass, 0 failures (no existing test changed)/healthz→200 "ok";/metrics→200, 12.5 KB, all expected series present incl. Go runtime./contents/a|b|cproduced exactly one series,route="GET /contents/{slug}"— no raw slugs leaked into labels.X-Request-Idset, the export failure logged as WARN, and shutdown was clean.trace_idwas a real 32-char OTel trace ID from the active span, not a generated placeholder.golangci-lintstill can't run in the sandbox (binary built with Go 1.24 < this repo's 1.26 target). Please confirm CI lint.To try it out
Then uncomment the
prometheus.scrape "api"block indocker/observability-agent/alloy/config.alloy(homelab#5) to start scrapingapi:9090.With tracing left off, this PR is inert apart from the new
/metricsport — safe to merge and enable later.