Skip to content

Commit 23628b8

Browse files
authored
Implement quorum reads and merging for /v1/silences and /v1/silence/{id}. (#4146)
Reading silence listings via /v1/silences and /v1/silence/{id} will now read from a quorum of replicas and return a merged response. The implementation re-uses the logic for /v2, with the exception of handling the outer body of the response (status/data fields). Signed-off-by: Steve Simpson <[email protected]>
1 parent 15283ba commit 23628b8

File tree

6 files changed

+347
-16
lines changed

6 files changed

+347
-16
lines changed

pkg/alertmanager/distributor.go

+7-3
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,12 @@ func (d *Distributor) isQuorumReadPath(p string) (bool, merger.Merger) {
9494
if strings.HasSuffix(p, "/v2/alerts/groups") {
9595
return true, merger.V2AlertGroups{}
9696
}
97+
if strings.HasSuffix(p, "/v1/silences") {
98+
return true, merger.V1Silences{}
99+
}
100+
if strings.HasSuffix(path.Dir(p), "/v1/silence") {
101+
return true, merger.V1SilenceID{}
102+
}
97103
if strings.HasSuffix(p, "/v2/silences") {
98104
return true, merger.V2Silences{}
99105
}
@@ -104,9 +110,7 @@ func (d *Distributor) isQuorumReadPath(p string) (bool, merger.Merger) {
104110
}
105111

106112
func (d *Distributor) isUnaryReadPath(p string) bool {
107-
return strings.HasSuffix(p, "/v1/silences") ||
108-
strings.HasSuffix(path.Dir(p), "/v1/silence") ||
109-
strings.HasSuffix(p, "/status") ||
113+
return strings.HasSuffix(p, "/status") ||
110114
strings.HasSuffix(p, "/receivers")
111115
}
112116

pkg/alertmanager/distributor_test.go

+6-13
Original file line numberDiff line numberDiff line change
@@ -119,14 +119,15 @@ func TestDistributor_DistributeRequest(t *testing.T) {
119119
headersNotPreserved: true,
120120
route: "/alerts/groups",
121121
}, {
122-
name: "Read /v1/silences is sent to only 1 AM",
122+
name: "Read /v1/silences is sent to 3 AMs",
123123
numAM: 5,
124124
numHappyAM: 5,
125125
replicationFactor: 3,
126126
isRead: true,
127127
expStatusCode: http.StatusOK,
128-
expectedTotalCalls: 1,
128+
expectedTotalCalls: 3,
129129
route: "/v1/silences",
130+
responseBody: []byte(`{"status":"success","data":[]}`),
130131
}, {
131132
name: "Read /v2/silences is sent to 3 AMs",
132133
numAM: 5,
@@ -146,14 +147,15 @@ func TestDistributor_DistributeRequest(t *testing.T) {
146147
expectedTotalCalls: 1,
147148
route: "/silences",
148149
}, {
149-
name: "Read /v1/silence/id is sent to only 1 AM",
150+
name: "Read /v1/silence/id is sent to 3 AMs",
150151
numAM: 5,
151152
numHappyAM: 5,
152153
replicationFactor: 3,
153154
isRead: true,
154155
expStatusCode: http.StatusOK,
155-
expectedTotalCalls: 1,
156+
expectedTotalCalls: 3,
156157
route: "/v1/silence/id",
158+
responseBody: []byte(`{"status":"success","data":{"id":"aaa","updatedAt":"2020-01-01T00:00:00Z"}}`),
157159
}, {
158160
name: "Read /v2/silence/id is sent to 3 AMs",
159161
numAM: 5,
@@ -174,15 +176,6 @@ func TestDistributor_DistributeRequest(t *testing.T) {
174176
expectedTotalCalls: 0,
175177
headersNotPreserved: true,
176178
route: "/silence/id",
177-
}, {
178-
name: "Read /silence/id is sent to only 1 AM",
179-
numAM: 5,
180-
numHappyAM: 5,
181-
replicationFactor: 3,
182-
isRead: true,
183-
expStatusCode: http.StatusOK,
184-
expectedTotalCalls: 1,
185-
route: "/silence/id",
186179
}, {
187180
name: "Delete /silence/id is sent to only 1 AM",
188181
numAM: 5,
+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package merger
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"fmt"
7+
8+
v2_models "github.com/prometheus/alertmanager/api/v2/models"
9+
)
10+
11+
// V1Silences implements the Merger interface for GET /v1/silences. This re-uses the logic for
12+
// merging /v2/silences, with additional handling for the enclosing status/data fields. Unlike for
13+
// alerts, the API definitions for silences are almost identical between v1 and v2. The differences
14+
// are that the fields in the JSON output are ordered differently, and the timestamps have more
15+
// precision in v1, but these differences should not be problematic to clients.
16+
type V1SilenceID struct{}
17+
18+
func (V1SilenceID) MergeResponses(in [][]byte) ([]byte, error) {
19+
type bodyType struct {
20+
Status string `json:"status"`
21+
Data *v2_models.GettableSilence `json:"data"`
22+
}
23+
24+
silences := make(v2_models.GettableSilences, 0)
25+
for _, body := range in {
26+
parsed := bodyType{}
27+
if err := json.Unmarshal(body, &parsed); err != nil {
28+
return nil, err
29+
}
30+
if parsed.Status != statusSuccess {
31+
return nil, fmt.Errorf("unable to merge response of status: %s", parsed.Status)
32+
}
33+
silences = append(silences, parsed.Data)
34+
}
35+
36+
merged, err := mergeV2Silences(silences)
37+
if err != nil {
38+
return nil, err
39+
}
40+
41+
if len(merged) != 1 {
42+
return nil, errors.New("unexpected mismatched silence ids")
43+
}
44+
45+
body := bodyType{
46+
Status: statusSuccess,
47+
Data: merged[0],
48+
}
49+
50+
return json.Marshal(body)
51+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package merger
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
)
8+
9+
func TestV1SilenceID_ReturnsNewestSilence(t *testing.T) {
10+
in := [][]byte{
11+
[]byte(`{"status":"success","data":{` +
12+
`"id":"77b580dd-1d9c-4b7e-9bba-13ac173cb4e5",` +
13+
`"matchers":[` +
14+
`{` +
15+
`"name":"instance",` +
16+
`"value":"prometheus-one",` +
17+
`"isRegex":false,` +
18+
`"isEqual":true` +
19+
`}` +
20+
`],` +
21+
`"startsAt":"2021-04-28T17:31:01.725956017Z",` +
22+
`"endsAt":"2021-04-28T20:31:01.722829007+02:00",` +
23+
`"updatedAt":"2021-04-28T17:32:01.725956017Z",` +
24+
`"createdBy":"",` +
25+
`"comment":"The newer silence",` +
26+
`"status":{"state":"active"}` +
27+
`}}`),
28+
[]byte(`{"status":"success","data":{` +
29+
`"id":"77b580dd-1d9c-4b7e-9bba-13ac173cb4e5",` +
30+
`"matchers":[` +
31+
`{` +
32+
`"name":"instance",` +
33+
`"value":"prometheus-one",` +
34+
`"isRegex":false,` +
35+
`"isEqual":true` +
36+
`}` +
37+
`],` +
38+
`"startsAt":"2021-04-28T17:31:01.725956017Z",` +
39+
`"endsAt":"2021-04-28T20:31:01.722829007+02:00",` +
40+
`"updatedAt":"2021-04-28T17:31:01.725956017Z",` +
41+
`"createdBy":"",` +
42+
`"comment":"Silence Comment #1",` +
43+
`"status":{"state":"active"}` +
44+
`}}`),
45+
}
46+
47+
expected := []byte(`{"status":"success","data":{` +
48+
`"id":"77b580dd-1d9c-4b7e-9bba-13ac173cb4e5",` +
49+
`"status":{"state":"active"},` +
50+
`"updatedAt":"2021-04-28T17:32:01.725Z",` +
51+
`"comment":"The newer silence",` +
52+
`"createdBy":"",` +
53+
`"endsAt":"2021-04-28T20:31:01.722+02:00",` +
54+
`"matchers":[` +
55+
`{` +
56+
`"isEqual":true,` +
57+
`"isRegex":false,` +
58+
`"name":"instance",` +
59+
`"value":"prometheus-one"` +
60+
`}` +
61+
`],` +
62+
`"startsAt":"2021-04-28T17:31:01.725Z"` +
63+
`}}`)
64+
65+
out, err := V1SilenceID{}.MergeResponses(in)
66+
require.NoError(t, err)
67+
require.Equal(t, string(expected), string(out))
68+
}
69+
70+
func TestV1SilenceID_InvalidDifferentIDs(t *testing.T) {
71+
in := [][]byte{
72+
[]byte(`{"status":"success","data":{` +
73+
`"id":"77b580dd-1d9c-4b7e-9bba-13ac173cb4e5",` +
74+
`"matchers":[` +
75+
`{` +
76+
`"name":"instance",` +
77+
`"value":"prometheus-one",` +
78+
`"isRegex":false,` +
79+
`"isEqual":true` +
80+
`}` +
81+
`],` +
82+
`"startsAt":"2021-04-28T17:31:01.725956017Z",` +
83+
`"endsAt":"2021-04-28T20:31:01.722829007+02:00",` +
84+
`"updatedAt":"2021-04-28T17:32:01.725956017Z",` +
85+
`"createdBy":"",` +
86+
`"comment":"Silence Comment #1",` +
87+
`"status":{"state":"active"}` +
88+
`}}`),
89+
[]byte(`{"status":"success","data":{` +
90+
`"id":"261248d1-4ff7-4cf1-9957-850c65f4e48b",` +
91+
`"matchers":[` +
92+
`{` +
93+
`"name":"instance",` +
94+
`"value":"prometheus-one",` +
95+
`"isRegex":false,` +
96+
`"isEqual":true` +
97+
`}` +
98+
`],` +
99+
`"startsAt":"2021-04-28T17:31:01.725956017Z",` +
100+
`"endsAt":"2021-04-28T20:31:01.722829007+02:00",` +
101+
`"updatedAt":"2021-04-28T17:31:01.725956017Z",` +
102+
`"createdBy":"",` +
103+
`"comment":"Silence Comment #2",` +
104+
`"status":{"state":"active"}` +
105+
`}}`),
106+
}
107+
108+
_, err := V1SilenceID{}.MergeResponses(in)
109+
require.Error(t, err)
110+
}
+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package merger
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
7+
v2_models "github.com/prometheus/alertmanager/api/v2/models"
8+
)
9+
10+
// V1Silences implements the Merger interface for GET /v1/silences. Unlike for alerts, the API
11+
// definitions for silences are almost identical between v1 and v2. The differences are that the
12+
// fields in the JSON output are ordered differently, and the timestamps have more precision in v1,
13+
// but these differences should not be problematic to clients. Therefore, the implementation
14+
// re-uses the v2 types, with additional handling for the enclosing status/data fields.
15+
type V1Silences struct{}
16+
17+
func (V1Silences) MergeResponses(in [][]byte) ([]byte, error) {
18+
type bodyType struct {
19+
Status string `json:"status"`
20+
Data v2_models.GettableSilences `json:"data"`
21+
}
22+
23+
silences := make(v2_models.GettableSilences, 0)
24+
for _, body := range in {
25+
parsed := bodyType{}
26+
if err := json.Unmarshal(body, &parsed); err != nil {
27+
return nil, err
28+
}
29+
if parsed.Status != statusSuccess {
30+
return nil, fmt.Errorf("unable to merge response of status: %s", parsed.Status)
31+
}
32+
silences = append(silences, parsed.Data...)
33+
}
34+
35+
merged, err := mergeV2Silences(silences)
36+
if err != nil {
37+
return nil, err
38+
}
39+
body := bodyType{
40+
Status: statusSuccess,
41+
Data: merged,
42+
}
43+
44+
return json.Marshal(body)
45+
}

0 commit comments

Comments
 (0)