G findings4 watchman: the watchman's findings are the seat's generated type, written through mesh-sdk's finding writer; the resolved words go

Builds: design 47 G §689, §686 (A-3), §461, §384, A-13, A-14, A-22
Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
2026-11-11 21:55:43 +01:00
parent c87fc4e6cf
commit 8cbb86dfbd
9 changed files with 218 additions and 52 deletions
@@ -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")
}
@@ -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) {
@@ -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())
+1 -1
View File
@@ -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
@@ -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.
@@ -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 `<scope>.<id>.<kind>` 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 `<scope>.<id>.<kind>` 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 `<id>.<ref>` (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.
@@ -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)
}
}
}
@@ -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 {
@@ -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 {