package metrics import ( "context" "errors" "net/http" "time" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" ) type Metrics struct { Registry *prometheus.Registry CheckRuns *prometheus.CounterVec CheckSkipped *prometheus.CounterVec CheckDuration *prometheus.HistogramVec CheckStatus *prometheus.GaugeVec CheckExitCode *prometheus.GaugeVec CheckLastRun *prometheus.GaugeVec CheckLastSuccess *prometheus.GaugeVec CheckInterval *prometheus.GaugeVec ChecksConfigured prometheus.Gauge SpoolDepth prometheus.Gauge SpoolBytes prometheus.Gauge SendAttempts *prometheus.CounterVec SendFailures *prometheus.CounterVec EventsAccepted prometheus.Counter EventsDedup prometheus.Counter LastHeartbeat prometheus.Gauge BuildInfo *prometheus.GaugeVec } // StatusCode maps a CheckResultEvent status string to a numeric gauge value. func StatusCode(status string) float64 { switch status { case "ok": return 0 case "warning": return 1 case "critical": return 2 default: return 3 } } func New() *Metrics { reg := prometheus.NewRegistry() m := &Metrics{Registry: reg} m.CheckRuns = prometheus.NewCounterVec(prometheus.CounterOpts{ Name: "monlet_agent_check_runs_total", Help: "Number of check runs by id and status.", }, []string{"check_id", "status"}) m.CheckSkipped = prometheus.NewCounterVec(prometheus.CounterOpts{ Name: "monlet_agent_check_skipped_total", Help: "Number of ticks skipped due to overlap.", }, []string{"check_id"}) m.CheckDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{ Name: "monlet_agent_check_duration_seconds", Help: "Check execution duration.", Buckets: prometheus.DefBuckets, }, []string{"check_id"}) m.SpoolDepth = prometheus.NewGauge(prometheus.GaugeOpts{ Name: "monlet_agent_spool_events", Help: "Current spool depth in events.", }) m.SpoolBytes = prometheus.NewGauge(prometheus.GaugeOpts{ Name: "monlet_agent_spool_bytes", Help: "Current spool size in bytes.", }) m.SendAttempts = prometheus.NewCounterVec(prometheus.CounterOpts{ Name: "monlet_agent_send_attempts_total", Help: "Outbound send attempts by kind.", }, []string{"kind"}) m.SendFailures = prometheus.NewCounterVec(prometheus.CounterOpts{ Name: "monlet_agent_send_failures_total", Help: "Outbound send failures by kind.", }, []string{"kind"}) m.EventsAccepted = prometheus.NewCounter(prometheus.CounterOpts{ Name: "monlet_agent_events_accepted_total", Help: "Events accepted by the server.", }) m.EventsDedup = prometheus.NewCounter(prometheus.CounterOpts{ Name: "monlet_agent_events_deduplicated_total", Help: "Events the server reported as duplicates.", }) m.CheckStatus = prometheus.NewGaugeVec(prometheus.GaugeOpts{ Name: "monlet_agent_check_status", Help: "Last observed status per check: 0=ok,1=warning,2=critical,3=unknown.", }, []string{"check_id"}) m.CheckExitCode = prometheus.NewGaugeVec(prometheus.GaugeOpts{ Name: "monlet_agent_check_exit_code", Help: "Last exit code per check.", }, []string{"check_id"}) m.CheckLastRun = prometheus.NewGaugeVec(prometheus.GaugeOpts{ Name: "monlet_agent_check_last_run_timestamp_seconds", Help: "Unix timestamp of the last completed run per check.", }, []string{"check_id"}) m.CheckLastSuccess = prometheus.NewGaugeVec(prometheus.GaugeOpts{ Name: "monlet_agent_check_last_success_timestamp_seconds", Help: "Unix timestamp of the last ok run per check.", }, []string{"check_id"}) m.CheckInterval = prometheus.NewGaugeVec(prometheus.GaugeOpts{ Name: "monlet_agent_check_interval_seconds", Help: "Configured interval per check.", }, []string{"check_id"}) m.ChecksConfigured = prometheus.NewGauge(prometheus.GaugeOpts{ Name: "monlet_agent_checks_configured", Help: "Number of checks configured.", }) m.LastHeartbeat = prometheus.NewGauge(prometheus.GaugeOpts{ Name: "monlet_agent_last_heartbeat_timestamp_seconds", Help: "Unix timestamp of the last successful heartbeat ack.", }) m.BuildInfo = prometheus.NewGaugeVec(prometheus.GaugeOpts{ Name: "monlet_agent_build_info", Help: "Constant 1 with build labels.", }, []string{"version", "mode"}) reg.MustRegister(m.CheckRuns, m.CheckSkipped, m.CheckDuration, m.CheckStatus, m.CheckExitCode, m.CheckLastRun, m.CheckLastSuccess, m.CheckInterval, m.ChecksConfigured, m.SpoolDepth, m.SpoolBytes, m.SendAttempts, m.SendFailures, m.EventsAccepted, m.EventsDedup, m.LastHeartbeat, m.BuildInfo) return m } // Serve blocks until ctx is done; the listener is closed on shutdown. func (m *Metrics) Serve(ctx context.Context, addr string) error { mux := http.NewServeMux() mux.Handle("/metrics", promhttp.HandlerFor(m.Registry, promhttp.HandlerOpts{})) srv := &http.Server{Addr: addr, Handler: mux, ReadHeaderTimeout: 5 * time.Second} errCh := make(chan error, 1) go func() { errCh <- srv.ListenAndServe() }() select { case <-ctx.Done(): shutCtx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() _ = srv.Shutdown(shutCtx) return nil case err := <-errCh: if errors.Is(err, http.ErrServerClosed) { return nil } return err } }