checker: add RuleName field to CheckState instead of overloading Code

Rules can now set Code freely without the server clobbering it; the
originating rule is reported separately via RuleName.
This commit is contained in:
nemunaire 2026-04-23 14:50:00 +07:00
commit c9ee6655ca
4 changed files with 57 additions and 13 deletions

View file

@ -302,11 +302,12 @@ var checkResultTemplate = template.Must(template.New("result").Funcs(templateFun
{{if .States}}
<h2>Check states</h2>
<table>
<thead><tr><th>Status</th><th>Code</th><th>Subject</th><th>Message</th></tr></thead>
<thead><tr><th>Status</th><th>Rule</th><th>Code</th><th>Subject</th><th>Message</th></tr></thead>
<tbody>
{{range .States}}
<tr>
<td><span class="badge {{statusClass .Status}}">{{statusString .Status}}</span></td>
<td>{{.RuleName}}</td>
<td>{{.Code}}</td>
<td>{{.Subject}}</td>
<td>{{.Message}}</td>

View file

@ -312,9 +312,7 @@ func (s *Server) evaluateRules(ctx context.Context, obs ObservationGetter, opts
}}
}
for _, state := range ruleStates {
if state.Code == "" {
state.Code = rule.Name()
}
state.RuleName = rule.Name()
states = append(states, state)
}
}

View file

@ -69,6 +69,18 @@ func (r *dummyRule) Evaluate(ctx context.Context, obs ObservationGetter, opts Ch
return []CheckState{{Status: StatusOK, Message: r.name + " passed"}}
}
// codedRule emits a CheckState with a pre-set Code, to verify the server
// stamps RuleName without clobbering rule-provided codes.
type codedRule struct {
name, code string
}
func (r *codedRule) Name() string { return r.name }
func (r *codedRule) Description() string { return "" }
func (r *codedRule) Evaluate(ctx context.Context, obs ObservationGetter, opts CheckerOptions) []CheckState {
return []CheckState{{Status: StatusWarn, Code: r.code, Message: "coded finding"}}
}
// --- helpers ---
func newTestServer(p *testProvider) *Server {
@ -349,8 +361,11 @@ func TestServer_Evaluate(t *testing.T) {
if len(resp.States) != 2 {
t.Fatalf("evaluate states = %d, want 2", len(resp.States))
}
if resp.States[0].Code != "rule1" {
t.Errorf("evaluate state[0].Code = %q, want \"rule1\"", resp.States[0].Code)
if resp.States[0].RuleName != "rule1" {
t.Errorf("evaluate state[0].RuleName = %q, want \"rule1\"", resp.States[0].RuleName)
}
if resp.States[0].Code != "" {
t.Errorf("evaluate state[0].Code = %q, want empty (rule did not set one)", resp.States[0].Code)
}
}
@ -379,8 +394,37 @@ func TestServer_Evaluate_DisabledRule(t *testing.T) {
if len(resp.States) != 1 {
t.Fatalf("evaluate with disabled rule: states = %d, want 1", len(resp.States))
}
if resp.States[0].Code != "rule2" {
t.Errorf("remaining state code = %q, want \"rule2\"", resp.States[0].Code)
if resp.States[0].RuleName != "rule2" {
t.Errorf("remaining state rule name = %q, want \"rule2\"", resp.States[0].RuleName)
}
}
func TestServer_Evaluate_RulePreservesCode(t *testing.T) {
def := &CheckerDefinition{
ID: "test-checker",
Rules: []CheckRule{
&codedRule{name: "ruleA", code: "too_many_lookups"},
},
}
p := &testProvider{key: "test", definition: def}
srv := newTestServer(p)
rec := doRequest(srv.Handler(), "POST", "/evaluate", ExternalEvaluateRequest{
Observations: map[ObservationKey]json.RawMessage{"test": json.RawMessage(`{}`)},
}, nil)
if rec.Code != http.StatusOK {
t.Fatalf("POST /evaluate = %d, want %d", rec.Code, http.StatusOK)
}
var resp ExternalEvaluateResponse
json.NewDecoder(rec.Body).Decode(&resp)
if len(resp.States) != 1 {
t.Fatalf("states = %d, want 1", len(resp.States))
}
if resp.States[0].RuleName != "ruleA" {
t.Errorf("state.RuleName = %q, want \"ruleA\"", resp.States[0].RuleName)
}
if resp.States[0].Code != "too_many_lookups" {
t.Errorf("state.Code = %q, want \"too_many_lookups\" (rule-set code must be preserved)", resp.States[0].Code)
}
}

View file

@ -187,11 +187,12 @@ func (s Status) String() string {
// (a hostname, a record key, a serial, …). Leave Subject empty for rules
// that produce a single, global result.
type CheckState struct {
Status Status `json:"status"`
Message string `json:"message"`
Code string `json:"code,omitempty"`
Subject string `json:"subject,omitempty"`
Meta map[string]any `json:"meta,omitempty"`
Status Status `json:"status"`
Message string `json:"message"`
RuleName string `json:"rule,omitempty"`
Code string `json:"code,omitempty"`
Subject string `json:"subject,omitempty"`
Meta map[string]any `json:"meta,omitempty"`
}
// CheckMetric represents a single metric produced by a check.