mirror of
https://github.com/prymitive/karma
synced 2026-05-07 03:26:52 +00:00
600 lines
15 KiB
Go
600 lines
15 KiB
Go
package models_test
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"slices"
|
|
"testing"
|
|
|
|
"github.com/beme/abide"
|
|
"github.com/prometheus/prometheus/model/labels"
|
|
|
|
"github.com/prymitive/karma/internal/models"
|
|
)
|
|
|
|
func TestColorString(t *testing.T) {
|
|
type testCaseT struct {
|
|
color models.Color
|
|
expected string
|
|
}
|
|
|
|
testCases := []testCaseT{
|
|
// verifies zero-value color produces all zeros
|
|
{
|
|
color: models.Color{Red: 0, Green: 0, Blue: 0, Alpha: 0},
|
|
expected: "rgba(0,0,0,0)",
|
|
},
|
|
// verifies max-value color produces all 255s
|
|
{
|
|
color: models.Color{Red: 255, Green: 255, Blue: 255, Alpha: 255},
|
|
expected: "rgba(255,255,255,255)",
|
|
},
|
|
// verifies mixed values are formatted correctly
|
|
{
|
|
color: models.Color{Red: 10, Green: 20, Blue: 30, Alpha: 128},
|
|
expected: "rgba(10,20,30,128)",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
result := tc.color.String()
|
|
if result != tc.expected {
|
|
t.Errorf("Color.String() returned %q, expected %q", result, tc.expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDedupSharedMaps(t *testing.T) {
|
|
ag := models.AlertGroup{
|
|
Receiver: "default",
|
|
Labels: labels.FromStrings("alertname", "FakeAlert"),
|
|
Alerts: models.AlertList{
|
|
models.Alert{
|
|
Receiver: "default",
|
|
State: models.AlertStateSuppressed,
|
|
Annotations: models.Annotations{
|
|
models.Annotation{
|
|
Name: "summary",
|
|
Value: "this is summary",
|
|
},
|
|
models.Annotation{
|
|
Name: "foo",
|
|
Value: "bar",
|
|
},
|
|
},
|
|
Labels: labels.FromStrings("alertname", "FakeAlert", "instance", "1", "job", "node_exporter"),
|
|
Alertmanager: []models.AlertmanagerInstance{
|
|
{
|
|
State: models.AlertStateSuppressed,
|
|
Fingerprint: "1",
|
|
Name: "am1",
|
|
Cluster: "fakeCluster",
|
|
SilencedBy: []string{"fakeSilence1", "fakeSilence2"},
|
|
Source: "https://prom.example.com/graph?foo",
|
|
},
|
|
{
|
|
State: models.AlertStateSuppressed,
|
|
Fingerprint: "2",
|
|
Name: "am2",
|
|
Cluster: "fakeCluster",
|
|
SilencedBy: []string{"fakeSilence1", "fakeSilence2"},
|
|
Source: "https://prom.example.com/subdir/graph?bar",
|
|
},
|
|
},
|
|
},
|
|
models.Alert{
|
|
Receiver: "default",
|
|
State: models.AlertStateActive,
|
|
Annotations: models.Annotations{
|
|
models.Annotation{
|
|
Name: "summary",
|
|
Value: "this is summary",
|
|
},
|
|
},
|
|
Labels: labels.FromStrings("alertname", "FakeAlert", "instance", "2", "job", "node_exporter"),
|
|
Alertmanager: []models.AlertmanagerInstance{
|
|
{
|
|
State: models.AlertStateActive,
|
|
Fingerprint: "1",
|
|
Name: "am1",
|
|
Cluster: "fakeCluster",
|
|
SilencedBy: []string{"fakeSilence1", "fakeSilence2"},
|
|
Source: "https://am.example.com",
|
|
},
|
|
{
|
|
State: models.AlertStateActive,
|
|
Fingerprint: "1",
|
|
Name: "am2",
|
|
Cluster: "fakeCluster",
|
|
SilencedBy: []string{"fakeSilence1", "fakeSilence2"},
|
|
Source: "https://am.example.com",
|
|
},
|
|
},
|
|
},
|
|
models.Alert{
|
|
Receiver: "default",
|
|
State: models.AlertStateSuppressed,
|
|
Annotations: models.Annotations{
|
|
models.Annotation{
|
|
Name: "summary",
|
|
Value: "this is summary",
|
|
},
|
|
},
|
|
Labels: labels.FromStrings("alertname", "FakeAlert", "extra", "ignore", "instance", "3", "job", "blackbox"),
|
|
Alertmanager: []models.AlertmanagerInstance{
|
|
{
|
|
State: models.AlertStateSuppressed,
|
|
Fingerprint: "1",
|
|
Name: "am1",
|
|
Cluster: "fakeCluster",
|
|
SilencedBy: []string{"fakeSilence1", "fakeSilence2"},
|
|
Source: "https://am.example.com/graph",
|
|
},
|
|
{
|
|
State: models.AlertStateSuppressed,
|
|
Fingerprint: "1",
|
|
Name: "am2",
|
|
Cluster: "fakeCluster",
|
|
SilencedBy: []string{"fakeSilence1", "fakeSilence2"},
|
|
Source: "https://am.example.com/graph",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
shared, allLabels := ag.DedupSharedMaps(nil)
|
|
apiAG := models.NewAPIAlertGroup(ag, shared, allLabels, len(ag.Alerts))
|
|
|
|
agJSON, _ := json.MarshalIndent(apiAG, "", " ")
|
|
abide.AssertReader(t, "SharedMaps", bytes.NewReader(agJSON))
|
|
}
|
|
|
|
func TestDedupSharedMapsSingleGroup(t *testing.T) {
|
|
ag := models.AlertGroup{
|
|
Alerts: models.AlertList{
|
|
models.Alert{
|
|
State: models.AlertStateActive,
|
|
Labels: labels.FromStrings("foo", "bar"),
|
|
},
|
|
models.Alert{
|
|
State: models.AlertStateUnprocessed,
|
|
Labels: labels.FromStrings("foo", "bar"),
|
|
},
|
|
},
|
|
}
|
|
shared, _ := ag.DedupSharedMaps(nil)
|
|
if len(shared.Annotations) > 0 {
|
|
t.Errorf("Expected empty shared annotations, got %v", shared.Annotations)
|
|
}
|
|
if shared.Labels.Len() == 0 {
|
|
t.Errorf("Expected non-empty shared labels, got %v", shared.Labels)
|
|
}
|
|
}
|
|
|
|
func TestDedupSharedMapsWithSingleAlert(t *testing.T) {
|
|
ag := models.AlertGroup{
|
|
Alerts: models.AlertList{
|
|
models.Alert{},
|
|
},
|
|
}
|
|
shared, _ := ag.DedupSharedMaps(nil)
|
|
if len(shared.Annotations) > 0 {
|
|
t.Errorf("Expected empty shared annotations, got %v", shared.Annotations)
|
|
}
|
|
if shared.Labels.Len() > 0 {
|
|
t.Errorf("Expected empty shared labels, got %v", shared.Labels)
|
|
}
|
|
}
|
|
|
|
func TestDedupWithBadSource(t *testing.T) {
|
|
ag := models.AlertGroup{
|
|
Alerts: models.AlertList{
|
|
models.Alert{Alertmanager: []models.AlertmanagerInstance{{Source: "%gh&%ij"}}},
|
|
models.Alert{Alertmanager: []models.AlertmanagerInstance{{Source: ""}}},
|
|
},
|
|
}
|
|
shared, _ := ag.DedupSharedMaps(nil)
|
|
if len(shared.Sources) > 0 {
|
|
t.Errorf("Expected empty sources list, got %v", shared.Sources)
|
|
}
|
|
}
|
|
|
|
func TestDedupSharedMapsWithDropNames(t *testing.T) {
|
|
// verifies that passing dropNames to DedupSharedMaps removes those labels
|
|
// from both group labels and alert labels
|
|
ag := models.AlertGroup{
|
|
Receiver: "default",
|
|
Labels: labels.FromStrings("alertname", "TestAlert", "cluster", "prod"),
|
|
Alerts: models.AlertList{
|
|
models.Alert{
|
|
State: models.AlertStateActive,
|
|
Labels: labels.FromStrings("alertname", "TestAlert", "cluster", "prod", "instance", "1"),
|
|
},
|
|
models.Alert{
|
|
State: models.AlertStateActive,
|
|
Labels: labels.FromStrings("alertname", "TestAlert", "cluster", "prod", "instance", "2"),
|
|
},
|
|
},
|
|
}
|
|
ag.DedupSharedMaps([]string{"cluster"})
|
|
|
|
// "cluster" should be removed from group labels
|
|
if ag.Labels.Get("cluster") != "" {
|
|
t.Error("Expected 'cluster' label to be removed from group labels")
|
|
}
|
|
// "alertname" should remain in group labels
|
|
if ag.Labels.Get("alertname") == "" {
|
|
t.Error("Expected 'alertname' label to remain in group labels")
|
|
}
|
|
// alert labels should not contain "cluster" (dropped) or "alertname" (shared with group)
|
|
for i, alert := range ag.Alerts {
|
|
if alert.Labels.Get("cluster") != "" {
|
|
t.Errorf("Alert[%d]: expected 'cluster' label to be removed", i)
|
|
}
|
|
if alert.Labels.Get("alertname") != "" {
|
|
t.Errorf("Alert[%d]: expected 'alertname' label to be removed (shared with group)", i)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAPIAlertGroupJSONFieldsNonNull(t *testing.T) {
|
|
type testCaseT struct {
|
|
desc string
|
|
ag models.AlertGroup
|
|
}
|
|
|
|
testCases := []testCaseT{
|
|
{
|
|
// verifies that a group with multiple alerts produces non-null alerts and labels
|
|
desc: "multiple alerts",
|
|
ag: models.AlertGroup{
|
|
Receiver: "default",
|
|
Labels: labels.FromStrings("alertname", "Test"),
|
|
Alerts: models.AlertList{
|
|
{State: models.AlertStateActive, Labels: labels.FromStrings("alertname", "Test", "instance", "1")},
|
|
{State: models.AlertStateActive, Labels: labels.FromStrings("alertname", "Test", "instance", "2")},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// verifies that a group with a single alert produces non-null alerts and labels
|
|
desc: "single alert",
|
|
ag: models.AlertGroup{
|
|
Receiver: "default",
|
|
Labels: labels.FromStrings("alertname", "Test"),
|
|
Alerts: models.AlertList{
|
|
{State: models.AlertStateActive, Labels: labels.FromStrings("alertname", "Test")},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
// verifies that a group with empty labels produces non-null labels array
|
|
desc: "empty labels",
|
|
ag: models.AlertGroup{
|
|
Alerts: models.AlertList{
|
|
{State: models.AlertStateActive, Labels: labels.EmptyLabels()},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.desc, func(t *testing.T) {
|
|
shared, allLabels := tc.ag.DedupSharedMaps(nil)
|
|
apiAG := models.NewAPIAlertGroup(tc.ag, shared, allLabels, len(tc.ag.Alerts))
|
|
|
|
b, err := json.Marshal(apiAG)
|
|
if err != nil {
|
|
t.Fatalf("json.Marshal failed: %v", err)
|
|
}
|
|
|
|
var raw map[string]json.RawMessage
|
|
if err := json.Unmarshal(b, &raw); err != nil {
|
|
t.Fatalf("json.Unmarshal into raw map failed: %v", err)
|
|
}
|
|
|
|
// alerts field must be a non-null JSON array
|
|
alertsRaw, ok := raw["alerts"]
|
|
if !ok {
|
|
t.Fatal("JSON output missing 'alerts' field")
|
|
}
|
|
if string(alertsRaw) == "null" {
|
|
t.Error("JSON 'alerts' field is null, expected a non-null array")
|
|
}
|
|
|
|
// labels field must be a non-null JSON array
|
|
labelsRaw, ok := raw["labels"]
|
|
if !ok {
|
|
t.Fatal("JSON output missing 'labels' field")
|
|
}
|
|
if string(labelsRaw) == "null" {
|
|
t.Error("JSON 'labels' field is null, expected a non-null array")
|
|
}
|
|
|
|
// shared.labels must also be non-null
|
|
var sharedRaw map[string]json.RawMessage
|
|
if err := json.Unmarshal(raw["shared"], &sharedRaw); err != nil {
|
|
t.Fatalf("json.Unmarshal shared failed: %v", err)
|
|
}
|
|
sharedLabelsRaw, ok := sharedRaw["labels"]
|
|
if !ok {
|
|
t.Fatal("JSON output missing 'shared.labels' field")
|
|
}
|
|
if string(sharedLabelsRaw) == "null" {
|
|
t.Error("JSON 'shared.labels' field is null, expected a non-null array")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompareLabelValueStats(t *testing.T) {
|
|
type testCaseT struct {
|
|
a models.LabelValueStats
|
|
b models.LabelValueStats
|
|
expected int
|
|
}
|
|
|
|
testCases := []testCaseT{
|
|
// verifies that higher hits sorts before lower hits
|
|
{
|
|
a: models.LabelValueStats{Value: "a", Hits: 10},
|
|
b: models.LabelValueStats{Value: "b", Hits: 5},
|
|
expected: -1,
|
|
},
|
|
// verifies that lower hits sorts after higher hits
|
|
{
|
|
a: models.LabelValueStats{Value: "a", Hits: 5},
|
|
b: models.LabelValueStats{Value: "b", Hits: 10},
|
|
expected: 1,
|
|
},
|
|
// verifies that equal hits falls back to natural value ordering (a < b)
|
|
{
|
|
a: models.LabelValueStats{Value: "a", Hits: 5},
|
|
b: models.LabelValueStats{Value: "b", Hits: 5},
|
|
expected: -1,
|
|
},
|
|
// verifies that equal hits falls back to natural value ordering (b > a)
|
|
{
|
|
a: models.LabelValueStats{Value: "b", Hits: 5},
|
|
b: models.LabelValueStats{Value: "a", Hits: 5},
|
|
expected: 1,
|
|
},
|
|
// verifies that identical hits and values returns 0
|
|
{
|
|
a: models.LabelValueStats{Value: "a", Hits: 5},
|
|
b: models.LabelValueStats{Value: "a", Hits: 5},
|
|
expected: 0,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
result := models.CompareLabelValueStats(tc.a, tc.b)
|
|
if result != tc.expected {
|
|
t.Errorf("CompareLabelValueStats(%v, %v) returned %d, expected %d", tc.a, tc.b, result, tc.expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNameStatsSort(t *testing.T) {
|
|
nameStats := models.LabelNameStatsList{
|
|
{
|
|
Name: "@state",
|
|
Hits: 24,
|
|
Values: models.LabelValueStatsList{
|
|
models.LabelValueStats{
|
|
Value: "suppressed",
|
|
Raw: "@state=suppressed",
|
|
Hits: 8,
|
|
Percent: 33,
|
|
Offset: 67,
|
|
},
|
|
models.LabelValueStats{
|
|
Value: "active",
|
|
Raw: "@state=actuve",
|
|
Hits: 16,
|
|
Percent: 67,
|
|
Offset: 0,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "cluster",
|
|
Hits: 24,
|
|
Values: models.LabelValueStatsList{
|
|
models.LabelValueStats{
|
|
Value: "dev",
|
|
Raw: "cluster=dev",
|
|
Hits: 10,
|
|
Percent: 42,
|
|
Offset: 0,
|
|
},
|
|
models.LabelValueStats{
|
|
Value: "prod",
|
|
Raw: "cluster=prod",
|
|
Hits: 6,
|
|
Percent: 25,
|
|
Offset: 42,
|
|
},
|
|
models.LabelValueStats{
|
|
Value: "staging",
|
|
Raw: "cluster=staging",
|
|
Hits: 8,
|
|
Percent: 33,
|
|
Offset: 67,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "alertname",
|
|
Hits: 24,
|
|
Values: models.LabelValueStatsList{
|
|
models.LabelValueStats{
|
|
Value: "HTTP_Probe_Failed",
|
|
Raw: "alertname=HTTP_Probe_Failed",
|
|
Hits: 4,
|
|
Percent: 17,
|
|
Offset: 0,
|
|
},
|
|
models.LabelValueStats{
|
|
Value: "Host_Down",
|
|
Raw: "alertname=Host_Down",
|
|
Hits: 16,
|
|
Percent: 67,
|
|
Offset: 17,
|
|
},
|
|
models.LabelValueStats{
|
|
Value: "Free_Disk_Space_Too_Low",
|
|
Raw: "alertname=Free_Disk_Space_Too_Low",
|
|
|
|
Hits: 2,
|
|
Percent: 8,
|
|
Offset: 84,
|
|
},
|
|
models.LabelValueStats{
|
|
Value: "Memory_Usage_Too_High",
|
|
Raw: "alertname=Memory_Usage_Too_High",
|
|
Hits: 2,
|
|
Percent: 8,
|
|
Offset: 92,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "instance",
|
|
Hits: 24,
|
|
Values: models.LabelValueStatsList{
|
|
models.LabelValueStats{
|
|
Value: "server4",
|
|
Raw: "instance=server4",
|
|
Hits: 2,
|
|
Percent: 8,
|
|
},
|
|
models.LabelValueStats{
|
|
Value: "server5",
|
|
Raw: "instance=server5",
|
|
Hits: 4,
|
|
Percent: 17,
|
|
Offset: 17,
|
|
},
|
|
models.LabelValueStats{
|
|
Value: "server6",
|
|
Raw: "instance=server6",
|
|
Hits: 2,
|
|
Percent: 8,
|
|
Offset: 17,
|
|
},
|
|
models.LabelValueStats{
|
|
Value: "server1",
|
|
Raw: "instance=server1",
|
|
Hits: 2,
|
|
Percent: 8,
|
|
Offset: 17,
|
|
},
|
|
models.LabelValueStats{
|
|
Value: "server2",
|
|
Raw: "instance=server2",
|
|
Hits: 4,
|
|
Percent: 17,
|
|
Offset: 17,
|
|
},
|
|
models.LabelValueStats{
|
|
Value: "server3",
|
|
Raw: "instance=server3",
|
|
Hits: 2,
|
|
Percent: 8,
|
|
Offset: 17,
|
|
},
|
|
models.LabelValueStats{
|
|
Value: "server7",
|
|
Raw: "instance=server7",
|
|
Hits: 2,
|
|
Percent: 8,
|
|
Offset: 17,
|
|
},
|
|
models.LabelValueStats{
|
|
Value: "server8",
|
|
Raw: "instance=server8",
|
|
Hits: 2,
|
|
Percent: 8,
|
|
Offset: 17,
|
|
},
|
|
models.LabelValueStats{
|
|
Value: "web1",
|
|
Raw: "instance=web1",
|
|
Hits: 2,
|
|
Percent: 8,
|
|
Offset: 17,
|
|
},
|
|
models.LabelValueStats{
|
|
Value: "web2",
|
|
Raw: "instance=web2",
|
|
Hits: 2,
|
|
Percent: 8,
|
|
Offset: 17,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "@receiver",
|
|
Hits: 24,
|
|
Values: models.LabelValueStatsList{
|
|
models.LabelValueStats{
|
|
Value: "by-name",
|
|
Raw: "@receiver=by-name",
|
|
Hits: 12,
|
|
Percent: 50,
|
|
Offset: 0,
|
|
},
|
|
models.LabelValueStats{
|
|
Value: "by-cluster-service",
|
|
Raw: "@receiver=by-cluster-service",
|
|
Hits: 12,
|
|
Percent: 50,
|
|
Offset: 50,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "job",
|
|
Hits: 16,
|
|
Values: models.LabelValueStatsList{
|
|
models.LabelValueStats{
|
|
Value: "node_exporter",
|
|
Raw: "job=node_exporter",
|
|
Hits: 8,
|
|
Percent: 50,
|
|
Offset: 0,
|
|
},
|
|
models.LabelValueStats{
|
|
Value: "node_ping",
|
|
Raw: "job=node_ping",
|
|
Hits: 8,
|
|
Percent: 50,
|
|
Offset: 50,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
b, err := json.Marshal(nameStats)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
before := string(b)
|
|
|
|
for _, n := range nameStats {
|
|
slices.SortFunc(n.Values, models.CompareLabelValueStats)
|
|
}
|
|
slices.SortFunc(nameStats, models.CompareLabelNameStats)
|
|
|
|
a, err := json.Marshal(nameStats)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
after := string(a)
|
|
|
|
if after == before {
|
|
t.Errorf("Sorting LabelNameStatsList produces the same output as unsorted instance")
|
|
}
|
|
}
|