Skip to content

Commit a2f54d6

Browse files
authored
Merge pull request prometheus#16398 from siavashs/notifier-refactor
chore: refactor notifier package
2 parents 683e698 + ef48e4c commit a2f54d6

File tree

9 files changed

+541
-391
lines changed

9 files changed

+541
-391
lines changed

notifier/alert.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Copyright 2013 The Prometheus Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package notifier
15+
16+
import (
17+
"fmt"
18+
"time"
19+
20+
"github.com/prometheus/prometheus/model/labels"
21+
"github.com/prometheus/prometheus/model/relabel"
22+
)
23+
24+
// Alert is a generic representation of an alert in the Prometheus eco-system.
25+
type Alert struct {
26+
// Label value pairs for purpose of aggregation, matching, and disposition
27+
// dispatching. This must minimally include an "alertname" label.
28+
Labels labels.Labels `json:"labels"`
29+
30+
// Extra key/value information which does not define alert identity.
31+
Annotations labels.Labels `json:"annotations"`
32+
33+
// The known time range for this alert. Both ends are optional.
34+
StartsAt time.Time `json:"startsAt,omitempty"`
35+
EndsAt time.Time `json:"endsAt,omitempty"`
36+
GeneratorURL string `json:"generatorURL,omitempty"`
37+
}
38+
39+
// Name returns the name of the alert. It is equivalent to the "alertname" label.
40+
func (a *Alert) Name() string {
41+
return a.Labels.Get(labels.AlertName)
42+
}
43+
44+
// Hash returns a hash over the alert. It is equivalent to the alert labels hash.
45+
func (a *Alert) Hash() uint64 {
46+
return a.Labels.Hash()
47+
}
48+
49+
func (a *Alert) String() string {
50+
s := fmt.Sprintf("%s[%s]", a.Name(), fmt.Sprintf("%016x", a.Hash())[:7])
51+
if a.Resolved() {
52+
return s + "[resolved]"
53+
}
54+
return s + "[active]"
55+
}
56+
57+
// Resolved returns true iff the activity interval ended in the past.
58+
func (a *Alert) Resolved() bool {
59+
return a.ResolvedAt(time.Now())
60+
}
61+
62+
// ResolvedAt returns true iff the activity interval ended before
63+
// the given timestamp.
64+
func (a *Alert) ResolvedAt(ts time.Time) bool {
65+
if a.EndsAt.IsZero() {
66+
return false
67+
}
68+
return !a.EndsAt.After(ts)
69+
}
70+
71+
func relabelAlerts(relabelConfigs []*relabel.Config, externalLabels labels.Labels, alerts []*Alert) []*Alert {
72+
lb := labels.NewBuilder(labels.EmptyLabels())
73+
var relabeledAlerts []*Alert
74+
75+
for _, a := range alerts {
76+
lb.Reset(a.Labels)
77+
externalLabels.Range(func(l labels.Label) {
78+
if a.Labels.Get(l.Name) == "" {
79+
lb.Set(l.Name, l.Value)
80+
}
81+
})
82+
83+
keep := relabel.ProcessBuilder(lb, relabelConfigs...)
84+
if !keep {
85+
continue
86+
}
87+
a.Labels = lb.Labels()
88+
relabeledAlerts = append(relabeledAlerts, a)
89+
}
90+
return relabeledAlerts
91+
}

