Skip to content

Commit

Permalink
Add new command on mimirtool used to render template
Browse files Browse the repository at this point in the history
  • Loading branch information
ncharaf authored and ncharaf committed Feb 7, 2024
1 parent 1af7e13 commit ef781c1
Show file tree
Hide file tree
Showing 14 changed files with 373 additions and 5 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions cmd/mimirtool/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ var (
remoteReadCommand commands.RemoteReadCommand
ruleCommand commands.RuleCommand
backfillCommand commands.BackfillCommand
templateCommand commands.TemplateCommand
)

func main() {
Expand All @@ -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"))
Expand Down
18 changes: 18 additions & 0 deletions docs/sources/mimir/manage/tools/mimirtool.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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).
2 changes: 1 addition & 1 deletion pkg/alertmanager/alertmanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/alertmanager/alertmanager_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion pkg/alertmanager/alertmanager_template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
{
Expand Down
2 changes: 1 addition & 1 deletion pkg/alertmanager/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
23 changes: 23 additions & 0 deletions pkg/mimirtool/commands/template.go
Original file line number Diff line number Diff line change
@@ -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)
}
127 changes: 127 additions & 0 deletions pkg/mimirtool/commands/template_render.go
Original file line number Diff line number Diff line change
@@ -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
}
97 changes: 97 additions & 0 deletions pkg/mimirtool/commands/template_render_test.go
Original file line number Diff line number Diff line change
@@ -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
<a href="https://foo.bar/explore?left=%7B%22range%22%3A%7B%22from%22%3A%22now-12h%22%2C%22to%22%3A%22now%22%7D%2C%22queries%22%3A%5B%7B%22datasource%22%3A%7B%22type%22%3A%22prometheus%22%2C%22uid%22%3A%22xyz%22%7D%2C%22expr%22%3A%22up%22%2C%22instant%22%3Afalse%2C%22range%22%3Atrue%2C%22refId%22%3A%22A%22%7D%5D%7D">Grafana Explorer URL</a>
`,
},
}

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)
})
}
}
36 changes: 36 additions & 0 deletions pkg/mimirtool/commands/testdata/template/alert_data1.json
Original file line number Diff line number Diff line change
@@ -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"
}
Loading

0 comments on commit ef781c1

Please sign in to comment.