Files
mesh-delivery/modules/mesh-delivery-watchman/internal/watch/finding.go
T

212 lines
7.6 KiB
Go

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 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 = Observer + "_findings"
// Observer is the name a finding of the watchman carries.
const Observer = "mesh-delivery-watchman"
// The scopes and severities a finding of the watchman names, as the controller's conditions name them, so the keeper's
// `compare` meets the same keys.
const (
ScopeMachine = mcc.ScopeMachine
ScopeDelivery = mcc.ScopeDelivery
ScopeModule = mcc.ScopeModule
ScopeProvider = mcc.ScopeProvider
ScopeBus = mcc.ScopeBus
ScopeCore = mcc.ScopeCore
Urgent = mcc.Urgent
Warning = mcc.Warning
)
// Finding is what the watchman builds of one thing it observes wrong: its subject (scope, id, the machine and the others
// it concerns), kind and severity, its evidence (Summary, and Said with Since, its one observation), the words the
// operator reads (ADR 0253; empty leaves the keeper's wording of the kind), and the row or fact it comes from (Source).
// It is written as the seat's finding.
type Finding struct {
Scope, ID, Token, Kind, Machine string
Also []string
Severity string
Summary, Said string
Headline, Explanation 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
// thing observed again writes the same bytes.
Since time.Time
}
// FindingKey composes a finding's key as the controller composes a condition's (mcc.Key).
func FindingKey(scope, id, token string) string { return mcc.Key(scope, id, token) }
// Key is the finding's key: its token, or its kind when it has none.
func (f Finding) Key() string {
token := f.Token
if token == "" {
token = f.Kind
}
return FindingKey(f.Scope, f.ID, token)
}
// KeyID is an id as the parts of a key: each dot-separated part made safe for a bus key, as the key of a condition is
// (mcc.Key), so the finding's key keeps the shape its condition had.
func KeyID(id string) string {
parts := strings.Split(id, ".")
for i, p := range parts {
p = unsafeKey.ReplaceAllString(strings.TrimSpace(p), "_")
if p == "" {
p = "_"
}
parts[i] = p
}
return strings.Join(parts, ".")
}
// 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 := KeyID(f.ID), ""
if f.Token != "" && f.Token != f.Kind {
ref = f.Token
id = id + "." + KeyID(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) {
var h condseat.Finding
if err := json.Unmarshal(data, &h); err != nil {
return Finding{}, err
}
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, Source: h.Source, Observer: h.Observer, Since: h.Since}
if len(h.Evidence) > 0 {
f.Said = h.Evidence[0].Said
}
return f, nil
}
// find writes a finding when what it says differs from what the bucket holds (ADR 0289 §1).
func (w *Watchman) find(f Finding) {
value, err := EncodeFinding(f)
if err != nil {
w.say("no finding: %v", err)
return
}
key := f.Key()
delete(w.seededFindings, key)
if string(w.findings[key]) == string(value) {
return
}
if err := w.out.PutFinding(key, value); err != nil {
w.say("cannot write the finding %s: %v", key, err)
return
}
w.findings[key] = value
}
// unfind deletes a finding the watchman no longer observes.
func (w *Watchman) unfind(key string) {
if _, ok := w.findings[key]; !ok {
return
}
if err := w.out.DeleteFinding(key); err != nil {
w.say("cannot delete the finding %s: %v", key, err)
return
}
delete(w.findings, key)
}
// SeedFinding tells the watchman a finding that stands in its bucket at its start; ReconcileFindings deletes it unless what
// the watchman reads at its start observes it again.
func (w *Watchman) SeedFinding(key string, value []byte) {
w.mu.Lock()
defer w.mu.Unlock()
w.findings[key] = value
w.seededFindings[key] = true
}
// Findings is every finding the watchman holds now, by key.
func (w *Watchman) Findings() map[string]Finding {
w.mu.Lock()
defer w.mu.Unlock()
out := map[string]Finding{}
for k, v := range w.findings {
if f, err := DecodeFinding(v); err == nil {
out[k] = f
}
}
return out
}
// Rewrite writes every finding the watchman holds again (resync, ADR 0564 §12): a bucket cleared or a key lost by hand is
// written back as the watchman observes it. It answers how many it wrote.
func (w *Watchman) Rewrite() int {
w.mu.Lock()
defer w.mu.Unlock()
n := 0
for key, value := range w.findings {
if err := w.out.PutFinding(key, value); err != nil {
w.say("cannot write the finding %s again: %v", key, err)
continue
}
n++
}
return n
}
// NotHeard is the watchman's finding of a family it does not hear (heard.Kind, ADR 0615 §5): a consumer not there yet, a
// hearing or a watch that could not start; consequence says what goes unheard. Heard ends it, once the consumer binds.
func (w *Watchman) NotHeard(family, consequence string, why error) {
w.mu.Lock()
defer w.mu.Unlock()
summary, headline, explanation := heard.Words(Observer, family, consequence, why)
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})
}
// Heard ends the finding that the watchman does not hear family.
func (w *Watchman) Heard(family string) {
w.mu.Lock()
defer w.mu.Unlock()
w.unfind(heard.Key(Observer, family))
}