From 135b2711973a89c20637514945cb31b526a4f167 Mon Sep 17 00:00:00 2001 From: shankar0123 Date: Thu, 30 Apr 2026 15:25:38 +0000 Subject: [PATCH] feat(metrics): per-target-type deploy counters wired into /metrics/prometheus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 10 of the deploy-hardening I master bundle. Mirrors the production-hardening-II Phase 8 OCSP-counter pattern. Per frozen decision 0.9, the metric naming convention is `certctl_deploy__total` with target_type + sub-label. internal/service/deploy_counters.go: - DeployCounters struct with sync.Map of per-target-type buckets (apache, nginx, etc.). Lock-free fast path via sync/atomic Uint64 counters; LoadOrStore on first tick. - 8 sub-counters per target-type bucket: - attemptsSuccess / attemptsFailure - validateFailures (PreCommit returned error) - reloadFailures (PostCommit returned error → rollback ran) - postVerifyFails (post-deploy TLS handshake failed) - rollbackRestored (rollback succeeded) - rollbackAlsoFail (operator-actionable escalation) - idempotentSkips (SHA-256 match → no-op deploy) - Snapshot returns []DeploySnapshot for the Prometheus exposer. internal/service/deploy_counters_test.go: - 5 tests: zero-state, per-target-type tick isolation, race-detector smoke under concurrent ticks, cross-target bucket isolation, snapshot-mutation-doesn't-affect-counter. internal/api/handler/metrics.go: - New DeployCounterSnapshotter interface (mirrors CounterSnapshotter for the OCSP counters but uses the per-target-type tuple shape). - New DeploySnapshotEntry struct copying the service-layer shape; avoids importing the service package directly so the handler stays dependency-light. - New SetDeployCounters setter on MetricsHandler (mirrors SetOCSPCounters wiring). - Prometheus exposer extended with 6 new metric blocks per frozen decision 0.9: - certctl_deploy_attempts_total{target_type, result} - certctl_deploy_validate_failures_total{target_type} - certctl_deploy_reload_failures_total{target_type} - certctl_deploy_post_verify_failures_total{target_type} - certctl_deploy_rollback_total{target_type, outcome} - certctl_deploy_idempotent_skip_total{target_type} - Output sorted by target_type for stable diffs across requests. The agent-side wire-up (cmd/agent/main.go ticking counters in the DeployCertificate dispatch site) is intentionally deferred to a follow-up commit — Phase 10's load-bearing change is the infrastructure; per-connector tick wiring is a mechanical follow-on. Build + go vet clean. go test -count=1 green for service + handler packages. Phase 11 next: cross-cutting integration tests at deploy/test/. --- internal/api/handler/metrics.go | 78 +++++++++++ internal/service/deploy_counters.go | 158 +++++++++++++++++++++++ internal/service/deploy_counters_test.go | 106 +++++++++++++++ 3 files changed, 342 insertions(+) create mode 100644 internal/service/deploy_counters.go create mode 100644 internal/service/deploy_counters_test.go diff --git a/internal/api/handler/metrics.go b/internal/api/handler/metrics.go index 8b7040a..ac41008 100644 --- a/internal/api/handler/metrics.go +++ b/internal/api/handler/metrics.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "net/http" + "sort" "time" "github.com/shankar0123/certctl/internal/api/middleware" @@ -25,6 +26,31 @@ type CounterSnapshotter interface { Snapshot() map[string]uint64 } +// DeploySnapshotEntry is the per-target-type tuple emitted by the +// deploy package's counter table. Avoids importing the service +// package's DeploySnapshot directly so the handler stays +// dependency-light (the interface uses primitives only). +// +// Phase 10 of the deploy-hardening I master bundle. +type DeploySnapshotEntry struct { + TargetType string + AttemptsSuccess uint64 + AttemptsFailure uint64 + ValidateFailures uint64 + ReloadFailures uint64 + PostVerifyFails uint64 + RollbackRestored uint64 + RollbackAlsoFail uint64 + IdempotentSkips uint64 +} + +// DeployCounterSnapshotter is the surface MetricsHandler consumes +// for the per-target-type deploy counters. The DeployCounters type +// in internal/service satisfies this via an adapter. +type DeployCounterSnapshotter interface { + Snapshot() []DeploySnapshotEntry +} + // MetricsHandler handles HTTP requests for metrics. // Supports both JSON format (GET /api/v1/metrics) and Prometheus exposition format // (GET /api/v1/metrics/prometheus) for integration with Prometheus, Grafana, Datadog, etc. @@ -36,6 +62,8 @@ type MetricsHandler struct { // wires the instances at startup. The naming convention is // certctl__