diff --git a/cmd/server/main.go b/cmd/server/main.go
index 568583f..0acd40c 100644
--- a/cmd/server/main.go
+++ b/cmd/server/main.go
@@ -329,7 +329,12 @@ func main() {
// counters get wired in Phase 8 when the Prometheus exposer reads
// them.
ocspResponseCacheRepo := postgres.NewOCSPResponseCacheRepository(db)
- ocspResponseCacheService := service.NewOCSPResponseCacheService(ocspResponseCacheRepo, caOperationsSvc, nil, logger)
+ // Production hardening II Phase 8: share a single OCSPCounters
+ // instance between the cache service (Phase 2) and the Prometheus
+ // exposer (Phase 8) so the metrics endpoint reflects every counter
+ // tick that happens inside the cache service's hot path.
+ ocspCounters := service.NewOCSPCounters()
+ ocspResponseCacheService := service.NewOCSPResponseCacheService(ocspResponseCacheRepo, caOperationsSvc, ocspCounters, logger)
caOperationsSvc.SetOCSPCacheSvc(ocspResponseCacheService)
// Load-bearing security wire: invalidate the cache after a successful
// revocation so the next OCSP fetch returns "revoked" (not the stale
@@ -524,6 +529,11 @@ func main() {
notificationHandler := handler.NewNotificationHandler(notificationService)
statsHandler := handler.NewStatsHandler(statsService)
metricsHandler := handler.NewMetricsHandler(statsService, time.Now())
+ // Production hardening II Phase 8: wire the per-area counter
+ // snapshotters so the Prometheus exposer surfaces them. Operators
+ // alert on certctl_ocsp_counter_total{label="rate_limited"},
+ // {label="nonce_malformed"}, etc.
+ metricsHandler.SetOCSPCounters(ocspCounters)
// Bundle-5 / H-006: pass the *sql.DB pool so /ready can probe DB
// connectivity via PingContext. /health stays shallow (liveness signal).
healthHandler := handler.NewHealthHandler(cfg.Auth.Type, db)
diff --git a/internal/api/handler/metrics.go b/internal/api/handler/metrics.go
index ccc1909..8b7040a 100644
--- a/internal/api/handler/metrics.go
+++ b/internal/api/handler/metrics.go
@@ -15,12 +15,27 @@ type MetricsService interface {
GetDashboardSummary(ctx context.Context) (interface{}, error)
}
+// CounterSnapshotter is the minimum surface MetricsHandler consumes
+// from a counter table for the Prometheus exposer. The OCSPCounters
+// type in internal/service satisfies this; future per-area counter
+// tabs (CRL, cert-export, EST, SCEP, Intune) plug in the same way.
+//
+// Production hardening II Phase 8.
+type CounterSnapshotter interface {
+ Snapshot() map[string]uint64
+}
+
// 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.
type MetricsHandler struct {
svc MetricsService
serverStarted time.Time
+ // Production hardening II Phase 8 — per-area counter snapshotters.
+ // nil values omit the corresponding metric block; cmd/server/main.go
+ // wires the instances at startup. The naming convention is
+ // certctl__