Repository: VictoriaMetrics/VictoriaMetrics
Pull request #2901 (2022-07-21 13:44:45)

vmalert: allow configuring custom headers per group
See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2860

Signed-off-by: hagen1778 <roman@victoriametrics.com>


Patch:
diff --git a/app/vmalert/README.md b/app/vmalert/README.md
index 05c3dede8..06dcb42ea 100644
--- a/app/vmalert/README.md
+++ b/app/vmalert/README.md
@@ -125,6 +125,16 @@ name: <string>
 params:
   [ <string>: [<string>, ...]]
 
+# Optional list of HTTP headers in form `header-name: value`
+# applied for all rules requests within a group
+# For example:
+#  headers:
+#    - "CustomHeader: foo"
+#    - "CustomHeader2: bar"
+# Headers set via this param have priority over headers set via `-datasource.headers` flag. 
+headers:
+  [ <string>, ...]
+
 # Optional list of labels added to every rule within a group.
 # It has priority over the external labels.
 # Labels are commonly used for adding environment
diff --git a/app/vmalert/alerting.go b/app/vmalert/alerting.go
index 7cfe1076a..c0f3938c7 100644
--- a/app/vmalert/alerting.go
+++ b/app/vmalert/alerting.go
@@ -75,6 +75,7 @@ func newAlertingRule(qb datasource.QuerierBuilder, group *Group, cfg config.Rule
 			DataSourceType:     &group.Type,
 			EvaluationInterval: group.Interval,
 			QueryParams:        group.Params,
+			Headers:            group.Headers,
 		}),
 		alerts:  make(map[uint64]*notifier.Alert),
 		metrics: &alertingRuleMetrics{},
