mesh-self-check: a run is the seat's checked event and a finding the seat mesh-conditions' value (B5d; ADR 0634 §1, §4; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
@@ -2,35 +2,36 @@ package main
|
||||
|
||||
// What a probe observes, and the finding it is written as (novox/hq ADR 0564 §2). A probe says what it sees as an
|
||||
// Observation, the controller's observation: its token, its evidence in one line, and whether one look can be wrong
|
||||
// about it. It is written into the bucket `findings` as mesh-sdk's health.Finding, whose key is the controller's key
|
||||
// about it. It is written into the bucket `findings` as the seat mesh-conditions' finding, whose key is the controller's key
|
||||
// for the same condition, so the condition the keeper raises from it and the one the controller raised share one key.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/health"
|
||||
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
|
||||
meshconditions "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
|
||||
)
|
||||
|
||||
// The severities and scopes, as mesh-sdk's health names them.
|
||||
const (
|
||||
Urgent = health.Urgent
|
||||
Warning = health.Warning
|
||||
ScopeBus = health.ScopeBus
|
||||
ScopeCore = health.ScopeCore
|
||||
ScopeProbe = health.ScopeProbe
|
||||
ScopeMesh = health.ScopeMesh
|
||||
Urgent = meshconditionsclient.Urgent
|
||||
Warning = meshconditionsclient.Warning
|
||||
ScopeBus = meshconditionsclient.ScopeBus
|
||||
ScopeCore = meshconditionsclient.ScopeCore
|
||||
ScopeProbe = meshconditionsclient.ScopeProbe
|
||||
ScopeMesh = meshconditionsclient.ScopeMesh
|
||||
)
|
||||
|
||||
// Subject is what a finding is about.
|
||||
type Subject = health.Subject
|
||||
type Subject = meshconditions.Subject
|
||||
|
||||
// Observation is one thing a probe sees wrong now.
|
||||
type Observation struct {
|
||||
Kind string
|
||||
Ref string
|
||||
Subject Subject
|
||||
Severity health.Severity
|
||||
Severity string
|
||||
Summary string
|
||||
Said string
|
||||
Headline string
|
||||
@@ -49,14 +50,14 @@ func (o Observation) Key() string {
|
||||
if token == "" {
|
||||
token = o.Kind
|
||||
}
|
||||
return health.Key(o.Subject.Scope, o.Subject.ID, token)
|
||||
return meshconditionsclient.Key(o.Subject.Scope, o.Subject.ID, token)
|
||||
}
|
||||
|
||||
// Finding is the observation as written by run at, its evidence the one line it said.
|
||||
func (o Observation) Finding(run string, at time.Time) health.Finding {
|
||||
f := health.Finding{Key: o.Key(), Kind: o.Kind, Subject: o.Subject, Severity: o.Severity, Summary: o.Summary,
|
||||
func (o Observation) Finding(run string, at time.Time) meshconditions.FindingsByKeyValue {
|
||||
f := meshconditions.FindingsByKeyValue{V: meshconditionsclient.FindingVersion, Key: o.Key(), Kind: o.Kind, Subject: o.Subject, Severity: o.Severity, Summary: o.Summary,
|
||||
Headline: o.Headline, Explanation: o.Explanation, Resolved: o.Resolved, Needs: o.Needs, Source: o.Source,
|
||||
Observer: Module, Run: run, At: at.UTC(), Evidence: []health.Evidence{}}
|
||||
Observer: Module, Run: run, At: at.UTC(), Evidence: []meshconditions.Evidence{}}
|
||||
if o.Ref != "" && o.Ref != o.Kind {
|
||||
f.Ref = o.Ref
|
||||
}
|
||||
@@ -64,17 +65,17 @@ func (o Observation) Finding(run string, at time.Time) health.Finding {
|
||||
if said == "" {
|
||||
said = o.Summary
|
||||
}
|
||||
f.Evidence = append(f.Evidence, health.Evidence{At: at.UTC(), Said: said})
|
||||
f.Evidence = append(f.Evidence, meshconditions.Evidence{At: at.UTC(), Said: said})
|
||||
return f
|
||||
}
|
||||
|
||||
// says is what a finding says, its times and the run that wrote it aside: two findings that say the same are one write
|
||||
// (ADR 0289 §1).
|
||||
func says(f health.Finding) string {
|
||||
func says(f meshconditions.FindingsByKeyValue) string {
|
||||
f.At, f.Run, f.V = time.Time{}, "", 0
|
||||
ev := make([]health.Evidence, len(f.Evidence))
|
||||
ev := make([]meshconditions.Evidence, len(f.Evidence))
|
||||
for i, e := range f.Evidence {
|
||||
ev[i] = health.Evidence{Said: e.Said}
|
||||
ev[i] = meshconditions.Evidence{Said: e.Said}
|
||||
}
|
||||
f.Evidence = ev
|
||||
raw, _ := json.Marshal(f)
|
||||
|
||||
@@ -23,7 +23,6 @@ import (
|
||||
"git.novox.be/novox/mesh-catalog/modules/restic/backupgood"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/health"
|
||||
)
|
||||
|
||||
// The states D13 reads (declared under `reads`).
|
||||
@@ -201,7 +200,7 @@ func probeData(ctx context.Context, c *Checker) ([]Observation, error) {
|
||||
return dataFindings(f), nil
|
||||
}
|
||||
|
||||
func severityOf(class string) health.Severity {
|
||||
func severityOf(class string) string {
|
||||
if class == catalogue.ClassIrreplaceable {
|
||||
return Urgent
|
||||
}
|
||||
@@ -264,7 +263,7 @@ func firstSeen(r diskreadings.Reading, now time.Time) time.Time {
|
||||
return now
|
||||
}
|
||||
|
||||
func obs(kind string, id, machine string, sev health.Severity, summary, said string) Observation {
|
||||
func obs(kind string, id, machine string, sev string, summary, said string) Observation {
|
||||
return Observation{Kind: kind, Ref: kind, Severity: sev, Summary: summary, Said: said,
|
||||
Subject: Subject{Scope: ScopeMachine, ID: id, Machine: machine}}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/health"
|
||||
)
|
||||
|
||||
// The states the machine probes read (declared under `reads`), and the verb D8 asks each node.
|
||||
@@ -28,7 +28,7 @@ const (
|
||||
PresenceRead = delivery.WatchmanModule + ".presence"
|
||||
BannedSeat = "node-intrusion-prevention"
|
||||
BannedVerb = "seat:" + BannedSeat + ".banned"
|
||||
ScopeMachine = health.ScopeMachine
|
||||
ScopeMachine = meshconditionsclient.ScopeMachine
|
||||
)
|
||||
|
||||
// placements is every node's placement, by node, from the state's keys in either form: a node key and its module keys,
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"git.novox.be/novox/mesh-sdk/go/buscompose"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/health"
|
||||
)
|
||||
|
||||
// bus is the bus module's answers as a probe asks them: every mesh stream and consumer as defined, unless changed.
|
||||
@@ -302,7 +301,7 @@ func filling(stream string) *memStore {
|
||||
s := newStore()
|
||||
o := Observation{Kind: "bucket-or-log-filling", Ref: "filling", Severity: Warning, Subject: Subject{Scope: ScopeBus, ID: stream},
|
||||
Summary: "filling", Source: "D-fill"}
|
||||
raw, _ := health.EncodeFinding(o.Finding("run", time.Now()))
|
||||
raw, _ := json.Marshal(o.Finding("run", time.Now()))
|
||||
_ = s.Put(FindingsState, o.Key(), json.RawMessage(raw))
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -17,7 +17,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/health"
|
||||
meshconditions "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
|
||||
meshselfcheck "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshselfcheck"
|
||||
)
|
||||
|
||||
// Module and Seat are this module and the seat it holds once for the mesh.
|
||||
@@ -28,11 +29,11 @@ const (
|
||||
|
||||
// The states it keeps, and the event that ends every run.
|
||||
const (
|
||||
FindingsState = health.FindingsState
|
||||
FindingsState = meshconditions.StateFindings
|
||||
VerdictState = "verdict"
|
||||
VerdictsState = "verdicts"
|
||||
LastKey = "last"
|
||||
CheckedEvent = health.EventChecked
|
||||
CheckedEvent = meshselfcheck.EventChecked
|
||||
// CheckedSubject is where the seat says it: a seat's events are on mesh.seat.<seat>.event.<name>, which its
|
||||
// consumers hear (catalogue.SeatSays).
|
||||
CheckedSubject = "mesh.seat." + Seat + ".event." + CheckedEvent
|
||||
@@ -45,12 +46,12 @@ var (
|
||||
Within = 30 * time.Second
|
||||
)
|
||||
|
||||
// The verdicts a probe can have, as mesh-sdk's health names them.
|
||||
// The verdicts a probe can have, as the seat's contract names them.
|
||||
const (
|
||||
VerdictPass = health.VerdictPassed
|
||||
VerdictFail = health.VerdictFailed
|
||||
VerdictFailedToRun = health.VerdictFailedToRun
|
||||
VerdictDeferred = health.VerdictDeferred
|
||||
VerdictPass = meshselfcheck.ProbeVerdictVerdictPassed
|
||||
VerdictFail = meshselfcheck.ProbeVerdictVerdictFailed
|
||||
VerdictFailedToRun = meshselfcheck.ProbeVerdictVerdictFailedToRun
|
||||
VerdictDeferred = meshselfcheck.ProbeVerdictVerdictDeferred
|
||||
)
|
||||
|
||||
// sourceSelf is what writes a probe's failure to run: the self-check itself.
|
||||
@@ -96,12 +97,12 @@ type Probe struct {
|
||||
Run func(ctx context.Context, c *Checker) ([]Observation, error)
|
||||
}
|
||||
|
||||
// ProbeVerdict, Counts and Run are a run as mesh-sdk's health holds it: the body of `checked`, the controller's
|
||||
// ProbeVerdict, Counts and Run are a run as the seat mesh-self-check's contract holds it: the body of `checked`, the controller's
|
||||
// doctor-heartbeat body unchanged (ADR 0564 §6), Controller naming the machine that ran it.
|
||||
type (
|
||||
ProbeVerdict = health.ProbeVerdict
|
||||
Counts = health.CheckedCounts
|
||||
Run = health.Checked
|
||||
ProbeVerdict = meshselfcheck.ProbeVerdict
|
||||
Counts = meshselfcheck.CheckedEventCounts
|
||||
Run = meshselfcheck.CheckedEvent
|
||||
)
|
||||
|
||||
// Checker is the self-check: the registry and what its probes reach.
|
||||
@@ -157,8 +158,8 @@ func (c *Checker) RunOnce(ctx context.Context, why string) Run {
|
||||
n := c.n
|
||||
c.mu.Unlock()
|
||||
started := c.Now()
|
||||
run := Run{Run: fmt.Sprintf("doctor-%d-%d", started.Unix(), n), Started: started.UTC(),
|
||||
IntervalSeconds: int(Every / time.Second), Controller: c.Host, Why: why}
|
||||
run := Run{Run: fmt.Sprintf("doctor-%d-%d", started.Unix(), n), Started: &started, Counts: &Counts{},
|
||||
IntervalSeconds: int64(Every / time.Second), Controller: c.Host, Why: why}
|
||||
|
||||
on := c.listed()
|
||||
results := make([]result, len(c.Registry))
|
||||
@@ -253,7 +254,7 @@ func (c *Checker) RunOnce(ctx context.Context, why string) Run {
|
||||
run.At = c.Now().UTC()
|
||||
run.Took = run.At.Sub(started).Round(time.Millisecond).String()
|
||||
c.mu.Lock()
|
||||
run.Unsaid = c.unsaid
|
||||
run.Unsaid = int64(c.unsaid)
|
||||
c.mu.Unlock()
|
||||
c.keep(run)
|
||||
return run
|
||||
@@ -290,7 +291,7 @@ func (c *Checker) seenBefore(key string) bool {
|
||||
}
|
||||
|
||||
// writtenFindings are the findings in the bucket, by key.
|
||||
type writtenFindings map[string]health.Finding
|
||||
type writtenFindings map[string]meshconditions.FindingsByKeyValue
|
||||
|
||||
func (w writtenFindings) has(key string) bool { _, ok := w[key]; return ok }
|
||||
|
||||
@@ -309,8 +310,8 @@ func (c *Checker) written() (writtenFindings, error) {
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
f, err := health.DecodeFinding(raw)
|
||||
if err != nil {
|
||||
var f meshconditions.FindingsByKeyValue
|
||||
if err := json.Unmarshal(raw, &f); err != nil {
|
||||
return out, fmt.Errorf("the finding %s is not readable: %w", k, err)
|
||||
}
|
||||
out[k] = f
|
||||
@@ -333,7 +334,7 @@ func (c *Checker) write(written writtenFindings, want map[string]Observation, ra
|
||||
if was, ok := written[k]; ok && says(was) == says(f) {
|
||||
continue
|
||||
}
|
||||
raw, err := health.EncodeFinding(f)
|
||||
raw, err := json.Marshal(f)
|
||||
if err == nil {
|
||||
err = c.Store.Put(FindingsState, k, json.RawMessage(raw))
|
||||
}
|
||||
@@ -388,7 +389,7 @@ func Overview(run Run) Run {
|
||||
for i, p := range run.Probes {
|
||||
total := len(p.Found) + len(p.Unconfirmed)
|
||||
if total > overviewFindings {
|
||||
p.FoundCount = len(p.Found)
|
||||
p.FoundCount = int64(len(p.Found))
|
||||
if len(p.Found) > overviewFindings {
|
||||
p.Found = p.Found[:overviewFindings:overviewFindings]
|
||||
}
|
||||
@@ -396,7 +397,7 @@ func Overview(run Run) Run {
|
||||
p.Unconfirmed = p.Unconfirmed[:room:room]
|
||||
}
|
||||
p.More = fmt.Sprintf("%d found and %d unconfirmed, %d of them shown — `%s.doctor probe=%s` gives them all",
|
||||
p.FoundCount, total-p.FoundCount, len(p.Found)+len(p.Unconfirmed), Seat, p.ID)
|
||||
p.FoundCount, int64(total)-p.FoundCount, len(p.Found)+len(p.Unconfirmed), Seat, p.ID)
|
||||
}
|
||||
probes[i] = p
|
||||
}
|
||||
|
||||
@@ -5,14 +5,13 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
meshconditions "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/health"
|
||||
)
|
||||
|
||||
// memStore is the bus's buckets in memory, counting the writes and the reads one key at a time, and handing every
|
||||
@@ -191,7 +190,7 @@ func TestARunWritesFindingsAsTheyChange(t *testing.T) {
|
||||
found, fail = []Observation{missing}, nil
|
||||
c.RunOnce(context.Background(), "the test")
|
||||
raw, _, _ = store.Get(FindingsState, "bus.EVENTS.stream-missing")
|
||||
f, err := health.DecodeFinding(raw)
|
||||
f, err := decodeFinding(raw)
|
||||
if err != nil || f.Kind != "stream-wrong" || f.Source != "P1" || f.Observer != Module || f.Headline == "" ||
|
||||
f.Explanation == "" || f.Ref != "stream-missing" || len(f.Evidence) != 1 || f.Run == "" {
|
||||
t.Errorf("the written finding is %+v", f)
|
||||
@@ -319,3 +318,10 @@ func TestOnlyTheProbesTheSettingListsRun(t *testing.T) {
|
||||
t.Fatal("no setting runs every probe, and the default list is D13 and D-fill")
|
||||
}
|
||||
}
|
||||
|
||||
// decodeFinding reads a finding as the seat mesh-conditions lays it out.
|
||||
func decodeFinding(raw []byte) (meshconditions.FindingsByKeyValue, error) {
|
||||
var f meshconditions.FindingsByKeyValue
|
||||
err := json.Unmarshal(raw, &f)
|
||||
return f, err
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ import (
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/health"
|
||||
"git.novox.be/novox/mesh-sdk/go/testbus"
|
||||
)
|
||||
|
||||
@@ -295,7 +294,7 @@ func TestTheSelfCheckWholeRunsOnTheBus(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("the missing stream is not a finding on the bus: %v", err)
|
||||
}
|
||||
f, err := health.DecodeFinding(e.Value())
|
||||
f, err := decodeFinding(e.Value())
|
||||
if err != nil || f.Kind != "stream-wrong" || f.Severity != Urgent || f.Source != "D7" {
|
||||
t.Errorf("the finding is %s", e.Value())
|
||||
}
|
||||
|
||||
@@ -31,3 +31,5 @@ replace git.novox.be/novox/mesh-catalog/modules/disk-load/diskreadings => ../dis
|
||||
require git.novox.be/novox/mesh-catalog/modules/restic/backupgood v0.0.0-00010101000000-000000000000
|
||||
|
||||
replace git.novox.be/novox/mesh-catalog/modules/restic/backupgood => ../restic/backupgood
|
||||
require git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions v0.0.0-00010101000000-000000000000
|
||||
require git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshselfcheck v0.0.0-00010101000000-000000000000
|
||||
|
||||
Reference in New Issue
Block a user