init monlet repo with stage 0-2 (docs, contract, agent MVP)

This commit is contained in:
Stanislav Rossovskii
2026-05-26 16:41:27 +04:00
commit dcea096327
42 changed files with 3183 additions and 0 deletions

View File

@@ -0,0 +1,152 @@
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
}
}

View File

@@ -0,0 +1,34 @@
package metrics
import (
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
func TestMetricsExposeRegistered(t *testing.T) {
m := New()
m.CheckRuns.WithLabelValues("c1", "ok").Inc()
m.SpoolDepth.Set(7)
srv := httptest.NewServer(promhttp.HandlerFor(m.Registry, promhttp.HandlerOpts{}))
defer srv.Close()
resp, err := http.Get(srv.URL)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
s := string(body)
for _, want := range []string{
`monlet_agent_check_runs_total{check_id="c1",status="ok"} 1`,
`monlet_agent_spool_events 7`,
} {
if !strings.Contains(s, want) {
t.Errorf("missing %q in output", want)
}
}
}