mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 22:51:30 +00:00
feat: M14 — Observability (dashboard charts, agent fleet, stats API, metrics, structured logging, rollback)
Backend: StatsService with 5 aggregation methods, JSON metrics endpoint, slog-based structured logging middleware. Stats API: dashboard summary, certificates-by-status, expiration timeline, job trends, issuance rate. 23 new backend tests. Frontend: Recharts-powered dashboard with 4 charts (status pie, expiration heatmap, job trends line, issuance bar), agent fleet overview page with OS/arch grouping and version breakdown, deployment rollback buttons on version history. 7 new frontend tests. 78 API endpoints, 744+ total tests (658 Go + 86 Vitest). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/shankar0123/certctl/internal/api/middleware"
|
||||
)
|
||||
|
||||
// StatsService defines the service interface for statistics operations.
|
||||
type StatsService interface {
|
||||
GetDashboardSummary(ctx context.Context) (interface{}, error)
|
||||
GetCertificatesByStatus(ctx context.Context) (interface{}, error)
|
||||
GetExpirationTimeline(ctx context.Context, days int) (interface{}, error)
|
||||
GetJobStats(ctx context.Context, days int) (interface{}, error)
|
||||
GetIssuanceRate(ctx context.Context, days int) (interface{}, error)
|
||||
}
|
||||
|
||||
// StatsHandler handles HTTP requests for statistics and observability endpoints.
|
||||
type StatsHandler struct {
|
||||
svc StatsService
|
||||
}
|
||||
|
||||
// NewStatsHandler creates a new StatsHandler with a service dependency.
|
||||
func NewStatsHandler(svc StatsService) StatsHandler {
|
||||
return StatsHandler{svc: svc}
|
||||
}
|
||||
|
||||
// GetDashboardSummary returns a high-level summary of system state.
|
||||
// GET /api/v1/stats/summary
|
||||
func (h StatsHandler) GetDashboardSummary(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
Error(w, http.StatusMethodNotAllowed, "Method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
requestID := middleware.GetRequestID(r.Context())
|
||||
|
||||
summary, err := h.svc.GetDashboardSummary(r.Context())
|
||||
if err != nil {
|
||||
ErrorWithRequestID(w, http.StatusInternalServerError, "Failed to get dashboard summary", requestID)
|
||||
return
|
||||
}
|
||||
|
||||
JSON(w, http.StatusOK, summary)
|
||||
}
|
||||
|
||||
// GetCertificatesByStatus returns certificate counts grouped by status.
|
||||
// GET /api/v1/stats/certificates-by-status
|
||||
func (h StatsHandler) GetCertificatesByStatus(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
Error(w, http.StatusMethodNotAllowed, "Method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
requestID := middleware.GetRequestID(r.Context())
|
||||
|
||||
counts, err := h.svc.GetCertificatesByStatus(r.Context())
|
||||
if err != nil {
|
||||
ErrorWithRequestID(w, http.StatusInternalServerError, "Failed to get certificate status counts", requestID)
|
||||
return
|
||||
}
|
||||
|
||||
JSON(w, http.StatusOK, counts)
|
||||
}
|
||||
|
||||
// GetExpirationTimeline returns certificates expiring over the next N days.
|
||||
// GET /api/v1/stats/expiration-timeline?days=30
|
||||
func (h StatsHandler) GetExpirationTimeline(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
Error(w, http.StatusMethodNotAllowed, "Method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
requestID := middleware.GetRequestID(r.Context())
|
||||
|
||||
// Parse query parameter
|
||||
days := 30
|
||||
if d := r.URL.Query().Get("days"); d != "" {
|
||||
if parsed, err := strconv.Atoi(d); err == nil && parsed > 0 && parsed <= 365 {
|
||||
days = parsed
|
||||
}
|
||||
}
|
||||
|
||||
timeline, err := h.svc.GetExpirationTimeline(r.Context(), days)
|
||||
if err != nil {
|
||||
ErrorWithRequestID(w, http.StatusInternalServerError, "Failed to get expiration timeline", requestID)
|
||||
return
|
||||
}
|
||||
|
||||
JSON(w, http.StatusOK, timeline)
|
||||
}
|
||||
|
||||
// GetJobTrends returns job success/failure trends over the past N days.
|
||||
// GET /api/v1/stats/job-trends?days=30
|
||||
func (h StatsHandler) GetJobTrends(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
Error(w, http.StatusMethodNotAllowed, "Method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
requestID := middleware.GetRequestID(r.Context())
|
||||
|
||||
// Parse query parameter
|
||||
days := 30
|
||||
if d := r.URL.Query().Get("days"); d != "" {
|
||||
if parsed, err := strconv.Atoi(d); err == nil && parsed > 0 && parsed <= 365 {
|
||||
days = parsed
|
||||
}
|
||||
}
|
||||
|
||||
trends, err := h.svc.GetJobStats(r.Context(), days)
|
||||
if err != nil {
|
||||
ErrorWithRequestID(w, http.StatusInternalServerError, "Failed to get job trends", requestID)
|
||||
return
|
||||
}
|
||||
|
||||
JSON(w, http.StatusOK, trends)
|
||||
}
|
||||
|
||||
// GetIssuanceRate returns the rate of new certificate issuance over the past N days.
|
||||
// GET /api/v1/stats/issuance-rate?days=30
|
||||
func (h StatsHandler) GetIssuanceRate(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
Error(w, http.StatusMethodNotAllowed, "Method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
requestID := middleware.GetRequestID(r.Context())
|
||||
|
||||
// Parse query parameter
|
||||
days := 30
|
||||
if d := r.URL.Query().Get("days"); d != "" {
|
||||
if parsed, err := strconv.Atoi(d); err == nil && parsed > 0 && parsed <= 365 {
|
||||
days = parsed
|
||||
}
|
||||
}
|
||||
|
||||
issuanceRate, err := h.svc.GetIssuanceRate(r.Context(), days)
|
||||
if err != nil {
|
||||
ErrorWithRequestID(w, http.StatusInternalServerError, "Failed to get issuance rate", requestID)
|
||||
return
|
||||
}
|
||||
|
||||
JSON(w, http.StatusOK, issuanceRate)
|
||||
}
|
||||
Reference in New Issue
Block a user