notifier/alertmanager.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Copyright 2013 The Prometheus Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package notifier
15+
16+
import (
17+
"fmt"
18+
"net/url"
19+
"path"
20+
21+
"github.com/prometheus/common/model"
22+
23+
"github.com/prometheus/prometheus/config"
24+
"github.com/prometheus/prometheus/discovery/targetgroup"
25+
"github.com/prometheus/prometheus/model/labels"
26+
"github.com/prometheus/prometheus/model/relabel"
27+
)
28+
29+
// Alertmanager holds Alertmanager endpoint information.
30+
type alertmanager interface {
31+
url() *url.URL
32+
}
33+
34+
type alertmanagerLabels struct{ labels.Labels }
35+
36+
const pathLabel = "__alerts_path__"
37+
38+
func (a alertmanagerLabels) url() *url.URL {
39+
return &url.URL{
40+
Scheme: a.Get(model.SchemeLabel),
41+
Host: a.Get(model.AddressLabel),
42+
Path: a.Get(pathLabel),
43+
}
44+
}
45+
46+
// AlertmanagerFromGroup extracts a list of alertmanagers from a target group
47+
// and an associated AlertmanagerConfig.
48+
func AlertmanagerFromGroup(tg *targetgroup.Group, cfg *config.AlertmanagerConfig) ([]alertmanager, []alertmanager, error) {
49+
var res []alertmanager
50+
var droppedAlertManagers []alertmanager
51+
lb := labels.NewBuilder(labels.EmptyLabels())
52+
53+
for _, tlset := range tg.Targets {
54+
lb.Reset(labels.EmptyLabels())
55+
56+
for ln, lv := range tlset {
57+
lb.Set(string(ln), string(lv))
58+
}
59+
// Set configured scheme as the initial scheme label for overwrite.
60+
lb.Set(model.SchemeLabel, cfg.Scheme)
61+
lb.Set(pathLabel, postPath(cfg.PathPrefix, cfg.APIVersion))
62+
63+
// Combine target labels with target group labels.
64+
for ln, lv := range tg.Labels {
65+
if _, ok := tlset[ln]; !ok {
66+
lb.Set(string(ln), string(lv))
67+
}
68+
}
69+
70+
preRelabel := lb.Labels()
71+
keep := relabel.ProcessBuilder(lb, cfg.RelabelConfigs...)
72+
if !keep {
73+
droppedAlertManagers = append(droppedAlertManagers, alertmanagerLabels{preRelabel})
74+
continue
75+
}
76+
77+
addr := lb.Get(model.AddressLabel)
78+
if err := config.CheckTargetAddress(model.LabelValue(addr)); err != nil {
79+
return nil, nil, err
80+
}
81+
82+
res = append(res, alertmanagerLabels{lb.Labels()})
83+
}
84+
return res, droppedAlertManagers, nil
85+
}
86+
87+
func postPath(pre string, v config.AlertmanagerAPIVersion) string {
88+
alertPushEndpoint := fmt.Sprintf("/api/%v/alerts", string(v))
89+
return path.Join("/", pre, alertPushEndpoint)
90+
}

notifier/alertmanager_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright 2013 The Prometheus Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package notifier
15+
16+
import (
17+
"testing"
18+
19+
"github.com/stretchr/testify/require"
20+
21+
"github.com/prometheus/prometheus/config"
22+
)
23+
24+
func TestPostPath(t *testing.T) {
25+
cases := []struct {
26+
in, out string
27+
}{
28+
{
29+
in: "",
30+
out: "/api/v2/alerts",
31+
},
32+
{
33+
in: "/",
34+
out: "/api/v2/alerts",
35+
},
36+
{
37+
in: "/prefix",
38+
out: "/prefix/api/v2/alerts",
39+
},
40+
{
41+
in: "/prefix//",
42+
out: "/prefix/api/v2/alerts",
43+
},
44+
{
45+
in: "prefix//",
46+
out: "/prefix/api/v2/alerts",
47+
},
48+
}
49+
for _, c := range cases {
50+
require.Equal(t, c.out, postPath(c.in, config.AlertmanagerAPIVersionV2))
51+
}
52+
}
53+
54+
func TestLabelSetNotReused(t *testing.T) {
55+
tg := makeInputTargetGroup()
56+
_, _, err := AlertmanagerFromGroup(tg, &config.AlertmanagerConfig{})
57+
58+
require.NoError(t, err)
59+
60+
// Target modified during alertmanager extraction
61+
require.Equal(t, tg, makeInputTargetGroup())
62+
}

