Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,17 @@ annotations:
description: "Generated by: {{ .Expr }}"
```

Use `humanizeDuration` to format a value in seconds. For example, a query value of
`4800` renders as `1h 20m 0s`:

```yaml
annotations:
summary: "{{ $labels.service }}: last reading was {{ $value | humanizeDuration }} ago"
```

The helper also accepts `.Value`. Formatting changes only the annotation; the
numeric value remains in seconds.

### Environment Variable Substitution

Use `%{ENV_VAR}` syntax in rule files (same as vmalert):
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.25.6
require (
github.com/ClickHouse/clickhouse-go/v2 v2.43.0
github.com/prometheus/client_golang v1.23.2
github.com/prometheus/common v0.66.1
github.com/testcontainers/testcontainers-go v0.40.0
github.com/testcontainers/testcontainers-go/modules/clickhouse v0.40.0
gopkg.in/yaml.v3 v3.0.1
Expand Down Expand Up @@ -58,7 +59,6 @@ require (
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/segmentio/asm v1.2.1 // indirect
github.com/shirou/gopsutil/v4 v4.25.6 // indirect
Expand Down
6 changes: 5 additions & 1 deletion rule/rule.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ import (
"text/template"
"time"

"github.com/prometheus/common/helpers/templates"

"github.com/garbett1/chalert/config"
"github.com/garbett1/chalert/datasource"
)
Expand Down Expand Up @@ -432,7 +434,9 @@ func (ar *AlertingRule) renderAnnotations(m datasource.Metric, _ *AlertInstance)
// Rewrite legacy {{ $labels.X }} and {{ $value }} to Go template syntax.
normalized := normalizeLegacyTemplate(tpl)

t, err := template.New(k).Option("missingkey=zero").Parse(normalized)
t, err := template.New(k).Funcs(template.FuncMap{
"humanizeDuration": templates.HumanizeDuration,
}).Option("missingkey=zero").Parse(normalized)
if err != nil {
// Fall back to raw template on parse error.
slog.Warn("chalert annotation template parse error",
Expand Down
61 changes: 61 additions & 0 deletions rule/rule_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -552,3 +552,64 @@ func TestAnnotationTemplates_FullGoTemplate(t *testing.T) {
}
}
}

func TestAlertingRule_HumanizeDuration(t *testing.T) {
q := &fakeQuerier{
results: datasource.Result{
Data: []datasource.Metric{{
Labels: []datasource.Label{
{Name: "pod_site", Value: "buf101"},
{Name: "pod", Value: "pod1"},
},
Values: []float64{0},
}},
},
}
r := NewAlertingRule(&fakeQuerierBuilder{q: q}, "g", time.Minute, config.Rule{
Alert: "TPUCCPodDataStale",
Expr: "SELECT 'buf101' AS pod_site, 'pod1' AS pod, 1200 AS value",
Annotations: map[string]string{
"summary": "{{ $labels.pod_site }} {{ $labels.pod }}: last inlet reading was {{ $value | humanizeDuration }} ago",
"native": "{{ .Labels.pod_site }} {{ .Labels.pod }}: last inlet reading was {{ humanizeDuration .Value }} ago",
},
ID: 778,
})

tests := []struct {
name string
seconds float64
want string
}{
{name: "minutes", seconds: 1200, want: "20m 0s"},
{name: "hours", seconds: 4800, want: "1h 20m 0s"},
{name: "days", seconds: 93600, want: "1d 2h 0m 0s"},
}
var alertID uint64
now := time.Now()
for i, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
q.results.Data[0].Values[0] = tt.seconds
alerts, err := r.Exec(context.Background(), now.Add(time.Duration(i)*time.Minute), 0)
if err != nil {
t.Fatal(err)
}
if len(alerts) != 1 {
t.Fatalf("expected 1 alert, got %d", len(alerts))
}
want := "buf101 pod1: last inlet reading was " + tt.want + " ago"
for _, key := range []string{"summary", "native"} {
if got := alerts[0].Annotations[key]; got != want {
t.Errorf("annotation %q: want %q, got %q", key, want, got)
}
}
if got := alerts[0].Value; got != tt.seconds {
t.Errorf("formatting changed value: want %v seconds, got %v", tt.seconds, got)
}
if i == 0 {
alertID = alerts[0].ID
} else if alerts[0].ID != alertID {
t.Errorf("duration change created a new alert: want ID %d, got %d", alertID, alerts[0].ID)
}
})
}
}
Loading