forked from cortexproject/cortex-tools
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathalerts.go
More file actions
297 lines (249 loc) · 10.5 KB
/
Copy pathalerts.go
File metadata and controls
297 lines (249 loc) · 10.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
package commands
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"os/signal"
"path/filepath"
"strings"
"time"
"net/url"
"github.com/pkg/errors"
"github.com/prometheus/alertmanager/config"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/collectors"
"github.com/prometheus/client_golang/prometheus/promhttp"
log "github.com/sirupsen/logrus"
"gopkg.in/alecthomas/kingpin.v2"
"github.com/cortexproject/cortex-tools/pkg/client"
"github.com/cortexproject/cortex-tools/pkg/printer"
)
var (
nonDuplicateAlerts = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "cortextool_alerts_single_source",
Help: "Alerts found by the alerts verify command that are coming from a single source rather than multiple sources..",
},
)
)
// AlertmanagerCommand configures and executes rule related cortex api operations
type AlertmanagerCommand struct {
ClientConfig client.Config
AlertmanagerURL url.URL
AlertmanagerConfigFile string
TemplateFiles []string
DisableColor bool
cli *client.CortexClient
}
// AlertCommand configures and executes rule related PromQL queries for alerts comparison.
type AlertCommand struct {
CortexURL string
IgnoreString string
IgnoreAlerts map[string]interface{}
SourceLabel string
NumSources int
GracePeriod int
CheckFrequency int
ClientConfig client.Config
cli *client.CortexClient
}
// Register rule related commands and flags with the kingpin application
func (a *AlertmanagerCommand) Register(app *kingpin.Application) {
alertCmd := app.Command("alertmanager", "View & edit alertmanager configs stored in cortex.").PreAction(a.setup)
alertCmd.Flag("address", "Address of the cortex cluster, alternatively set CORTEX_ADDRESS.").Envar("CORTEX_ADDRESS").Required().StringVar(&a.ClientConfig.Address)
alertCmd.Flag("id", "Cortex tenant id, alternatively set CORTEX_TENANT_ID.").Envar("CORTEX_TENANT_ID").Required().StringVar(&a.ClientConfig.ID)
alertCmd.Flag("authToken", "Authentication token for bearer token or JWT auth, alternatively set CORTEX_AUTH_TOKEN.").Default("").Envar("CORTEX_AUTH_TOKEN").StringVar(&a.ClientConfig.AuthToken)
alertCmd.Flag("user", "API user to use when contacting cortex, alternatively set CORTEX_API_USER. If empty, CORTEX_TENANT_ID will be used instead.").Default("").Envar("CORTEX_API_USER").StringVar(&a.ClientConfig.User)
alertCmd.Flag("key", "API key to use when contacting cortex, alternatively set CORTEX_API_KEY.").Default("").Envar("CORTEX_API_KEY").StringVar(&a.ClientConfig.Key)
alertCmd.Flag("tls-ca-path", "TLS CA certificate to verify cortex API as part of mTLS, alternatively set CORTEX_TLS_CA_PATH.").Default("").Envar("CORTEX_TLS_CA_PATH").StringVar(&a.ClientConfig.TLS.CAPath)
alertCmd.Flag("tls-cert-path", "TLS client certificate to authenticate with cortex API as part of mTLS, alternatively set CORTEX_TLS_CERT_PATH.").Default("").Envar("CORTEX_TLS_CERT_PATH").StringVar(&a.ClientConfig.TLS.CertPath)
alertCmd.Flag("tls-key-path", "TLS client certificate private key to authenticate with cortex API as part of mTLS, alternatively set CORTEX_TLS_KEY_PATH.").Default("").Envar("CORTEX_TLS_KEY_PATH").StringVar(&a.ClientConfig.TLS.KeyPath)
// Get Alertmanager Configs Command
getAlertsCmd := alertCmd.Command("get", "Get the alertmanager config currently in the cortex alertmanager.").Action(a.getConfig)
getAlertsCmd.Flag("disable-color", "disable colored output").BoolVar(&a.DisableColor)
alertCmd.Command("delete", "Delete the alertmanager config currently in the cortex alertmanager.").Action(a.deleteConfig)
loadalertCmd := alertCmd.Command("load", "load a set of rules to a designated cortex endpoint").Action(a.loadConfig)
loadalertCmd.Arg("config", "alertmanager configuration to load").Required().StringVar(&a.AlertmanagerConfigFile)
loadalertCmd.Arg("template-files", "The template files to load").ExistingFilesVar(&a.TemplateFiles)
}
func (a *AlertmanagerCommand) setup(_ *kingpin.ParseContext) error {
cli, err := client.New(a.ClientConfig)
if err != nil {
return err
}
a.cli = cli
return nil
}
func (a *AlertmanagerCommand) getConfig(_ *kingpin.ParseContext) error {
cfg, templates, err := a.cli.GetAlertmanagerConfig(context.Background())
if err != nil {
if err == client.ErrResourceNotFound {
log.Infof("no alertmanager config currently exist for this user")
return nil
}
return err
}
p := printer.New(a.DisableColor)
return p.PrintAlertmanagerConfig(cfg, templates)
}
func (a *AlertmanagerCommand) loadConfig(_ *kingpin.ParseContext) error {
content, err := os.ReadFile(a.AlertmanagerConfigFile)
if err != nil {
return errors.Wrap(err, "unable to load config file: "+a.AlertmanagerConfigFile)
}
cfg := string(content)
_, err = config.Load(cfg)
if err != nil {
return err
}
templates, err := createTemplates(a.TemplateFiles)
if err != nil {
return err
}
return a.cli.CreateAlertmanagerConfig(context.Background(), cfg, templates)
}
func createTemplates(templateFiles []string) (map[string]string, error) {
templates := make(map[string]string)
for _, f := range templateFiles {
tmpl, err := os.ReadFile(f)
if err != nil {
return nil, errors.Wrap(err, "unable to load template file: "+f)
}
baseName := filepath.Base(f)
if _, ok := templates[baseName]; ok {
return nil, fmt.Errorf("duplicate template file name: %s", baseName)
}
templates[baseName] = string(tmpl)
}
return templates, nil
}
func (a *AlertmanagerCommand) deleteConfig(_ *kingpin.ParseContext) error {
err := a.cli.DeleteAlermanagerConfig(context.Background())
if err != nil && err != client.ErrResourceNotFound {
return err
}
return nil
}
func (a *AlertCommand) Register(app *kingpin.Application) {
alertCmd := app.Command("alerts", "View active alerts in alertmanager.").PreAction(a.setup)
alertCmd.Flag("address", "Address of the cortex cluster, alternatively set CORTEX_ADDRESS.").Envar("CORTEX_ADDRESS").Required().StringVar(&a.ClientConfig.Address)
alertCmd.Flag("id", "Cortex tenant id, alternatively set CORTEX_TENANT_ID.").Envar("CORTEX_TENANT_ID").Required().StringVar(&a.ClientConfig.ID)
alertCmd.Flag("authToken", "Authentication token for bearer token or JWT auth, alternatively set CORTEX_AUTH_TOKEN.").Default("").Envar("CORTEX_AUTH_TOKEN").StringVar(&a.ClientConfig.AuthToken)
alertCmd.Flag("user", "API user to use when contacting cortex, alternatively set CORTEX_API_USER. If empty, CORTEX_TENANT_ID will be used instead.").Default("").Envar("CORTEX_API_USER").StringVar(&a.ClientConfig.User)
alertCmd.Flag("key", "API key to use when contacting cortex, alternatively set CORTEX_API_KEY.").Default("").Envar("CORTEX_API_KEY").StringVar(&a.ClientConfig.Key)
verifyAlertsCmd := alertCmd.Command("verify", "Verifies alerts in an alertmanager cluster are deduplicated; useful for verifying correct configuration when transferring from Prometheus to Cortex alert evaluation.").Action(a.verifyConfig)
verifyAlertsCmd.Flag("ignore-alerts", "A comma separated list of Alert names to ignore in deduplication checks.").StringVar(&a.IgnoreString)
verifyAlertsCmd.Flag("source-label", "Label to look for when deciding if two alerts are duplicates of eachother from separate sources.").Default("prometheus").StringVar(&a.SourceLabel)
verifyAlertsCmd.Flag("grace-period", "Grace period, don't consider alert groups with the incorrect amount of alert replicas erroneous unless the alerts have existed for more than this amount of time, in minutes.").Default("2").IntVar(&a.GracePeriod)
verifyAlertsCmd.Flag("frequency", "Setting this value will turn cortextool into a long-running process, running the alerts verify check every # of minutes specified").IntVar(&a.CheckFrequency)
}
func (a *AlertCommand) setup(_ *kingpin.ParseContext) error {
cli, err := client.New(a.ClientConfig)
if err != nil {
return err
}
a.cli = cli
return nil
}
type queryResult struct {
Status string `json:"status"`
Data queryData `json:"data"`
}
type queryData struct {
ResultType string `json:"resultType"`
Result []metric `json:"result"`
}
type metric struct {
Metric map[string]string `json:"metric"`
}
func (a *AlertCommand) verifyConfig(_ *kingpin.ParseContext) error {
var empty interface{}
if a.IgnoreString != "" {
a.IgnoreAlerts = make(map[string]interface{})
chunks := strings.Split(a.IgnoreString, ",")
for _, name := range chunks {
a.IgnoreAlerts[name] = empty
log.Info("Ignoring alerts with name: ", name)
}
}
lhs := fmt.Sprintf("ALERTS{source!=\"%s\", alertstate=\"firing\"} offset %dm unless ignoring(source) ALERTS{source=\"%s\", alertstate=\"firing\"}",
a.SourceLabel,
a.GracePeriod,
a.SourceLabel)
rhs := fmt.Sprintf("ALERTS{source=\"%s\", alertstate=\"firing\"} offset %dm unless ignoring(source) ALERTS{source!=\"%s\", alertstate=\"firing\"}",
a.SourceLabel,
a.GracePeriod,
a.SourceLabel)
query := fmt.Sprintf("%s or %s", lhs, rhs)
if a.CheckFrequency <= 0 {
_, err := a.runVerifyQuery(context.Background(), query)
return err
}
// Use a different registerer than default so we don't get all the Cortex metrics, but include Go runtime metrics.
goStats := collectors.NewGoCollector()
reg := prometheus.NewRegistry()
reg.MustRegister(nonDuplicateAlerts)
reg.MustRegister(goStats)
http.Handle("/metrics", promhttp.HandlerFor(
reg,
promhttp.HandlerOpts{},
))
go func() {
log.Fatal(http.ListenAndServe(":9090", nil))
}()
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
defer func() {
signal.Stop(c)
cancel()
}()
var lastErr error
var n int
go func() {
ticker := time.NewTicker(time.Duration(a.CheckFrequency) * time.Minute)
for {
n, lastErr = a.runVerifyQuery(ctx, query)
nonDuplicateAlerts.Set(float64(n))
select {
case <-c:
cancel()
return
case <-ticker.C:
continue
}
}
}()
<-ctx.Done()
return lastErr
}
func (a *AlertCommand) runVerifyQuery(ctx context.Context, query string) (int, error) {
res, err := a.cli.Query(ctx, query)
if err != nil {
return 0, err
}
body, err := io.ReadAll(res.Body)
if err != nil {
return 0, err
}
defer res.Body.Close()
var data queryResult
err = json.Unmarshal(body, &data)
if err != nil {
return 0, err
}
for _, m := range data.Data.Result {
if _, ok := a.IgnoreAlerts[m.Metric["alertname"]]; !ok {
log.WithFields(log.Fields{
"alertname": m.Metric["alertname"],
"state": m.Metric,
}).Infof("alert found that was not in both sources")
}
}
log.WithFields(log.Fields{"count": len(data.Data.Result)}).Infof("found mismatching alerts")
return len(data.Data.Result), nil
}