notifier/alertmanagerset.go

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// Copyright 2013 The Prometheus Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package notifier
15+
16+
import (
17+
"crypto/md5"
18+
"encoding/hex"
19+
"log/slog"
20+
"net/http"
21+
"sync"
22+
23+
config_util "github.com/prometheus/common/config"
24+
"github.com/prometheus/sigv4"
25+
"gopkg.in/yaml.v2"
26+
27+
"github.com/prometheus/prometheus/config"
28+
"github.com/prometheus/prometheus/discovery/targetgroup"
29+
)
30+
31+
// alertmanagerSet contains a set of Alertmanagers discovered via a group of service
32+
// discovery definitions that have a common configuration on how alerts should be sent.
33+
type alertmanagerSet struct {
34+
cfg *config.AlertmanagerConfig
35+
client *http.Client
36+
37+
metrics *alertMetrics
38+
39+
mtx sync.RWMutex
40+
ams []alertmanager
41+
droppedAms []alertmanager
42+
logger *slog.Logger
43+
}
44+
45+
func newAlertmanagerSet(cfg *config.AlertmanagerConfig, logger *slog.Logger, metrics *alertMetrics) (*alertmanagerSet, error) {
46+
client, err := config_util.NewClientFromConfig(cfg.HTTPClientConfig, "alertmanager")
47+
if err != nil {
48+
return nil, err
49+
}
50+
t := client.Transport
51+
52+
if cfg.SigV4Config != nil {
53+
t, err = sigv4.NewSigV4RoundTripper(cfg.SigV4Config, client.Transport)
54+
if err != nil {
55+
return nil, err
56+
}
57+
}
58+
59+
client.Transport = t
60+
61+
s := &alertmanagerSet{
62+
client: client,
63+
cfg: cfg,
64+
logger: logger,
65+
metrics: metrics,
66+
}
67+
return s, nil
68+
}
69+
70+
// sync extracts a deduplicated set of Alertmanager endpoints from a list
71+
// of target groups definitions.
72+
func (s *alertmanagerSet) sync(tgs []*targetgroup.Group) {
73+
allAms := []alertmanager{}
74+
allDroppedAms := []alertmanager{}
75+
76+
for _, tg := range tgs {
77+
ams, droppedAms, err := AlertmanagerFromGroup(tg, s.cfg)
78+
if err != nil {
79+
s.logger.Error("Creating discovered Alertmanagers failed", "err", err)
80+
continue
81+
}
82+
allAms = append(allAms, ams...)
83+
allDroppedAms = append(allDroppedAms, droppedAms...)
84+
}
85+
86+
s.mtx.Lock()
87+
defer s.mtx.Unlock()
88+
previousAms := s.ams
89+
// Set new Alertmanagers and deduplicate them along their unique URL.
90+
s.ams = []alertmanager{}
91+
s.droppedAms = []alertmanager{}
92+
s.droppedAms = append(s.droppedAms, allDroppedAms...)
93+
seen := map[string]struct{}{}
94+
95+
for _, am := range allAms {
96+
us := am.url().String()
97+
if _, ok := seen[us]; ok {
98+
continue
99+
}
100+
101+
// This will initialize the Counters for the AM to 0.
102+
s.metrics.sent.WithLabelValues(us)
103+
s.metrics.errors.WithLabelValues(us)
104+
105+
seen[us] = struct{}{}
106+
s.ams = append(s.ams, am)
107+
}
108+
// Now remove counters for any removed Alertmanagers.
109+
for _, am := range previousAms {
110+
us := am.url().String()
111+
if _, ok := seen[us]; ok {
112+
continue
113+
}
114+
s.metrics.latency.DeleteLabelValues(us)
115+
s.metrics.sent.DeleteLabelValues(us)
116+
s.metrics.errors.DeleteLabelValues(us)
117+
seen[us] = struct{}{}
118+
}
119+
}
120+
121+
func (s *alertmanagerSet) configHash() (string, error) {
122+
b, err := yaml.Marshal(s.cfg)
123+
if err != nil {
124+
return "", err
125+
}
126+
hash := md5.Sum(b)
127+
return hex.EncodeToString(hash[:]), nil
128+
}

0 commit comments

Comments
 (0)