diff --git a/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/findingwriter.go b/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/findingwriter.go new file mode 100644 index 00000000..9d1ddabe --- /dev/null +++ b/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/findingwriter.go @@ -0,0 +1,104 @@ +package main + +import ( + "context" + "sync" + + "github.com/nats-io/nats.go" + "github.com/nats-io/nats.go/jetstream" + + condseat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions" + "git.novox.be/novox/mesh-sdk/go/process" + "git.novox.be/novox/mesh-sdk/go/views" + + "mesh-delivery-watchman/internal/watch" +) + +type findingWriterOf = views.Writer[condseat.Finding, condseat.FindingCleared, condseat.FindingsStated, condseat.FindingsAskedEvent] + +// heldFindings is the watchman's findings through mesh-sdk's finding writer (design 47 G §461, §689): every finding the +// watchman holds, by its own key, which the writer states at its start and when the keeper asks, and writes and clears as the +// watchman puts and deletes them. It has a lock of its own, never the watchman's: the writer calls observe under its own +// lock while the watchman writes under its. +type heldFindings struct { + mu sync.Mutex + held map[string]condseat.Finding + w *findingWriterOf +} + +// seedFindings reads the findings that stand in the state `findings` at the start, by the watchman's key of each. +func seedFindings(state process.State) (*heldFindings, map[string][]byte, error) { + h := &heldFindings{held: map[string]condseat.Finding{}} + seeds := map[string][]byte{} + keys, err := state.Keys() + if err != nil { + return nil, nil, err + } + for _, k := range keys { + e, err := state.Get(k) + if err != nil { + return nil, nil, err + } + if e == nil { + continue + } + f, err := watch.DecodeFinding(e.Value) + if err != nil { + continue + } + h.held[f.Key()] = f.Contract() + seeds[f.Key()] = e.Value + } + return h, seeds, nil +} + +// start makes and starts the writer: it states what is held at once, so the seeds are made before it. +func (h *heldFindings) start(ctx context.Context, cred process.Account, conn *nats.Conn, js jetstream.JetStream, state process.State, logf func(string, ...any)) error { + w, err := views.NewWriter(views.WriterConfig[condseat.Finding, condseat.FindingCleared, condseat.FindingsStated, condseat.FindingsAskedEvent]{ + Module: cred.Module, State: state, Node: cred.Node, JS: js, Conn: conn, Logf: logf, + Observe: func(context.Context) (views.Observed[condseat.Finding], error) { + h.mu.Lock() + defer h.mu.Unlock() + var o views.Observed[condseat.Finding] + for _, f := range h.held { + o.Findings = append(o.Findings, f) + } + return o, nil + }, + }) + if err != nil { + return err + } + h.w = w + // A writer that cannot start (the stream of its asks is not there yet) is said; its findings are then kept in the + // state alone and said by the next start (as the node-engine's is). + if err := w.Start(ctx); err != nil { + logf("the finding writer cannot start, its findings are kept in the state alone: %v", err) + } + return nil +} + +// Put writes the finding the watchman holds under key. +func (h *heldFindings) Put(ctx context.Context, key string, value []byte) error { + f, err := watch.DecodeFinding(value) + if err != nil { + return err + } + c := f.Contract() + h.mu.Lock() + h.held[key] = c + h.mu.Unlock() + return h.w.Write(ctx, c) +} + +// Delete clears the finding the watchman held under key. +func (h *heldFindings) Delete(ctx context.Context, key string) error { + h.mu.Lock() + c, ok := h.held[key] + delete(h.held, key) + h.mu.Unlock() + if !ok { + return nil + } + return h.w.Clear(ctx, c.Scope, c.ID, c.Kind, "the watchman no longer observes it") +} diff --git a/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/integration_test.go b/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/integration_test.go index df13bcfb..ec19015b 100644 --- a/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/integration_test.go +++ b/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/integration_test.go @@ -71,6 +71,14 @@ func serveOwners(t *testing.T, ctx context.Context, js jetstream.JetStream, nc * viewstest.Serve(t, ctx, js, nc, watch.IndexerModule, modulesState, watch.IndexerModule+".indexer_read_modules", "n1_"+watch.WatchmanModule) } +// findingsStream is the watchman's own stream of its findings' events (design 47 G §383), which its finding writer says into. +func findingsStream(t *testing.T, ctx context.Context, js jetstream.JetStream) { + t.Helper() + if _, err := js.CreateStream(ctx, jetstream.StreamConfig{Name: "WATCHMAN_FINDINGS", Subjects: []string{"mesh.stream." + watch.WatchmanModule + ".event.>"}}); err != nil { + t.Fatal(err) + } +} + // TestWatchmanWhole starts the program the way the node does (its entry, run, with its environment and a broker file) // against an embedded bus with the system exports and JetStream, and proves the four things the issue names. func TestWatchmanWhole(t *testing.T) { // 1135/AC8 @@ -132,6 +140,7 @@ func TestWatchmanWhole(t *testing.T) { // 1135/AC8 kvs[c.Bucket] = kv } serveOwners(t, ctx, js, ctl) + findingsStream(t, ctx, js) // The controller's consumer on DEAD_LETTER_NOTICES, the dead letters' streams and a source stream with one message a // consumer will give up on (1952/WM5); no lease bucket at all (issue 1126). @@ -383,6 +392,7 @@ func anAskRunningBeforeTheStartGetsItsPulse(t *testing.T) { // 1909 kvs[c.Bucket] = kv } serveOwners(t, ctx, js, ctl) + findingsStream(t, ctx, js) const id = "m0000009ffffff" body, err := buildstep.EncodeStep(buildstep.Step{ID: id, Tool: "build_go_test", Node: "n1", State: buildstep.StepRunning, Started: time.Now().UTC()}) if err != nil { @@ -470,6 +480,7 @@ func aWantIsMirroredAsAPulseAndTheHaveNamingItEndsIt(t *testing.T) { kvs[c.Bucket] = kv } serveOwners(t, ctx, js, ctl) + findingsStream(t, ctx, js) fpNew := meshcatalogclient.SourcePrefix2 + strings.Repeat("b", 64) fpOld := meshcatalogclient.SourcePrefix2 + strings.Repeat("a", 64) putWant := func(node, module, fp string) { diff --git a/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/main.go b/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/main.go index dc99c4ef..135fd11e 100644 --- a/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/main.go +++ b/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/main.go @@ -18,8 +18,6 @@ import ( "syscall" "time" - sdkfindings "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient/findings" - "git.novox.be/novox/mesh-sdk/go/process" "git.novox.be/novox/mesh-sdk/go/views" @@ -51,12 +49,13 @@ func main() { } } -// kvOut writes the watchman's four buckets. +// kvOut writes the watchman's three buckets and its findings, which go through mesh-sdk's finding writer. type kvOut struct { - ctx context.Context - presence, pulse, stuk, findings jetstream.KeyValue - js jetstream.JetStream - cred process.Account + ctx context.Context + presence, pulse, stuk jetstream.KeyValue + findings *heldFindings + js jetstream.JetStream + cred process.Account } // Say publishes an event of the seat mesh-watchdogs, said under the watchman's own account (design 47 G §389, §436). @@ -74,10 +73,7 @@ func (o *kvOut) Say(subject string, body []byte, id string) error { return err } -func (o *kvOut) PutFinding(key string, value []byte) error { - _, err := o.findings.Put(o.ctx, key, value) - return err -} +func (o *kvOut) PutFinding(key string, value []byte) error { return o.findings.Put(o.ctx, key, value) } func (o *kvOut) DeleteFinding(key string) error { return o.findings.Delete(o.ctx, key) } @@ -233,11 +229,11 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro // Its own presence, pulses and stuck rows are what it keeps: each stops the start, naming its grant. The assigner's wants // and the node-engine's haves only make the pulses of a move: without the haves the watchman watches presence and the // build seats' steps, and says that moves are not watched (issue 1945). - var kvs struct{ presence, pulse, stuck, findings, haves jetstream.KeyValue } + var kvs struct{ presence, pulse, stuck, haves jetstream.KeyValue } for _, b := range []struct { name string into *jetstream.KeyValue - }{{watch.PresenceBucket, &kvs.presence}, {watch.PulseBucket, &kvs.pulse}, {watch.StuckBucket, &kvs.stuck}, {watch.FindingsBucket, &kvs.findings}} { + }{{watch.PresenceBucket, &kvs.presence}, {watch.PulseBucket, &kvs.pulse}, {watch.StuckBucket, &kvs.stuck}} { if *b.into, err = process.EssentialBucket(ctx, js, "watchman", b.name, "say who is online or what stopped moving"); err != nil { return err } @@ -255,10 +251,19 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro // The module has one durable consumer of EVENTS: one hub pulls it for the views reader, the indexer's take-in events // and the seats' events, started once every part is registered. hub := process.NewEventHub(logger.Printf) + // Its findings: those that stand in its state are read first, so the writer's statement at its start says them and + // deletes none (the watchman reconciles them below, once it has read what it observes). + findingsState := calls.State("findings") + held, seeds, err := seedFindings(findingsState) + if err != nil { + return fmt.Errorf("the watchman's findings cannot be read: %w", err) + } + if err := held.start(ctx, cred, conn, js, findingsState, logger.Printf); err != nil { + return fmt.Errorf("the watchman's finding writer: %w", err) + } + defer held.w.Close() reader := &views.ProcessReader{Module: watch.WatchmanModule, Call: calls.Call, Logf: logger.Printf, Hub: hub, - Findings: func() views.FindingWriter { - return sdkfindings.Writer(ctx, func() jetstream.KeyValue { return kvs.findings }) - }} + Findings: func() views.FindingWriter { return views.WriterFindings{Writer: held.w} }} wants, err := reader.Follow(watch.AssignerModule, wantsState, watch.AssignerModule+".assigner_read_wants") if err != nil { return err @@ -268,7 +273,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro return err } - w := watch.New(&kvOut{ctx: ctx, presence: kvs.presence, pulse: kvs.pulse, stuk: kvs.stuck, findings: kvs.findings, js: js, cred: cred}, time.Now) + w := watch.New(&kvOut{ctx: ctx, presence: kvs.presence, pulse: kvs.pulse, stuk: kvs.stuck, findings: held, js: js, cred: cred}, time.Now) w.Logf(logger.Printf) // What the wants and the modules hold is ended by their own late read (watch.ReadLate), never by the start's end. w.Awaits(watch.AssignerWantsBucket) @@ -282,6 +287,9 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro w.SetBound(d) } + for key, value := range seeds { + w.SeedFinding(key, value) + } // What stands now: presence and stuck as written, pulses as the server holds them, then the checks. for _, s := range []struct { kv jetstream.KeyValue @@ -292,11 +300,6 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro w.SeedPresence(e.Key(), p.Online, e.Created()) } }}, - {kvs.findings, func(e jetstream.KeyValueEntry) { - if opOf(e) == watch.Put { - w.SeedFinding(e.Key(), e.Value()) - } - }}, {kvs.stuck, func(e jetstream.KeyValueEntry) { if opOf(e) == watch.Put { w.SeedStuck(e.Key(), e.Value()) diff --git a/modules/mesh-delivery-watchman/go.mod b/modules/mesh-delivery-watchman/go.mod index cf227cfd..39040a2f 100644 --- a/modules/mesh-delivery-watchman/go.mod +++ b/modules/mesh-delivery-watchman/go.mod @@ -47,7 +47,7 @@ replace git.novox.be/novox/mesh-delivery/sdk => ../../sdk require git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient v0.0.0-00010101000000-000000000000 require ( - git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions v0.0.0-00010101000000-000000000000 // indirect + git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhandacts v0.0.0-00010101000000-000000000000 // indirect git.novox.be/novox/mesh-delivery/contracts/go/clients/containerimageregistryclient v0.0.0-00010101000000-000000000000 // indirect git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient v0.0.0-00010101000000-000000000000 // indirect diff --git a/modules/mesh-delivery-watchman/internal/watch/busstate.go b/modules/mesh-delivery-watchman/internal/watch/busstate.go index 8b1b66b4..5e07bf53 100644 --- a/modules/mesh-delivery-watchman/internal/watch/busstate.go +++ b/modules/mesh-delivery-watchman/internal/watch/busstate.go @@ -83,8 +83,7 @@ func (w *Watchman) Accepted(a meshcatalog.ManifestAcceptedEvent) { Explanation: "The indexer accepted a manifest of " + a.Module + " asking the bus module for " + e.Name + ", and the bus " + "module said neither created nor refused within " + OverdueBound.String() + ": it is down, or behind. " + a.Module + " fails at its start while the entry's binding is missing (novox/hq ADR 0595 §1).", - Needs: "a look at the bus module's health and its process's log; nats_first_view_again asks it to read the verdicts again", - Resolved: "the bus module said " + e.Name + " of " + a.Module + " created, or a later manifest no longer asks for it"}}) + Needs: "a look at the bus module's health and its process's log; nats_first_view_again asks it to read the verdicts again"}}) } for _, local := range w.busStateEntriesOf(a.Module) { if asked[local] { @@ -151,8 +150,7 @@ func (w *Watchman) BrokerRefused(s BrokerSaid) { Headline: "The bus state " + s.Local + " of " + s.Module + " was refused", Explanation: "The bus module could not create " + s.Local + " for " + s.Module + ", so no binding is handed and " + s.Module + " fails at its start, naming the entry (novox/hq ADR 0595 §1): " + why, - Needs: "a pull request that mends the entry in the manifest of " + s.Module + ", or the bus module's cause fixed", - Resolved: "the bus module said " + s.Local + " of " + s.Module + " created"}) + Needs: "a pull request that mends the entry in the manifest of " + s.Module + ", or the bus module's cause fixed"}) } // busStateEntriesOf are the entries of module this watchman waits on or holds a finding of, by local name, sorted. diff --git a/modules/mesh-delivery-watchman/internal/watch/finding.go b/modules/mesh-delivery-watchman/internal/watch/finding.go index 6ba41b80..b0707089 100644 --- a/modules/mesh-delivery-watchman/internal/watch/finding.go +++ b/modules/mesh-delivery-watchman/internal/watch/finding.go @@ -1,20 +1,23 @@ package watch import ( + "encoding/json" + "strings" "time" mcc "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient" + condseat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions" "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient/heard" ) // The watchman is an observer of the seat mesh-watchdogs (novox/hq ADR 0564 §2, §4): what it observes wrong it writes as a -// finding, one key `..` of its own bucket `findings` in the seat mesh-conditions' layout (meshconditionsclient), only when what -// it says changes, and it deletes the key when it no longer observes it. The condition keeper (mesh-conditions) raises and +// finding, one key `..` of its own state `findings` in the seat mesh-conditions' generated layout, written +// through mesh-sdk's finding writer (design 47 G §689), only when what it says changes, and it deletes the key when it no longer observes it. The condition keeper (mesh-conditions) raises and // clears the conditions from them; the watchman writes no condition and reads none (Limits 11). // FindingsBucket is the watchman's findings. -var FindingsBucket = mcc.FindingsBucket(Observer) +var FindingsBucket = Observer + "_findings" // Observer is the name a finding of the watchman carries. const Observer = "mesh-delivery-watchman" @@ -43,7 +46,7 @@ type Finding struct { Severity string Summary, Said string Headline, Explanation string - Needs, Resolved string + Needs string Source string Observer string // Since is when what it says began, as the watchman saw it: the finding's time and its observation's, so the same @@ -63,22 +66,42 @@ func (f Finding) Key() string { return FindingKey(f.Scope, f.ID, token) } -// EncodeFinding is f in the seat's layout, refusing one the keeper could not make a condition of. -func EncodeFinding(f Finding) ([]byte, error) { - return mcc.EncodeFinding(mcc.Observation{Scope: f.Scope, ID: f.ID, Kind: f.Kind, Ref: f.Token, Machine: f.Machine, Also: f.Also, - Severity: f.Severity, Summary: f.Summary, Said: f.Said, Headline: f.Headline, Explanation: f.Explanation, - Resolved: f.Resolved, Needs: f.Needs, Source: f.Source}.Finding(Observer, "", f.Since)) +// Contract is f as the seat mesh-conditions' generated finding (design 47 G §686, §689): a ref, where the finding has a +// token other than its kind, goes into the id as `.` (A-14), and is kept in `ref` so the finding reads back as +// it was written. The resolved words are not a field of the seat (A-22), and its `at` is its `since`, so the same thing +// observed again writes the same bytes. +func (f Finding) Contract() condseat.Finding { + id, ref := f.ID, "" + if f.Token != "" && f.Token != f.Kind { + ref = f.Token + id = f.ID + "." + ref + } + v := condseat.Finding{Scope: f.Scope, ID: id, Kind: f.Kind, Ref: ref, Severity: f.Severity, Summary: f.Summary, + Headline: f.Headline, Explanation: f.Explanation, Needs: f.Needs, Source: f.Source, Observer: Observer, + At: f.Since.UTC(), Since: f.Since.UTC(), + Subject: condseat.Subject{Scope: f.Scope, ID: id, Machine: f.Machine, Also: f.Also}} + if f.Said != "" { + v.Evidence = []condseat.Evidence{{At: f.Since.UTC(), Said: f.Said}} + } + return v } +// EncodeFinding is f in the seat's generated layout. +func EncodeFinding(f Finding) ([]byte, error) { return json.Marshal(f.Contract()) } + // DecodeFinding reads a finding the watchman wrote. func DecodeFinding(data []byte) (Finding, error) { - h, err := mcc.DecodeFinding(data) - if err != nil { + var h condseat.Finding + if err := json.Unmarshal(data, &h); err != nil { return Finding{}, err } - f := Finding{Scope: h.Subject.Scope, ID: h.Subject.ID, Token: h.Ref, Kind: h.Kind, Machine: h.Subject.Machine, + id := h.ID + if h.Ref != "" { + id = strings.TrimSuffix(id, "."+h.Ref) + } + f := Finding{Scope: h.Scope, ID: id, Token: h.Ref, Kind: h.Kind, Machine: h.Subject.Machine, Also: h.Subject.Also, Severity: h.Severity, Summary: h.Summary, Headline: h.Headline, Explanation: h.Explanation, - Needs: h.Needs, Resolved: h.Resolved, Source: h.Source, Observer: h.Observer, Since: h.At} + Needs: h.Needs, Source: h.Source, Observer: h.Observer, Since: h.Since} if len(h.Evidence) > 0 { f.Said = h.Evidence[0].Said } @@ -163,7 +186,7 @@ func (w *Watchman) NotHeard(family, consequence string, why error) { w.say("%s", summary) w.find(Finding{Scope: ScopeModule, ID: heard.ID(Observer, family), Token: heard.Kind, Kind: heard.Kind, Severity: Warning, Source: Observer + "." + heard.Kind, Since: w.now().UTC().Truncate(time.Second), Summary: summary, Headline: headline, - Explanation: explanation, Resolved: "the watchman's consumer of " + family + " is bound"}) + Explanation: explanation}) } // Heard ends the finding that the watchman does not hear family. diff --git a/modules/mesh-delivery-watchman/internal/watch/findings_test.go b/modules/mesh-delivery-watchman/internal/watch/findings_test.go index 2363ea72..6a99ed7b 100644 --- a/modules/mesh-delivery-watchman/internal/watch/findings_test.go +++ b/modules/mesh-delivery-watchman/internal/watch/findings_test.go @@ -1,6 +1,7 @@ package watch import ( + "encoding/json" "errors" "strings" "sync/atomic" @@ -9,7 +10,7 @@ import ( "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" - mcc "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient" + condseat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshwantsclient" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" @@ -325,7 +326,8 @@ func TestTheWatchmansFindings(t *testing.T) { case !row.noState && row.want == "" && stands: t.Errorf("%s: the finding stands once bound", row.name) case row.want != "": - f, err := mcc.DecodeFinding(raw) + var f condseat.Finding + err := json.Unmarshal(raw, &f) // Review round 2: it says it clears at the part's next start, since Bind runs at the start. if err != nil || f.Kind != heard.Kind || f.Summary != row.want || f.Observer != "mesh-delivery-indexer" || !strings.Contains(f.Explanation, "next start") { t.Errorf("%s: the finding %+v (%v), want summary %q", row.name, f, err, row.want) @@ -820,3 +822,33 @@ func takeInBody(t *testing.T, kind, key string, since time.Time) []byte { } return b } + +// Design 47 G §689, A-14, A-22: a token other than the kind goes into the contract finding's id and reads back as it was +// written; the finding's time is its `since`; the resolved words are not on the wire. +func TestAFindingIsTheContractsWithItsRefInItsID(t *testing.T) { + since := time.Date(2026, 11, 11, 20, 0, 0, 0, time.UTC) + for _, f := range []Finding{ + {Scope: ScopeModule, ID: "web.ace", Token: "unhealthy", Kind: KindUnhealthy, Machine: "ace", Severity: Warning, Source: "health", Since: since, Summary: "s", Said: "said"}, + {Scope: ScopeModule, ID: "web", Token: KindBusStateOverdue + "-state", Kind: KindBusStateOverdue, Severity: Warning, Source: "bus", Since: since, Summary: "s"}, + {Scope: ScopeMachine, ID: "ace", Kind: KindMachineUnits, Severity: Warning, Source: "units", Since: since, Summary: "s", Needs: "mend"}, + } { + c := f.Contract() + wantID := f.ID + if f.Token != "" && f.Token != f.Kind { + wantID = f.ID + "." + f.Token + } + if c.ID != wantID || c.Kind != f.Kind || !c.At.Equal(since) || !c.Since.Equal(since) || c.Subject.ID != wantID { + t.Errorf("%+v: contract %+v, want id %q, kind %q, at and since %v", f, c, wantID, f.Kind, since) + } + raw, err := EncodeFinding(f) + if err != nil { + t.Fatal(err) + } + if back, err := DecodeFinding(raw); err != nil || back.ID != f.ID || back.Token != f.Token || back.Said != f.Said || back.Needs != f.Needs { + t.Errorf("%+v read back as %+v (%v)", f, back, err) + } + if strings.Contains(string(raw), "esolved") { + t.Errorf("%+v: resolved words are on the wire: %s", f, raw) + } + } +} diff --git a/modules/mesh-delivery-watchman/internal/watch/nodehealth.go b/modules/mesh-delivery-watchman/internal/watch/nodehealth.go index a53a3d8a..da6adfe1 100644 --- a/modules/mesh-delivery-watchman/internal/watch/nodehealth.go +++ b/modules/mesh-delivery-watchman/internal/watch/nodehealth.go @@ -198,8 +198,7 @@ func unitsOf(node string, u *nodeengine.UnitsHealth) (Finding, bool) { "mend or remove it there, or have a module place it", node, len(u.Failed), strings.Join(names, ", ")), Said: strings.Join(said, "; "), Headline: strings.ToUpper(failed[:1]) + failed[1:] + " on " + node, - Needs: fmt.Sprintf("mend or remove them on %s, or silence this if they do not matter.", node), - Resolved: "No failed services on " + node + " any more"}, true + Needs: fmt.Sprintf("mend or remove them on %s, or silence this if they do not matter.", node)}, true } // reloginOf is one finding per module whose accounts wait for a new login. @@ -251,8 +250,7 @@ func foundOf(node string, dirs []nodeengine.NodeDirectory) []Finding { Headline: fmt.Sprintf("%s waits for a directory on %s", m, node), Explanation: fmt.Sprintf("Needs you: hand the directory over from the control-node, as the operator; the details name it "+ "and the line to type. A directory of %s was already on %s, with another owner or mode than %s declares.", m, node, m), - Needs: "hand the directory over from the control-node, as the operator; the details name it and the line to type.", - Resolved: fmt.Sprintf("%s's directory on %s is the mesh's", m, node)}) + Needs: "hand the directory over from the control-node, as the operator; the details name it and the line to type."}) } return out } @@ -266,8 +264,7 @@ func fallbackOf(node string, f *nodeengine.LoginFallback) (Finding, bool) { return Finding{Scope: ScopeMachine, ID: node, Token: KindAccountFallback, Kind: KindAccountFallback, Machine: node, Severity: Warning, Source: "health-report", Since: f.At, Summary: said, Said: said, Headline: fmt.Sprintf("%s fell back to its old bus login", node), - Needs: fmt.Sprintf("abandon the rotation of %s's account, or rotate it again once its new login authenticates.", node), - Resolved: fmt.Sprintf("%s no longer falls back to its old bus login", node)}, true + Needs: fmt.Sprintf("abandon the rotation of %s's account, or rotate it again once its new login authenticates.", node)}, true } func orModule(m string) string { diff --git a/modules/mesh-delivery-watchman/internal/watch/signals.go b/modules/mesh-delivery-watchman/internal/watch/signals.go index 8cfef6d4..e72c31da 100644 --- a/modules/mesh-delivery-watchman/internal/watch/signals.go +++ b/modules/mesh-delivery-watchman/internal/watch/signals.go @@ -763,7 +763,7 @@ func (w *Watchman) applyCheck(node string) { } } f := Finding{Scope: ScopeMachine, ID: node, Kind: KindApplyFailing, Machine: node, Severity: Warning, Source: "S21", Since: since, - Said: strings.Join(words, "; "), Resolved: node + " applies what it is sent again", Headline: node + " is not applying its update"} + Said: strings.Join(words, "; "), Headline: node + " is not applying its update"} switch { case len(applied) == 0: f.Summary = fmt.Sprintf("%s's provisioner has failed to place the files of %s since %s (bound %s)", node, @@ -845,7 +845,6 @@ func (w *Watchman) Closed(kind string, c BusClosure) { f.Headline = clip(capital(who)+" was too slow for the bus", 60) f.Explanation = capital(fmt.Sprintf("%s could not keep up with what the bus sent it, so the bus closed its connection (%s "+ "in the last hour) and what was on its way was lost. It connects again by itself; this clears an hour after the last time.", who, count)) - f.Resolved = "Resolved: " + who + " kept up for an hour" } else { reason := strings.ToLower(c.Reason) f.Said = fmt.Sprintf("the bus closed %s's connection %d: %s", c.User, c.CID, c.Reason) @@ -854,7 +853,6 @@ func (w *Watchman) Closed(kind string, c BusClosure) { f.Headline = clip(capital(who)+" was cut off by the bus", 60) f.Explanation = capital(fmt.Sprintf("The bus closed the connection of %s because it broke the bus's rules (%s, %s in the "+ "last hour), so what it was sending was lost. This clears an hour after the last time.", who, reason, count)) - f.Resolved = "Resolved: " + who + " kept to the bus's rules for an hour" } w.find(f) if quiet := AdvisoryQuiet - w.now().Sub(last); quiet > 0 {