diff --git a/pkg/agentdrain/README.md b/pkg/agentdrain/README.md index 75e1424894b..222c5beb1b1 100644 --- a/pkg/agentdrain/README.md +++ b/pkg/agentdrain/README.md @@ -88,12 +88,11 @@ Describes anomalies detected for a log line. ```go type AnomalyReport struct { - IsNewTemplate bool // Line created a new cluster - LowSimilarity bool // Best match score was below SimThreshold - RareCluster bool // Matched cluster has been seen ≤ RareClusterThreshold times - NewClusterCreated bool // This event produced a brand-new cluster - AnomalyScore float64 // Weighted composite score in [0, 1] - Reason string // Human-readable anomaly description + IsNewTemplate bool // Line produced a brand-new log cluster + LowSimilarity bool // Best match score was below SimThreshold + RareCluster bool // Matched cluster has been seen ≤ RareClusterThreshold times + AnomalyScore float64 // Weighted composite score in [0, 1] + Reason string // Human-readable anomaly description } ``` diff --git a/pkg/agentdrain/anomaly.go b/pkg/agentdrain/anomaly.go index fe7a9b44885..8b8d41be904 100644 --- a/pkg/agentdrain/anomaly.go +++ b/pkg/agentdrain/anomaly.go @@ -36,8 +36,7 @@ func NewAnomalyDetector(simThreshold float64, rareClusterThreshold int) (*Anomal // - cluster is the cluster that was matched or created. func (d *AnomalyDetector) Analyze(result *MatchResult, isNew bool, cluster *Cluster) *AnomalyReport { report := &AnomalyReport{ - IsNewTemplate: isNew, - NewClusterCreated: isNew, + IsNewTemplate: isNew, // LowSimilarity is mutually exclusive with IsNewTemplate: brand-new templates are // already classified as anomalies, so we only evaluate similarity for existing ones. LowSimilarity: !isNew && result.Similarity < d.threshold, diff --git a/pkg/agentdrain/anomaly_test.go b/pkg/agentdrain/anomaly_test.go index cbd545c0880..30be6d67568 100644 --- a/pkg/agentdrain/anomaly_test.go +++ b/pkg/agentdrain/anomaly_test.go @@ -18,14 +18,13 @@ func TestAnomalyDetector_Analyze(t *testing.T) { isNew bool cluster *Cluster wantIsNewTemplate bool - wantNewCluster bool wantLowSimilarity bool wantRareCluster bool wantScore float64 wantReason string }{ { - // isNew=true → both IsNewTemplate and NewClusterCreated; size=1 ≤ rareThreshold=2 → RareCluster. + // isNew=true → IsNewTemplate; size=1 ≤ rareThreshold=2 → RareCluster. // score = (1.0 + 0.3) / 2.0 = 0.65 name: "new template creates cluster and is also rare", simThreshold: 0.4, @@ -34,7 +33,6 @@ func TestAnomalyDetector_Analyze(t *testing.T) { isNew: true, cluster: &Cluster{ID: 1, Template: []string{"stage=plan"}, Size: 1}, wantIsNewTemplate: true, - wantNewCluster: true, wantLowSimilarity: false, wantRareCluster: true, wantScore: 0.65, @@ -50,7 +48,6 @@ func TestAnomalyDetector_Analyze(t *testing.T) { isNew: false, cluster: &Cluster{ID: 1, Template: []string{"a", "b", "c"}, Size: 5}, wantIsNewTemplate: false, - wantNewCluster: false, wantLowSimilarity: true, wantRareCluster: false, wantScore: 0.35, @@ -66,7 +63,6 @@ func TestAnomalyDetector_Analyze(t *testing.T) { isNew: false, cluster: &Cluster{ID: 1, Template: []string{"a"}, Size: 1}, wantIsNewTemplate: false, - wantNewCluster: false, wantLowSimilarity: false, wantRareCluster: true, wantScore: 0.15, @@ -81,7 +77,6 @@ func TestAnomalyDetector_Analyze(t *testing.T) { isNew: false, cluster: &Cluster{ID: 1, Template: []string{"a", "b"}, Size: 100}, wantIsNewTemplate: false, - wantNewCluster: false, wantLowSimilarity: false, wantRareCluster: false, wantScore: 0.0, @@ -96,7 +91,6 @@ func TestAnomalyDetector_Analyze(t *testing.T) { isNew: false, cluster: &Cluster{ID: 1, Template: []string{"a"}, Size: 5}, wantIsNewTemplate: false, - wantNewCluster: false, wantLowSimilarity: false, wantRareCluster: false, wantScore: 0.0, @@ -112,7 +106,6 @@ func TestAnomalyDetector_Analyze(t *testing.T) { isNew: false, cluster: &Cluster{ID: 1, Template: []string{"a"}, Size: 5}, wantIsNewTemplate: false, - wantNewCluster: false, wantLowSimilarity: true, wantRareCluster: false, wantScore: 0.35, @@ -128,7 +121,6 @@ func TestAnomalyDetector_Analyze(t *testing.T) { isNew: false, cluster: &Cluster{ID: 1, Template: []string{"a"}, Size: 1}, wantIsNewTemplate: false, - wantNewCluster: false, wantLowSimilarity: true, wantRareCluster: true, wantScore: 0.5, @@ -143,7 +135,6 @@ func TestAnomalyDetector_Analyze(t *testing.T) { isNew: false, cluster: nil, wantIsNewTemplate: false, - wantNewCluster: false, wantLowSimilarity: false, wantRareCluster: false, wantScore: 0.0, @@ -159,7 +150,6 @@ func TestAnomalyDetector_Analyze(t *testing.T) { isNew: true, cluster: &Cluster{ID: 1, Template: []string{"a"}, Size: 5}, wantIsNewTemplate: true, - wantNewCluster: true, wantLowSimilarity: false, wantRareCluster: true, wantScore: 0.65, @@ -177,7 +167,6 @@ func TestAnomalyDetector_Analyze(t *testing.T) { require.NotNil(t, report, "Analyze should always return a non-nil report") assert.Equal(t, tt.wantIsNewTemplate, report.IsNewTemplate, "IsNewTemplate mismatch") - assert.Equal(t, tt.wantNewCluster, report.NewClusterCreated, "NewClusterCreated mismatch") assert.Equal(t, tt.wantLowSimilarity, report.LowSimilarity, "LowSimilarity mismatch") assert.Equal(t, tt.wantRareCluster, report.RareCluster, "RareCluster mismatch") assert.InDelta(t, tt.wantScore, report.AnomalyScore, 1e-9, "AnomalyScore mismatch") @@ -342,28 +331,24 @@ func TestAnalyzeEvent(t *testing.T) { name string event AgentEvent wantIsNew bool - wantNewCluster bool errorDescription string }{ { name: "first occurrence is flagged as new template", event: evtPlan, wantIsNew: true, - wantNewCluster: true, errorDescription: "first event", }, { name: "second identical occurrence is not flagged as new", event: evtPlan, wantIsNew: false, - wantNewCluster: false, errorDescription: "second identical event", }, { name: "distinct event creates its own new template", event: evtFinish, wantIsNew: true, - wantNewCluster: true, errorDescription: "distinct event", }, } @@ -375,7 +360,6 @@ func TestAnalyzeEvent(t *testing.T) { require.NotNil(t, result, "AnalyzeEvent should return a non-nil result") require.NotNil(t, report, "AnalyzeEvent should return a non-nil report") assert.Equal(t, tt.wantIsNew, report.IsNewTemplate, "IsNewTemplate mismatch") - assert.Equal(t, tt.wantNewCluster, report.NewClusterCreated, "NewClusterCreated mismatch") }) } } diff --git a/pkg/agentdrain/spec_test.go b/pkg/agentdrain/spec_test.go index 57f5327edd1..c6e78e84322 100644 --- a/pkg/agentdrain/spec_test.go +++ b/pkg/agentdrain/spec_test.go @@ -425,7 +425,7 @@ func TestSpec_Types_MatchResult(t *testing.T) { } // TestSpec_Types_AnomalyReport validates the documented AnomalyReport type structure. -// Spec: IsNewTemplate, LowSimilarity, RareCluster, NewClusterCreated, AnomalyScore in [0,1], Reason. +// Spec: IsNewTemplate, LowSimilarity, RareCluster, AnomalyScore in [0,1], Reason. func TestSpec_Types_AnomalyReport(t *testing.T) { cfg := agentdrain.DefaultConfig() miner, err := agentdrain.NewMiner(cfg) @@ -439,7 +439,6 @@ func TestSpec_Types_AnomalyReport(t *testing.T) { _ = report.IsNewTemplate _ = report.LowSimilarity _ = report.RareCluster - _ = report.NewClusterCreated _ = report.Reason assert.GreaterOrEqual(t, report.AnomalyScore, 0.0, "AnomalyReport.AnomalyScore should be in documented range [0, 1]") assert.LessOrEqual(t, report.AnomalyScore, 1.0, "AnomalyReport.AnomalyScore should be in documented range [0, 1]") diff --git a/pkg/agentdrain/types.go b/pkg/agentdrain/types.go index 08df039cd43..e74af00f132 100644 --- a/pkg/agentdrain/types.go +++ b/pkg/agentdrain/types.go @@ -56,14 +56,12 @@ type MatchResult struct { // AnomalyReport describes anomalies detected for a log line. type AnomalyReport struct { - // IsNewTemplate is true when the log line created a new cluster. + // IsNewTemplate is true when the log line produced a brand-new log cluster. IsNewTemplate bool // LowSimilarity is true when the best match score was below the configured threshold. LowSimilarity bool // RareCluster is true when the matched cluster has been seen fewer times than the rare threshold. RareCluster bool - // NewClusterCreated is true when this event produced a brand-new cluster. - NewClusterCreated bool // AnomalyScore is a weighted composite score in the range [0, 1]. AnomalyScore float64 // Reason is a human-readable description of all anomalies that were detected.