diff --git a/app/vmalert/config/config.go b/app/vmalert/config/config.go
index fc50de5ee..b186b55cb 100644
--- a/app/vmalert/config/config.go
+++ b/app/vmalert/config/config.go
@@ -38,6 +38,8 @@ type Group struct {
 	Checksum string
 	// Optional HTTP URL parameters added to each rule request
 	Params url.Values `yaml:"params"`
+	// Headers contains optional HTTP headers added to each rule request
+	Headers []datasource.Header `yaml:"headers,omitempty"`
 
 	// Catches all undefined fields and must be empty after parsing.
 	XXX map[string]interface{} `yaml:",inline"`
diff --git a/app/vmalert/datasource/datasource.go b/app/vmalert/datasource/datasource.go
index 5fd7f7acf..53cfd94d3 100644
--- a/app/vmalert/datasource/datasource.go
+++ b/app/vmalert/datasource/datasource.go
@@ -22,6 +22,7 @@ type QuerierParams struct {
 	DataSourceType     *Type
 	EvaluationInterval time.Duration
 	QueryParams        url.Values
+	Headers            []Header
 }
 
 // Metric is the basic entity which should be return by datasource
diff --git a/app/vmalert/datasource/init.go b/app/vmalert/datasource/init.go
index c05a42c04..fd5465bcb 100644
--- a/app/vmalert/datasource/init.go
+++ b/app/vmalert/datasource/init.go
@@ -15,7 +15,7 @@ var (
 		"E.g. http://127.0.0.1:8428 . See also -remoteRead.disablePathAppend")
 	appendTypePrefix = flag.Bool("datasource.appendTypePrefix", false, "Whether to add type prefix to -datasource.url based on the query type. Set to true if sending different query types to the vmselect URL.")
 
-	headers = flag.String("datasource.headers", "", "Optional HTTP headers to send with each request to the corresponding -datasource.url. "+
+	headers = flag.String("datasource.headers", "", "Optional HTTP extraHeaders to send with each request to the corresponding -datasource.url. "+
 		"For example, -datasource.headers='My-Auth:foobar' would send 'My-Auth: foobar' HTTP header with every request to the corresponding -datasource.url. "+
 		"Multiple headers must be delimited by '^^': -datasource.headers='header1:value1^^header2:value2'")
 
diff --git a/app/vmalert/datasource/type.go b/app/vmalert/datasource/type.go
index 975a642e4..716ebfac5 100644
--- a/app/vmalert/datasource/type.go
+++ b/app/vmalert/datasource/type.go
@@ -2,6 +2,7 @@ package datasource
 
 import (
 	"fmt"
+	"strings"
 
 	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmselect/graphiteql"
 	"github.com/VictoriaMetrics/metricsql"
@@ -89,3 +90,27 @@ func (t *Type) UnmarshalYAML(unmarshal func(interface{}) error) error {
 func (t Type) MarshalYAML() (interface{}, error) {
 	return t.name, nil
 }
+
+// Header is a Key - Value struct for holding an HTTP header.
+type Header struct {
+	Key   string
+	Value string
+}
+
+// UnmarshalYAML implements the yaml.Unmarshaler interface.
+func (h *Header) UnmarshalYAML(unmarshal func(interface{}) error) error {
+	var s string
+	if err := unmarshal(&s); err != nil {
+		return err
+	}
+	if s == "" {
+		return nil
+	}
+	n := strings.IndexByte(s, ':')
+	if n < 0 {
+		return fmt.Errorf(`missing ':' in header %q; expecting "key: value" format`, s)
+	}
+	h.Key = strings.TrimSpace(s[:n])
+	h.Value = strings.TrimSpace(s[n+1:])
+	return nil
+}
diff --git a/app/vmalert/datasource/vm.go b/app/vmalert/datasource/vm.go
index df415dd66..f07fec6be 100644
--- a/app/vmalert/datasource/vm.go
+++ b/app/vmalert/datasource/vm.go
@@ -24,6 +24,7 @@ type VMStorage struct {
 	dataSourceType     Type
 	evaluationInterval time.Duration
 	extraParams        url.Values
+	extraHeaders       []Header
 }
 
 // Clone makes clone of VMStorage, shares http client.
@@ -46,6 +47,7 @@ func (s *VMStorage) ApplyParams(params QuerierParams) *VMStorage {
 	}
 	s.evaluationInterval = params.EvaluationInterval
 	s.extraParams = params.QueryParams
+	s.extraHeaders = params.Headers
 	return s
 }
 
@@ -148,5 +150,8 @@ func (s *VMStorage) newRequestPOST() (*http.Request, error) {
 	if s.authCfg != nil {
 		s.authCfg.SetHeaders(req, true)
 	}
+	for _, h := range s.extraHeaders {
+		req.Header.Set(h.Key, h.Value)
+	}
 	return req, nil
 }
diff --git a/app/vmalert/group.go b/app/vmalert/group.go
index ac8e712bd..cafdb2d91 100644
--- a/app/vmalert/group.go
+++ b/app/vmalert/group.go
@@ -35,8 +35,9 @@ type Group struct {
 	Checksum       string
 	LastEvaluation time.Time
 
-	Labels map[string]string
-	Params url.Values
+	Labels  map[string]string
+	Params  url.Values
+	Headers []datasource.Header
 
 	doneCh     chan struct{}
 	finishedCh chan struct{}
@@ -96,6 +97,7 @@ func newGroup(cfg config.Group, qb datasource.QuerierBuilder, defaultInterval ti
 		Concurrency: cfg.Concurrency,
 		Checksum:    cfg.Checksum,
 		Params:      cfg.Params,
+		Headers:     cfg.Headers,
 		Labels:      cfg.Labels,
 
 		doneCh:     make(chan struct{}),
@@ -217,6 +219,7 @@ func (g *Group) updateWith(newGroup *Group) error {
 	g.Type = newGroup.Type
 	g.Concurrency = newGroup.Concurrency
 	g.Params = newGroup.Params
+	g.Headers = newGroup.Headers
 	g.Labels = newGroup.Labels
 	g.Limit = newGroup.Limit
 	g.Checksum = newGroup.Checksum
diff --git a/app/vmalert/manager.go b/app/vmalert/manager.go
index 3ab70c6f4..e3f3a6452 100644
--- a/app/vmalert/manager.go
+++ b/app/vmalert/manager.go
@@ -170,6 +170,7 @@ func (g *Group) toAPI() APIGroup {
 		LastEvaluation: g.LastEvaluation,
 		Concurrency:    g.Concurrency,
 		Params:         urlValuesToStrings(g.Params),
+		Headers:        headersToStrings(g.Headers),
 		Labels:         g.Labels,
 	}
 	for _, r := range g.Rules {
@@ -198,3 +199,14 @@ func urlValuesToStrings(values url.Values) []string {
 	}
 	return res
 }
+
+func headersToStrings(headers []datasource.Header) []string {
+	if len(headers) < 1 {
+		return nil
+	}
+	var res []string
+	for _, h := range headers {
+		res = append(res, fmt.Sprintf("%s: %s", h.Key, h.Value))
+	}
+	return res
+}
diff --git a/app/vmalert/recording.go b/app/vmalert/recording.go
index a22285906..ab5eb270d 100644
--- a/app/vmalert/recording.go
+++ b/app/vmalert/recording.go
@@ -73,6 +73,7 @@ func newRecordingRule(qb datasource.QuerierBuilder, group *Group, cfg config.Rul
 			DataSourceType:     &group.Type,
 			EvaluationInterval: group.Interval,
 			QueryParams:        group.Params,
+			Headers:            group.Headers,
 		}),
 	}
 
diff --git a/app/vmalert/web.qtpl b/app/vmalert/web.qtpl
index 7e5db0a5c..693186550 100644
--- a/app/vmalert/web.qtpl
+++ b/app/vmalert/web.qtpl
@@ -57,6 +57,13 @@
                     {% endfor %}
                     </div>
                 {% endif %}
+                {% if len(g.Headers) > 0 %}
+                    <div class="fs-6 fw-lighter">Extra headers
+                    {% for _, header := range g.Headers %}
+                            <span class="float-left badge bg-primary">{%s header %}</span>
+                    {% endfor %}
+                    </div>
+                {% endif %}
             </div>
             <div class="collapse" id="rules-{%s g.ID %}">
                 <table class="table table-striped table-hover table-sm">
diff --git a/app/vmalert/web.qtpl.go b/app/vmalert/web.qtpl.go
index aa73307c6..6f67cd551 100644
--- a/app/vmalert/web.qtpl.go
+++ b/app/vmalert/web.qtpl.go
@@ -232,11 +232,38 @@ func StreamListGroups(qw422016 *qt422016.Writer, r *http.Request, groups []APIGr
 			}
 //line app/vmalert/web.qtpl:59
 			qw422016.N().S(`
+                `)
+//line app/vmalert/web.qtpl:60
+			if len(g.Headers) > 0 {
+//line app/vmalert/web.qtpl:60
+				qw422016.N().S(`
+                    <div class="fs-6 fw-lighter">Extra headers
+                    `)
+//line app/vmalert/web.qtpl:62
+				for _, header := range g.Headers {
+//line app/vmalert/web.qtpl:62
+					qw422016.N().S(`
+                            <span class="float-left badge bg-primary">`)
+//line app/vmalert/web.qtpl:63
+					qw422016.E().S(header)
+//line app/vmalert/web.qtpl:63
+					qw422016.N().S(`</span>
+                    `)
+//line app/vmalert/web.qtpl:64
+				}
+//line app/vmalert/web.qtpl:64
+				qw422016.N().S(`
+                    </div>
+                `)
+//line app/vmalert/web.qtpl:66
+			}
+//line app/vmalert/web.qtpl:66
+			qw422016.N().S(`
             </div>
             <div class="collapse" id="rules-`)
-//line app/vmalert/web.qtpl:61
+//line app/vmalert/web.qtpl:68
 			qw422016.E().S(g.ID)
-//line app/vmalert/web.qtpl:61
+//line app/vmalert/web.qtpl:68
 			qw422016.N().S(`">
                 <table class="table table-striped table-hover table-sm">
                     <thead>
@@ -248,262 +275,262 @@ func StreamListGroups(qw422016 *qt422016.Writer, r *http.Request, groups []APIGr
                     </thead>
                     <tbody>
                     `)
-//line app/vmalert/web.qtpl:71
+//line app/vmalert/web.qtpl:78
 			for _, r := range g.Rules {
-//line app/vmalert/web.qtpl:71
+//line app/vmalert/web.qtpl:78
 				qw422016.N().S(`
                         <tr`)
-//line app/vmalert/web.qtpl:72
+//line app/vmalert/web.qtpl:79
 				if r.LastError != "" {
-//line app/vmalert/web.qtpl:72
+//line app/vmalert/web.qtpl:79
 					qw422016.N().S(` class="alert-danger"`)
-//line app/vmalert/web.qtpl:72
+//line app/vmalert/web.qtpl:79
 				}
-//line app/vmalert/web.qtpl:72
+//line app/vmalert/web.qtpl:79
 				qw422016.N().S(`>
                             <td>
                                 <div class="row">
                                     <div class="col-12 mb-2">
                                         `)
-//line app/vmalert/web.qtpl:76
+//line app/vmalert/web.qtpl:83
 				if r.Type == "alerting" {
-//line app/vmalert/web.qtpl:76
+//line app/vmalert/web.qtpl:83
 					qw422016.N().S(`
                                         <b>alert:</b> `)
-//line app/vmalert/web.qtpl:77
+//line app/vmalert/web.qtpl:84
 					qw422016.E().S(r.Name)
-//line app/vmalert/web.qtpl:77
+//line app/vmalert/web.qtpl:84
 					qw422016.N().S(` (for: `)
-//line app/vmalert/web.qtpl:77
+//line app/vmalert/web.qtpl:84
 					qw422016.E().V(r.Duration)
-//line app/vmalert/web.qtpl:77
+//line app/vmalert/web.qtpl:84
 					qw422016.N().S(` seconds)
                                         `)
-//line app/vmalert/web.qtpl:78
+//line app/vmalert/web.qtpl:85
 				} else {
-//line app/vmalert/web.qtpl:78
+//line app/vmalert/web.qtpl:85
 					qw422016.N().S(`
                                         <b>record:</b> `)
-//line app/vmalert/web.qtpl:79
+//line app/vmalert/web.qtpl:86
 					qw422016.E().S(r.Name)
-//line app/vmalert/web.qtpl:79
+//line app/vmalert/web.qtpl:86
 					qw422016.N().S(`
                                         `)
-//line app/vmalert/web.qtpl:80
+//line app/vmalert/web.qtpl:87
 				}
-//line app/vmalert/web.qtpl:80
+//line app/vmalert/web.qtpl:87
 				qw422016.N().S(`
                                     </div>
                                     <div class="col-12">
                                         <code><pre>`)
-//line app/vmalert/web.qtpl:83
+//line app/vmalert/web.qtpl:90
 				qw422016.E().S(r.Query)
-//line app/vmalert/web.qtpl:83
+//line app/vmalert/web.qtpl:90
 				qw422016.N().S(`</pre></code>
                                     </div>
                                     <div class="col-12 mb-2">
                                         `)
-//line app/vmalert/web.qtpl:86
+//line app/vmalert/web.qtpl:93
 				if len(r.Labels) > 0 {
-//line app/vmalert/web.qtpl:86
+//line app/vmalert/web.qtpl:93
 					qw422016.N().S(` <b>Labels:</b>`)
-//line app/vmalert/web.qtpl:86
+//line app/vmalert/web.qtpl:93
 				}
-//line app/vmalert/web.qtpl:86
+//line app/vmalert/web.qtpl:93
 				qw422016.N().S(`
                                         `)
-//line app/vmalert/web.qtpl:87
+//line app/vmalert/web.qtpl:94
 				for k, v := range r.Labels {
-//line app/vmalert/web.qtpl:87
+//line app/vmalert/web.qtpl:94
 					qw422016.N().S(`
                                                 <span class="ms-1 badge bg-primary">`)
-//line app/vmalert/web.qtpl:88
+//line app/vmalert/web.qtpl:95
 					qw422016.E().S(k)
-//line app/vmalert/web.qtpl:88
+//line app/vmalert/web.qtpl:95
 					qw422016.N().S(`=`)
-//line app/vmalert/web.qtpl:88
+//line app/vmalert/web.qtpl:95
 					qw422016.E().S(v)
-//line app/vmalert/web.qtpl:88
+//line app/vmalert/web.qtpl:95
 					qw422016.N().S(`</span>
                                         `)
-//line app/vmalert/web.qtpl:89
+//line app/vmalert/web.qtpl:96
 				}
-//line app/vmalert/web.qtpl:89
+//line app/vmalert/web.qtpl:96
 				qw422016.N().S(`
                                     </div>
                                     `)
-//line app/vmalert/web.qtpl:91
+//line app/vmalert/web.qtpl:98
 				if r.LastError != "" {
-//line app/vmalert/web.qtpl:91
+//line app/vmalert/web.qtpl:98
 					qw422016.N().S(`
                                     <div class="col-12">
                                         <b>Error:</b>
                                         <div class="error-cell">
                                         `)
-//line app/vmalert/web.qtpl:95
+//line app/vmalert/web.qtpl:102
 					qw422016.E().S(r.LastError)
-//line app/vmalert/web.qtpl:95
+//line app/vmalert/web.qtpl:102
 					qw422016.N().S(`
                                         </div>
                                     </div>
                                     `)
-//line app/vmalert/web.qtpl:98
+//line app/vmalert/web.qtpl:105
 				}
-//line app/vmalert/web.qtpl:98
+//line app/vmalert/web.qtpl:105
 				qw422016.N().S(`
                                 </div>
                             </td>
                             <td class="text-center">`)
-//line app/vmalert/web.qtpl:101
+//line app/vmalert/web.qtpl:108
 				qw422016.N().D(r.LastSamples)
-//line app/vmalert/web.qtpl:101
+//line app/vmalert/web.qtpl:108
 				qw422016.N().S(`</td>
                             <td class="text-center">`)
-//line app/vmalert/web.qtpl:102
+//line app/vmalert/web.qtpl:109
 				qw422016.N().FPrec(time.Since(r.LastEvaluation).Seconds(), 3)
-//line app/vmalert/web.qtpl:102
+//line app/vmalert/web.qtpl:109
 				qw422016.N().S(`s ago</td>
                         </tr>
                     `)
-//line app/vmalert/web.qtpl:104
+//line app/vmalert/web.qtpl:111
 			}
-//line app/vmalert/web.qtpl:104
+//line app/vmalert/web.qtpl:111
 			qw422016.N().S(`
                  </tbody>
                 </table>
             </div>
         `)
-//line app/vmalert/web.qtpl:108
+//line app/vmalert/web.qtpl:115
 		}
-//line app/vmalert/web.qtpl:108
+//line app/vmalert/web.qtpl:115
 		qw422016.N().S(`
 
     `)
-//line app/vmalert/web.qtpl:110
+//line app/vmalert/web.qtpl:117
 	} else {
-//line app/vmalert/web.qtpl:110
+//line app/vmalert/web.qtpl:117
 		qw422016.N().S(`
         <div>
             <p>No items...</p>
         </div>
     `)
-//line app/vmalert/web.qtpl:114
+//line app/vmalert/web.qtpl:121
 	}
-//line app/vmalert/web.qtpl:114
+//line app/vmalert/web.qtpl:121
 	qw422016.N().S(`
 
     `)
-//line app/vmalert/web.qtpl:116
+//line app/vmalert/web.qtpl:123
 	tpl.StreamFooter(qw422016, r)
-//line app/vmalert/web.qtpl:116
+//line app/vmalert/web.qtpl:123
 	qw422016.N().S(`
 
 `)
-//line app/vmalert/web.qtpl:118
+//line app/vmalert/web.qtpl:125
 }
 
-//line app/vmalert/web.qtpl:118
+//line app/vmalert/web.qtpl:125
 func WriteListGroups(qq422016 qtio422016.Writer, r *http.Request, groups []APIGroup) {
-//line app/vmalert/web.qtpl:118
+//line app/vmalert/web.qtpl:125
 	qw422016 := qt422016.AcquireWriter(qq422016)
-//line app/vmalert/web.qtpl:118
+//line app/vmalert/web.qtpl:125
 	StreamListGroups(qw422016, r, groups)
-//line app/vmalert/web.qtpl:118
+//line app/vmalert/web.qtpl:125
 	qt422016.ReleaseWriter(qw422016)
-//line app/vmalert/web.qtpl:118
+//line app/vmalert/web.qtpl:125
 }
 
-//line app/vmalert/web.qtpl:118
+//line app/vmalert/web.qtpl:125
 func ListGroups(r *http.Request, groups []APIGroup) string {
-//line app/vmalert/web.qtpl:118
+//line app/vmalert/web.qtpl:125
 	qb422016 := qt422016.AcquireByteBuffer()
-//line app/vmalert/web.qtpl:118
+//line app/vmalert/web.qtpl:125
 	WriteListGroups(qb422016, r, groups)
-//line app/vmalert/web.qtpl:118
+//line app/vmalert/web.qtpl:125
 	qs422016 := string(qb422016.B)
-//line app/vmalert/web.qtpl:118
+//line app/vmalert/web.qtpl:125
 	qt422016.ReleaseByteBuffer(qb422016)
-//line app/vmalert/web.qtpl:118
+//line app/vmalert/web.qtpl:125
 	return qs422016
-//line app/vmalert/web.qtpl:118
+//line app/vmalert/web.qtpl:125
 }
 
-//line app/vmalert/web.qtpl:121
+//line app/vmalert/web.qtpl:128
 func StreamListAlerts(qw422016 *qt422016.Writer, r *http.Request, groupAlerts []GroupAlerts) {
-//line app/vmalert/web.qtpl:121
+//line app/vmalert/web.qtpl:128
 	qw422016.N().S(`
     `)
-//line app/vmalert/web.qtpl:122
+//line app/vmalert/web.qtpl:129
 	prefix := utils.Prefix(r.URL.Path)
 
-//line app/vmalert/web.qtpl:122
+//line app/vmalert/web.qtpl:129
 	qw422016.N().S(`
     `)
-//line app/vmalert/web.qtpl:123
+//line app/vmalert/web.qtpl:130
 	tpl.StreamHeader(qw422016, r, navItems, "Alerts")
-//line app/vmalert/web.qtpl:123
+//line app/vmalert/web.qtpl:130
 	qw422016.N().S(`
     `)
-//line app/vmalert/web.qtpl:124
+//line app/vmalert/web.qtpl:131
 	if len(groupAlerts) > 0 {
-//line app/vmalert/web.qtpl:124
+//line app/vmalert/web.qtpl:131
 		qw422016.N().S(`
          <a class="btn btn-primary" role="button" onclick="collapseAll()">Collapse All</a>
          <a class="btn btn-primary" role="button" onclick="expandAll()">Expand All</a>
          `)
-//line app/vmalert/web.qtpl:127
+//line app/vmalert/web.qtpl:134
 		for _, ga := range groupAlerts {
-//line app/vmalert/web.qtpl:127
+//line app/vmalert/web.qtpl:134
 			qw422016.N().S(`
             `)
-//line app/vmalert/web.qtpl:128
+//line app/vmalert/web.qtpl:135
 			g := ga.Group
 
-//line app/vmalert/web.qtpl:128
+//line app/vmalert/web.qtpl:135
 			qw422016.N().S(`
             <div class="group-heading alert-danger" data-bs-target="rules-`)
-//line app/vmalert/web.qtpl:129
+//line app/vmalert/web.qtpl:136
 			qw422016.E().S(g.ID)
-//line app/vmalert/web.qtpl:129
+//line app/vmalert/web.qtpl:136
 			qw422016.N().S(`">
                 <span class="anchor" id="group-`)
-//line app/vmalert/web.qtpl:130
+//line app/vmalert/web.qtpl:137
 			qw422016.E().S(g.ID)
-//line app/vmalert/web.qtpl:130
+//line app/vmalert/web.qtpl:137
 			qw422016.N().S(`"></span>
                 <a href="#group-`)
-//line app/vmalert/web.qtpl:131
+//line app/vmalert/web.qtpl:138
 			qw422016.E().S(g.ID)
-//line app/vmalert/web.qtpl:131
+//line app/vmalert/web.qtpl:138
 			qw422016.N().S(`">`)
-//line app/vmalert/web.qtpl:131
+//line app/vmalert/web.qtpl:138
 			qw422016.E().S(g.Name)
-//line app/vmalert/web.qtpl:131
+//line app/vmalert/web.qtpl:138
 			if g.Type != "prometheus" {
-//line app/vmalert/web.qtpl:131
+//line app/vmalert/web.qtpl:138
 				qw422016.N().S(` (`)
-//line app/vmalert/web.qtpl:131
+//line app/vmalert/web.qtpl:138
 				qw422016.E().S(g.Type)
-//line app/vmalert/web.qtpl:131
+//line app/vmalert/web.qtpl:138
 				qw422016.N().S(`)`)
-//line app/vmalert/web.qtpl:131
+//line app/vmalert/web.qtpl:138
 			}
-//line app/vmalert/web.qtpl:131
+//line app/vmalert/web.qtpl:138
 			qw422016.N().S(`</a>
                 <span class="badge bg-danger" title="Number of active alerts">`)
-//line app/vmalert/web.qtpl:132
+//line app/vmalert/web.qtpl:139
 			qw422016.N().D(len(ga.Alerts))
-//line app/vmalert/web.qtpl:132
+//line app/vmalert/web.qtpl:139
 			qw422016.N().S(`</span>
                 <br>
                 <p class="fs-6 fw-lighter">`)
-//line app/vmalert/web.qtpl:134
+//line app/vmalert/web.qtpl:141
 			qw422016.E().S(g.File)
-//line app/vmalert/web.qtpl:134
+//line app/vmalert/web.qtpl:141
 			qw422016.N().S(`</p>
             </div>
             `)
-//line app/vmalert/web.qtpl:137
+//line app/vmalert/web.qtpl:144
 			var keys []string
 			alertsByRule := make(map[string][]*APIAlert)
 			for _, alert := range ga.Alerts {
@@ -514,20 +541,20 @@ func StreamListAlerts(qw422016 *qt422016.Writer, r *http.Request, groupAlerts []
 			}
 			sort.Strings(keys)
 
-//line app/vmalert/web.qtpl:146
+//line app/vmalert/web.qtpl:153
 			qw422016.N().S(`
             <div class="collapse" id="rules-`)
-//line app/vmalert/web.qtpl:147
+//line app/vmalert/web.qtpl:154
 			qw422016.E().S(g.ID)
-//line app/vmalert/web.qtpl:147
+//line app/vmalert/web.qtpl:154
 			qw422016.N().S(`">
                 `)
-//line app/vmalert/web.qtpl:148
+//line app/vmalert/web.qtpl:155
 			for _, ruleID := range keys {
-//line app/vmalert/web.qtpl:148
+//line app/vmalert/web.qtpl:155
 				qw422016.N().S(`
                     `)
-//line app/vmalert/web.qtpl:150
+//line app/vmalert/web.qtpl:157
 				defaultAR := alertsByRule[ruleID][0]
 				var labelKeys []string
 				for k := range defaultAR.Labels {
@@ -535,28 +562,28 @@ func StreamListAlerts(qw422016 *qt422016.Writer, r *http.Request, groupAlerts []
 				}
 				sort.Strings(labelKeys)
 
-//line app/vmalert/web.qtpl:156
+//line app/vmalert/web.qtpl:163
 				qw422016.N().S(`
                     <br>
                     <b>alert:</b> `)
-//line app/vmalert/web.qtpl:158
+//line app/vmalert/web.qtpl:165
 				qw422016.E().S(defaultAR.Name)
-//line app/vmalert/web.qtpl:158
+//line app/vmalert/web.qtpl:165
 				qw422016.N().S(` (`)
-//line app/vmalert/web.qtpl:158
+//line app/vmalert/web.qtpl:165
 				qw422016.N().D(len(alertsByRule[ruleID]))
-//line app/vmalert/web.qtpl:158
+//line app/vmalert/web.qtpl:165
 				qw422016.N().S(`)
                      | <span><a target="_blank" href="`)
-//line app/vmalert/web.qtpl:159
+//line app/vmalert/web.qtpl:166
 				qw422016.E().S(defaultAR.SourceLink)
-//line app/vmalert/web.qtpl:159
+//line app/vmalert/web.qtpl:166
 				qw422016.N().S(`">Source</a></span>
                     <br>
                     <b>expr:</b><code><pre>`)
-//line app/vmalert/web.qtpl:161
+//line app/vmalert/web.qtpl:168
 				qw422016.E().S(defaultAR.Expression)
-//line app/vmalert/web.qtpl:161
+//line app/vmalert/web.qtpl:168
 				qw422016.N().S(`</pre></code>
                     <table class="table table-striped table-hover table-sm">
                         <thead>
@@ -570,204 +597,204 @@ func StreamListAlerts(qw422016 *qt422016.Writer, r *http.Request, groupAlerts []
                         </thead>
                         <tbody>
                         `)
-//line app/vmalert/web.qtpl:173
+//line app/vmalert/web.qtpl:180
 				for _, ar := range alertsByRule[ruleID] {
-//line app/vmalert/web.qtpl:173
+//line app/vmalert/web.qtpl:180
 					qw422016.N().S(`
                             <tr>
                                 <td>
                                     `)
-//line app/vmalert/web.qtpl:176
+//line app/vmalert/web.qtpl:183
 					for _, k := range labelKeys {
-//line app/vmalert/web.qtpl:176
+//line app/vmalert/web.qtpl:183
 						qw422016.N().S(`
                                         <span class="ms-1 badge bg-primary">`)
-//line app/vmalert/web.qtpl:177
+//line app/vmalert/web.qtpl:184
 						qw422016.E().S(k)
-//line app/vmalert/web.qtpl:177
+//line app/vmalert/web.qtpl:184
 						qw422016.N().S(`=`)
-//line app/vmalert/web.qtpl:177
+//line app/vmalert/web.qtpl:184
 						qw422016.E().S(ar.Labels[k])
-//line app/vmalert/web.qtpl:177
+//line app/vmalert/web.qtpl:184
 						qw422016.N().S(`</span>
                                     `)
-//line app/vmalert/web.qtpl:178
+//line app/vmalert/web.qtpl:185
 					}
-//line app/vmalert/web.qtpl:178
+//line app/vmalert/web.qtpl:185
 					qw422016.N().S(`
                                 </td>
                                 <td>`)
-//line app/vmalert/web.qtpl:180
+//line app/vmalert/web.qtpl:187
 					streambadgeState(qw422016, ar.State)
-//line app/vmalert/web.qtpl:180
+//line app/vmalert/web.qtpl:187
 					qw422016.N().S(`</td>
                                 <td>
                                     `)
-//line app/vmalert/web.qtpl:182
+//line app/vmalert/web.qtpl:189
 					qw422016.E().S(ar.ActiveAt.Format("2006-01-02T15:04:05Z07:00"))
-//line app/vmalert/web.qtpl:182
+//line app/vmalert/web.qtpl:189
 					qw422016.N().S(`
                                     `)
-//line app/vmalert/web.qtpl:183
+//line app/vmalert/web.qtpl:190
 					if ar.Restored {
-//line app/vmalert/web.qtpl:183
+//line app/vmalert/web.qtpl:190
 						streambadgeRestored(qw422016)
-//line app/vmalert/web.qtpl:183
+//line app/vmalert/web.qtpl:190
 					}
-//line app/vmalert/web.qtpl:183
+//line app/vmalert/web.qtpl:190
 					qw422016.N().S(`
                                 </td>
                                 <td>`)
-//line app/vmalert/web.qtpl:185
+//line app/vmalert/web.qtpl:192
 					qw422016.E().S(ar.Value)
-//line app/vmalert/web.qtpl:185
+//line app/vmalert/web.qtpl:192
 					qw422016.N().S(`</td>
                                 <td>
                                     <a href="`)
-//line app/vmalert/web.qtpl:187
+//line app/vmalert/web.qtpl:194
 					qw422016.E().S(prefix + ar.WebLink())
-//line app/vmalert/web.qtpl:187
+//line app/vmalert/web.qtpl:194
 					qw422016.N().S(`">Details</a>
                                 </td>
                             </tr>
                         `)
-//line app/vmalert/web.qtpl:190
+//line app/vmalert/web.qtpl:197
 				}
-//line app/vmalert/web.qtpl:190
+//line app/vmalert/web.qtpl:197
 				qw422016.N().S(`
                      </tbody>
                     </table>
                 `)
-//line app/vmalert/web.qtpl:193
+//line app/vmalert/web.qtpl:200
 			}
-//line app/vmalert/web.qtpl:193
+//line app/vmalert/web.qtpl:200
 			qw422016.N().S(`
             </div>
             <br>
         `)
-//line app/vmalert/web.qtpl:196
+//line app/vmalert/web.qtpl:203
 		}
-//line app/vmalert/web.qtpl:196
+//line app/vmalert/web.qtpl:203
 		qw422016.N().S(`
 
     `)
-//line app/vmalert/web.qtpl:198
+//line app/vmalert/web.qtpl:205
 	} else {
-//line app/vmalert/web.qtpl:198
+//line app/vmalert/web.qtpl:205
 		qw422016.N().S(`
         <div>
             <p>No items...</p>
         </div>
     `)
-//line app/vmalert/web.qtpl:202
+//line app/vmalert/web.qtpl:209
 	}
-//line app/vmalert/web.qtpl:202
+//line app/vmalert/web.qtpl:209
 	qw422016.N().S(`
 
     `)
-//line app/vmalert/web.qtpl:204
+//line app/vmalert/web.qtpl:211
 	tpl.StreamFooter(qw422016, r)
-//line app/vmalert/web.qtpl:204
+//line app/vmalert/web.qtpl:211
 	qw422016.N().S(`
 
 `)
-//line app/vmalert/web.qtpl:206
+//line app/vmalert/web.qtpl:213
 }
 
-//line app/vmalert/web.qtpl:206
+//line app/vmalert/web.qtpl:213
 func WriteListAlerts(qq422016 qtio422016.Writer, r *http.Request, groupAlerts []GroupAlerts) {
-//line app/vmalert/web.qtpl:206
+//line app/vmalert/web.qtpl:213
 	qw422016 := qt422016.AcquireWriter(qq422016)
-//line app/vmalert/web.qtpl:206
+//line app/vmalert/web.qtpl:213
 	StreamListAlerts(qw422016, r, groupAlerts)
-//line app/vmalert/web.qtpl:206
+//line app/vmalert/web.qtpl:213
 	qt422016.ReleaseWriter(qw422016)
-//line app/vmalert/web.qtpl:206
+//line app/vmalert/web.qtpl:213
 }
 
-//line app/vmalert/web.qtpl:206
+//line app/vmalert/web.qtpl:213
 func ListAlerts(r *http.Request, groupAlerts []GroupAlerts) string {
-//line app/vmalert/web.qtpl:206
+//line app/vmalert/web.qtpl:213
 	qb422016 := qt422016.AcquireByteBuffer()
-//line app/vmalert/web.qtpl:206
+//line app/vmalert/web.qtpl:213
 	WriteListAlerts(qb422016, r, groupAlerts)
-//line app/vmalert/web.qtpl:206
+//line app/vmalert/web.qtpl:213
 	qs422016 := string(qb422016.B)
-//line app/vmalert/web.qtpl:206
+//line app/vmalert/web.qtpl:213
 	qt422016.ReleaseByteBuffer(qb422016)
-//line app/vmalert/web.qtpl:206
+//line app/vmalert/web.qtpl:213
 	return qs422016
-//line app/vmalert/web.qtpl:206
+//line app/vmalert/web.qtpl:213
 }
 
-//line app/vmalert/web.qtpl:208
+//line app/vmalert/web.qtpl:215
 func StreamListTargets(qw422016 *qt422016.Writer, r *http.Request, targets map[notifier.TargetType][]notifier.Target) {
-//line app/vmalert/web.qtpl:208
+//line app/vmalert/web.qtpl:215
 	qw422016.N().S(`
     `)
-//line app/vmalert/web.qtpl:209
+//line app/vmalert/web.qtpl:216
 	tpl.StreamHeader(qw422016, r, navItems, "Notifiers")
-//line app/vmalert/web.qtpl:209
+//line app/vmalert/web.qtpl:216
 	qw422016.N().S(`
     `)
-//line app/vmalert/web.qtpl:210
+//line app/vmalert/web.qtpl:217
 	if len(targets) > 0 {
-//line app/vmalert/web.qtpl:210
+//line app/vmalert/web.qtpl:217
 		qw422016.N().S(`
          <a class="btn btn-primary" role="button" onclick="collapseAll()">Collapse All</a>
          <a class="btn btn-primary" role="button" onclick="expandAll()">Expand All</a>
 
          `)
-//line app/vmalert/web.qtpl:215
+//line app/vmalert/web.qtpl:222
 		var keys []string
 		for key := range targets {
 			keys = append(keys, string(key))
 		}
 		sort.Strings(keys)
 
-//line app/vmalert/web.qtpl:220
+//line app/vmalert/web.qtpl:227
 		qw422016.N().S(`
 
          `)
-//line app/vmalert/web.qtpl:222
+//line app/vmalert/web.qtpl:229
 		for i := range keys {
-//line app/vmalert/web.qtpl:222
+//line app/vmalert/web.qtpl:229
 			qw422016.N().S(`
            `)
-//line app/vmalert/web.qtpl:223
+//line app/vmalert/web.qtpl:230
 			typeK, ns := keys[i], targets[notifier.TargetType(keys[i])]
 			count := len(ns)
 
-//line app/vmalert/web.qtpl:225
+//line app/vmalert/web.qtpl:232
 			qw422016.N().S(`
            <div class="group-heading data-bs-target="rules-`)
-//line app/vmalert/web.qtpl:226
+//line app/vmalert/web.qtpl:233
 			qw422016.E().S(typeK)
-//line app/vmalert/web.qtpl:226
+//line app/vmalert/web.qtpl:233
 			qw422016.N().S(`">
              <span class="anchor" id="notifiers-`)
-//line app/vmalert/web.qtpl:227
+//line app/vmalert/web.qtpl:234
 			qw422016.E().S(typeK)
-//line app/vmalert/web.qtpl:227
+//line app/vmalert/web.qtpl:234
 			qw422016.N().S(`"></span>
              <a href="#notifiers-`)
-//line app/vmalert/web.qtpl:228
+//line app/vmalert/web.qtpl:235
 			qw422016.E().S(typeK)
-//line app/vmalert/web.qtpl:228
+//line app/vmalert/web.qtpl:235
 			qw422016.N().S(`">`)
-//line app/vmalert/web.qtpl:228
+//line app/vmalert/web.qtpl:235
 			qw422016.E().S(typeK)
-//line app/vmalert/web.qtpl:228
+//line app/vmalert/web.qtpl:235
 			qw422016.N().S(` (`)
-//line app/vmalert/web.qtpl:228
+//line app/vmalert/web.qtpl:235
 			qw422016.N().D(count)
-//line app/vmalert/web.qtpl:228
+//line app/vmalert/web.qtpl:235
 			qw422016.N().S(`)</a>
          </div>
          <div class="collapse show" id="notifiers-`)
-//line app/vmalert/web.qtpl:230
+//line app/vmalert/web.qtpl:237
 			qw422016.E().S(typeK)
-//line app/vmalert/web.qtpl:230
+//line app/vmalert/web.qtpl:237
 			qw422016.N().S(`">
              <table class="table table-striped table-hover table-sm">
                  <thead>
@@ -778,119 +805,119 @@ func StreamListTargets(qw422016 *qt422016.Writer, r *http.Request, targets map[n
                  </thead>
                  <tbody>
                  `)
-//line app/vmalert/web.qtpl:239
+//line app/vmalert/web.qtpl:246
 			for _, n := range ns {
-//line app/vmalert/web.qtpl:239
+//line app/vmalert/web.qtpl:246
 				qw422016.N().S(`
                      <tr>
                          <td>
                               `)
-//line app/vmalert/web.qtpl:242
+//line app/vmalert/web.qtpl:249
 				for _, l := range n.Labels {
-//line app/vmalert/web.qtpl:242
+//line app/vmalert/web.qtpl:249
 					qw422016.N().S(`
                                       <span class="ms-1 badge bg-primary">`)
-//line app/vmalert/web.qtpl:243
+//line app/vmalert/web.qtpl:250
 					qw422016.E().S(l.Name)
-//line app/vmalert/web.qtpl:243
+//line app/vmalert/web.qtpl:250
 					qw422016.N().S(`=`)
-//line app/vmalert/web.qtpl:243
+//line app/vmalert/web.qtpl:250
 					qw422016.E().S(l.Value)
-//line app/vmalert/web.qtpl:243
+//line app/vmalert/web.qtpl:250
 					qw422016.N().S(`</span>
                               `)
-//line app/vmalert/web.qtpl:244
+//line app/vmalert/web.qtpl:251
 				}
-//line app/vmalert/web.qtpl:244
+//line app/vmalert/web.qtpl:251
 				qw422016.N().S(`
                           </td>
                          <td>`)
-//line app/vmalert/web.qtpl:246
+//line app/vmalert/web.qtpl:253
 				qw422016.E().S(n.Notifier.Addr())
-//line app/vmalert/web.qtpl:246
+//line app/vmalert/web.qtpl:253
 				qw422016.N().S(`</td>
                      </tr>
                  `)
-//line app/vmalert/web.qtpl:248
+//line app/vmalert/web.qtpl:255
 			}
-//line app/vmalert/web.qtpl:248
+//line app/vmalert/web.qtpl:255
 			qw422016.N().S(`
               </tbody>
              </table>
          </div>
      `)
-//line app/vmalert/web.qtpl:252
+//line app/vmalert/web.qtpl:259
 		}
-//line app/vmalert/web.qtpl:252
+//line app/vmalert/web.qtpl:259
 		qw422016.N().S(`
 
     `)
-//line app/vmalert/web.qtpl:254
+//line app/vmalert/web.qtpl:261
 	} else {
-//line app/vmalert/web.qtpl:254
+//line app/vmalert/web.qtpl:261
 		qw422016.N().S(`
         <div>
             <p>No items...</p>
         </div>
     `)
-//line app/vmalert/web.qtpl:258
+//line app/vmalert/web.qtpl:265
 	}
-//line app/vmalert/web.qtpl:258
+//line app/vmalert/web.qtpl:265
 	qw422016.N().S(`
 
     `)
-//line app/vmalert/web.qtpl:260
+//line app/vmalert/web.qtpl:267
 	tpl.StreamFooter(qw422016, r)
-//line app/vmalert/web.qtpl:260
+//line app/vmalert/web.qtpl:267
 	qw422016.N().S(`
 
 `)
-//line app/vmalert/web.qtpl:262
+//line app/vmalert/web.qtpl:269
 }
 
-//line app/vmalert/web.qtpl:262
+//line app/vmalert/web.qtpl:269
 func WriteListTargets(qq422016 qtio422016.Writer, r *http.Request, targets map[notifier.TargetType][]notifier.Target) {
-//line app/vmalert/web.qtpl:262
+//line app/vmalert/web.qtpl:269
 	qw422016 := qt422016.AcquireWriter(qq422016)
-//line app/vmalert/web.qtpl:262
+//line app/vmalert/web.qtpl:269
 	StreamListTargets(qw422016, r, targets)
-//line app/vmalert/web.qtpl:262
+//line app/vmalert/web.qtpl:269
 	qt422016.ReleaseWriter(qw422016)
-//line app/vmalert/web.qtpl:262
+//line app/vmalert/web.qtpl:269
 }
 
-//line app/vmalert/web.qtpl:262
+//line app/vmalert/web.qtpl:269
 func ListTargets(r *http.Request, targets map[notifier.TargetType][]notifier.Target) string {
-//line app/vmalert/web.qtpl:262
+//line app/vmalert/web.qtpl:269
 	qb422016 := qt422016.AcquireByteBuffer()
-//line app/vmalert/web.qtpl:262
+//line app/vmalert/web.qtpl:269
 	WriteListTargets(qb422016, r, targets)
-//line app/vmalert/web.qtpl:262
+//line app/vmalert/web.qtpl:269
 	qs422016 := string(qb422016.B)
-//line app/vmalert/web.qtpl:262
+//line app/vmalert/web.qtpl:269
 	qt422016.ReleaseByteBuffer(qb422016)
-//line app/vmalert/web.qtpl:262
+//line app/vmalert/web.qtpl:269
 	return qs422016
-//line app/vmalert/web.qtpl:262
+//line app/vmalert/web.qtpl:269
 }
 
-//line app/vmalert/web.qtpl:264
+//line app/vmalert/web.qtpl:271
 func StreamAlert(qw422016 *qt422016.Writer, r *http.Request, alert *APIAlert) {
-//line app/vmalert/web.qtpl:264
+//line app/vmalert/web.qtpl:271
 	qw422016.N().S(`
     `)
-//line app/vmalert/web.qtpl:265
+//line app/vmalert/web.qtpl:272
 	prefix := utils.Prefix(r.URL.Path)
 
-//line app/vmalert/web.qtpl:265
+//line app/vmalert/web.qtpl:272
 	qw422016.N().S(`
     `)
-//line app/vmalert/web.qtpl:266
+//line app/vmalert/web.qtpl:273
 	tpl.StreamHeader(qw422016, r, navItems, "")
-//line app/vmalert/web.qtpl:266
+//line app/vmalert/web.qtpl:273
 	qw422016.N().S(`
     `)
-//line app/vmalert/web.qtpl:268
+//line app/vmalert/web.qtpl:275
 	var labelKeys []string
 	for k := range alert.Labels {
 		labelKeys = append(labelKeys, k)
@@ -903,28 +930,28 @@ func StreamAlert(qw422016 *qt422016.Writer, r *http.Request, alert *APIAlert) {
 	}
 	sort.Strings(annotationKeys)
 
-//line app/vmalert/web.qtpl:279
+//line app/vmalert/web.qtpl:286
 	qw422016.N().S(`
     <div class="display-6 pb-3 mb-3">`)
-//line app/vmalert/web.qtpl:280
+//line app/vmalert/web.qtpl:287
 	qw422016.E().S(alert.Name)
-//line app/vmalert/web.qtpl:280
+//line app/vmalert/web.qtpl:287
 	qw422016.N().S(`<span class="ms-2 badge `)
-//line app/vmalert/web.qtpl:280
+//line app/vmalert/web.qtpl:287
 	if alert.State == "firing" {
-//line app/vmalert/web.qtpl:280
+//line app/vmalert/web.qtpl:287
 		qw422016.N().S(`bg-danger`)
-//line app/vmalert/web.qtpl:280
+//line app/vmalert/web.qtpl:287
 	} else {
-//line app/vmalert/web.qtpl:280
+//line app/vmalert/web.qtpl:287
 		qw422016.N().S(` bg-warning text-dark`)
-//line app/vmalert/web.qtpl:280
+//line app/vmalert/web.qtpl:287
 	}
-//line app/vmalert/web.qtpl:280
+//line app/vmalert/web.qtpl:287
 	qw422016.N().S(`">`)
-//line app/vmalert/web.qtpl:280
+//line app/vmalert/web.qtpl:287
 	qw422016.E().S(alert.State)
-//line app/vmalert/web.qtpl:280
+//line app/vmalert/web.qtpl:287
 	qw422016.N().S(`</span></div>
     <div class="container border-bottom p-2">
       <div class="row">
@@ -933,9 +960,9 @@ func StreamAlert(qw422016 *qt422016.Writer, r *http.Request, alert *APIAlert) {
         </div>
         <div class="col">
           `)
-//line app/vmalert/web.qtpl:287
+//line app/vmalert/web.qtpl:294
 	qw422016.E().S(alert.ActiveAt.Format("2006-01-02T15:04:05Z07:00"))
-//line app/vmalert/web.qtpl:287
+//line app/vmalert/web.qtpl:294
 	qw422016.N().S(`
         </div>
       </div>
@@ -947,9 +974,9 @@ func StreamAlert(qw422016 *qt422016.Writer, r *http.Request, alert *APIAlert) {
         </div>
         <div class="col">
           <code><pre>`)
-//line app/vmalert/web.qtpl:297
+//line app/vmalert/web.qtpl:304
 	qw422016.E().S(alert.Expression)
-//line app/vmalert/web.qtpl:297
+//line app/vmalert/web.qtpl:304
 	qw422016.N().S(`</pre></code>
         </div>
       </div>
@@ -961,23 +988,23 @@ func StreamAlert(qw422016 *qt422016.Writer, r *http.Request, alert *APIAlert) {
         </div>
         <div class="col">
            `)
-//line app/vmalert/web.qtpl:307
+//line app/vmalert/web.qtpl:314
 	for _, k := range labelKeys {
-//line app/vmalert/web.qtpl:307
+//line app/vmalert/web.qtpl:314
 		qw422016.N().S(`
                 <span class="m-1 badge bg-primary">`)
-//line app/vmalert/web.qtpl:308
+//line app/vmalert/web.qtpl:315
 		qw422016.E().S(k)
-//line app/vmalert/web.qtpl:308
+//line app/vmalert/web.qtpl:315
 		qw422016.N().S(`=`)
-//line app/vmalert/web.qtpl:308
+//line app/vmalert/web.qtpl:315
 		qw422016.E().S(alert.Labels[k])
-//line app/vmalert/web.qtpl:308
+//line app/vmalert/web.qtpl:315
 		qw422016.N().S(`</span>
           `)
-//line app/vmalert/web.qtpl:309
+//line app/vmalert/web.qtpl:316
 	}
-//line app/vmalert/web.qtpl:309
+//line app/vmalert/web.qtpl:316
 	qw422016.N().S(`
         </div>
       </div>
@@ -989,24 +1016,24 @@ func StreamAlert(qw422016 *qt422016.Writer, r *http.Request, alert *APIAlert) {
         </div>
         <div class="col">
            `)
-//line app/vmalert/web.qtpl:319
+//line app/vmalert/web.qtpl:326
 	for _, k := range annotationKeys {
-//line app/vmalert/web.qtpl:319
+//line app/vmalert/web.qtpl:326
 		qw422016.N().S(`
                 <b>`)
-//line app/vmalert/web.qtpl:320
+//line app/vmalert/web.qtpl:327
 		qw422016.E().S(k)
-//line app/vmalert/web.qtpl:320
+//line app/vmalert/web.qtpl:327
 		qw422016.N().S(`:</b><br>
                 <p>`)
-//line app/vmalert/web.qtpl:321
+//line app/vmalert/web.qtpl:328
 		qw422016.E().S(alert.Annotations[k])
-//line app/vmalert/web.qtpl:321
+//line app/vmalert/web.qtpl:328
 		qw422016.N().S(`</p>
           `)
-//line app/vmalert/web.qtpl:322
+//line app/vmalert/web.qtpl:329
 	}
-//line app/vmalert/web.qtpl:322
+//line app/vmalert/web.qtpl:329
 	qw422016.N().S(`
         </div>
       </div>
@@ -1018,17 +1045,17 @@ func StreamAlert(qw422016 *qt422016.Writer, r *http.Request, alert *APIAlert) {
         </div>
         <div class="col">
            <a target="_blank" href="`)
-//line app/vmalert/web.qtpl:332
+//line app/vmalert/web.qtpl:339
 	qw422016.E().S(prefix)
-//line app/vmalert/web.qtpl:332
+//line app/vmalert/web.qtpl:339
 	qw422016.N().S(`groups#group-`)
-//line app/vmalert/web.qtpl:332
+//line app/vmalert/web.qtpl:339
 	qw422016.E().S(alert.GroupID)
-//line app/vmalert/web.qtpl:332
+//line app/vmalert/web.qtpl:339
 	qw422016.N().S(`">`)
-//line app/vmalert/web.qtpl:332
+//line app/vmalert/web.qtpl:339
 	qw422016.E().S(alert.GroupID)
-//line app/vmalert/web.qtpl:332
+//line app/vmalert/web.qtpl:339
 	qw422016.N().S(`</a>
         </div>
       </div>
@@ -1040,132 +1067,132 @@ func StreamAlert(qw422016 *qt422016.Writer, r *http.Request, alert *APIAlert) {
         </div>
         <div class="col">
            <a target="_blank" href="`)
-//line app/vmalert/web.qtpl:342
+//line app/vmalert/web.qtpl:349
 	qw422016.E().S(alert.SourceLink)
-//line app/vmalert/web.qtpl:342
+//line app/vmalert/web.qtpl:349
 	qw422016.N().S(`">Link</a>
         </div>
       </div>
     </div>
     `)
-//line app/vmalert/web.qtpl:346
+//line app/vmalert/web.qtpl:353
 	tpl.StreamFooter(qw422016, r)
-//line app/vmalert/web.qtpl:346
+//line app/vmalert/web.qtpl:353
 	qw422016.N().S(`
 
 `)
-//line app/vmalert/web.qtpl:348
+//line app/vmalert/web.qtpl:355
 }
 
-//line app/vmalert/web.qtpl:348
+//line app/vmalert/web.qtpl:355
 func WriteAlert(qq422016 qtio422016.Writer, r *http.Request, alert *APIAlert) {
-//line app/vmalert/web.qtpl:348
+//line app/vmalert/web.qtpl:355
 	qw422016 := qt422016.AcquireWriter(qq422016)
-//line app/vmalert/web.qtpl:348
+//line app/vmalert/web.qtpl:355
 	StreamAlert(qw422016, r, alert)
-//line app/vmalert/web.qtpl:348
+//line app/vmalert/web.qtpl:355
 	qt422016.ReleaseWriter(qw422016)
-//line app/vmalert/web.qtpl:348
+//line app/vmalert/web.qtpl:355
 }
 
-//line app/vmalert/web.qtpl:348
+//line app/vmalert/web.qtpl:355
 func Alert(r *http.Request, alert *APIAlert) string {
-//line app/vmalert/web.qtpl:348
+//line app/vmalert/web.qtpl:355
 	qb422016 := qt422016.AcquireByteBuffer()
-//line app/vmalert/web.qtpl:348
+//line app/vmalert/web.qtpl:355
 	WriteAlert(qb422016, r, alert)
-//line app/vmalert/web.qtpl:348
+//line app/vmalert/web.qtpl:355
 	qs422016 := string(qb422016.B)
-//line app/vmalert/web.qtpl:348
+//line app/vmalert/web.qtpl:355
 	qt422016.ReleaseByteBuffer(qb422016)
-//line app/vmalert/web.qtpl:348
+//line app/vmalert/web.qtpl:355
 	return qs422016
-//line app/vmalert/web.qtpl:348
+//line app/vmalert/web.qtpl:355
 }
 
-//line app/vmalert/web.qtpl:350
+//line app/vmalert/web.qtpl:357
 func streambadgeState(qw422016 *qt422016.Writer, state string) {
-//line app/vmalert/web.qtpl:350
+//line app/vmalert/web.qtpl:357
 	qw422016.N().S(`
 `)
-//line app/vmalert/web.qtpl:352
+//line app/vmalert/web.qtpl:359
 	badgeClass := "bg-warning text-dark"
 	if state == "firing" {
 		badgeClass = "bg-danger"
 	}
 
-//line app/vmalert/web.qtpl:356
+//line app/vmalert/web.qtpl:363
 	qw422016.N().S(`
 <span class="badge `)
-//line app/vmalert/web.qtpl:357
+//line app/vmalert/web.qtpl:364
 	qw422016.E().S(badgeClass)
-//line app/vmalert/web.qtpl:357
+//line app/vmalert/web.qtpl:364
 	qw422016.N().S(`">`)
-//line app/vmalert/web.qtpl:357
+//line app/vmalert/web.qtpl:364
 	qw422016.E().S(state)
-//line app/vmalert/web.qtpl:357
+//line app/vmalert/web.qtpl:364
 	qw422016.N().S(`</span>
 `)
-//line app/vmalert/web.qtpl:358
+//line app/vmalert/web.qtpl:365
 }
 
-//line app/vmalert/web.qtpl:358
+//line app/vmalert/web.qtpl:365
 func writebadgeState(qq422016 qtio422016.Writer, state string) {
-//line app/vmalert/web.qtpl:358
+//line app/vmalert/web.qtpl:365
 	qw422016 := qt422016.AcquireWriter(qq422016)
-//line app/vmalert/web.qtpl:358
+//line app/vmalert/web.qtpl:365
 	streambadgeState(qw422016, state)
-//line app/vmalert/web.qtpl:358
+//line app/vmalert/web.qtpl:365
 	qt422016.ReleaseWriter(qw422016)
-//line app/vmalert/web.qtpl:358
+//line app/vmalert/web.qtpl:365
 }
 
-//line app/vmalert/web.qtpl:358
+//line app/vmalert/web.qtpl:365
 func badgeState(state string) string {
-//line app/vmalert/web.qtpl:358
+//line app/vmalert/web.qtpl:365
 	qb422016 := qt422016.AcquireByteBuffer()
-//line app/vmalert/web.qtpl:358
+//line app/vmalert/web.qtpl:365
 	writebadgeState(qb422016, state)
-//line app/vmalert/web.qtpl:358
+//line app/vmalert/web.qtpl:365
 	qs422016 := string(qb422016.B)
-//line app/vmalert/web.qtpl:358
+//line app/vmalert/web.qtpl:365
 	qt422016.ReleaseByteBuffer(qb422016)
-//line app/vmalert/web.qtpl:358
+//line app/vmalert/web.qtpl:365
 	return qs422016
-//line app/vmalert/web.qtpl:358
+//line app/vmalert/web.qtpl:365
 }
 
-//line app/vmalert/web.qtpl:360
+//line app/vmalert/web.qtpl:367
 func streambadgeRestored(qw422016 *qt422016.Writer) {
-//line app/vmalert/web.qtpl:360
+//line app/vmalert/web.qtpl:367
 	qw422016.N().S(`
 <span class="badge bg-warning text-dark" title="Alert state was restored after the service restart from remote storage">restored</span>
 `)
-//line app/vmalert/web.qtpl:362
+//line app/vmalert/web.qtpl:369
 }
 
-//line app/vmalert/web.qtpl:362
+//line app/vmalert/web.qtpl:369
 func writebadgeRestored(qq422016 qtio422016.Writer) {
-//line app/vmalert/web.qtpl:362
+//line app/vmalert/web.qtpl:369
 	qw422016 := qt422016.AcquireWriter(qq422016)
-//line app/vmalert/web.qtpl:362
+//line app/vmalert/web.qtpl:369
 	streambadgeRestored(qw422016)
-//line app/vmalert/web.qtpl:362
+//line app/vmalert/web.qtpl:369
 	qt422016.ReleaseWriter(qw422016)
-//line app/vmalert/web.qtpl:362
+//line app/vmalert/web.qtpl:369
 }
 
-//line app/vmalert/web.qtpl:362
+//line app/vmalert/web.qtpl:369
 func badgeRestored() string {
-//line app/vmalert/web.qtpl:362
+//line app/vmalert/web.qtpl:369
 	qb422016 := qt422016.AcquireByteBuffer()
-//line app/vmalert/web.qtpl:362
+//line app/vmalert/web.qtpl:369
 	writebadgeRestored(qb422016)
-//line app/vmalert/web.qtpl:362
+//line app/vmalert/web.qtpl:369
 	qs422016 := string(qb422016.B)
-//line app/vmalert/web.qtpl:362
+//line app/vmalert/web.qtpl:369
 	qt422016.ReleaseByteBuffer(qb422016)
-//line app/vmalert/web.qtpl:362
+//line app/vmalert/web.qtpl:369
 	return qs422016
-//line app/vmalert/web.qtpl:362
+//line app/vmalert/web.qtpl:369
 }
diff --git a/app/vmalert/web_types.go b/app/vmalert/web_types.go
index b2e150807..27789c897 100644
--- a/app/vmalert/web_types.go
+++ b/app/vmalert/web_types.go
@@ -70,6 +70,8 @@ type APIGroup struct {
 	Concurrency int `json:"concurrency"`
 	// Params contains HTTP URL parameters added to each Rule's request
 	Params []string `json:"params,omitempty"`
+	// Headers contains HTTP headers added to each Rule's request
+	Headers []string `json:"headers,omitempty"`
 	// Labels is a set of label value pairs, that will be added to every rule.
 	Labels map[string]string `json:"labels,omitempty"`
 }
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index bcb247f3e..228b371c7 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -15,7 +15,7 @@ The following tip changes can be tested by building VictoriaMetrics components f
 
 ## tip
 
-* FEATURE: [vmalert](https://docs.victoriametrics.com/vmalert.html): allow configuring additional headers for `datasource.url`, `remoteWrite.url` and `remoteRead.url` URLs. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2860) for details.
+* FEATURE: [vmalert](https://docs.victoriametrics.com/vmalert.html): allow configuring additional headers for `datasource.url`, `remoteWrite.url` and `remoteRead.url` URLs. Headers also can be set on group level via `headers` param. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2860) for details.
 * FEATURE: [MetricsQL](https://docs.victoriametrics.com/MetricsQL.html): execute left and right sides of certain operations in parallel. For example, `q1 or q2`, `aggr_func(q1) <op> q2`, `q1 <op> aggr_func(q1)`. This may improve query performance if VictoriaMetrics has enough free resources for parallel processing of both sides of the operation. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2886).
 * FEATURE: [vmauth](https://docs.victoriametrics.com/vmagent.html): allow duplicate username records with different passwords at configuration file. It should allow password rotation without username change.
 
diff --git a/docs/vmalert.md b/docs/vmalert.md
index 18346e9ad..79c31e418 100644
--- a/docs/vmalert.md
+++ b/docs/vmalert.md
@@ -129,6 +129,16 @@ name: <string>
 params:
   [ <string>: [<string>, ...]]
 
+# Optional list of HTTP headers in form `header-name: value`
+# applied for all rules requests within a group
+# For example:
+#  headers:
+#    - "CustomHeader: foo"
+#    - "CustomHeader2: bar"
+# Headers set via this param have priority over headers set via `-datasource.headers` flag. 
+headers:
+  [ <string>, ...]
+
 # Optional list of labels added to every rule within a group.
 # It has priority over the external labels.
 # Labels are commonly used for adding environment
diff --git a/lib/promauth/config.go b/lib/promauth/config.go
index f5f5e8baa..cef24202a 100644
--- a/lib/promauth/config.go
+++ b/lib/promauth/config.go
@@ -308,7 +308,7 @@ func (ac *Config) HeadersNoAuthString() string {
 	return strings.Join(a, "")
 }
 
-// SetHeaders sets the configuted ac headers to req.
+// SetHeaders sets the configured ac headers to req.
 func (ac *Config) SetHeaders(req *http.Request, setAuthHeader bool) {
 	reqHeaders := req.Header
 	for _, h := range ac.headers {


Test patch:
diff --git a/app/vmalert/config/config_test.go b/app/vmalert/config/config_test.go
index ba2d18667..e383fe6d7 100644
--- a/app/vmalert/config/config_test.go
+++ b/app/vmalert/config/config_test.go
@@ -60,6 +60,10 @@ func TestParseBad(t *testing.T) {
 			[]string{"testdata/rules/rules1-bad.rules"},
 			"bad graphite expr",
 		},
+		{
+			[]string{"testdata/dir/rules6-bad.rules"},
+			"missing ':' in header",
+		},
 	}
 	for _, tc := range testCases {
 		_, err := Parse(tc.path, true, true)
@@ -505,6 +509,24 @@ rules:
 `, `
 name: TestGroup
 limit: 10
+rules:
+  - alert: foo
+    expr: sum by(job) (up == 1)
+`)
+	})
+
+	t.Run("`headers` change", func(t *testing.T) {
+		f(t, `
+name: TestGroup
+headers:
+  - "TenantID: foo"
+rules:
+  - alert: foo
+    expr: sum by(job) (up == 1)
+`, `
+name: TestGroup
+headers:
+  - "TenantID: bar"
 rules:
   - alert: foo
     expr: sum by(job) (up == 1)
diff --git a/app/vmalert/config/testdata/dir/rules6-bad.rules b/app/vmalert/config/testdata/dir/rules6-bad.rules
new file mode 100644
index 000000000..98a4f1ec0
--- /dev/null
+++ b/app/vmalert/config/testdata/dir/rules6-bad.rules
@@ -0,0 +1,7 @@
+groups:
+  - name: group
+    headers:
+    - 'foobar'
+    rules:
+      - alert: rows
+        expr: vm_rows > 0
\ No newline at end of file
diff --git a/app/vmalert/config/testdata/rules/rules2-good.rules b/app/vmalert/config/testdata/rules/rules2-good.rules
index 658adacfb..94b15bea9 100644
--- a/app/vmalert/config/testdata/rules/rules2-good.rules
+++ b/app/vmalert/config/testdata/rules/rules2-good.rules
@@ -3,9 +3,10 @@ groups:
     interval: 2s
     concurrency: 2
     limit: 1000
+    headers:
+      - "MyHeader: foo"
     params:
       denyPartialResponse: ["true"]
-      extra_label: ["env=dev"]
     rules:
       - alert: Conns
         expr: sum(vm_tcplistener_conns) by(instance) > 1
diff --git a/app/vmalert/datasource/vm_test.go b/app/vmalert/datasource/vm_test.go
index 1cd433cee..76cd7290b 100644
--- a/app/vmalert/datasource/vm_test.go
+++ b/app/vmalert/datasource/vm_test.go
@@ -487,7 +487,7 @@ func TestRequestParams(t *testing.T) {
 	}
 }
 
-func TestAuthConfig(t *testing.T) {
+func TestHeaders(t *testing.T) {
 	var testCases = []struct {
 		name    string
 		vmFn    func() *VMStorage
@@ -527,6 +527,40 @@ func TestAuthConfig(t *testing.T) {
 				checkEqualString(t, "foo", token)
 			},
 		},
+		{
+			name: "custom extraHeaders",
+			vmFn: func() *VMStorage {
+				return &VMStorage{extraHeaders: []Header{
+					{Key: "Foo", Value: "bar"},
+					{Key: "Baz", Value: "qux"},
+				}}
+			},
+			checkFn: func(t *testing.T, r *http.Request) {
+				h1 := r.Header.Get("Foo")
+				checkEqualString(t, "bar", h1)
+				h2 := r.Header.Get("Baz")
+				checkEqualString(t, "qux", h2)
+			},
+		},
+		{
+			name: "custom header overrides basic auth",
+			vmFn: func() *VMStorage {
+				cfg, err := utils.AuthConfig(utils.WithBasicAuth("foo", "bar", ""))
+				if err != nil {
+					t.Errorf("Error get auth config: %s", err)
+				}
+				return &VMStorage{
+					authCfg: cfg,
+					extraHeaders: []Header{
+						{Key: "Authorization", Value: "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="},
+					}}
+			},
+			checkFn: func(t *testing.T, r *http.Request) {
+				u, p, _ := r.BasicAuth()
+				checkEqualString(t, "Aladdin", u)
+				checkEqualString(t, "open sesame", p)
+			},
+		},
 	}
 	for _, tt := range testCases {
 		t.Run(tt.name, func(t *testing.T) {