Skip to content

Commit 98be58e

Browse files
committed
feat: Add resiliency data and functions in resiliency package
Signed-off-by: Abhinav Sharma <abhinavs1920bpl@gmail.com>
1 parent ac5c0f3 commit 98be58e

1 file changed

Lines changed: 191 additions & 0 deletions

File tree

pkg/resiliency/resiliency.go

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
package resiliency
2+
3+
// Package resiliency centralises all logic related to parsing and aggregating
4+
// resiliency reports that are emitted by the krkn engine. A report is printed
5+
// on a single log line prefixed by the token `KRKN_RESILIENCY_REPORT_JSON:`
6+
// and contains a JSON payload describing the outcome of one(for krknctl) or more chaos
7+
// scenarios executed by the engine.
8+
//
9+
// The goal of this package is to provide:
10+
// 1. A small set of structs mirroring the JSON schema.
11+
// 2. A parser capable of extracting and unmarshalling the payload from a
12+
// raw block of log text.
13+
// 3. Helper utilities to merge many individual reports into a single final
14+
// summary which is later persisted to `resiliency-report.json` by the
15+
// CLI.
16+
17+
import (
18+
"encoding/json"
19+
"errors"
20+
"fmt"
21+
"os"
22+
"regexp"
23+
)
24+
25+
// ----------------------------------------------------------------------------
26+
// Data structures
27+
// ----------------------------------------------------------------------------
28+
29+
type OverallResiliencyReport struct {
30+
Scenarios map[string]float64 `json:"scenarios"`
31+
ResiliencyScore float64 `json:"resiliency_score"`
32+
PassedSlos int `json:"passed_slos"`
33+
TotalSlos int `json:"total_slos"`
34+
}
35+
36+
37+
// ----------------------------------------------------------------------------
38+
39+
40+
type DetailedScenarioReport struct {
41+
OverallReport OverallResiliencyReport
42+
}
43+
44+
// ----------------------------------------------------------------------------
45+
46+
type FinalReport struct {
47+
Scenarios map[string]float64 `json:"scenarios"`
48+
ResiliencyScore float64 `json:"resiliency_score"`
49+
PassedSlos int `json:"passed_slos"`
50+
TotalSlos int `json:"total_slos"`
51+
}
52+
53+
// ----------------------------------------------------------------------------
54+
// Parser & Aggregator
55+
// ----------------------------------------------------------------------------
56+
57+
var reportRegex = regexp.MustCompile(`KRKN_RESILIENCY_REPORT_JSON:\s*(\{.*)`)
58+
59+
// ParseResiliencyReport searches the supplied log bytes for a line prefixed by
60+
// the special token and, if found, attempts to unmarshal the trailing JSON into
61+
// a DetailedScenarioReport.
62+
func ParseResiliencyReport(logContent []byte) (*DetailedScenarioReport, error) {
63+
match := reportRegex.FindSubmatch(logContent)
64+
if len(match) < 2 {
65+
return nil, errors.New("resiliency report marker not found in logs")
66+
}
67+
68+
raw := match[1]
69+
70+
var rep DetailedScenarioReport
71+
72+
// 1. Direct overall_resiliency_report at root.
73+
type root1 struct {
74+
Overall OverallResiliencyReport `json:"overall_resiliency_report"`
75+
}
76+
var r1 root1
77+
if err := json.Unmarshal(raw, &r1); err == nil && r1.Overall.ResiliencyScore != 0 {
78+
rep.OverallReport = r1.Overall
79+
return &rep, nil
80+
}
81+
82+
// 2. Nested under telemetry.overall_resiliency_report.
83+
type root2 struct {
84+
Telemetry struct {
85+
Overall OverallResiliencyReport `json:"overall_resiliency_report"`
86+
} `json:"telemetry"`
87+
}
88+
var r2 root2
89+
if err := json.Unmarshal(raw, &r2); err == nil && r2.Telemetry.Overall.ResiliencyScore != 0 {
90+
rep.OverallReport = r2.Telemetry.Overall
91+
return &rep, nil
92+
}
93+
94+
// 3. As a map of scenario scores at root with optional aggregate values.
95+
type root3 struct {
96+
Scenarios map[string]float64 `json:"scenarios"`
97+
ResiliencyScore float64 `json:"resiliency_score"`
98+
PassedSlos int `json:"passed_slos"`
99+
TotalSlos int `json:"total_slos"`
100+
}
101+
var r3 root3
102+
if err := json.Unmarshal(raw, &r3); err == nil && len(r3.Scenarios) > 0 {
103+
rep.OverallReport = OverallResiliencyReport{
104+
Scenarios: r3.Scenarios,
105+
ResiliencyScore: r3.ResiliencyScore,
106+
PassedSlos: r3.PassedSlos,
107+
TotalSlos: r3.TotalSlos,
108+
}
109+
return &rep, nil
110+
}
111+
112+
// 4. scenarios as an array of objects with name+score.
113+
type scenarioItem struct {
114+
Name string `json:"name"`
115+
Score float64 `json:"score"`
116+
Breakdown struct {
117+
Passed int `json:"passed"`
118+
Failed int `json:"failed"`
119+
} `json:"breakdown"`
120+
}
121+
type root4 struct {
122+
Scenarios []scenarioItem `json:"scenarios"`
123+
}
124+
var r4 root4
125+
if err := json.Unmarshal(raw, &r4); err == nil && len(r4.Scenarios) > 0 {
126+
m := make(map[string]float64)
127+
var total float64
128+
var passed, totalSLOs int
129+
for _, it := range r4.Scenarios {
130+
m[it.Name] = it.Score
131+
total += it.Score
132+
passed += it.Breakdown.Passed
133+
totalSLOs += it.Breakdown.Passed + it.Breakdown.Failed
134+
}
135+
avg := total / float64(len(r4.Scenarios))
136+
rep.OverallReport = OverallResiliencyReport{
137+
Scenarios: m,
138+
ResiliencyScore: avg,
139+
PassedSlos: passed,
140+
TotalSlos: totalSLOs,
141+
}
142+
return &rep, nil
143+
}
144+
145+
return nil, errors.New("unrecognised resiliency report JSON structure")
146+
}
147+
148+
/// TODO: @abhinavs1920 (Implement weighted average of scores)
149+
func AggregateReports(reports []DetailedScenarioReport) FinalReport {
150+
final := FinalReport{
151+
Scenarios: make(map[string]float64),
152+
}
153+
154+
var scoreSum float64
155+
var scoreCount int
156+
157+
for _, rep := range reports {
158+
// Merge per-scenario scores.
159+
for name, score := range rep.OverallReport.Scenarios {
160+
final.Scenarios[name] = score
161+
scoreSum += score
162+
scoreCount++
163+
}
164+
// Aggregate SLO counters.
165+
final.PassedSlos += rep.OverallReport.PassedSlos
166+
final.TotalSlos += rep.OverallReport.TotalSlos
167+
}
168+
169+
if scoreCount > 0 {
170+
final.ResiliencyScore = scoreSum / float64(scoreCount)
171+
} else {
172+
// No data -> perfect score.
173+
final.ResiliencyScore = 100.0
174+
}
175+
176+
return final
177+
}
178+
179+
func WriteFinalReport(report FinalReport, filename string) error {
180+
data, err := json.MarshalIndent(report, "", " ")
181+
if err != nil {
182+
return err
183+
}
184+
return os.WriteFile(filename, data, 0o644)
185+
}
186+
187+
func PrintHumanSummary(report FinalReport) {
188+
if data, err := json.MarshalIndent(report, "", " "); err == nil {
189+
fmt.Printf("Overall Resiliency Report Summary:\n%s\n", string(data))
190+
}
191+
}

0 commit comments

Comments
 (0)