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 }}