Repository: VictoriaMetrics/VictoriaMetrics
Pull request #387 (2020-03-26 18:35:19)

[vmalert] - parse template annotaions

codecov[bot]: # [Codecov](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/387?src=pr&el=h1) Report
> Merging [#387](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/387?src=pr&el=desc) into [master](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/commit/df91d2d91f841501370ca6eadc6001f6607f56e2&el=desc) will **decrease** coverage by `0.03%`.
> The diff coverage is `83.33%`.

[![Impacted file tree graph](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/387/graphs/tree.svg?width=650&height=150&src=pr&token=YND8dYVcXn)](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/387?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #387      +/-   ##
==========================================
- Coverage   71.58%   71.54%   -0.04%     
==========================================
  Files         126      126              
  Lines       19923    19943      +20     
==========================================
+ Hits        14261    14268       +7     
- Misses       4562     4566       +4     
- Partials     1100     1109       +9     
```


| [Impacted Files](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/387?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [app/vmalert/provider/common.go](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/387/diff?src=pr&el=tree#diff-YXBwL3ZtYWxlcnQvcHJvdmlkZXIvY29tbW9uLmdv) | `83.33% <83.33%> (+83.33%)` | :arrow_up: |
| [lib/fs/dir\_remover.go](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/387/diff?src=pr&el=tree#diff-bGliL2ZzL2Rpcl9yZW1vdmVyLmdv) | `3.33% <0.00%> (-23.34%)` | :arrow_down: |
| [lib/mergeset/table.go](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/387/diff?src=pr&el=tree#diff-bGliL21lcmdlc2V0L3RhYmxlLmdv) | `68.71% <0.00%> (-1.00%)` | :arrow_down: |
| [lib/storage/index\_db.go](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/387/diff?src=pr&el=tree#diff-bGliL3N0b3JhZ2UvaW5kZXhfZGIuZ28=) | `76.00% <0.00%> (-0.87%)` | :arrow_down: |
| [app/vmselect/promql/aggr\_incremental.go](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/387/diff?src=pr&el=tree#diff-YXBwL3Ztc2VsZWN0L3Byb21xbC9hZ2dyX2luY3JlbWVudGFsLmdv) | `97.05% <0.00%> (ø)` | |
| [lib/storage/merge.go](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/387/diff?src=pr&el=tree#diff-bGliL3N0b3JhZ2UvbWVyZ2UuZ28=) | `84.29% <0.00%> (+1.65%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/387?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/387?src=pr&el=footer). Last update [25f585e...b4d308c](https://codecov.io/gh/VictoriaMetrics/VictoriaMetrics/pull/387?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/app/vmalert/main.go b/app/vmalert/main.go
index c3a01f251..4cabe5532 100644
--- a/app/vmalert/main.go
+++ b/app/vmalert/main.go
@@ -81,6 +81,7 @@ func (w *watchdog) run(ctx context.Context, a config.Group, evaluationInterval t
 	for {
 		select {
 		case <-t.C:
+			start := time.Now()
 			for _, r := range a.Rules {
 				if metrics, err = w.storage.Query(ctx, r.Expr); err != nil {
 					logger.Errorf("error reading metrics %s", err)
@@ -90,7 +91,8 @@ func (w *watchdog) run(ctx context.Context, a config.Group, evaluationInterval t
 				if len(metrics) < 1 {
 					continue
 				}
-				alerts = provider.AlertsFromMetrics(metrics, a.Name, r)
+				// todo define alert end time
+				alerts = provider.AlertsFromMetrics(metrics, a.Name, r, start, time.Time{})
 				// todo save to storage
 				if err := w.alertProvider.Send(alerts); err != nil {
 					logger.Errorf("error sending alerts %s", err)
diff --git a/app/vmalert/provider/common.go b/app/vmalert/provider/common.go
index 5fcf72159..1ed767efd 100644
--- a/app/vmalert/provider/common.go
+++ b/app/vmalert/provider/common.go
@@ -1,11 +1,14 @@
 package provider
 
 import (
-	"sort"
+	"bytes"
+	"strings"
+	"text/template"
 	"time"
 
 	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/config"
 	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
+	"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
 )
 
 // AlertProvider is common interface for alert manager provider
@@ -25,41 +28,74 @@ type Alert struct {
 	Value float64
 }
 
+type alertTplData struct {
+	Labels         map[string]string
+	ExternalLabels map[string]string
+	Value          float64
+}
+
+const tplHeader = `{{ $value := .Value }}{{ $labels := .Labels }}{{ $externalLabels := .ExternalLabels }}`
+
 // AlertsFromMetrics converts metrics to alerts by alert Rule
-func AlertsFromMetrics(metrics []datasource.Metric, group string, rule config.Rule) []Alert {
+func AlertsFromMetrics(metrics []datasource.Metric, group string, rule config.Rule, start, end time.Time) []Alert {
 	alerts := make([]Alert, 0, len(metrics))
 	for i, m := range metrics {
 		a := Alert{
-			Group:  group,
-			Name:   rule.Name,
-			Labels: metrics[i].Labels,
-			// todo eval template in annotations
-			Annotations: rule.Annotations,
-			Start:       time.Unix(m.Timestamp, 0),
+			Group: group,
+			Name:  rule.Name,
+			Start: start,
+			End:   end,
+			Value: m.Value,
 		}
-		for k, v := range rule.Labels {
-			a.Labels = append(a.Labels, datasource.Label{
-				Name:  k,
-				Value: v,
-			})
-		}
-		a.Labels = removeDuplicated(a.Labels)
+		tplData := alertTplData{Value: m.Value, ExternalLabels: make(map[string]string)}
+		tplData.Labels, a.Labels = mergeLabels(metrics[i].Labels, rule.Labels)
+		a.Annotations = templateAnnotations(rule.Annotations, tplHeader, tplData)
 		alerts = append(alerts, a)
 	}
 	return alerts
 }
 
-func removeDuplicated(l []datasource.Label) []datasource.Label {
-	sort.Slice(l, func(i, j int) bool {
-		return l[i].Name < l[j].Name
-	})
-	j := 0
-	for i := 1; i < len(l); i++ {
-		if l[j] == l[i] {
+func mergeLabels(ml []datasource.Label, rl map[string]string) (map[string]string, []datasource.Label) {
+	set := make(map[string]string, len(ml)+len(rl))
+	sl := append([]datasource.Label(nil), ml...)
+	for _, i := range ml {
+		set[i.Name] = i.Value
+	}
+	for name, value := range rl {
+		if _, ok := set[name]; ok {
+			continue
+		}
+		set[name] = value
+		sl = append(sl, datasource.Label{
+			Name:  name,
+			Value: value,
+		})
+	}
+	return set, sl
+}
+
+func templateAnnotations(annotations map[string]string, header string, data alertTplData) map[string]string {
+	var builder strings.Builder
+	var buf bytes.Buffer
+	r := make(map[string]string, len(annotations))
+	for key, text := range annotations {
+		r[key] = text
+		buf.Reset()
+		builder.Reset()
+		builder.Grow(len(header) + len(text))
+		builder.WriteString(header)
+		builder.WriteString(text)
+		// todo add template helper func from Prometheus
+		tpl, err := template.New("").Option("missingkey=zero").Parse(builder.String())
+		if err != nil {
+			logger.Errorf("error parsing annotation template %q for %q:%s", text, key, err)
+			continue
+		}
+		if err = tpl.Execute(&buf, data); err != nil {
+			logger.Errorf("error evaluating annotation template %s for %s:%s", text, key, err)
 			continue
 		}
-		j++
-		l[j] = l[i]
+		r[key] = buf.String()
 	}
-	return l[:j+1]
+	return r
 }
diff --git a/lib/uint64set/uint64set.go b/lib/uint64set/uint64set.go
index 0686473e5..194b4aeeb 100644
--- a/lib/uint64set/uint64set.go
+++ b/lib/uint64set/uint64set.go
@@ -261,11 +261,12 @@ func (s *Set) union(a *Set, mayOwn bool) {
 	s.sort()
 	i := 0
 	j := 0
+	sBucketsLen := len(s.buckets)
 	for {
-		for i < len(s.buckets) && j < len(a.buckets) && s.buckets[i].hi < a.buckets[j].hi {
+		for i < sBucketsLen && j < len(a.buckets) && s.buckets[i].hi < a.buckets[j].hi {
 			i++
 		}
-		if i >= len(s.buckets) {
+		if i >= sBucketsLen {
 			for j < len(a.buckets) {
 				b32 := s.addBucket32()
 				a.buckets[j].copyTo(b32)
@@ -404,11 +405,12 @@ func (b *bucket32) getLen() int {
 func (b *bucket32) union(a *bucket32, mayOwn bool) {
 	i := 0
 	j := 0
+	bBucketsLen := len(b.buckets)
 	for {
-		for i < len(b.b16his) && j < len(a.b16his) && b.b16his[i] < a.b16his[j] {
+		for i < bBucketsLen && j < len(a.b16his) && b.b16his[i] < a.b16his[j] {
 			i++
 		}
-		if i >= len(b.b16his) {
+		if i >= bBucketsLen {
 			for j < len(a.b16his) {
 				b16 := b.addBucket16(a.b16his[j])
 				if mayOwn {
@@ -481,6 +483,23 @@ func (b *bucket32) intersect(a *bucket32) {
 			j++
 		}
 	}
+	// Remove zero buckets
+	b16his := b.b16his[:0]
+	bs := b.buckets[:0]
+	for i := range b.buckets {
+		b32 := &b.buckets[i]
+		if b32.isZero() {
+			continue
+		}
+		b16his = append(b16his, b.b16his[i])
+		bs = append(bs, *b32)
+	}
+	for i := len(bs); i < len(b.buckets); i++ {
+		b.buckets[i] = bucket16{}
+	}
+	b.hint = 0
+	b.b16his = b16his
+	b.buckets = bs
 }
 
 func (b *bucket32) forEach(f func(part []uint64) bool) bool {
@@ -650,6 +669,10 @@ type bucket16 struct {
 	smallPool    [56]uint16
 }
 
+func (b *bucket16) isZero() bool {
+	return b.bits == nil && b.smallPoolLen == 0
+}
+
 func (b *bucket16) getLen() int {
 	if b.bits == nil {
 		return b.smallPoolLen


Test patch:
diff --git a/app/vmalert/provider/common_test.go b/app/vmalert/provider/common_test.go
new file mode 100644
index 000000000..0f12f4424
--- /dev/null
+++ b/app/vmalert/provider/common_test.go
@@ -0,0 +1,100 @@
+package provider
+
+import (
+	"reflect"
+	"sort"
+	"testing"
+	"time"
+
+	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/config"
+	"github.com/VictoriaMetrics/VictoriaMetrics/app/vmalert/datasource"
+)
+
+func TestAlertsFromMetrics(t *testing.T) {
+	now := time.Now()
+	metrics := []datasource.Metric{
+		{
+			Labels: []datasource.Label{
+				{Name: "__name__", Value: "foo"},
+				{Name: "label", Value: "value"},
+			},
+			Timestamp: 10,
+			Value:     20,
+		},
+		{
+			Labels: []datasource.Label{
+				{Name: "__name__", Value: "bar"},
+				{Name: "label", Value: "value"},
+			},
+			Timestamp: 10,
+			Value:     30,
+		},
+	}
+	rule := config.Rule{
+		Name: "alertname",
+		Expr: "up==0",
+		Labels: map[string]string{
+			"label2": "value",
+		},
+		Annotations: map[string]string{
+			"tpl": "{{$value}} {{ $labels.label}}",
+		},
+	}
+	alerts := AlertsFromMetrics(metrics, "group", rule, now, now)
+	if len(alerts) != 2 {
+		t.Fatalf("expecting 2 alerts got %d", len(alerts))
+	}
+
+	f := func(got, exp Alert) {
+		t.Helper()
+		if got.Group != exp.Group ||
+			got.Value != exp.Value ||
+			got.End != exp.End ||
+			got.Name != exp.Name ||
+			got.Start != exp.Start {
+			t.Errorf("alerts are not equal: \nwant %#v \ngot  %#v", exp, got)
+		}
+		sort.Slice(got.Labels, func(i, j int) bool {
+			return got.Labels[i].Name < got.Labels[j].Name
+		})
+		sort.Slice(exp.Labels, func(i, j int) bool {
+			return got.Labels[i].Name < got.Labels[j].Name
+		})
+		if !reflect.DeepEqual(got.Labels, exp.Labels) {
+			t.Errorf("alerts labels are not equal: want  %+v got %+v", exp.Labels, got.Labels)
+		}
+		if !reflect.DeepEqual(got.Annotations, exp.Annotations) {
+			t.Errorf("alerts annotations are not equal: want %+v got %+v", exp.Annotations, got.Annotations)
+		}
+	}
+	f(alerts[0], Alert{
+		Group: "group",
+		Name:  "alertname",
+		Labels: []datasource.Label{
+			{Name: "__name__", Value: "foo"},
+			{Name: "label", Value: "value"},
+			{Name: "label2", Value: "value"},
+		},
+		Annotations: map[string]string{
+			"tpl": "20 value",
+		},
+		Start: now,
+		End:   now,
+		Value: 20,
+	})
+	f(alerts[1], Alert{
+		Group: "group",
+		Name:  "alertname",
+		Labels: []datasource.Label{
+			{Name: "__name__", Value: "bar"},
+			{Name: "label", Value: "value"},
+			{Name: "label2", Value: "value"},
+		},
+		Annotations: map[string]string{
+			"tpl": "30 value",
+		},
+		Start: now,
+		End:   now,
+		Value: 30,
+	})
+}