Skip to content

feat(metrics): add Prometheus /metrics endpoint with DNS, dialer, connection, and runtime stats - #1015

Open
MaurUppi wants to merge 7 commits into
daeuniverse:mainfrom
MaurUppi:feat/metrics-endpoint-clean
Open

feat(metrics): add Prometheus /metrics endpoint with DNS, dialer, connection, and runtime stats#1015
MaurUppi wants to merge 7 commits into
daeuniverse:mainfrom
MaurUppi:feat/metrics-endpoint-clean

Conversation

@MaurUppi

Copy link
Copy Markdown

Summary

  • Adds a full Prometheus /metrics HTTP endpoint to dae, gated behind endpoint_listen_address in global {} (disabled by default)
  • Exposes DNS, dialer health, connection pool, and runtime/node metrics via a custom registry with per-collector isolation
  • Wires the three previously-stub upstream DNS metrics (inFlight, latency, errTotal) in dialSend
  • Includes BasicAuth + TLS support, hot-reload safety via atomic.Pointer[ControlPlane], and pprof co-hosting
  • Ships a Grafana dashboard (.plan/metrics/dae_Transparent_Proxy-Grafana_dashboard.json) covering all exposed metric families

What's in this PR

Infrastructure (pkg/metrics/, pkg/metricshttp/)

  • state.gometrics.State holding atomic.Pointer[control.ControlPlane] for reload-safe scraping
  • registry.goNewRegistry: registers 4 collectors + process_* + go_*
  • metricshttp/auth.go — BasicAuth middleware
  • metricshttp/server.goNewEndpointServer / StartEndpointServer with optional TLS

Collectors

  • collector_dialer.go — dialer/health-check metrics (alive, latency, moving avg, health check totals)
  • collector_dns.go — DNS metrics (cache, concurrency, forwarder, upstream counters + histograms)
  • collector_conn.go — connection metrics (TCP active/total, UDP endpoints/queues/total)
  • collector_runtime.go — runtime/node metrics (SnapshotRuntimeStats, SnapshotNodeLatencies), labeled with dialer group/name

DNS write-side wiring (control/dns_control.go)

Three upstream metrics were structurally present but had no write side. Now instrumented in dialSend around the forwardWithFallback call, with inFlight released immediately after network I/O (not deferred) so the gauge stays scoped to actual upstream wait time.

Config (config/config.go, example.dae)

New endpoint_* fields in global {}:

endpoint_listen_address   (default: "" = disabled)
endpoint_username / endpoint_password
endpoint_tls_certificate / endpoint_tls_key
endpoint_prometheus_enabled / endpoint_prometheus_path

TLS file permission policy enforced at startup and reload (cert: 0640/0644, key: 0600).

Hot-reload (cmd/run.go)

  • New ControlPlane → metricsState.SetControlPlane(newC) (atomic)
  • Endpoint config change → endpointServer.Shutdown() → new server

Test plan

  • go test ./pkg/metrics/... ./pkg/metricshttp/... — descriptor tests + endpoint server tests PASS
  • go test ./control/... ./cmd/... ./common/... PASS
  • go vet ./... CLEAN
  • Manual scrape: curl http://localhost:5556/metrics → all metric families present
  • dae_dns_forwarder_in_flight{upstream=...} > 0 under concurrent DNS load
  • dae_dns_upstream_err_total increments on upstream error
  • dae_dns_upstream_latency_seconds has non-zero bucket data
  • BasicAuth: curl without credentials → 401; with credentials → 200
  • SIGUSR1 reload: endpoint survives, gauges reflect new ControlPlane
  • SIGUSR1 with address change: old port stops, new port responds
  • Import the included Grafana dashboard and confirm all panels populate

@MaurUppi

Copy link
Copy Markdown
Author

Grafana Dashboard UI

dae_dashboard_0 dae_dashboard_1 dae_dashboard_2

Ref file:
dae Transparent Proxy-Grafana_dashboard.json

Codex added 2 commits June 19, 2026 11:25
In the staged same-port reload path, metricsState.SetControlPlane(newC)
was never called because the preparation phase hits `continue` before
reaching the call site at line 679 (the non-staged path).

After the staged handoff completes and clearPendingStagedHandoff() is
called, metricsState still pointed to the retired old control plane.
Once that plane's outbounds and DNS controller were torn down during
retirement, all dialer and DNS collectors emitted no samples, causing
every Grafana panel to show "no data" until a full process restart.

Mirror the non-staged path pattern: switch metricsState to the new
control plane before retiring the old one.
dae_dialer_latency_last_seconds is only emitted when hasLastLatency=true
(set only after a successful health probe). New dialers introduced by a
routing config change won't emit this metric until their first probe
completes (up to check_interval=900s), leaving the \$network variable
empty and all \$network-filtered panels showing "no data".

Switch to dae_dialer_alive which is emitted immediately at dialer
creation, so \$network populates instantly after reload.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant