Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions internal/evaluator/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ func EvaluateCacheHit(rule rules.Rule, metrics parser.MetricsData, loadLevel rul
result := rules.EvaluationResult{
RuleName: rule.DisplayName,
Status: rules.StatusGreen,
Details: make(map[string]interface{}),
Details: []string{},
Timestamp: time.Now(),
}

Expand Down Expand Up @@ -80,12 +80,13 @@ func EvaluateCacheHit(rule rules.Rule, metrics parser.MetricsData, loadLevel rul
"hits": hits,
"misses": misses,
})
if result.Status == rules.StatusYellow {
switch result.Status {
case rules.StatusYellow:
result.Message = interpolate(rule.Messages.Yellow, hitRate, map[string]interface{}{
"hits": hits,
"misses": misses,
})
} else if result.Status == rules.StatusRed {
case rules.StatusRed:
result.Message = interpolate(rule.Messages.Red, hitRate, map[string]interface{}{
"hits": hits,
"misses": misses,
Expand Down
4 changes: 2 additions & 2 deletions internal/evaluator/composite.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ func EvaluateComposite(rule rules.Rule, metrics parser.MetricsData, loadLevel ru
result := rules.EvaluationResult{
RuleName: rule.DisplayName,
Status: rules.StatusGreen,
Details: make(map[string]interface{}),
Details: []string{},
Timestamp: time.Now(),
}

Expand All @@ -33,7 +33,7 @@ func EvaluateComposite(rule rules.Rule, metrics parser.MetricsData, loadLevel ru
}
value, _ := metric.GetSingleValue()
metricValues[metricDef.Name] = value
result.Details[metricDef.Name] = value
result.Details = append(result.Details, fmt.Sprintf("%s: %.3f", metricDef.Name, value))
}

// Evaluate checks in order
Expand Down
19 changes: 18 additions & 1 deletion internal/evaluator/evaluator_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,9 @@ func EvaluateAllRules(rulesList []rules.Rule, metrics parser.MetricsData, loadLe
result = EvaluateCorrelation(rule, metrics, result)
}

// Add remediation
// Add potential actions (user-facing)
result.Remediation = getRemediation(rule, result.Status)
result.PotentialActionUser = result.Remediation
result.Timestamp = time.Now()

report.Results = append(report.Results, result)
Expand All @@ -125,6 +126,22 @@ func EvaluateAllRules(rulesList []rules.Rule, metrics parser.MetricsData, loadLe
}
}

// Apply general histogram +Inf overflow rule to all histogram metrics
infOverflowResults := EvaluateHistogramInfOverflow(metrics, loadLevel)
for _, result := range infOverflowResults {
report.Results = append(report.Results, result)

// Update summary
switch result.Status {
case rules.StatusRed:
report.Summary.RedCount++
case rules.StatusYellow:
report.Summary.YellowCount++
case rules.StatusGreen:
report.Summary.GreenCount++
}
}

report.Summary.TotalAnalyzed = len(report.Results)

return report
Expand Down
138 changes: 138 additions & 0 deletions internal/evaluator/evaluator_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package evaluator

import (
"strings"
"testing"

"github.com/stackrox/sensor-metrics-analyzer/internal/parser"
Expand Down Expand Up @@ -403,3 +404,140 @@ func TestIsRuleApplicable(t *testing.T) {
})
}
}

func TestEvaluateHistogramInfOverflow(t *testing.T) {
tests := map[string]struct {
metrics parser.MetricsData
wantStatus map[string]rules.Status // metric name -> expected status
wantCount int // expected number of results
}{
"should return red status when >50% in +Inf": {
metrics: parser.MetricsData{
"test_histogram_bucket": &parser.Metric{
Name: "test_histogram_bucket",
Type: "histogram",
Values: []parser.MetricValue{
{Value: 10, Labels: map[string]string{"le": "0.1"}},
{Value: 20, Labels: map[string]string{"le": "0.5"}},
{Value: 30, Labels: map[string]string{"le": "1.0"}},
{Value: 100, Labels: map[string]string{"le": "+Inf"}}, // 70 in +Inf (70%)
},
},
},
wantStatus: map[string]rules.Status{
"test_histogram (+Inf overflow check)": rules.StatusRed,
},
wantCount: 1,
},
"should return yellow status when >25% but <=50% in +Inf": {
metrics: parser.MetricsData{
"test_histogram_bucket": &parser.Metric{
Name: "test_histogram_bucket",
Type: "histogram",
Values: []parser.MetricValue{
{Value: 10, Labels: map[string]string{"le": "0.1"}},
{Value: 20, Labels: map[string]string{"le": "0.5"}},
{Value: 30, Labels: map[string]string{"le": "1.0"}},
{Value: 50, Labels: map[string]string{"le": "+Inf"}}, // 20 in +Inf (40%)
},
},
},
wantStatus: map[string]rules.Status{
"test_histogram (+Inf overflow check)": rules.StatusYellow,
},
wantCount: 1,
},
"should return green status when <=25% in +Inf": {
metrics: parser.MetricsData{
"test_histogram_bucket": &parser.Metric{
Name: "test_histogram_bucket",
Type: "histogram",
Values: []parser.MetricValue{
{Value: 10, Labels: map[string]string{"le": "0.1"}},
{Value: 20, Labels: map[string]string{"le": "0.5"}},
{Value: 30, Labels: map[string]string{"le": "1.0"}},
{Value: 35, Labels: map[string]string{"le": "+Inf"}}, // 5 in +Inf (14.3%)
},
},
},
wantStatus: map[string]rules.Status{
"test_histogram (+Inf overflow check)": rules.StatusGreen,
},
wantCount: 1,
},
"should skip histogram without +Inf bucket": {
metrics: parser.MetricsData{
"test_histogram_bucket": &parser.Metric{
Name: "test_histogram_bucket",
Type: "histogram",
Values: []parser.MetricValue{
{Value: 10, Labels: map[string]string{"le": "0.1"}},
{Value: 20, Labels: map[string]string{"le": "0.5"}},
},
},
},
wantStatus: map[string]rules.Status{},
wantCount: 0,
},
"should handle multiple histograms": {
metrics: parser.MetricsData{
"hist1_bucket": &parser.Metric{
Name: "hist1_bucket",
Type: "histogram",
Values: []parser.MetricValue{
{Value: 10, Labels: map[string]string{"le": "1.0"}},
{Value: 100, Labels: map[string]string{"le": "+Inf"}}, // 90 in +Inf (90%)
},
},
"hist2_bucket": &parser.Metric{
Name: "hist2_bucket",
Type: "histogram",
Values: []parser.MetricValue{
{Value: 10, Labels: map[string]string{"le": "1.0"}},
{Value: 15, Labels: map[string]string{"le": "+Inf"}}, // 5 in +Inf (33%)
},
},
},
wantStatus: map[string]rules.Status{
"hist1 (+Inf overflow check)": rules.StatusRed,
"hist2 (+Inf overflow check)": rules.StatusYellow,
},
wantCount: 2,
},
}

for name, tt := range tests {
t.Run(name, func(t *testing.T) {
results := EvaluateHistogramInfOverflow(tt.metrics, rules.LoadLevelMedium)

if len(results) != tt.wantCount {
t.Errorf("EvaluateHistogramInfOverflow() returned %d results, want %d", len(results), tt.wantCount)
}

for _, result := range results {
wantStatus, exists := tt.wantStatus[result.RuleName]
if !exists {
t.Errorf("Unexpected result for metric %s", result.RuleName)
continue
}

if result.Status != wantStatus {
t.Errorf("EvaluateHistogramInfOverflow() for %s = %v, want %v", result.RuleName, result.Status, wantStatus)
}

// Verify message contains expected information
if result.Status != rules.StatusGreen {
if result.Message == "" {
t.Error("Message should not be empty for non-green status")
}
if !strings.Contains(result.Message, "Highest non-infinity bucket") {
t.Error("Message should contain 'Highest non-infinity bucket'")
}
if !strings.Contains(result.Message, "didn't expect") {
t.Error("Message should contain explanation about designer expectations")
}
}
}
})
}
}
26 changes: 12 additions & 14 deletions internal/evaluator/gauge.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ func EvaluateGauge(rule rules.Rule, metrics parser.MetricsData, loadLevel rules.
result := rules.EvaluationResult{
RuleName: rule.MetricName,
Status: rules.StatusGreen,
Details: make(map[string]interface{}),
Details: []string{},
Timestamp: time.Now(),
}

Expand Down Expand Up @@ -98,20 +98,18 @@ func interpolate(template string, value float64, extras map[string]interface{})
})

// Replace other placeholders from extras map
if extras != nil {
for key, val := range extras {
// Handle format specifiers
keyRe := regexp.MustCompile(`\{` + regexp.QuoteMeta(key) + `(?::[^}]+)?\}`)
result = keyRe.ReplaceAllStringFunc(result, func(match string) string {
if strings.Contains(match, ":") {
formatMatch := regexp.MustCompile(`:([^}]+)`)
if fm := formatMatch.FindStringSubmatch(match); len(fm) == 2 {
return fmt.Sprintf("%"+fm[1], val)
}
for key, val := range extras {
// Handle format specifiers
keyRe := regexp.MustCompile(`\{` + regexp.QuoteMeta(key) + `(?::[^}]+)?\}`)
result = keyRe.ReplaceAllStringFunc(result, func(match string) string {
if strings.Contains(match, ":") {
formatMatch := regexp.MustCompile(`:([^}]+)`)
if fm := formatMatch.FindStringSubmatch(match); len(fm) == 2 {
return fmt.Sprintf("%"+fm[1], val)
}
return fmt.Sprintf("%v", val)
})
}
}
return fmt.Sprintf("%v", val)
})
}

return result
Expand Down
Loading
Loading