diff --git a/CHANGELOG.md b/CHANGELOG.md
index f3c6b4427f5..311d60cc66a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -152,6 +152,7 @@
### Mimirtool
+* [ENHANCEMENT] Add template render command to render locally a template. #7325
* [ENHANCEMENT] Add `--extra-headers` option to `mimirtool rules` command to add extra headers to requests for auth. #7141
* [ENHANCEMENT] Analyze Prometheus: set tenant header. #6737
* [ENHANCEMENT] Add argument `--output-dir` to `mimirtool alertmanager get` where the config and templates will be written to and can be loaded via `mimirtool alertmanager load` #6760
diff --git a/cmd/mimirtool/main.go b/cmd/mimirtool/main.go
index 416dcef20c5..23f3398fe08 100644
--- a/cmd/mimirtool/main.go
+++ b/cmd/mimirtool/main.go
@@ -30,6 +30,7 @@ var (
remoteReadCommand commands.RemoteReadCommand
ruleCommand commands.RuleCommand
backfillCommand commands.BackfillCommand
+ templateCommand commands.TemplateCommand
)
func main() {
@@ -48,6 +49,7 @@ func main() {
pushGateway.Register(app, envVars)
remoteReadCommand.Register(app, envVars)
ruleCommand.Register(app, envVars, prometheus.DefaultRegisterer)
+ templateCommand.Register(app, envVars)
app.Command("version", "Get the version of the mimirtool CLI").Action(func(k *kingpin.ParseContext) error {
fmt.Fprintln(os.Stdout, mimirversion.Print("Mimirtool"))
diff --git a/docs/sources/mimir/manage/tools/mimirtool.md b/docs/sources/mimir/manage/tools/mimirtool.md
index 7aed6e611f9..5a00cb311af 100644
--- a/docs/sources/mimir/manage/tools/mimirtool.md
+++ b/docs/sources/mimir/manage/tools/mimirtool.md
@@ -48,6 +48,10 @@ Mimirtool is a command-line tool that operators and tenants can use to execute a
For more information about the `backfill` command, refer to [Backfill]({{< relref "#backfill" >}})
+- The `template` command enables you to render your alertmanager template.
+
+ For more information about the `template` command, refer to [template]({{< relref "#template" >}})
+
Mimirtool interacts with:
- User-facing APIs provided by Grafana Mimir.
@@ -1006,6 +1010,20 @@ INFO[0000] block uploaded successfully block=01G8CB7GTTC5ZXY23WTXHS
INFO[0001] finished uploading blocks already_exists=1 failed=0 succeeded=2
```
+### Template
+
+You can render your alertmanager template by using the subcommand `render`.
+
+##### Example
+
+The following command render a template and prints it to the terminal.
+
+```bash
+mimirtool template render --template.glob "alertmanager_template1.tmpl" --template.data "alert_data1.json" --template.text '{{ template "my_message" . }}'
+```
+
+It can also take a glob path that will be expended.
+
## License
This software is licensed as AGPLv3. For more information, see [LICENSE](https://github.com/grafana/mimir/blob/main/LICENSE).
diff --git a/pkg/alertmanager/alertmanager.go b/pkg/alertmanager/alertmanager.go
index 364a03cd131..cfe32bf0a99 100644
--- a/pkg/alertmanager/alertmanager.go
+++ b/pkg/alertmanager/alertmanager.go
@@ -324,7 +324,7 @@ func (am *Alertmanager) ApplyConfig(userID string, conf *config.Config, rawCfg s
templateFiles[i] = templateFilepath
}
- tmpl, err := template.FromGlobs(templateFiles, withCustomFunctions(userID))
+ tmpl, err := template.FromGlobs(templateFiles, WithCustomFunctions(userID))
if err != nil {
return err
}
diff --git a/pkg/alertmanager/alertmanager_template.go b/pkg/alertmanager/alertmanager_template.go
index 18500ac6e30..4212c426a2e 100644
--- a/pkg/alertmanager/alertmanager_template.go
+++ b/pkg/alertmanager/alertmanager_template.go
@@ -76,9 +76,9 @@ func queryFromGeneratorURL(generatorURL string) (string, error) {
return query, nil
}
-// withCustomFunctions returns template.Option which adds additional template functions
+// WithCustomFunctions returns template.Option which adds additional template functions
// to the default ones.
-func withCustomFunctions(userID string) template.Option {
+func WithCustomFunctions(userID string) template.Option {
funcs := tmpltext.FuncMap{
"tenantID": func() string { return userID },
"grafanaExploreURL": grafanaExploreURL,
diff --git a/pkg/alertmanager/alertmanager_template_test.go b/pkg/alertmanager/alertmanager_template_test.go
index 1dcda88e9ed..89bebbef5b9 100644
--- a/pkg/alertmanager/alertmanager_template_test.go
+++ b/pkg/alertmanager/alertmanager_template_test.go
@@ -19,7 +19,7 @@ func Test_withCustomFunctions(t *testing.T) {
result string
expectError bool
}
- tmpl, err := template.FromGlobs([]string{}, withCustomFunctions("test"))
+ tmpl, err := template.FromGlobs([]string{}, WithCustomFunctions("test"))
assert.NoError(t, err)
cases := []tc{
{
diff --git a/pkg/alertmanager/api.go b/pkg/alertmanager/api.go
index 65638f243f5..b3ba2c43e16 100644
--- a/pkg/alertmanager/api.go
+++ b/pkg/alertmanager/api.go
@@ -263,7 +263,7 @@ func validateUserConfig(logger log.Logger, cfg alertspb.AlertConfigDesc, limits
templateFiles[i] = filepath.Join(userTempDir, t)
}
- _, err = template.FromGlobs(templateFiles, withCustomFunctions(user))
+ _, err = template.FromGlobs(templateFiles, WithCustomFunctions(user))
if err != nil {
return err
}
diff --git a/pkg/mimirtool/commands/template.go b/pkg/mimirtool/commands/template.go
new file mode 100644
index 00000000000..8c794a54ffb
--- /dev/null
+++ b/pkg/mimirtool/commands/template.go
@@ -0,0 +1,23 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+
+package commands
+
+import (
+ "github.com/alecthomas/kingpin/v2"
+)
+
+type TemplateCommand struct{}
+
+func (cmd *TemplateCommand) Register(app *kingpin.Application, envVars EnvVarNames) {
+ templateCmd := app.Command("template", "Render template files.")
+ trCmd := &TemplateRenderCmd{}
+ renderCmd := templateCmd.Command("render", "Render a given definition in a template file to standard output.").Action(trCmd.render)
+ renderCmd.Flag("template.glob", "Glob of paths that will be expanded and used for rendering.").Required().StringsVar(&trCmd.templateFilesGlobs)
+ renderCmd.Flag("template.text", "The template that will be rendered.").Required().StringVar(&trCmd.templateText)
+ renderCmd.Flag("template.type", "The type of the template. Can be either text (default) or html.").EnumVar(&trCmd.templateType, "html", "text")
+ renderCmd.Flag("template.data", "Full path to a file which contains the data of the alert(-s) with which the --template.text will be rendered. Must be in JSON. File must be formatted according to the following layout: https://pkg.go.dev/github.com/prometheus/alertmanager/template#Data. If none has been specified then a predefined, simple alert will be used for rendering.").FileVar(&trCmd.templateData)
+ renderCmd.Flag("id", "Basic auth username to use when contacting Prometheus or Grafana Mimir, also set as tenant ID; alternatively, set "+envVars.TenantID+".").
+ Envar(envVars.TenantID).
+ Default("").
+ StringVar(&trCmd.tenantID)
+}
diff --git a/pkg/mimirtool/commands/template_render.go b/pkg/mimirtool/commands/template_render.go
new file mode 100644
index 00000000000..726728caa8b
--- /dev/null
+++ b/pkg/mimirtool/commands/template_render.go
@@ -0,0 +1,127 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+
+package commands
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "os"
+ "time"
+
+ "github.com/alecthomas/kingpin/v2"
+ "github.com/prometheus/alertmanager/template"
+
+ MimirAlertManager "github.com/grafana/mimir/pkg/alertmanager"
+)
+
+var defaultData = template.Data{
+ Receiver: "receiver",
+ Status: "alertstatus",
+ Alerts: template.Alerts{
+ template.Alert{
+ Status: "alertstatus",
+ Labels: template.KV{
+ "label1": "value1",
+ "label2": "value2",
+ "instance": "foo.bar:1234",
+ "commonlabelkey1": "commonlabelvalue1",
+ "commonlabelkey2": "commonlabelvalue2",
+ },
+ Annotations: template.KV{
+ "annotation1": "value1",
+ "annotation2": "value2",
+ "commonannotationkey1": "commonannotationvalue1",
+ "commonannotationkey2": "commonannotationvalue2",
+ },
+ StartsAt: time.Now().Add(-5 * time.Minute),
+ EndsAt: time.Now(),
+ GeneratorURL: "https://generatorurl.com",
+ Fingerprint: "fingerprint1",
+ },
+ template.Alert{
+ Status: "alertstatus",
+ Labels: template.KV{
+ "foo": "bar",
+ "baz": "qux",
+ "commonlabelkey1": "commonlabelvalue1",
+ "commonlabelkey2": "commonlabelvalue2",
+ },
+ Annotations: template.KV{
+ "aaa": "bbb",
+ "ccc": "ddd",
+ "commonannotationkey1": "commonannotationvalue1",
+ "commonannotationkey2": "commonannotationvalue2",
+ },
+ StartsAt: time.Now().Add(-10 * time.Minute),
+ EndsAt: time.Now(),
+ GeneratorURL: "https://generatorurl.com",
+ Fingerprint: "fingerprint2",
+ },
+ },
+ GroupLabels: template.KV{
+ "grouplabelkey1": "grouplabelvalue1",
+ "grouplabelkey2": "grouplabelvalue2",
+ },
+ CommonLabels: template.KV{
+ "alertname": "AlertNameExample",
+ "customer": "testing_purpose",
+ "environment": "lab",
+ "commonlabelkey1": "commonlabelvalue1",
+ "commonlabelkey2": "commonlabelvalue2",
+ },
+ CommonAnnotations: template.KV{
+ "commonannotationkey1": "commonannotationvalue1",
+ "commonannotationkey2": "commonannotationvalue2",
+ },
+ ExternalURL: "https://example.com",
+}
+
+// TemplateRenderCmd Render a given definition in a template file to standard output
+type TemplateRenderCmd struct {
+ templateFilesGlobs []string
+ templateType string
+ templateText string
+ templateData *os.File
+ tenantID string // Needed in the function WithCustomFunctions
+}
+
+func (cmd *TemplateRenderCmd) render(_ *kingpin.ParseContext) error {
+ rendered, err := TemplateRender(cmd)
+ if err != nil {
+ return err
+ }
+ fmt.Print(rendered)
+ return nil
+}
+
+func TemplateRender(cmd *TemplateRenderCmd) (string, error) {
+ tmpl, err := template.FromGlobs(cmd.templateFilesGlobs, MimirAlertManager.WithCustomFunctions(cmd.tenantID))
+ if err != nil {
+ return "", err
+ }
+
+ f := tmpl.ExecuteTextString
+ if cmd.templateType == "html" {
+ f = tmpl.ExecuteHTMLString
+ }
+
+ var data template.Data
+ if cmd.templateData == nil {
+ data = defaultData
+ } else {
+ content, err := io.ReadAll(cmd.templateData)
+ if err != nil {
+ return "", err
+ }
+ if err := json.Unmarshal(content, &data); err != nil {
+ return "", err
+ }
+ }
+
+ rendered, err := f(cmd.templateText, data)
+ if err != nil {
+ return "", err
+ }
+ return rendered, nil
+}
diff --git a/pkg/mimirtool/commands/template_render_test.go b/pkg/mimirtool/commands/template_render_test.go
new file mode 100644
index 00000000000..cfd38c057e5
--- /dev/null
+++ b/pkg/mimirtool/commands/template_render_test.go
@@ -0,0 +1,97 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+
+package commands
+
+import (
+ "os"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+// Based on https://github.com/grafana/mimir/blob/main/pkg/alertmanager/alertmanager_template_test.go
+func TestTemplateRender(t *testing.T) {
+ type tc struct {
+ name string
+ templateOptions TemplateRenderCmd
+ result string
+ expectError bool
+ }
+ jsonFilesStr := []string{"testdata/template/alert_data1.json", "testdata/template/alert_data1.json", "testdata/template/alert_data2.json"}
+ jsonFiles := make([]*os.File, len(jsonFilesStr))
+
+ for index, jsonFile := range jsonFilesStr {
+ file, err := os.OpenFile(jsonFile, os.O_RDONLY, 0)
+ assert.NoError(t, err, "Json template data doesn't exist")
+ jsonFiles[index] = file
+
+ }
+ cases := []tc{
+ {
+ name: "testing basic message template",
+ templateOptions: TemplateRenderCmd{
+ templateFilesGlobs: []string{"testdata/template/alertmanager_template1.tmpl"},
+ templateType: "text",
+ templateText: `{{ template "my_message" . }}`,
+ templateData: jsonFiles[0],
+ tenantID: "",
+ },
+ result: `[AlertNameExample | testing_purpose | lab]`,
+ },
+ {
+ name: "testing basic description template",
+ templateOptions: TemplateRenderCmd{
+ templateFilesGlobs: []string{"testdata/template/alertmanager_template1.tmpl"},
+ templateType: "text",
+ templateText: `{{ template "my_description" . }}`,
+ templateData: jsonFiles[1],
+ tenantID: "",
+ },
+ result: `
+Alertname: AlertNameExample
+Severity: warning
+
+Details:
+• Customer: testing_purpose
+• Environment: lab
+• Description: blablablabla
+
+
+`,
+ },
+ {
+ name: "testing custom description template", // Using Specific Mimir Template function
+ templateOptions: TemplateRenderCmd{
+ templateFilesGlobs: []string{"testdata/template/alertmanager_template2.tmpl"},
+ templateType: "text",
+ templateText: `{{ template "my_description" . }}`,
+ templateData: jsonFiles[2],
+ tenantID: "",
+ },
+ result: `
+Alertname: AlertNameExample2
+Severity: warning
+
+Details:
+• Customer: testing_purpose
+• Environment: lab
+• Description: blablablabla
+
+Grafana Explorer URL
+`,
+ },
+ }
+
+ for _, c := range cases {
+ c := c
+ t.Run(c.name, func(t *testing.T) {
+ renderedTemplate, err := TemplateRender(&c.templateOptions)
+ if c.expectError {
+ assert.Error(t, err)
+ return
+ }
+ assert.NoError(t, err)
+ assert.Equal(t, c.result, renderedTemplate)
+ })
+ }
+}
diff --git a/pkg/mimirtool/commands/testdata/template/alert_data1.json b/pkg/mimirtool/commands/testdata/template/alert_data1.json
new file mode 100644
index 00000000000..638e70b5d5c
--- /dev/null
+++ b/pkg/mimirtool/commands/testdata/template/alert_data1.json
@@ -0,0 +1,36 @@
+{
+ "receiver": "receiver",
+ "status": "alertstatus",
+ "alerts": [
+ {
+ "status": "alertstatus",
+ "labels": {
+ "customer": "testing_purpose",
+ "environment": "lab",
+ "severity": "warning",
+ "alertname": "AlertNameExample"
+ },
+ "annotations": {
+ "description": "blablablabla"
+ },
+ "startsAt": "2024-02-07T09:00:51.0982886+01:00",
+ "endsAt": "2024-02-07T09:10:51.0982886+01:00",
+ "generatorURL": "https://generatorurl.com/graph?g0.expr=up",
+ "fingerprint": "fingerprint2"
+ }
+ ],
+ "groupLabels": {
+ "grouplabelkey1": "grouplabelvalue1",
+ "grouplabelkey2": "grouplabelvalue2"
+ },
+ "commonLabels": {
+ "alertname": "AlertNameExample",
+ "customer": "testing_purpose",
+ "environment": "lab"
+ },
+ "commonAnnotations": {
+ "commonannotationkey1": "commonannotationvalue1",
+ "commonannotationkey2": "commonannotationvalue2"
+ },
+ "externalURL": "https://example.com"
+}
\ No newline at end of file
diff --git a/pkg/mimirtool/commands/testdata/template/alert_data2.json b/pkg/mimirtool/commands/testdata/template/alert_data2.json
new file mode 100644
index 00000000000..314fc1eae85
--- /dev/null
+++ b/pkg/mimirtool/commands/testdata/template/alert_data2.json
@@ -0,0 +1,36 @@
+{
+ "receiver": "receiver",
+ "status": "alertstatus",
+ "alerts": [
+ {
+ "status": "alertstatus",
+ "labels": {
+ "customer": "testing_purpose",
+ "environment": "lab",
+ "severity": "warning",
+ "alertname": "AlertNameExample2"
+ },
+ "annotations": {
+ "description": "blablablabla"
+ },
+ "startsAt": "2024-02-07T09:00:51.0982886+01:00",
+ "endsAt": "2024-02-07T09:10:51.0982886+01:00",
+ "generatorURL": "https://generatorurl.com/graph?g0.expr=up",
+ "fingerprint": "fingerprint2"
+ }
+ ],
+ "groupLabels": {
+ "grouplabelkey1": "grouplabelvalue1",
+ "grouplabelkey2": "grouplabelvalue2"
+ },
+ "commonLabels": {
+ "alertname": "AlertNameExample2",
+ "customer": "testing_purpose",
+ "environment": "lab"
+ },
+ "commonAnnotations": {
+ "commonannotationkey1": "commonannotationvalue1",
+ "commonannotationkey2": "commonannotationvalue2"
+ },
+ "externalURL": "https://example.com"
+}
\ No newline at end of file
diff --git a/pkg/mimirtool/commands/testdata/template/alertmanager_template1.tmpl b/pkg/mimirtool/commands/testdata/template/alertmanager_template1.tmpl
new file mode 100644
index 00000000000..67b84882ea4
--- /dev/null
+++ b/pkg/mimirtool/commands/testdata/template/alertmanager_template1.tmpl
@@ -0,0 +1,14 @@
+{{ define "my_message" }}[{{ .CommonLabels.alertname }} | {{ .CommonLabels.customer }} | {{ .CommonLabels.environment }}]{{ end }}
+
+{{ define "my_description" }}
+{{ range .Alerts -}}
+Alertname: {{ .Labels.alertname }}
+Severity: {{ .Labels.severity }}
+
+Details:
+• Customer: {{ .Labels.customer }}
+• Environment: {{ .Labels.environment }}
+• Description: {{ .Annotations.description }}
+
+{{ end }}
+{{ end }}
diff --git a/pkg/mimirtool/commands/testdata/template/alertmanager_template2.tmpl b/pkg/mimirtool/commands/testdata/template/alertmanager_template2.tmpl
new file mode 100644
index 00000000000..467571537f2
--- /dev/null
+++ b/pkg/mimirtool/commands/testdata/template/alertmanager_template2.tmpl
@@ -0,0 +1,14 @@
+{{ define "my_message" }}[{{ .CommonLabels.alertname }} | {{ .CommonLabels.customer }} | {{ .CommonLabels.environment }}]{{ end }}
+
+{{ define "my_description" }}
+{{ range .Alerts -}}
+Alertname: {{ .Labels.alertname }}
+Severity: {{ .Labels.severity }}
+
+Details:
+• Customer: {{ .Labels.customer }}
+• Environment: {{ .Labels.environment }}
+• Description: {{ .Annotations.description }}
+{{ end }}
+Grafana Explorer URL
+{{ end }}