Skip to content

Commit c80c7bc

Browse files
committed
*: support Alertmanager API v2
Signed-off-by: Simon Pasquier <spasquie@redhat.com>
1 parent 3cf366a commit c80c7bc

File tree

12 files changed

+218
-28
lines changed

12 files changed

+218
-28
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ Compactor now properly handles partial block uploads for all operation like rete
4545
- [#1904](https://github.com/thanos-io/thanos/pull/1904) Add a skip-chunks option in Store Series API to improve the response time of `/api/v1/series` endpoint.
4646
- [#1910](https://github.com/thanos-io/thanos/pull/1910) Query: `/api/v1/labels` now understands `POST` - useful for sending bigger requests
4747
- [#1939](https://github.com/thanos-io/thanos/pull/1939) Ruler: Add TLS and authentication support for query endpoints with the `--query.config` and `--query.config-file` CLI flags. See [documentation](docs/components/rule.md/#configuration) for further information.
48+
- [#xxxx](https://github.com/thanos-io/thanos/pull/xxxx) Ruler: Add support for Alertmanager v2 API endpoints.
4849

4950
### Changed
5051

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ ME ?= $(shell whoami)
7171
PROM_VERSIONS ?= v2.4.3 v2.5.0 v2.8.1 v2.9.2 v2.13.0
7272
PROMS ?= $(GOBIN)/prometheus-v2.4.3 $(GOBIN)/prometheus-v2.5.0 $(GOBIN)/prometheus-v2.8.1 $(GOBIN)/prometheus-v2.9.2 $(GOBIN)/prometheus-v2.13.0
7373

74-
ALERTMANAGER_VERSION ?= v0.15.2
74+
ALERTMANAGER_VERSION ?= v0.20.0
7575
ALERTMANAGER ?= $(GOBIN)/alertmanager-$(ALERTMANAGER_VERSION)
7676

7777
MINIO_SERVER_VERSION ?= RELEASE.2018-10-06T00-15-16Z

cmd/thanos/rule.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ func runRule(
381381
// Discover and resolve Alertmanager addresses.
382382
addDiscoveryGroups(g, amClient, alertmgrsDNSSDInterval)
383383

384-
alertmgrs = append(alertmgrs, alert.NewAlertmanager(logger, amClient, time.Duration(cfg.Timeout)))
384+
alertmgrs = append(alertmgrs, alert.NewAlertmanager(logger, amClient, time.Duration(cfg.Timeout), cfg.APIVersion))
385385
}
386386

387387
// Run rule evaluation and alert notifications.

docs/components/rule.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,7 @@ alertmanagers:
416416
scheme: http
417417
path_prefix: ""
418418
timeout: 10s
419+
api_version: v1
419420
```
420421
421422
### Query API

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ require (
2929
github.com/fortytw2/leaktest v1.3.0
3030
github.com/fsnotify/fsnotify v1.4.7
3131
github.com/go-kit/kit v0.9.0
32+
github.com/go-openapi/strfmt v0.19.2
3233
github.com/gogo/protobuf v1.3.1
3334
github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9 // indirect
3435
github.com/golang/snappy v0.0.1
@@ -66,6 +67,7 @@ require (
6667
github.com/opentracing/basictracer-go v1.0.0
6768
github.com/opentracing/opentracing-go v1.1.0
6869
github.com/pkg/errors v0.8.1
70+
github.com/prometheus/alertmanager v0.20.0
6971
github.com/prometheus/client_golang v1.2.1
7072
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4
7173
github.com/prometheus/common v0.7.0

go.sum

Lines changed: 66 additions & 0 deletions
Large diffs are not rendered by default.

pkg/alert/alert.go

Lines changed: 64 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ import (
1616

1717
"github.com/go-kit/kit/log"
1818
"github.com/go-kit/kit/log/level"
19+
"github.com/go-openapi/strfmt"
1920
"github.com/pkg/errors"
21+
"github.com/prometheus/alertmanager/api/v2/models"
2022
"github.com/prometheus/client_golang/prometheus"
2123
"github.com/prometheus/prometheus/pkg/labels"
2224

@@ -26,7 +28,6 @@ import (
2628

2729
const (
2830
defaultAlertmanagerPort = 9093
29-
alertPushEndpoint = "/api/v1/alerts"
3031
contentTypeJSON = "application/json"
3132
)
3233

@@ -255,6 +256,7 @@ func (q *Queue) Push(alerts []*Alert) {
255256
type Sender struct {
256257
logger log.Logger
257258
alertmanagers []*Alertmanager
259+
versions []APIVersion
258260

259261
sent *prometheus.CounterVec
260262
errs *prometheus.CounterVec
@@ -272,9 +274,20 @@ func NewSender(
272274
if logger == nil {
273275
logger = log.NewNopLogger()
274276
}
277+
var (
278+
versions []APIVersion
279+
versionPresent map[APIVersion]struct{}
280+
)
281+
for _, am := range alertmanagers {
282+
if _, found := versionPresent[am.version]; found {
283+
continue
284+
}
285+
versions = append(versions, am.version)
286+
}
275287
s := &Sender{
276288
logger: logger,
277289
alertmanagers: alertmanagers,
290+
versions: versions,
278291

279292
sent: prometheus.NewCounterVec(prometheus.CounterOpts{
280293
Name: "thanos_alert_sender_alerts_sent_total",
@@ -302,6 +315,15 @@ func NewSender(
302315
return s
303316
}
304317

318+
func toAPILabels(labels labels.Labels) models.LabelSet {
319+
apiLabels := make(models.LabelSet, len(labels))
320+
for _, label := range labels {
321+
apiLabels[label.Name] = label.Value
322+
}
323+
324+
return apiLabels
325+
}
326+
305327
// Send an alert batch to all given Alertmanager clients.
306328
// TODO(bwplotka): https://github.com/thanos-io/thanos/issues/660.
307329
func (s *Sender) Send(ctx context.Context, alerts []*Alert) {
@@ -310,10 +332,38 @@ func (s *Sender) Send(ctx context.Context, alerts []*Alert) {
310332
if len(alerts) == 0 {
311333
return
312334
}
313-
b, err := json.Marshal(alerts)
314-
if err != nil {
315-
level.Warn(s.logger).Log("msg", "sending alerts failed", "err", err)
316-
return
335+
336+
payload := make(map[APIVersion][]byte)
337+
for _, version := range s.versions {
338+
var (
339+
b []byte
340+
err error
341+
)
342+
switch version {
343+
case APIv1:
344+
if b, err = json.Marshal(alerts); err != nil {
345+
level.Warn(s.logger).Log("msg", "encoding alerts for v1 API failed", "err", err)
346+
return
347+
}
348+
case APIv2:
349+
apiAlerts := make(models.PostableAlerts, 0, len(alerts))
350+
for _, a := range alerts {
351+
apiAlerts = append(apiAlerts, &models.PostableAlert{
352+
Annotations: toAPILabels(a.Annotations),
353+
EndsAt: strfmt.DateTime(a.EndsAt),
354+
StartsAt: strfmt.DateTime(a.StartsAt),
355+
Alert: models.Alert{
356+
GeneratorURL: strfmt.URI(a.GeneratorURL),
357+
Labels: toAPILabels(a.Labels),
358+
},
359+
})
360+
}
361+
if b, err = json.Marshal(apiAlerts); err != nil {
362+
level.Warn(s.logger).Log("msg", "encoding alerts for v2 API failed", "err", err)
363+
return
364+
}
365+
}
366+
payload[version] = b
317367
}
318368

319369
var (
@@ -323,18 +373,19 @@ func (s *Sender) Send(ctx context.Context, alerts []*Alert) {
323373
for _, am := range s.alertmanagers {
324374
for _, u := range am.dispatcher.Endpoints() {
325375
wg.Add(1)
326-
go func(am *Alertmanager, u *url.URL) {
376+
go func(am *Alertmanager, u url.URL) {
327377
defer wg.Done()
328378

329379
level.Debug(s.logger).Log("msg", "sending alerts", "alertmanager", u.Host, "numAlerts", len(alerts))
330380
start := time.Now()
381+
u.Path = path.Join(u.Path, fmt.Sprintf("/api/%s/alerts", string(am.version)))
331382
span, ctx := tracing.StartSpan(ctx, "post_alerts HTTP[client]")
332383
defer span.Finish()
333-
if err := am.postAlerts(ctx, *u, bytes.NewReader(b)); err != nil {
384+
if err := am.postAlerts(ctx, u, bytes.NewReader(payload[am.version])); err != nil {
334385
level.Warn(s.logger).Log(
335386
"msg", "sending alerts failed",
336387
"alertmanager", u.Host,
337-
"numAlerts", len(alerts),
388+
"alerts", string(payload[am.version]),
338389
"err", err,
339390
)
340391
s.errs.WithLabelValues(u.Host).Inc()
@@ -344,7 +395,7 @@ func (s *Sender) Send(ctx context.Context, alerts []*Alert) {
344395
s.sent.WithLabelValues(u.Host).Add(float64(len(alerts)))
345396

346397
atomic.AddUint64(&numSuccess, 1)
347-
}(am, u)
398+
}(am, *u)
348399
}
349400
}
350401
wg.Wait()
@@ -354,7 +405,7 @@ func (s *Sender) Send(ctx context.Context, alerts []*Alert) {
354405
}
355406

356407
s.dropped.Add(float64(len(alerts)))
357-
level.Warn(s.logger).Log("msg", "failed to send alerts to all alertmanagers", "alerts", string(b))
408+
level.Warn(s.logger).Log("msg", "failed to send alerts to all alertmanagers")
358409
}
359410

360411
type Dispatcher interface {
@@ -369,10 +420,11 @@ type Alertmanager struct {
369420
logger log.Logger
370421
dispatcher Dispatcher
371422
timeout time.Duration
423+
version APIVersion
372424
}
373425

374426
// NewAlertmanager returns a new Alertmanager client.
375-
func NewAlertmanager(logger log.Logger, dispatcher Dispatcher, timeout time.Duration) *Alertmanager {
427+
func NewAlertmanager(logger log.Logger, dispatcher Dispatcher, timeout time.Duration, version APIVersion) *Alertmanager {
376428
if logger == nil {
377429
logger = log.NewNopLogger()
378430
}
@@ -381,11 +433,11 @@ func NewAlertmanager(logger log.Logger, dispatcher Dispatcher, timeout time.Dura
381433
logger: logger,
382434
dispatcher: dispatcher,
383435
timeout: timeout,
436+
version: version,
384437
}
385438
}
386439

387440
func (a *Alertmanager) postAlerts(ctx context.Context, u url.URL, r io.Reader) error {
388-
u.Path = path.Join(u.Path, alertPushEndpoint)
389441
req, err := http.NewRequest("POST", u.String(), r)
390442
if err != nil {
391443
return err

pkg/alert/alert_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ func TestSenderSendsOk(t *testing.T) {
7777
poster := &fakeClient{
7878
urls: []*url.URL{{Host: "am1:9090"}, {Host: "am2:9090"}},
7979
}
80-
s := NewSender(nil, nil, []*Alertmanager{NewAlertmanager(nil, poster, time.Minute)})
80+
s := NewSender(nil, nil, []*Alertmanager{NewAlertmanager(nil, poster, time.Minute, APIv1)})
8181

8282
s.Send(context.Background(), []*Alert{{}, {}})
8383

@@ -104,7 +104,7 @@ func TestSenderSendsOneFails(t *testing.T) {
104104
return rec.Result(), nil
105105
},
106106
}
107-
s := NewSender(nil, nil, []*Alertmanager{NewAlertmanager(nil, poster, time.Minute)})
107+
s := NewSender(nil, nil, []*Alertmanager{NewAlertmanager(nil, poster, time.Minute, APIv1)})
108108

109109
s.Send(context.Background(), []*Alert{{}, {}})
110110

@@ -125,7 +125,7 @@ func TestSenderSendsAllFail(t *testing.T) {
125125
return nil, errors.New("no such host")
126126
},
127127
}
128-
s := NewSender(nil, nil, []*Alertmanager{NewAlertmanager(nil, poster, time.Minute)})
128+
s := NewSender(nil, nil, []*Alertmanager{NewAlertmanager(nil, poster, time.Minute, APIv1)})
129129

130130
s.Send(context.Background(), []*Alert{{}, {}})
131131

pkg/alert/config.go

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"strings"
88
"time"
99

10+
"github.com/pkg/errors"
1011
"github.com/prometheus/common/model"
1112
"gopkg.in/yaml.v2"
1213

@@ -19,11 +20,39 @@ type AlertingConfig struct {
1920
}
2021

2122
// AlertmanagerConfig represents a client to a cluster of Alertmanager endpoints.
22-
// TODO(simonpasquier): add support for API version (v1 or v2).
2323
type AlertmanagerConfig struct {
2424
HTTPClientConfig http_util.ClientConfig `yaml:"http_config"`
2525
EndpointsConfig http_util.EndpointsConfig `yaml:",inline"`
2626
Timeout model.Duration `yaml:"timeout"`
27+
APIVersion APIVersion `yaml:"api_version"`
28+
}
29+
30+
// APIVersion represents the API version of the Alertmanager endpoint.
31+
type APIVersion string
32+
33+
const (
34+
APIv1 APIVersion = "v1"
35+
APIv2 APIVersion = "v2"
36+
)
37+
38+
var supportedAPIVersions = []APIVersion{
39+
APIv1, APIv2,
40+
}
41+
42+
// UnmarshalYAML implements the yaml.Unmarshaler interface.
43+
func (v *APIVersion) UnmarshalYAML(unmarshal func(interface{}) error) error {
44+
var s string
45+
if err := unmarshal(&s); err != nil {
46+
return errors.Wrap(err, "invalid Alertmanager API version")
47+
}
48+
49+
for _, ver := range supportedAPIVersions {
50+
if APIVersion(s) == ver {
51+
*v = ver
52+
return nil
53+
}
54+
}
55+
return errors.Errorf("expected Alertmanager API version to be one of %v but got %q", supportedAPIVersions, s)
2756
}
2857

2958
func DefaultAlertmanagerConfig() AlertmanagerConfig {
@@ -33,7 +62,8 @@ func DefaultAlertmanagerConfig() AlertmanagerConfig {
3362
StaticAddresses: []string{},
3463
FileSDConfigs: []http_util.FileSDConfig{},
3564
},
36-
Timeout: model.Duration(time.Second * 10),
65+
Timeout: model.Duration(time.Second * 10),
66+
APIVersion: APIv1,
3767
}
3868
}
3969

pkg/alert/config_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,43 @@ import (
44
"testing"
55
"time"
66

7+
"gopkg.in/yaml.v2"
8+
79
"github.com/thanos-io/thanos/pkg/http"
810
"github.com/thanos-io/thanos/pkg/testutil"
911
)
1012

13+
func TestUnmarshalAPIVersion(t *testing.T) {
14+
for _, tc := range []struct {
15+
v string
16+
17+
err bool
18+
expected APIVersion
19+
}{
20+
{
21+
v: "v1",
22+
expected: APIv1,
23+
},
24+
{
25+
v: "v3",
26+
err: true,
27+
},
28+
{
29+
v: "{}",
30+
err: true,
31+
},
32+
} {
33+
var got APIVersion
34+
err := yaml.Unmarshal([]byte(tc.v), &got)
35+
if tc.err {
36+
testutil.NotOk(t, err)
37+
continue
38+
}
39+
testutil.Ok(t, err)
40+
testutil.Equals(t, tc.expected, got)
41+
}
42+
}
43+
1144
func TestBuildAlertmanagerConfiguration(t *testing.T) {
1245
for _, tc := range []struct {
1346
address string

0 commit comments

Comments
 (0)