feat(alerts): add alert for failed systemd services (#2173)

Adds a user-configurable "Failed Services" alert that notifies when any
tracked systemd service enters the failed state, and again when all services
recover.

---------

Signed-off-by: Martin Stenröse <martin@stenrose.se>
Co-authored-by: henrygd <hank@henrygd.me>
This commit is contained in:
Martin Stenröse
2026-09-02 02:41:48 +02:00
committed by GitHub
parent ed88e6efae
commit 097180e8d7
17 changed files with 891 additions and 52 deletions

View File

@@ -250,8 +250,10 @@ func (sys *System) createRecords(data *system.CombinedData) (*core.Record, error
}
}
// add new systemd_stats record
if len(data.SystemdServices) > 0 {
// Update systemd service records when the agent reports a fresh snapshot.
// The length check keeps snapshots from older agents working, while the
// explicit marker lets newer agents report that a fresh snapshot is empty.
if data.SystemdServicesUpdated || len(data.SystemdServices) > 0 {
if err := createSystemdStatsRecords(txApp, data.SystemdServices, sys.Id); err != nil {
return err
}
@@ -307,7 +309,10 @@ func createSystemDetailsRecord(app core.App, data *system.Details, systemId stri
func createSystemdStatsRecords(app core.App, data []*systemd.Service, systemId string) error {
if len(data) == 0 {
return nil
_, err := app.DB().NewQuery(
"DELETE FROM systemd_services WHERE system = {:system}",
).Bind(dbx.Params{"system": systemId}).Execute()
return err
}
// shared params for all records
params := dbx.Params{
@@ -332,7 +337,16 @@ func createSystemdStatsRecords(app core.App, data []*systemd.Service, systemId s
"INSERT INTO systemd_services (id, system, name, state, sub, cpu, cpuPeak, memory, memPeak, updated) VALUES %s ON CONFLICT(id) DO UPDATE SET system = excluded.system, name = excluded.name, state = excluded.state, sub = excluded.sub, cpu = excluded.cpu, cpuPeak = excluded.cpuPeak, memory = excluded.memory, memPeak = excluded.memPeak, updated = excluded.updated",
strings.Join(valueStrings, ","),
)
_, err := app.DB().NewQuery(queryString).Bind(params).Execute()
if _, err := app.DB().NewQuery(queryString).Bind(params).Execute(); err != nil {
return err
}
// Remove services the agent no longer reports. Every row in this batch shares the
// same updated timestamp, so anything older no longer exists on the host. Left in
// place these rows survive until the retention sweep and surface inconsistently
// across the dashboard, the services table, and alerts.
_, err := app.DB().NewQuery(
"DELETE FROM systemd_services WHERE system = {:system} AND updated < {:updated}",
).Bind(dbx.Params{"system": systemId, "updated": params["updated"]}).Execute()
return err
}