Support of custom URL path for alert (#560)

app/vmalert: Support custom URL for alerts source

Add flag `external.alert.source` for configuring custom URL
for alert's source. This may be handy to re-point default source
URL to other systems like Grafana.
Updates #517
This commit is contained in:
kreedom 2020-06-21 13:32:46 +03:00 committed by GitHub
parent e149019c00
commit 7ec6711f06
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 170 additions and 78 deletions

View file

@ -7,6 +7,7 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"strconv"
"strings" "strings"
"time" "time"
@ -57,9 +58,11 @@ absolute path to all .yaml files in root.`)
remoteReadLookBack = flag.Duration("remoteRead.lookback", time.Hour, "Lookback defines how far to look into past for alerts timeseries."+ remoteReadLookBack = flag.Duration("remoteRead.lookback", time.Hour, "Lookback defines how far to look into past for alerts timeseries."+
" For example, if lookback=1h then range from now() to now()-1h will be scanned.") " For example, if lookback=1h then range from now() to now()-1h will be scanned.")
evaluationInterval = flag.Duration("evaluationInterval", time.Minute, "How often to evaluate the rules") evaluationInterval = flag.Duration("evaluationInterval", time.Minute, "How often to evaluate the rules")
notifierURL = flag.String("notifier.url", "", "Prometheus alertmanager URL. Required parameter. e.g. http://127.0.0.1:9093") notifierURL = flag.String("notifier.url", "", "Prometheus alertmanager URL. Required parameter. e.g. http://127.0.0.1:9093")
externalURL = flag.String("external.url", "", "External URL is used as alert's source for sent alerts to the notifier") externalURL = flag.String("external.url", "", "External URL is used as alert's source for sent alerts to the notifier")
externalAlertSource = flag.String("external.alert.source", "", `External Alert Source allows to override the Source link for alerts sent to AlertManager for cases where you want to build a custom link to Grafana, Prometheus or any other service.
eg. 'explore?orgId=1&left=[\"now-1h\",\"now\",\"VictoriaMetrics\",{\"expr\": \"{{$expr|quotesEscape|pathEscape}}\"},{\"mode\":\"Metrics\"},{\"ui\":[true,true,true,\"none\"]}]'.If empty '/api/v1/:groupID/alertID/status' is used`)
) )
func main() { func main() {
@ -76,13 +79,15 @@ func main() {
logger.Fatalf("can not get external url: %s ", err) logger.Fatalf("can not get external url: %s ", err)
} }
notifier.InitTemplateFunc(eu) notifier.InitTemplateFunc(eu)
aug, err := getAlertURLGenerator(eu, *externalAlertSource, *validateTemplates)
if err != nil {
logger.Fatalf("URL generator error: %s", err)
}
manager := &manager{ manager := &manager{
groups: make(map[uint64]*Group), groups: make(map[uint64]*Group),
storage: datasource.NewVMStorage(*datasourceURL, *basicAuthUsername, *basicAuthPassword, &http.Client{}), storage: datasource.NewVMStorage(*datasourceURL, *basicAuthUsername, *basicAuthPassword, &http.Client{}),
notifier: notifier.NewAlertManager(*notifierURL, func(group, alert string) string { notifier: notifier.NewAlertManager(*notifierURL, aug, &http.Client{}),
return fmt.Sprintf("%s/api/v1/%s/%s/status", eu, group, alert)
}, &http.Client{}),
} }
if *remoteWriteURL != "" { if *remoteWriteURL != "" {
c, err := remotewrite.NewClient(ctx, remotewrite.Config{ c, err := remotewrite.NewClient(ctx, remotewrite.Config{
@ -166,6 +171,31 @@ func getExternalURL(externalURL, httpListenAddr string, isSecure bool) (*url.URL
return url.Parse(fmt.Sprintf("%s%s%s", schema, hname, port)) return url.Parse(fmt.Sprintf("%s%s%s", schema, hname, port))
} }
func getAlertURLGenerator(externalURL *url.URL, externalAlertSource string, validateTemplate bool) (notifier.AlertURLGenerator, error) {
if externalAlertSource == "" {
return func(alert notifier.Alert) string {
return fmt.Sprintf("%s/api/v1/%s/%s/status", externalURL, strconv.FormatUint(alert.GroupID, 10), strconv.FormatUint(alert.ID, 10))
}, nil
}
if validateTemplate {
if err := notifier.ValidateTemplates(map[string]string{
"tpl": externalAlertSource,
}); err != nil {
return nil, fmt.Errorf("error validating source template %s:%w", externalAlertSource, err)
}
}
m := map[string]string{
"tpl": externalAlertSource,
}
return func(alert notifier.Alert) string {
templated, err := alert.ExecTemplate(m)
if err != nil {
logger.Errorf("can not exec source template %s", err)
}
return fmt.Sprintf("%s/%s", externalURL, templated["tpl"])
}, nil
}
func checkFlags() { func checkFlags() {
if *notifierURL == "" { if *notifierURL == "" {
flag.PrintDefaults() flag.PrintDefaults()

53
app/vmalert/main_test.go Normal file
View file

@ -0,0 +1,53 @@
package main
import (
"fmt"
"net/url"
"os"
"testing"
"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/notifier"
)
func TestGetExternalURL(t *testing.T) {
expURL := "https://vicotriametrics.com/path"
u, err := getExternalURL(expURL, "", false)
if err != nil {
t.Errorf("unexpected error %s", err)
}
if u.String() != expURL {
t.Errorf("unexpected url want %s, got %s", expURL, u.String())
}
h, _ := os.Hostname()
expURL = fmt.Sprintf("https://%s:4242", h)
u, err = getExternalURL("", "0.0.0.0:4242", true)
if err != nil {
t.Errorf("unexpected error %s", err)
}
if u.String() != expURL {
t.Errorf("unexpected url want %s, got %s", expURL, u.String())
}
}
func TestGetAlertURLGenerator(t *testing.T) {
testAlert := notifier.Alert{GroupID: 42, ID: 2, Value: 4}
u, _ := url.Parse("https://victoriametrics.com/path")
fn, err := getAlertURLGenerator(u, "", false)
if err != nil {
t.Errorf("unexpected error %s", err)
}
if exp := "https://victoriametrics.com/path/api/v1/42/2/status"; exp != fn(testAlert) {
t.Errorf("unexpected url want %s, got %s", exp, fn(testAlert))
}
_, err = getAlertURLGenerator(nil, "foo?{{invalid}}", true)
if err == nil {
t.Errorf("exptected tempalte validation error got nil")
}
fn, err = getAlertURLGenerator(u, "foo?query={{$value}}", true)
if err != nil {
t.Errorf("unexpected error %s", err)
}
if exp := "https://victoriametrics.com/path/foo?query=4"; exp != fn(testAlert) {
t.Errorf("unexpected url want %s, got %s", exp, fn(testAlert))
}
}

View file

@ -1,13 +1,10 @@
package notifier package notifier
import ( import (
"net/url"
"testing" "testing"
) )
func TestAlert_ExecTemplate(t *testing.T) { func TestAlert_ExecTemplate(t *testing.T) {
u, _ := url.Parse("https://victoriametrics.com/path")
InitTemplateFunc(u)
testCases := []struct { testCases := []struct {
name string name string
alert *Alert alert *Alert

View file

@ -46,7 +46,7 @@ func (am *AlertManager) Send(ctx context.Context, alerts []Alert) error {
} }
// AlertURLGenerator returns URL to single alert by given name // AlertURLGenerator returns URL to single alert by given name
type AlertURLGenerator func(group, alert string) string type AlertURLGenerator func(Alert) string
const alertManagerPath = "/api/v2/alerts" const alertManagerPath = "/api/v2/alerts"

View file

@ -1,15 +1,14 @@
{% import ( {% import (
"strconv"
"time" "time"
) %} ) %}
{% stripspace %} {% stripspace %}
{% func amRequest(alerts []Alert, generatorURL func(string, string) string) %} {% func amRequest(alerts []Alert, generatorURL func(Alert) string) %}
[ [
{% for i, alert := range alerts %} {% for i, alert := range alerts %}
{ {
"startsAt":{%q= alert.Start.Format(time.RFC3339Nano) %}, "startsAt":{%q= alert.Start.Format(time.RFC3339Nano) %},
"generatorURL": {%q= generatorURL(strconv.FormatUint(alert.GroupID, 10), strconv.FormatUint(alert.ID, 10)) %}, "generatorURL": {%q= generatorURL(alert) %},
{% if !alert.End.IsZero() %} {% if !alert.End.IsZero() %}
"endsAt":{%q= alert.End.Format(time.RFC3339Nano) %}, "endsAt":{%q= alert.End.Format(time.RFC3339Nano) %},
{% endif %} {% endif %}

View file

@ -1,131 +1,130 @@
// Code generated by qtc from "alertmanager_request.qtpl". DO NOT EDIT. // Code generated by qtc from "alertmanager_request.qtpl". DO NOT EDIT.
// See https://github.com/valyala/quicktemplate for details. // See https://github.com/valyala/quicktemplate for details.
//line notifier/alertmanager_request.qtpl:1 //line app/vmalert/notifier/alertmanager_request.qtpl:1
package notifier package notifier
//line notifier/alertmanager_request.qtpl:1 //line app/vmalert/notifier/alertmanager_request.qtpl:1
import ( import (
"strconv"
"time" "time"
) )
//line notifier/alertmanager_request.qtpl:7 //line app/vmalert/notifier/alertmanager_request.qtpl:6
import ( import (
qtio422016 "io" qtio422016 "io"
qt422016 "github.com/valyala/quicktemplate" qt422016 "github.com/valyala/quicktemplate"
) )
//line notifier/alertmanager_request.qtpl:7 //line app/vmalert/notifier/alertmanager_request.qtpl:6
var ( var (
_ = qtio422016.Copy _ = qtio422016.Copy
_ = qt422016.AcquireByteBuffer _ = qt422016.AcquireByteBuffer
) )
//line notifier/alertmanager_request.qtpl:7 //line app/vmalert/notifier/alertmanager_request.qtpl:6
func streamamRequest(qw422016 *qt422016.Writer, alerts []Alert, generatorURL func(string, string) string) { func streamamRequest(qw422016 *qt422016.Writer, alerts []Alert, generatorURL func(Alert) string) {
//line notifier/alertmanager_request.qtpl:7 //line app/vmalert/notifier/alertmanager_request.qtpl:6
qw422016.N().S(`[`) qw422016.N().S(`[`)
//line notifier/alertmanager_request.qtpl:9 //line app/vmalert/notifier/alertmanager_request.qtpl:8
for i, alert := range alerts { for i, alert := range alerts {
//line notifier/alertmanager_request.qtpl:9 //line app/vmalert/notifier/alertmanager_request.qtpl:8
qw422016.N().S(`{"startsAt":`) qw422016.N().S(`{"startsAt":`)
//line notifier/alertmanager_request.qtpl:11 //line app/vmalert/notifier/alertmanager_request.qtpl:10
qw422016.N().Q(alert.Start.Format(time.RFC3339Nano)) qw422016.N().Q(alert.Start.Format(time.RFC3339Nano))
//line notifier/alertmanager_request.qtpl:11 //line app/vmalert/notifier/alertmanager_request.qtpl:10
qw422016.N().S(`,"generatorURL":`) qw422016.N().S(`,"generatorURL":`)
//line notifier/alertmanager_request.qtpl:12 //line app/vmalert/notifier/alertmanager_request.qtpl:11
qw422016.N().Q(generatorURL(strconv.FormatUint(alert.GroupID, 10), strconv.FormatUint(alert.ID, 10))) qw422016.N().Q(generatorURL(alert))
//line notifier/alertmanager_request.qtpl:12 //line app/vmalert/notifier/alertmanager_request.qtpl:11
qw422016.N().S(`,`) qw422016.N().S(`,`)
//line notifier/alertmanager_request.qtpl:13 //line app/vmalert/notifier/alertmanager_request.qtpl:12
if !alert.End.IsZero() { if !alert.End.IsZero() {
//line notifier/alertmanager_request.qtpl:13 //line app/vmalert/notifier/alertmanager_request.qtpl:12
qw422016.N().S(`"endsAt":`) qw422016.N().S(`"endsAt":`)
//line notifier/alertmanager_request.qtpl:14 //line app/vmalert/notifier/alertmanager_request.qtpl:13
qw422016.N().Q(alert.End.Format(time.RFC3339Nano)) qw422016.N().Q(alert.End.Format(time.RFC3339Nano))
//line notifier/alertmanager_request.qtpl:14 //line app/vmalert/notifier/alertmanager_request.qtpl:13
qw422016.N().S(`,`) qw422016.N().S(`,`)
//line notifier/alertmanager_request.qtpl:15 //line app/vmalert/notifier/alertmanager_request.qtpl:14
} }
//line notifier/alertmanager_request.qtpl:15 //line app/vmalert/notifier/alertmanager_request.qtpl:14
qw422016.N().S(`"labels": {"alertname":`) qw422016.N().S(`"labels": {"alertname":`)
//line notifier/alertmanager_request.qtpl:17 //line app/vmalert/notifier/alertmanager_request.qtpl:16
qw422016.N().Q(alert.Name) qw422016.N().Q(alert.Name)
//line notifier/alertmanager_request.qtpl:18 //line app/vmalert/notifier/alertmanager_request.qtpl:17
for k, v := range alert.Labels { for k, v := range alert.Labels {
//line notifier/alertmanager_request.qtpl:18 //line app/vmalert/notifier/alertmanager_request.qtpl:17
qw422016.N().S(`,`) qw422016.N().S(`,`)
//line notifier/alertmanager_request.qtpl:19 //line app/vmalert/notifier/alertmanager_request.qtpl:18
qw422016.N().Q(k) qw422016.N().Q(k)
//line notifier/alertmanager_request.qtpl:19 //line app/vmalert/notifier/alertmanager_request.qtpl:18
qw422016.N().S(`:`) qw422016.N().S(`:`)
//line notifier/alertmanager_request.qtpl:19 //line app/vmalert/notifier/alertmanager_request.qtpl:18
qw422016.N().Q(v) qw422016.N().Q(v)
//line notifier/alertmanager_request.qtpl:20 //line app/vmalert/notifier/alertmanager_request.qtpl:19
} }
//line notifier/alertmanager_request.qtpl:20 //line app/vmalert/notifier/alertmanager_request.qtpl:19
qw422016.N().S(`},"annotations": {`) qw422016.N().S(`},"annotations": {`)
//line notifier/alertmanager_request.qtpl:23 //line app/vmalert/notifier/alertmanager_request.qtpl:22
c := len(alert.Annotations) c := len(alert.Annotations)
//line notifier/alertmanager_request.qtpl:24 //line app/vmalert/notifier/alertmanager_request.qtpl:23
for k, v := range alert.Annotations { for k, v := range alert.Annotations {
//line notifier/alertmanager_request.qtpl:25 //line app/vmalert/notifier/alertmanager_request.qtpl:24
c = c - 1 c = c - 1
//line notifier/alertmanager_request.qtpl:26 //line app/vmalert/notifier/alertmanager_request.qtpl:25
qw422016.N().Q(k) qw422016.N().Q(k)
//line notifier/alertmanager_request.qtpl:26 //line app/vmalert/notifier/alertmanager_request.qtpl:25
qw422016.N().S(`:`) qw422016.N().S(`:`)
//line notifier/alertmanager_request.qtpl:26 //line app/vmalert/notifier/alertmanager_request.qtpl:25
qw422016.N().Q(v) qw422016.N().Q(v)
//line notifier/alertmanager_request.qtpl:26 //line app/vmalert/notifier/alertmanager_request.qtpl:25
if c > 0 { if c > 0 {
//line notifier/alertmanager_request.qtpl:26 //line app/vmalert/notifier/alertmanager_request.qtpl:25
qw422016.N().S(`,`) qw422016.N().S(`,`)
//line notifier/alertmanager_request.qtpl:26 //line app/vmalert/notifier/alertmanager_request.qtpl:25
} }
//line notifier/alertmanager_request.qtpl:27 //line app/vmalert/notifier/alertmanager_request.qtpl:26
} }
//line notifier/alertmanager_request.qtpl:27 //line app/vmalert/notifier/alertmanager_request.qtpl:26
qw422016.N().S(`}}`) qw422016.N().S(`}}`)
//line notifier/alertmanager_request.qtpl:30 //line app/vmalert/notifier/alertmanager_request.qtpl:29
if i != len(alerts)-1 { if i != len(alerts)-1 {
//line notifier/alertmanager_request.qtpl:30 //line app/vmalert/notifier/alertmanager_request.qtpl:29
qw422016.N().S(`,`) qw422016.N().S(`,`)
//line notifier/alertmanager_request.qtpl:30 //line app/vmalert/notifier/alertmanager_request.qtpl:29
} }
//line notifier/alertmanager_request.qtpl:31 //line app/vmalert/notifier/alertmanager_request.qtpl:30
} }
//line notifier/alertmanager_request.qtpl:31 //line app/vmalert/notifier/alertmanager_request.qtpl:30
qw422016.N().S(`]`) qw422016.N().S(`]`)
//line notifier/alertmanager_request.qtpl:33 //line app/vmalert/notifier/alertmanager_request.qtpl:32
} }
//line notifier/alertmanager_request.qtpl:33 //line app/vmalert/notifier/alertmanager_request.qtpl:32
func writeamRequest(qq422016 qtio422016.Writer, alerts []Alert, generatorURL func(string, string) string) { func writeamRequest(qq422016 qtio422016.Writer, alerts []Alert, generatorURL func(Alert) string) {
//line notifier/alertmanager_request.qtpl:33 //line app/vmalert/notifier/alertmanager_request.qtpl:32
qw422016 := qt422016.AcquireWriter(qq422016) qw422016 := qt422016.AcquireWriter(qq422016)
//line notifier/alertmanager_request.qtpl:33 //line app/vmalert/notifier/alertmanager_request.qtpl:32
streamamRequest(qw422016, alerts, generatorURL) streamamRequest(qw422016, alerts, generatorURL)
//line notifier/alertmanager_request.qtpl:33 //line app/vmalert/notifier/alertmanager_request.qtpl:32
qt422016.ReleaseWriter(qw422016) qt422016.ReleaseWriter(qw422016)
//line notifier/alertmanager_request.qtpl:33 //line app/vmalert/notifier/alertmanager_request.qtpl:32
} }
//line notifier/alertmanager_request.qtpl:33 //line app/vmalert/notifier/alertmanager_request.qtpl:32
func amRequest(alerts []Alert, generatorURL func(string, string) string) string { func amRequest(alerts []Alert, generatorURL func(Alert) string) string {
//line notifier/alertmanager_request.qtpl:33 //line app/vmalert/notifier/alertmanager_request.qtpl:32
qb422016 := qt422016.AcquireByteBuffer() qb422016 := qt422016.AcquireByteBuffer()
//line notifier/alertmanager_request.qtpl:33 //line app/vmalert/notifier/alertmanager_request.qtpl:32
writeamRequest(qb422016, alerts, generatorURL) writeamRequest(qb422016, alerts, generatorURL)
//line notifier/alertmanager_request.qtpl:33 //line app/vmalert/notifier/alertmanager_request.qtpl:32
qs422016 := string(qb422016.B) qs422016 := string(qb422016.B)
//line notifier/alertmanager_request.qtpl:33 //line app/vmalert/notifier/alertmanager_request.qtpl:32
qt422016.ReleaseByteBuffer(qb422016) qt422016.ReleaseByteBuffer(qb422016)
//line notifier/alertmanager_request.qtpl:33 //line app/vmalert/notifier/alertmanager_request.qtpl:32
return qs422016 return qs422016
//line notifier/alertmanager_request.qtpl:33 //line app/vmalert/notifier/alertmanager_request.qtpl:32
} }

View file

@ -5,6 +5,7 @@ import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"strconv"
"testing" "testing"
"time" "time"
) )
@ -57,8 +58,8 @@ func TestAlertManager_Send(t *testing.T) {
}) })
srv := httptest.NewServer(mux) srv := httptest.NewServer(mux)
defer srv.Close() defer srv.Close()
am := NewAlertManager(srv.URL, func(group, name string) string { am := NewAlertManager(srv.URL, func(alert Alert) string {
return group + "/" + name return strconv.FormatUint(alert.GroupID, 10) + "/" + strconv.FormatUint(alert.ID, 10)
}, srv.Client()) }, srv.Client())
if err := am.Send(context.Background(), []Alert{{}, {}}); err == nil { if err := am.Send(context.Background(), []Alert{{}, {}}); err == nil {
t.Error("expected connection error got nil") t.Error("expected connection error got nil")

View file

@ -0,0 +1,13 @@
package notifier
import (
"net/url"
"os"
"testing"
)
func TestMain(m *testing.M) {
u, _ := url.Parse("https://victoriametrics.com/path")
InitTemplateFunc(u)
os.Exit(m.Run())
}