refactor(monitors): extract applyTransition and add passive monitor model fields
This commit is contained in:
@@ -103,7 +103,8 @@ func ListServerScheduledMonitors() ([]models.Monitor, error) {
|
||||
func listMonitorsForRunner(instanceID, runner string) ([]models.Monitor, error) {
|
||||
ctx, cancel := monCtx()
|
||||
defer cancel()
|
||||
filter := bson.M{"runner": runner, "enabled": true}
|
||||
filter := bson.M{"runner": runner, "enabled": true,
|
||||
"type": bson.M{"$nin": []string{models.MonitorMetric, models.MonitorHeartbeat}}}
|
||||
if instanceID != "" {
|
||||
filter["instance_id"] = instanceID
|
||||
}
|
||||
@@ -342,24 +343,7 @@ func ingestResult(instanceID, runner, monitorID string, res checker.Result) erro
|
||||
|
||||
now := time.Now()
|
||||
prev := m.State.Status
|
||||
retries := m.Retries
|
||||
if retries < 1 {
|
||||
retries = 1
|
||||
}
|
||||
|
||||
newStatus := prev
|
||||
fails := m.State.Fails
|
||||
if res.Up {
|
||||
fails = 0
|
||||
newStatus = models.StatusUp
|
||||
} else {
|
||||
fails++
|
||||
if fails >= retries {
|
||||
newStatus = models.StatusDown
|
||||
} else if prev == "" || prev == models.StatusPending {
|
||||
newStatus = models.StatusPending
|
||||
}
|
||||
}
|
||||
newStatus, fails := decideStatus(prev, m.State.Fails, m.Retries, res.Up)
|
||||
|
||||
state := bson.M{
|
||||
"state.status": newStatus,
|
||||
@@ -400,31 +384,11 @@ func ingestResult(instanceID, runner, monitorID string, res checker.Result) erro
|
||||
},
|
||||
options.UpdateOne().SetUpsert(true))
|
||||
|
||||
if newStatus != prev {
|
||||
switch newStatus {
|
||||
case models.StatusDown:
|
||||
inc := models.Incident{
|
||||
InstanceID: m.InstanceID,
|
||||
IncidentID: uuid.NewString(),
|
||||
MonitorID: monitorID,
|
||||
StartedAt: now,
|
||||
Cause: res.Message,
|
||||
}
|
||||
db.Col("incidents").InsertOne(ctx, inc)
|
||||
notifyTransition(m, newStatus, res.Message)
|
||||
case models.StatusUp:
|
||||
if prev == models.StatusDown {
|
||||
db.Col("incidents").UpdateOne(ctx,
|
||||
bson.M{"monitor_id": monitorID, "instance_id": m.InstanceID, "resolved_at": nil},
|
||||
bson.M{"$set": bson.M{"resolved_at": now}})
|
||||
notifyTransition(m, newStatus, res.Message)
|
||||
}
|
||||
}
|
||||
}
|
||||
applyTransition(ctx, m, "", "", prev, newStatus, res.Message, now)
|
||||
return nil
|
||||
}
|
||||
|
||||
func notifyTransition(m *models.Monitor, newStatus, message string) {
|
||||
func notifyTransition(m *models.Monitor, serverName, oldStatus, newStatus, message string) {
|
||||
if len(m.ChannelIDs) == 0 {
|
||||
return
|
||||
}
|
||||
@@ -435,8 +399,9 @@ func notifyTransition(m *models.Monitor, newStatus, message string) {
|
||||
}
|
||||
ev := notify.Event{
|
||||
MonitorName: m.Name,
|
||||
ServerName: serverName,
|
||||
Type: m.Type,
|
||||
OldStatus: m.State.Status,
|
||||
OldStatus: oldStatus,
|
||||
NewStatus: newStatus,
|
||||
Message: message,
|
||||
Time: time.Now(),
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/db"
|
||||
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/models"
|
||||
"github.com/google/uuid"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
// decideStatus is the pull-check retry state machine: a success is always up,
|
||||
// failures below the retry count leave the status alone (or pending when there
|
||||
// is none yet), and reaching the count is down.
|
||||
func decideStatus(prev string, fails, retries int, up bool) (string, int) {
|
||||
if retries < 1 {
|
||||
retries = 1
|
||||
}
|
||||
if up {
|
||||
return models.StatusUp, 0
|
||||
}
|
||||
fails++
|
||||
if fails >= retries {
|
||||
return models.StatusDown, fails
|
||||
}
|
||||
if prev == "" || prev == models.StatusPending {
|
||||
return models.StatusPending, fails
|
||||
}
|
||||
return prev, fails
|
||||
}
|
||||
|
||||
// applyTransition is the one place a status change becomes an incident and a
|
||||
// notification. serverID is empty for every monitor except a metric monitor,
|
||||
// which keeps one incident per server so each breach opens and resolves on its
|
||||
// own.
|
||||
func applyTransition(ctx context.Context, m *models.Monitor, serverID, serverName, prev, next, message string, now time.Time) {
|
||||
if next == prev {
|
||||
return
|
||||
}
|
||||
switch next {
|
||||
case models.StatusDown:
|
||||
inc := models.Incident{
|
||||
InstanceID: m.InstanceID,
|
||||
IncidentID: uuid.NewString(),
|
||||
MonitorID: m.MonitorID,
|
||||
ServerID: serverID,
|
||||
StartedAt: now,
|
||||
Cause: message,
|
||||
}
|
||||
if _, err := db.Col("incidents").InsertOne(ctx, inc); err != nil {
|
||||
log.Printf("monitors: open incident for %s: %v", m.MonitorID, err)
|
||||
}
|
||||
notifyTransition(m, serverName, prev, next, message)
|
||||
case models.StatusUp:
|
||||
if prev != models.StatusDown {
|
||||
return
|
||||
}
|
||||
resolveIncident(ctx, m, serverID, now)
|
||||
notifyTransition(m, serverName, prev, next, message)
|
||||
}
|
||||
}
|
||||
|
||||
// resolveIncident closes the open incident for a monitor (and server, for a
|
||||
// metric monitor) without notifying. It is also used when a server stops
|
||||
// matching a metric monitor's selector: nothing recovered, so nobody is told.
|
||||
func resolveIncident(ctx context.Context, m *models.Monitor, serverID string, now time.Time) {
|
||||
filter := bson.M{"monitor_id": m.MonitorID, "instance_id": m.InstanceID, "resolved_at": nil}
|
||||
if serverID != "" {
|
||||
filter["server_id"] = serverID
|
||||
} else {
|
||||
filter["server_id"] = bson.M{"$exists": false}
|
||||
}
|
||||
if _, err := db.Col("incidents").UpdateMany(ctx, filter, bson.M{"$set": bson.M{"resolved_at": now}}); err != nil {
|
||||
log.Printf("monitors: resolve incident for %s: %v", m.MonitorID, err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/models"
|
||||
)
|
||||
|
||||
// decideStatus is the retry state machine lifted out of ingestResult. These
|
||||
// cases pin its existing behaviour so the refactor cannot change it.
|
||||
func TestDecideStatus(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
prev string
|
||||
fails int
|
||||
retries int
|
||||
up bool
|
||||
wantState string
|
||||
wantFails int
|
||||
}{
|
||||
{"up resets fails", models.StatusDown, 3, 2, true, models.StatusUp, 0},
|
||||
{"first failure below retries is pending", models.StatusPending, 0, 3, false, models.StatusPending, 1},
|
||||
{"failure below retries keeps up", models.StatusUp, 0, 3, false, models.StatusUp, 1},
|
||||
{"failure reaching retries is down", models.StatusUp, 2, 3, false, models.StatusDown, 3},
|
||||
{"retries below one treated as one", models.StatusUp, 0, 0, false, models.StatusDown, 1},
|
||||
{"empty prev failing below retries is pending", "", 0, 2, false, models.StatusPending, 1},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
got, fails := decideStatus(c.prev, c.fails, c.retries, c.up)
|
||||
if got != c.wantState || fails != c.wantFails {
|
||||
t.Fatalf("got (%s,%d), want (%s,%d)", got, fails, c.wantState, c.wantFails)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user