From 25d82e3bb0f554784d224972d0eeef9bcfbb8aee Mon Sep 17 00:00:00 2001 From: Steve Simpson Date: Mon, 26 Apr 2021 22:37:04 +0200 Subject: [PATCH 1/2] Implement quorum reads and merging for /v2/alerts and /v2/alerts/groups. Reading alert listings via /v2/alerts and /v2/alerts/groups will now read from a quorum of replicas and return a merged response. Alerts are merged by returning the union of alerts over all responses. When multiple responses contain the same alert, the alert with the most recent "updatedAt" timestamp is chosen. Groups are returned by returning the union of groups over all responses. When the same group is present in multiple responses (same receiver and labels fingerprint), the groups are merged into by merging the sets of alerts in each. Signed-off-by: Steve Simpson --- integration/alertmanager_test.go | 26 ++- integration/e2ecortex/client.go | 31 +++- pkg/alertmanager/distributor.go | 10 +- pkg/alertmanager/distributor_test.go | 23 ++- pkg/alertmanager/merger/v2_alert_groups.go | 104 +++++++++++ .../merger/v2_alert_groups_test.go | 149 ++++++++++++++++ pkg/alertmanager/merger/v2_alerts.go | 67 ++++++++ pkg/alertmanager/merger/v2_alerts_test.go | 162 ++++++++++++++++++ 8 files changed, 554 insertions(+), 18 deletions(-) create mode 100644 pkg/alertmanager/merger/v2_alert_groups.go create mode 100644 pkg/alertmanager/merger/v2_alert_groups_test.go create mode 100644 pkg/alertmanager/merger/v2_alerts.go create mode 100644 pkg/alertmanager/merger/v2_alerts_test.go diff --git a/integration/alertmanager_test.go b/integration/alertmanager_test.go index 4073a519bf0..317f03bc558 100644 --- a/integration/alertmanager_test.go +++ b/integration/alertmanager_test.go @@ -505,21 +505,23 @@ func TestAlertmanagerSharding(t *testing.T) { // Therefore, the alerts we posted should always be visible. for _, c := range clients { - list, err := c.GetAlerts(context.Background()) + list, err := c.GetAlertsV1(context.Background()) require.NoError(t, err) assert.ElementsMatch(t, []string{"alert_1", "alert_2", "alert_3"}, alertNames(list)) } } - // Endpoint: GET /alerts/groups + // Endpoint: GET /v2/alerts { - // Writes do not block for the write slowest replica, and reads do not - // currently merge results from multiple replicas, so we have to wait. - require.NoError(t, alertmanagers.WaitSumMetricsWithOptions( - e2e.Equals(float64(3*testCfg.replicationFactor)), - []string{"cortex_alertmanager_alerts_received_total"}, - e2e.SkipMissingMetrics)) + for _, c := range clients { + list, err := c.GetAlertsV2(context.Background()) + require.NoError(t, err) + assert.ElementsMatch(t, []string{"alert_1", "alert_2", "alert_3"}, alertNames(list)) + } + } + // Endpoint: GET /v2/alerts/groups + { for _, c := range clients { list, err := c.GetAlertGroups(context.Background()) require.NoError(t, err) @@ -535,7 +537,15 @@ func TestAlertmanagerSharding(t *testing.T) { require.Contains(t, groups, "group_2") assert.ElementsMatch(t, []string{"alert_3"}, alertNames(groups["group_2"])) } + + // Note: /v1/alerts/groups does not exist. } + + // Check the alerts were eventually written to every replica. + require.NoError(t, alertmanagers.WaitSumMetricsWithOptions( + e2e.Equals(float64(3*testCfg.replicationFactor)), + []string{"cortex_alertmanager_alerts_received_total"}, + e2e.SkipMissingMetrics)) }) } } diff --git a/integration/e2ecortex/client.go b/integration/e2ecortex/client.go index 99621cf7204..0b642e8bfec 100644 --- a/integration/e2ecortex/client.go +++ b/integration/e2ecortex/client.go @@ -509,7 +509,7 @@ func (c *Client) SendAlertToAlermanager(ctx context.Context, alert *model.Alert) return nil } -func (c *Client) GetAlerts(ctx context.Context) ([]model.Alert, error) { +func (c *Client) GetAlertsV1(ctx context.Context) ([]model.Alert, error) { u := c.alertmanagerClient.URL("api/prom/api/v1/alerts", nil) req, err := http.NewRequest(http.MethodGet, u.String(), nil) @@ -547,6 +547,35 @@ func (c *Client) GetAlerts(ctx context.Context) ([]model.Alert, error) { return decoded.Data, nil } +func (c *Client) GetAlertsV2(ctx context.Context) ([]model.Alert, error) { + u := c.alertmanagerClient.URL("api/prom/api/v2/alerts", nil) + + req, err := http.NewRequest(http.MethodGet, u.String(), nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %v", err) + } + + resp, body, err := c.alertmanagerClient.Do(ctx, req) + if err != nil { + return nil, err + } + + if resp.StatusCode == http.StatusNotFound { + return nil, ErrNotFound + } + + if resp.StatusCode/100 != 2 { + return nil, fmt.Errorf("getting alerts failed with status %d and error %v", resp.StatusCode, string(body)) + } + + decoded := []model.Alert{} + if err := json.Unmarshal(body, &decoded); err != nil { + return nil, err + } + + return decoded, nil +} + type AlertGroup struct { Labels model.LabelSet `json:"labels"` Alerts []model.Alert `json:"alerts"` diff --git a/pkg/alertmanager/distributor.go b/pkg/alertmanager/distributor.go index df57b64e7ec..1d7d8f382dc 100644 --- a/pkg/alertmanager/distributor.go +++ b/pkg/alertmanager/distributor.go @@ -88,13 +88,17 @@ func (d *Distributor) isQuorumReadPath(p string) (bool, merger.Merger) { if strings.HasSuffix(p, "/v1/alerts") { return true, merger.V1Alerts{} } + if strings.HasSuffix(p, "/v2/alerts") { + return true, merger.V2Alerts{} + } + if strings.HasSuffix(p, "/v2/alerts/groups") { + return true, merger.V2AlertGroups{} + } return false, nil } func (d *Distributor) isUnaryReadPath(p string) bool { - return strings.HasSuffix(p, "/v2/alerts") || - strings.HasSuffix(p, "/alerts/groups") || - strings.HasSuffix(p, "/silences") || + return strings.HasSuffix(p, "/silences") || strings.HasSuffix(path.Dir(p), "/silence") || strings.HasSuffix(p, "/status") || strings.HasSuffix(p, "/receivers") diff --git a/pkg/alertmanager/distributor_test.go b/pkg/alertmanager/distributor_test.go index 978602fc660..4c2c2af6e4c 100644 --- a/pkg/alertmanager/distributor_test.go +++ b/pkg/alertmanager/distributor_test.go @@ -81,23 +81,34 @@ func TestDistributor_DistributeRequest(t *testing.T) { route: "/v1/alerts", responseBody: []byte(`{"status":"success","data":[]}`), }, { - name: "Read /v2/alerts is sent to only 1 AM", + name: "Read /v2/alerts is sent to 3 AMs", numAM: 5, numHappyAM: 5, replicationFactor: 3, isRead: true, expStatusCode: http.StatusOK, - expectedTotalCalls: 1, + expectedTotalCalls: 3, route: "/v2/alerts", + responseBody: []byte(`[]`), }, { - name: "Read /alerts/groups is sent to only 1 AM", + name: "Read /v2/alerts/groups is sent to 3 AMs", numAM: 5, numHappyAM: 5, replicationFactor: 3, isRead: true, expStatusCode: http.StatusOK, - expectedTotalCalls: 1, - route: "/alerts/groups", + expectedTotalCalls: 3, + route: "/v2/alerts/groups", + responseBody: []byte(`[]`), + }, { + name: "Read /v1/alerts/groups not supported", + numAM: 5, + numHappyAM: 5, + replicationFactor: 3, + expStatusCode: http.StatusNotFound, + expectedTotalCalls: 0, + headersNotPreserved: true, + route: "/v1/alerts/groups", }, { name: "Write /alerts/groups not supported", numAM: 5, @@ -256,7 +267,7 @@ func TestDistributor_DistributeRequest(t *testing.T) { func TestDistributor_IsPathSupported(t *testing.T) { supported := map[string]bool{ "/alertmanager/api/v1/alerts": true, - "/alertmanager/api/v1/alerts/groups": true, + "/alertmanager/api/v1/alerts/groups": false, "/alertmanager/api/v1/silences": true, "/alertmanager/api/v1/silence/id": true, "/alertmanager/api/v1/silence/anything": true, diff --git a/pkg/alertmanager/merger/v2_alert_groups.go b/pkg/alertmanager/merger/v2_alert_groups.go new file mode 100644 index 00000000000..ab660afa036 --- /dev/null +++ b/pkg/alertmanager/merger/v2_alert_groups.go @@ -0,0 +1,104 @@ +package merger + +import ( + "errors" + "sort" + + "github.com/go-openapi/swag" + v2 "github.com/prometheus/alertmanager/api/v2" + v2_models "github.com/prometheus/alertmanager/api/v2/models" + prom_model "github.com/prometheus/common/model" +) + +// V2AlertGroups implements the Merger interface for GET /v2/alerts/groups. It returns +// the union of alert groups over all the responses. When the same alert exists in the same +// group for multiple responses, the instance of that alert with the most recent UpdatedAt +// timestamp is returned in that group within the response. +type V2AlertGroups struct{} + +func (V2AlertGroups) MergeResponses(in [][]byte) ([]byte, error) { + groups := make(v2_models.AlertGroups, 0) + for _, body := range in { + parsed := make(v2_models.AlertGroups, 0) + if err := swag.ReadJSON(body, &parsed); err != nil { + return nil, err + } + groups = append(groups, parsed...) + } + + merged, err := mergeV2AlertGroups(groups) + if err != nil { + return nil, err + } + + return swag.WriteJSON(merged) +} + +func mergeV2AlertGroups(in v2_models.AlertGroups) (v2_models.AlertGroups, error) { + // Gather lists of all alerts for each distinct group. + groups := make(map[groupKey]*v2_models.AlertGroup) + for _, group := range in { + if group.Receiver == nil { + return nil, errors.New("unexpected nil receiver") + } + if group.Receiver.Name == nil { + return nil, errors.New("unexpected nil receiver name") + } + + key := getGroupKey(group) + if current, ok := groups[key]; ok { + current.Alerts = append(current.Alerts, group.Alerts...) + } else { + groups[key] = group + } + } + + // Merge duplicates of the same alert within each group. + for _, group := range groups { + var err error + group.Alerts, err = mergeV2Alerts(group.Alerts) + if err != nil { + return nil, err + } + } + + result := make(v2_models.AlertGroups, 0, len(groups)) + for _, group := range groups { + result = append(result, group) + } + + // Mimic Alertmanager which returns groups ordered by labels and receiver. + sort.Sort(byGroup(result)) + + return result, nil +} + +// getGroupKey returns an identity for a group which can be used to match it against other groups. +// Only the receiver name is necessary to ensure grouping by receiver, and for the labels, we again +// use the same method for matching the group labels as used internally, generating the fingerprint. +func getGroupKey(group *v2_models.AlertGroup) groupKey { + return groupKey{ + fingerprint: prom_model.LabelsToSignature(group.Labels), + receiver: *group.Receiver.Name, + } +} + +type groupKey struct { + fingerprint uint64 + receiver string +} + +// byGroup implements the ordering of Alertmanager dispatch.AlertGroups on the OpenAPI type. +type byGroup v2_models.AlertGroups + +func (ag byGroup) Swap(i, j int) { ag[i], ag[j] = ag[j], ag[i] } +func (ag byGroup) Less(i, j int) bool { + iLabels := v2.APILabelSetToModelLabelSet(ag[i].Labels) + jLabels := v2.APILabelSetToModelLabelSet(ag[j].Labels) + + if iLabels.Equal(jLabels) { + return *ag[i].Receiver.Name < *ag[j].Receiver.Name + } + return iLabels.Before(jLabels) +} +func (ag byGroup) Len() int { return len(ag) } diff --git a/pkg/alertmanager/merger/v2_alert_groups_test.go b/pkg/alertmanager/merger/v2_alert_groups_test.go new file mode 100644 index 00000000000..3c667a784c9 --- /dev/null +++ b/pkg/alertmanager/merger/v2_alert_groups_test.go @@ -0,0 +1,149 @@ +package merger + +import ( + "testing" + + v2_models "github.com/prometheus/alertmanager/api/v2/models" + "github.com/stretchr/testify/require" +) + +func TestV2AlertGroups(t *testing.T) { + + // This test is to check the parsing round-trip is working as expected, the merging logic is + // tested in TestMergeV2AlertGroups. The test data is based on captures from an actual Alertmanager. + + in := [][]byte{ + + []byte(`[` + + `{"alerts":[{"annotations":{},"endsAt":"2021-04-21T10:47:32.161+02:00","fingerprint":"c4b6b79a607b6ba0",` + + `"receivers":[{"name":"dummy"}],"startsAt":"2021-04-21T09:47:32.161+02:00",` + + `"status":{"inhibitedBy":[],"silencedBy":[],"state":"unprocessed"},` + + `"updatedAt":"2021-04-21T07:47:32.163Z","labels":{"group":"group_1","name":"alert_1"}}],` + + `"labels":{"group":"group_1"},"receiver":{"name":"dummy"}},` + + `{"alerts":[{"annotations":{},"endsAt":"2021-04-21T10:47:32.165+02:00","fingerprint":"465de60f606461c3",` + + `"receivers":[{"name":"dummy"}],"startsAt":"2021-04-21T09:47:32.165+02:00",` + + `"status":{"inhibitedBy":[],"silencedBy":[],"state":"unprocessed"},` + + `"updatedAt":"2021-04-21T07:47:32.167Z","labels":{"group":"group_2","name":"alert_3"}}],` + + `"labels":{"group":"group_2"},"receiver":{"name":"dummy"}}` + + `]`), + []byte(`[` + + `{"alerts":[{"annotations":{},"endsAt":"2021-04-21T10:47:32.163+02:00","fingerprint":"c4b8b79a607bee77",` + + `"receivers":[{"name":"dummy"}],"startsAt":"2021-04-21T09:47:32.163+02:00",` + + `"status":{"inhibitedBy":[],"silencedBy":[],"state":"unprocessed"},` + + `"updatedAt":"2021-04-21T07:47:32.165Z","labels":{"group":"group_1","name":"alert_2"}}],` + + `"labels":{"group":"group_1"},"receiver":{"name":"dummy"}}` + + `]`), + []byte(`[]`), + } + + expected := []byte(`[` + + `{"alerts":[{"annotations":{},"endsAt":"2021-04-21T10:47:32.161+02:00","fingerprint":"c4b6b79a607b6ba0",` + + `"receivers":[{"name":"dummy"}],"startsAt":"2021-04-21T09:47:32.161+02:00",` + + `"status":{"inhibitedBy":[],"silencedBy":[],"state":"unprocessed"},` + + `"updatedAt":"2021-04-21T07:47:32.163Z","labels":{"group":"group_1","name":"alert_1"}},` + + `{"annotations":{},"endsAt":"2021-04-21T10:47:32.163+02:00","fingerprint":"c4b8b79a607bee77",` + + `"receivers":[{"name":"dummy"}],"startsAt":"2021-04-21T09:47:32.163+02:00",` + + `"status":{"inhibitedBy":[],"silencedBy":[],"state":"unprocessed"},` + + `"updatedAt":"2021-04-21T07:47:32.165Z","labels":{"group":"group_1","name":"alert_2"}}],` + + `"labels":{"group":"group_1"},"receiver":{"name":"dummy"}},` + + `{"alerts":[{"annotations":{},"endsAt":"2021-04-21T10:47:32.165+02:00","fingerprint":"465de60f606461c3",` + + `"receivers":[{"name":"dummy"}],"startsAt":"2021-04-21T09:47:32.165+02:00",` + + `"status":{"inhibitedBy":[],"silencedBy":[],"state":"unprocessed"},` + + `"updatedAt":"2021-04-21T07:47:32.167Z","labels":{"group":"group_2","name":"alert_3"}}],` + + `"labels":{"group":"group_2"},"receiver":{"name":"dummy"}}]`) + + out, err := V2AlertGroups{}.MergeResponses(in) + require.NoError(t, err) + require.Equal(t, expected, out) +} + +func v2group(label, receiver string, alerts ...*v2_models.GettableAlert) *v2_models.AlertGroup { + return &v2_models.AlertGroup{ + Alerts: alerts, + Labels: v2_models.LabelSet{"some-label": label}, + Receiver: &v2_models.Receiver{Name: &receiver}, + } +} + +func v2groups(groups ...*v2_models.AlertGroup) v2_models.AlertGroups { + return groups +} + +func TestMergeV2AlertGroups(t *testing.T) { + var ( + alert1 = v2alert("1111111111111111", "a1-", "2020-01-01T12:00:00.000Z") + newerAlert1 = v2alert("1111111111111111", "a1+", "2020-01-01T12:00:00.001Z") + alert2 = v2alert("2222222222222222", "a2-", "2020-01-01T12:00:00.000Z") + alert3 = v2alert("3333333333333333", "a2-", "2020-01-01T12:00:00.000Z") + ) + cases := []struct { + name string + in v2_models.AlertGroups + err error + out v2_models.AlertGroups + }{ + { + name: "no groups, should return no groups", + in: v2groups(), + out: v2_models.AlertGroups{}, + }, + { + name: "one group with one alert, should return one group", + in: v2groups(v2group("g1", "r1", alert1)), + out: v2groups(v2group("g1", "r1", alert1)), + }, + { + name: "two groups with different labels, should return two groups", + in: v2groups(v2group("g1", "r1", alert1), v2group("g2", "r1", alert2)), + out: v2groups(v2group("g1", "r1", alert1), v2group("g2", "r1", alert2)), + }, + { + name: "two groups with different receiver, should return two groups", + in: v2groups(v2group("g1", "r1", alert1), v2group("g1", "r2", alert2)), + out: v2groups(v2group("g1", "r1", alert1), v2group("g1", "r2", alert2)), + }, + { + name: "two identical groups with different alerts, should return one group with two alerts", + in: v2groups(v2group("g1", "r1", alert1), v2group("g1", "r1", alert2)), + out: v2groups(v2group("g1", "r1", alert1, alert2)), + }, + { + name: "two identical groups with identical alerts, should return one group with one alert", + in: v2groups(v2group("g1", "r1", alert1), v2group("g1", "r1", alert1)), + out: v2groups(v2group("g1", "r1", alert1)), + }, + { + name: "two identical groups with diverged alerts, should return one group with the newer alert", + in: v2groups(v2group("g1", "r1", alert1), v2group("g1", "r1", newerAlert1)), + out: v2groups(v2group("g1", "r1", newerAlert1)), + }, + { + name: "two sets of identical groups with single alerts, should merge all into two groups ", + in: v2groups( + v2group("g1", "r1", alert1), v2group("g1", "r1", alert2), + v2group("g2", "r1", alert1), v2group("g2", "r1", alert3)), + out: v2groups( + v2group("g1", "r1", alert1, alert2), + v2group("g2", "r1", alert1, alert3)), + }, + { + name: "many unordered groups, should return groups ordered by labels then receiver", + in: v2groups( + v2group("g3", "r2", alert3), v2group("g2", "r1", alert2), + v2group("g1", "r2", alert1), v2group("g1", "r1", alert1), + v2group("g2", "r2", alert2), v2group("g3", "r1", alert3)), + out: v2groups( + v2group("g1", "r1", alert1), v2group("g1", "r2", alert1), + v2group("g2", "r1", alert2), v2group("g2", "r2", alert2), + v2group("g3", "r1", alert3), v2group("g3", "r2", alert3)), + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + out, err := mergeV2AlertGroups(c.in) + require.Equal(t, c.err, err) + require.Equal(t, c.out, out) + }) + } +} diff --git a/pkg/alertmanager/merger/v2_alerts.go b/pkg/alertmanager/merger/v2_alerts.go new file mode 100644 index 00000000000..a1cbe463d36 --- /dev/null +++ b/pkg/alertmanager/merger/v2_alerts.go @@ -0,0 +1,67 @@ +package merger + +import ( + "errors" + "sort" + "time" + + "github.com/go-openapi/swag" + v2_models "github.com/prometheus/alertmanager/api/v2/models" +) + +// V2Alerts implements the Merger interface for GET /v2/alerts. It returns the union +// of alerts over all the responses. When the same alert exists in multiple responses, the +// instance of that alert with the most recent UpdatedAt timestamp is returned in the response. +type V2Alerts struct{} + +func (V2Alerts) MergeResponses(in [][]byte) ([]byte, error) { + alerts := make(v2_models.GettableAlerts, 0) + for _, body := range in { + parsed := make(v2_models.GettableAlerts, 0) + if err := swag.ReadJSON(body, &parsed); err != nil { + return nil, err + } + alerts = append(alerts, parsed...) + } + + merged, err := mergeV2Alerts(alerts) + if err != nil { + return nil, err + } + + return swag.WriteJSON(merged) +} + +func mergeV2Alerts(in v2_models.GettableAlerts) (v2_models.GettableAlerts, error) { + // Select the most recently updated alert for each distinct alert. + alerts := make(map[string]*v2_models.GettableAlert) + for _, alert := range in { + if alert.Fingerprint == nil { + return nil, errors.New("unexpected nil fingerprint") + } + if alert.UpdatedAt == nil { + return nil, errors.New("unexpected nil updatedAt") + } + + key := *alert.Fingerprint + if current, ok := alerts[key]; ok { + if time.Time(*alert.UpdatedAt).After(time.Time(*current.UpdatedAt)) { + alerts[key] = alert + } + } else { + alerts[key] = alert + } + } + + result := make(v2_models.GettableAlerts, 0, len(alerts)) + for _, alert := range alerts { + result = append(result, alert) + } + + // Mimic Alertmanager which returns alerts ordered by fingerprint (as string). + sort.Slice(result, func(i, j int) bool { + return *result[i].Fingerprint < *result[j].Fingerprint + }) + + return result, nil +} diff --git a/pkg/alertmanager/merger/v2_alerts_test.go b/pkg/alertmanager/merger/v2_alerts_test.go new file mode 100644 index 00000000000..3067f846fd9 --- /dev/null +++ b/pkg/alertmanager/merger/v2_alerts_test.go @@ -0,0 +1,162 @@ +package merger + +import ( + "testing" + "time" + + "github.com/go-openapi/strfmt" + v2_models "github.com/prometheus/alertmanager/api/v2/models" + "github.com/stretchr/testify/require" +) + +func TestV2Alerts(t *testing.T) { + + // This test is to check the parsing round-trip is working as expected, the merging logic is + // tested in TestMergeV2Alerts. The test data is based on captures from an actual Alertmanager. + + in := [][]byte{ + []byte(`[` + + `{"annotations":{},"endsAt":"2021-04-21T10:47:32.161+02:00","fingerprint":"c4b6b79a607b6ba0",` + + `"receivers":[{"name":"dummy"}],"startsAt":"2021-04-21T09:47:32.161+02:00",` + + `"status":{"inhibitedBy":[],"silencedBy":[],"state":"unprocessed"},` + + `"updatedAt":"2021-04-21T07:47:32.163Z","labels":{"group":"group_1","name":"alert_1"}},` + + `{"annotations":{},"endsAt":"2021-04-21T10:47:32.163+02:00","fingerprint":"c4b8b79a607bee77",` + + `"receivers":[{"name":"dummy"}],"startsAt":"2021-04-21T09:47:32.163+02:00",` + + `"status":{"inhibitedBy":[],"silencedBy":[],"state":"unprocessed"},` + + `"updatedAt":"2021-04-21T07:47:32.165Z","labels":{"group":"group_1","name":"alert_2"}}` + + `]`), + []byte(`[{"annotations":{},"endsAt":"2021-04-21T10:47:32.165+02:00","fingerprint":"465de60f606461c3",` + + `"receivers":[{"name":"dummy"}],"startsAt":"2021-04-21T09:47:32.165+02:00",` + + `"status":{"inhibitedBy":[],"silencedBy":[],"state":"unprocessed"},` + + `"updatedAt":"2021-04-21T07:47:32.167Z","labels":{"group":"group_2","name":"alert_3"}}]`), + []byte(`[]`), + } + + expected := []byte(`[` + + `{"annotations":{},"endsAt":"2021-04-21T10:47:32.165+02:00","fingerprint":"465de60f606461c3",` + + `"receivers":[{"name":"dummy"}],"startsAt":"2021-04-21T09:47:32.165+02:00",` + + `"status":{"inhibitedBy":[],"silencedBy":[],"state":"unprocessed"},` + + `"updatedAt":"2021-04-21T07:47:32.167Z","labels":{"group":"group_2","name":"alert_3"}},` + + `{"annotations":{},"endsAt":"2021-04-21T10:47:32.161+02:00","fingerprint":"c4b6b79a607b6ba0",` + + `"receivers":[{"name":"dummy"}],"startsAt":"2021-04-21T09:47:32.161+02:00",` + + `"status":{"inhibitedBy":[],"silencedBy":[],"state":"unprocessed"},` + + `"updatedAt":"2021-04-21T07:47:32.163Z","labels":{"group":"group_1","name":"alert_1"}},` + + `{"annotations":{},"endsAt":"2021-04-21T10:47:32.163+02:00","fingerprint":"c4b8b79a607bee77",` + + `"receivers":[{"name":"dummy"}],"startsAt":"2021-04-21T09:47:32.163+02:00",` + + `"status":{"inhibitedBy":[],"silencedBy":[],"state":"unprocessed"},` + + `"updatedAt":"2021-04-21T07:47:32.165Z","labels":{"group":"group_1","name":"alert_2"}}` + + `]`) + + out, err := V2Alerts{}.MergeResponses(in) + require.NoError(t, err) + require.Equal(t, expected, out) +} + +func v2ParseTime(s string) *strfmt.DateTime { + t, _ := time.Parse(time.RFC3339, s) + dt := strfmt.DateTime(t) + return &dt +} + +// v2alert is a convenience function to create alert structures with certain important fields set +// and with sensible defaults for the remaining fields to test they are passed through. +func v2alert(fingerprint, annotation, updatedAt string) *v2_models.GettableAlert { + receiver := "dummy" + return &v2_models.GettableAlert{ + Annotations: v2_models.LabelSet{ + "annotation1": annotation, + }, + EndsAt: v2ParseTime("2020-01-01T12:00:00.000Z"), + Fingerprint: &fingerprint, + Receivers: []*v2_models.Receiver{ + { + Name: &receiver, + }, + }, + StartsAt: v2ParseTime("2020-01-01T12:00:00.000Z"), + Status: &v2_models.AlertStatus{}, + UpdatedAt: v2ParseTime(updatedAt), + Alert: v2_models.Alert{ + GeneratorURL: strfmt.URI("something"), + Labels: v2_models.LabelSet{"label1": "foo"}, + }, + } +} + +func v2alerts(alerts ...*v2_models.GettableAlert) v2_models.GettableAlerts { + return alerts +} + +func TestMergeV2Alerts(t *testing.T) { + var ( + alert1 = v2alert("1111111111111111", "a1-", "2020-01-01T12:00:00.000Z") + newerAlert1 = v2alert("1111111111111111", "a1+x", "2020-01-01T12:00:00.001Z") + alert2 = v2alert("2222222222222222", "a2-", "2020-01-01T12:00:00.000Z") + alert3 = v2alert("3333333333333333", "a3-", "2020-01-01T12:00:00.000Z") + ) + cases := []struct { + name string + in v2_models.GettableAlerts + err error + out v2_models.GettableAlerts + }{ + { + name: "no alerts, should return an empty list", + in: v2alerts(), + out: v2_models.GettableAlerts{}, + }, + { + name: "one alert, should return the alert", + in: v2alerts(alert1), + out: v2alerts(alert1), + }, + { + name: "two alerts, should return two alerts", + in: v2alerts(alert1, alert2), + out: v2alerts(alert1, alert2), + }, + { + name: "three alerts, should return three alerts", + in: v2alerts(alert1, alert2, alert3), + out: v2alerts(alert1, alert2, alert3), + }, + { + name: "three alerts out of order, should return three alerts in fingerprint order", + in: v2alerts(alert3, alert2, alert1), + out: v2alerts(alert1, alert2, alert3), + }, + { + name: "two identical alerts, should return one alert", + in: v2alerts(alert1, alert1), + out: v2alerts(alert1), + }, + { + name: "two identical alerts plus another, should return two alerts", + in: v2alerts(alert1, alert1, alert2), + out: v2alerts(alert1, alert2), + }, + { + name: "two duplicates out of sync alerts, should return newer alert", + in: v2alerts(alert1, newerAlert1), + out: v2alerts(newerAlert1), + }, + { + name: "two duplicates out of sync alerts (newer first), should return newer alert", + in: v2alerts(newerAlert1, alert1), + out: v2alerts(newerAlert1), + }, + { + name: "two duplicates plus others, should return newer alert and others", + in: v2alerts(newerAlert1, alert3, alert1, alert2), + out: v2alerts(newerAlert1, alert2, alert3), + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + out, err := mergeV2Alerts(c.in) + require.Equal(t, c.err, err) + require.Equal(t, c.out, out) + }) + } +} From 983c152c08bf66e3275cc7204642c27e283c5080 Mon Sep 17 00:00:00 2001 From: Steve Simpson Date: Tue, 27 Apr 2021 12:35:15 +0200 Subject: [PATCH 2/2] Explicitly require some modules used in alertmanager. Signed-off-by: Steve Simpson --- go.mod | 2 ++ vendor/modules.txt | 2 ++ 2 files changed, 4 insertions(+) diff --git a/go.mod b/go.mod index 7542fc0e009..6fccfd49282 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,8 @@ require ( github.com/felixge/fgprof v0.9.1 github.com/fsouza/fake-gcs-server v1.7.0 github.com/go-kit/kit v0.10.0 + github.com/go-openapi/strfmt v0.20.0 + github.com/go-openapi/swag v0.19.14 github.com/go-redis/redis/v8 v8.2.3 github.com/gocql/gocql v0.0.0-20200526081602-cd04bd7f22a7 github.com/gogo/protobuf v1.3.2 diff --git a/vendor/modules.txt b/vendor/modules.txt index 2bb30fed43d..3bdf0f7c3a9 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -177,8 +177,10 @@ github.com/go-openapi/runtime/security # github.com/go-openapi/spec v0.20.3 github.com/go-openapi/spec # github.com/go-openapi/strfmt v0.20.0 +## explicit github.com/go-openapi/strfmt # github.com/go-openapi/swag v0.19.14 +## explicit github.com/go-openapi/swag # github.com/go-openapi/validate v0.20.2 github.com/go-openapi/validate