Add stepmon component

This commit is contained in:
Torjus Håkestad 2025-05-24 01:15:22 +02:00
parent 3a7bc25dfb
commit e40cfed4f0
Signed by: torjus
SSH Key Fingerprint: SHA256:KjAds8wHfD2mBYK2H815s/+ABcSdcIHUndwHEdSxml4
7 changed files with 339 additions and 4 deletions

34
config/config.go Normal file
View File

@ -0,0 +1,34 @@
package config
import (
"os"
"github.com/pelletier/go-toml/v2"
)
type StepMonitor struct {
Enabled bool `toml:"Enabled"`
BaseURL string `toml:"BaseURL"`
RootID string `toml:"RootID"`
}
type Config struct {
ListenAddr string `toml:"ListenAddr"`
StepMonitors []StepMonitor `toml:"StepMonitors"`
}
func FromFile(file string) (*Config, error) {
var config Config
f, err := os.Open(file)
if err != nil {
return nil, err
}
decoder := toml.NewDecoder(f)
if err := decoder.Decode(&config); err != nil {
return nil, err
}
return &config, nil
}

View File

@ -48,7 +48,7 @@
version = version; version = version;
pname = "labmon"; pname = "labmon";
src = src; src = src;
vendorHash = pkgs.lib.fakeHash; vendorHash = "sha256-l94MnEsZ/HXpRhgtb0ckTUXVrLULUeAbXezXlKnYGQk=";
}; };
} }
); );

16
go.mod
View File

@ -1,3 +1,19 @@
module git.t-juice.club/torjus/labmon module git.t-juice.club/torjus/labmon
go 1.24.3 go 1.24.3
require (
github.com/pelletier/go-toml/v2 v2.2.4
github.com/prometheus/client_golang v1.22.0
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
golang.org/x/sys v0.30.0 // indirect
google.golang.org/protobuf v1.36.5 // indirect
)

34
go.sum Normal file
View File

@ -0,0 +1,34 @@
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

9
labmon.toml Normal file
View File

@ -0,0 +1,9 @@
# Endpoint for the metrics server
ListenAddr = ":9969"
# Monitor step-ca root certificate
[[StepMonitors]]
Enabled = true
BaseURL = "https://ca.home.2rjus.net"
RootID = "3381bda8015a86b9a3cd1851439d1091890a79005e0f1f7c4301fe4bccc29d80"

93
main.go
View File

@ -1,9 +1,96 @@
package main package main
import "fmt" import (
"context"
"fmt"
"log/slog"
"net/http"
"os"
"os/signal"
"git.t-juice.club/torjus/labmon/config"
"git.t-juice.club/torjus/labmon/stepmon"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
const Version = "0.1.0" const Version = "0.1.0"
func main() { func LoadConfig() (*config.Config, error) {
fmt.Println("Hello, World!") config, err := config.FromFile("labmon.toml")
if err != nil {
return nil, err
}
return config, nil
}
func main() {
// Load config
config, err := LoadConfig()
if err != nil {
fmt.Printf("Error loading config: %v\n", err)
os.Exit(1)
}
// Setup logger
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
}))
// Setup stepmons
var stepmons []*stepmon.StepMonitor
for _, s := range config.StepMonitors {
if s.Enabled {
sm := stepmon.NewStepMonitor(s.BaseURL, s.RootID)
sm.SetLogger(logger)
stepmons = append(stepmons, sm)
}
}
// Start stepmons
for _, sm := range stepmons {
go func(sm *stepmon.StepMonitor) {
sm.Start()
}(sm)
}
// Setup graceful shutdown
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()
shutdownDone := make(chan struct{}, 1)
// Start http server
srv := &http.Server{}
srv.Addr = config.ListenAddr
mux := http.NewServeMux()
mux.Handle("/metrics", promhttp.Handler())
srv.Handler = mux
// Start http server
go func() {
logger.Info("Starting HTTP server", "addr", config.ListenAddr)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.Error("HTTP server error", "error", err)
}
}()
// Wait for shutdown signal
go func() {
<-ctx.Done()
logger.Debug("Shutdown signal received")
// Shutdown metrics server
srv.Shutdown(context.Background())
logger.Debug("HTTP server shutdown complete")
// Shutdown stepmons
for _, sm := range stepmons {
sm.Shutdown()
logger.Debug("StepMonitor shutdown complete", "root_id", sm.RootID)
}
shutdownDone <- struct{}{}
}()
<-shutdownDone
logger.Info("Shutdown complete")
} }

