feat: Dateibasierten Docker-Healthcheck hinzugefügt
This commit is contained in:
80
cmd/mcbdd/health.go
Normal file
80
cmd/mcbdd/health.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// maxSyncAge ist die maximale Dauer seit dem letzten Sync-Lauf, bevor der
|
||||
// Healthcheck den Daemon als unhealthy meldet. Da der Sync alle 15 Minuten
|
||||
// läuft, erlauben wir 20 Minuten Toleranz.
|
||||
const maxSyncAge = 20 * time.Minute
|
||||
|
||||
// healthFile ist der Dateiname der Healthcheck-Statusdatei, die neben dem
|
||||
// State-File abgelegt wird.
|
||||
const healthFile = "health.json"
|
||||
|
||||
// healthStatus wird als JSON in die Healthcheck-Datei geschrieben.
|
||||
type healthStatus struct {
|
||||
LastSync time.Time `json:"last_sync"`
|
||||
LastError string `json:"last_error,omitempty"`
|
||||
}
|
||||
|
||||
// healthState hält den aktuellen Health-Status im Speicher und schreibt
|
||||
// ihn nach jedem Sync-Lauf in eine Datei.
|
||||
type healthState struct {
|
||||
mu sync.Mutex
|
||||
filePath string
|
||||
}
|
||||
|
||||
func newHealthState(stateFilepath string) *healthState {
|
||||
dir := filepath.Dir(stateFilepath)
|
||||
return &healthState{
|
||||
filePath: filepath.Join(dir, healthFile),
|
||||
}
|
||||
}
|
||||
|
||||
// update wird nach jedem Sync-Lauf aufgerufen und schreibt den Status
|
||||
// in die Health-Datei.
|
||||
func (h *healthState) update(err error) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
s := healthStatus{
|
||||
LastSync: time.Now(),
|
||||
}
|
||||
if err != nil {
|
||||
s.LastError = err.Error()
|
||||
}
|
||||
data, _ := json.Marshal(s)
|
||||
os.WriteFile(h.filePath, data, 0644)
|
||||
}
|
||||
|
||||
// runHealthcheck liest die Health-Datei und prüft, ob der Daemon healthy ist.
|
||||
// Exit-Code 0 = healthy, 1 = unhealthy. Wird von Docker HEALTHCHECK aufgerufen.
|
||||
func runHealthcheck() error {
|
||||
stateFilepath := os.Getenv("STATEFILE")
|
||||
if stateFilepath == "" {
|
||||
stateFilepath = "state.json"
|
||||
}
|
||||
healthPath := filepath.Join(filepath.Dir(stateFilepath), healthFile)
|
||||
|
||||
data, err := os.ReadFile(healthPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("health file not found: %w (daemon may still be starting)", err)
|
||||
}
|
||||
var s healthStatus
|
||||
if err := json.Unmarshal(data, &s); err != nil {
|
||||
return fmt.Errorf("invalid health file: %w", err)
|
||||
}
|
||||
if s.LastError != "" {
|
||||
return fmt.Errorf("last sync failed: %s", s.LastError)
|
||||
}
|
||||
if time.Since(s.LastSync) > maxSyncAge {
|
||||
return fmt.Errorf("last sync too old: %s ago", time.Since(s.LastSync).Round(time.Second))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -37,15 +37,25 @@ type Daemon struct {
|
||||
oldCalendarName string
|
||||
notificationEnabled bool
|
||||
notificationTrigger string
|
||||
health *healthState
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) > 1 && os.Args[1] == "cleanup" {
|
||||
if err := runCleanup(); err != nil {
|
||||
slog.Error("cleanup failed", "err", err)
|
||||
os.Exit(1)
|
||||
if len(os.Args) > 1 {
|
||||
switch os.Args[1] {
|
||||
case "cleanup":
|
||||
if err := runCleanup(); err != nil {
|
||||
slog.Error("cleanup failed", "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return
|
||||
case "healthcheck":
|
||||
if err := runHealthcheck(); err != nil {
|
||||
slog.Error("healthcheck failed", "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
if err := run(); err != nil {
|
||||
slog.Error("fatal error", "err", err)
|
||||
@@ -101,6 +111,7 @@ func run() error {
|
||||
if len(d.stateFilepath) == 0 {
|
||||
d.stateFilepath = "state.json"
|
||||
}
|
||||
d.health = newHealthState(d.stateFilepath)
|
||||
d.mailcowClient = mailcow.New(
|
||||
d.httpClient,
|
||||
mailcowBase,
|
||||
@@ -115,7 +126,9 @@ func run() error {
|
||||
|
||||
func (d *Daemon) daemonLoop() {
|
||||
for {
|
||||
if err := d.daemonRun(); err != nil {
|
||||
err := d.daemonRun()
|
||||
d.health.update(err)
|
||||
if err != nil {
|
||||
slog.Error("error while syncing birthdays", "err", err)
|
||||
}
|
||||
time.Sleep(time.Minute * 15)
|
||||
|
||||
Reference in New Issue
Block a user