Detect and alert on stuck jobs
CI / Test (rest) (pull_request) Successful in 39s
CI / Test (store) (pull_request) Successful in 40s
CI / Lint (pull_request) Failing after 11s
CI / Build (linux/amd64) (pull_request) Successful in 8s
CI / Build (linux/arm64) (pull_request) Successful in 7s
CI / Build (windows/amd64) (pull_request) Successful in 25s
CI / Test (server-http) (pull_request) Successful in 1m39s
e2e / Playwright vs docker-compose (pull_request) Successful in 1m16s
CI / Test (rest) (pull_request) Successful in 39s
CI / Test (store) (pull_request) Successful in 40s
CI / Lint (pull_request) Failing after 11s
CI / Build (linux/amd64) (pull_request) Successful in 8s
CI / Build (linux/arm64) (pull_request) Successful in 7s
CI / Build (windows/amd64) (pull_request) Successful in 25s
CI / Test (server-http) (pull_request) Successful in 1m39s
e2e / Playwright vs docker-compose (pull_request) Successful in 1m16s
This commit is contained in:
@@ -8,7 +8,9 @@ import (
|
||||
"net/netip"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.dcglab.co.uk/steve/restic-manager/internal/alert"
|
||||
"gitea.dcglab.co.uk/steve/restic-manager/internal/server/config"
|
||||
"gitea.dcglab.co.uk/steve/restic-manager/internal/server/metrics"
|
||||
"gitea.dcglab.co.uk/steve/restic-manager/internal/store"
|
||||
@@ -173,13 +175,37 @@ func (s *Server) gatherMetricsSnapshot(ctx context.Context) (metrics.Snapshot, e
|
||||
return metrics.Snapshot{}, err
|
||||
}
|
||||
bySeverity := map[string]int{"info": 0, "warning": 0, "critical": 0}
|
||||
stuckJobs := 0
|
||||
var oldestStuckAge time.Duration
|
||||
now := time.Now().UTC()
|
||||
running, err := s.deps.Store.ListRunningJobActivity(ctx)
|
||||
if err != nil {
|
||||
return metrics.Snapshot{}, err
|
||||
}
|
||||
activityByJob := make(map[string]time.Time, len(running))
|
||||
for _, job := range running {
|
||||
activityByJob[job.JobID] = job.LastActivity
|
||||
}
|
||||
for _, a := range open {
|
||||
bySeverity[a.Severity]++
|
||||
if a.Kind == alert.KindJobStuck {
|
||||
stuckJobs++
|
||||
lastActivity, ok := activityByJob[a.DedupKey]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if age := now.Sub(lastActivity); age > oldestStuckAge {
|
||||
oldestStuckAge = age
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reg := s.deps.Metrics
|
||||
if reg == nil {
|
||||
reg = metrics.NewRegistry() // empty histogram block
|
||||
}
|
||||
return reg.SnapshotWith(hostRows, bySeverity, version.Version, version.Commit, runtime.Version()), nil
|
||||
snap := reg.SnapshotWith(hostRows, bySeverity, version.Version, version.Commit, runtime.Version())
|
||||
snap.StuckJobs = stuckJobs
|
||||
snap.OldestStuckAge = oldestStuckAge
|
||||
return snap, nil
|
||||
}
|
||||
|
||||
@@ -155,6 +155,8 @@ type Snapshot struct {
|
||||
BuildCommit string
|
||||
GoVersion string
|
||||
JobDurationRows []HistogramRow
|
||||
StuckJobs int
|
||||
OldestStuckAge time.Duration
|
||||
}
|
||||
|
||||
// SnapshotWith builds a Snapshot from raw inputs and the registry's
|
||||
@@ -205,6 +207,13 @@ func Render(w io.Writer, s Snapshot) error {
|
||||
fmt.Fprintf(&b, "rm_build_info{version=%q,commit=%q,go_version=%q} 1\n",
|
||||
s.BuildVersion, s.BuildCommit, s.GoVersion)
|
||||
|
||||
b.WriteString("# HELP rm_stuck_jobs Number of open job_stuck alerts.\n")
|
||||
b.WriteString("# TYPE rm_stuck_jobs gauge\n")
|
||||
fmt.Fprintf(&b, "rm_stuck_jobs %d\n", s.StuckJobs)
|
||||
b.WriteString("# HELP rm_oldest_stuck_job_age_seconds Time since last activity for the oldest open stuck job.\n")
|
||||
b.WriteString("# TYPE rm_oldest_stuck_job_age_seconds gauge\n")
|
||||
fmt.Fprintf(&b, "rm_oldest_stuck_job_age_seconds %.0f\n", s.OldestStuckAge.Seconds())
|
||||
|
||||
// --- Per-host gauges -------------------------------------------------
|
||||
// Stable order: by host id.
|
||||
hosts := append([]HostRow(nil), s.Hosts...)
|
||||
|
||||
@@ -114,6 +114,8 @@ func TestRenderGolden(t *testing.T) {
|
||||
snap := r.SnapshotWith(hosts,
|
||||
map[string]int{"info": 0, "warning": 1, "critical": 0},
|
||||
"v1.2.3", "deadbeef", "go1.25.0")
|
||||
snap.StuckJobs = 2
|
||||
snap.OldestStuckAge = 90 * time.Minute
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := Render(&buf, snap); err != nil {
|
||||
@@ -129,6 +131,8 @@ func TestRenderGolden(t *testing.T) {
|
||||
`rm_active_alerts{severity="info"} 0`,
|
||||
`rm_active_alerts{severity="critical"} 0`,
|
||||
`rm_build_info{version="v1.2.3",commit="deadbeef",go_version="go1.25.0"} 1`,
|
||||
`rm_stuck_jobs 2`,
|
||||
`rm_oldest_stuck_job_age_seconds 5400`,
|
||||
`rm_host_agent_online{host_id="01H0001",host="alpha"} 1`,
|
||||
`rm_host_agent_online{host_id="01H0002",host="bravo"} 0`,
|
||||
`rm_host_last_backup_timestamp_seconds{host_id="01H0001",host="alpha"} 1700000000`,
|
||||
|
||||
Reference in New Issue
Block a user