155
stepmon/stepmon.go Normal file
View File

@ -0,0 +1,155 @@
package stepmon
import (
"context"
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"io"
"log/slog"
"net/http"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
var stepCertTimeLeft = promauto.NewGaugeVec(prometheus.GaugeOpts{
Namespace: "labmon",
Subsystem: "stepmon",
Name: "certificate_seconds_left",
Help: "Seconds left until the certificate expires.",
}, []string{"cert_id"})
var stepCertError = promauto.NewGaugeVec(prometheus.GaugeOpts{
Namespace: "labmon",
Subsystem: "stepmon",
Name: "certificate_check_error",
Help: "Error checking the certificate.",
}, []string{"cert_id"})
type StepMonitor struct {
BaseURL string
RootID string
logger *slog.Logger
certificate *x509.Certificate
shutdownCh chan struct{}
shutdownComplete chan struct{}
}
func NewStepMonitor(baseURL string, rootID string) *StepMonitor {
return &StepMonitor{
BaseURL: baseURL,
RootID: rootID,
logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
shutdownCh: make(chan struct{}),
shutdownComplete: make(chan struct{}, 1),
}
}
func (sm *StepMonitor) SetLogger(logger *slog.Logger) {
sm.logger = logger.With("component", "stepmon", "root_id", sm.RootID)
}
func (sm *StepMonitor) Start() {
sm.logger.Info("Starting monitoring")
err := sm.fetchCert()
if err != nil {
stepCertError.WithLabelValues(sm.RootID).Set(1)
} else {
stepCertError.WithLabelValues(sm.RootID).Set(0)
}
timerCertFetch := time.NewTimer(5 * time.Minute)
defer timerCertFetch.Stop()
timerUpdateMonitor := time.NewTimer(1 * time.Second)
defer timerUpdateMonitor.Stop()
for {
select {
case <-timerCertFetch.C:
if err := sm.fetchCert(); err != nil {
stepCertError.WithLabelValues(sm.RootID).Set(1)
} else {
stepCertError.WithLabelValues(sm.RootID).Set(0)
}
timerCertFetch.Reset(5 * time.Minute)
case <-timerUpdateMonitor.C:
if sm.certificate != nil {
secondsLeft := time.Until(sm.certificate.NotAfter).Seconds()
stepCertTimeLeft.WithLabelValues(sm.RootID).Set(secondsLeft)
}
timerUpdateMonitor.Reset(1 * time.Second)
case <-sm.shutdownCh:
close(sm.shutdownCh)
sm.shutdownComplete <- struct{}{}
return
}
}
}
func (sm *StepMonitor) Shutdown() error {
sm.shutdownCh <- struct{}{}
<-sm.shutdownComplete
close(sm.shutdownComplete)
sm.logger.Info("Monitoring stopped")
return nil
}
func (sm *StepMonitor) fetchCert() error {
sm.logger.Debug("Fetching certificate")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
reqUrl := fmt.Sprintf("%s/root/%s", sm.BaseURL, sm.RootID)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqUrl, nil)
if err != nil {
sm.logger.Error("Failed to create request", "error", err)
return err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
sm.logger.Error("Failed to fetch certificate", "error", err)
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
sm.logger.Error("Failed to fetch certificate", "status", resp.Status)
return fmt.Errorf("failed to fetch certificate: %s", resp.Status)
}
var responseBody struct {
CA string `json:"ca"`
}
decoder := json.NewDecoder(resp.Body)
if err := decoder.Decode(&responseBody); err != nil {
sm.logger.Error("Failed to decode response", "error", err)
return err
}
block, _ := pem.Decode([]byte(responseBody.CA))
if block.Type != "CERTIFICATE" {
sm.logger.Error("Invalid certificate type", "type", block.Type)
return fmt.Errorf("invalid certificate type: %s", block.Type)
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
sm.logger.Error("Failed to parse certificate", "error", err)
return err
}
sm.logger.Debug("Successfully fetched certificate", "not_after", cert.NotAfter)
sm.certificate = cert
return nil
}