diff --git a/docs/notifications.md b/docs/notifications.md index 7672c19..de3c505 100644 --- a/docs/notifications.md +++ b/docs/notifications.md @@ -179,6 +179,14 @@ To send notifications via shoutrrr, the following command-line options, or their Go to [containrrr.github.io/shoutrrr/services/overview](https://containrrr.github.io/shoutrrr/services/overview) to learn more about the different service URLs you can use. You can define multiple services by space separating the URLs. (See example below) +You can customize the message posted by setting a template. + +- `--notification-template` (env. `WATCHTOWER_NOTIFICATION_TEMPLATE`): The template used for the message. + +The template is a Go [template](https://golang.org/pkg/text/template/) and the you format a list of [log entries](https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry). + +The default value if not set is `{{range .}}{{.Message}}{{println}}{{end}}`. The example below uses a template that also outputs timestamp and log level. + Example: ```bash @@ -187,5 +195,6 @@ docker run -d \ -v /var/run/docker.sock:/var/run/docker.sock \ -e WATCHTOWER_NOTIFICATIONS=shoutrrr \ -e WATCHTOWER_NOTIFICATION_URL="discord://token@channel slack://watchtower@token-a/token-b/token-c" \ + -e WATCHTOWER_NOTIFICATION_TEMPLATE="{{range .}}{{.Time.Format \"2006-01-02 15:04:05\"}} ({{.Level}}): {{.Message}}{{println}}{{end}}" \ containrrr/watchtower ``` \ No newline at end of file diff --git a/internal/flags/flags.go b/internal/flags/flags.go index 20e2cfb..9554a3c 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -259,6 +259,12 @@ Should only be used for testing. viper.GetString("WATCHTOWER_NOTIFICATION_GOTIFY_TOKEN"), "The Gotify Application required to query the Gotify API") + flags.StringP( + "notification-template", + "", + viper.GetString("WATCHTOWER_NOTIFICATION_TEMPLATE"), + "The shoutrrr text/template for the messages") + flags.StringArrayP( "notification-url", "", diff --git a/pkg/notifications/shoutrrr.go b/pkg/notifications/shoutrrr.go index 108e3b6..04fbf8b 100644 --- a/pkg/notifications/shoutrrr.go +++ b/pkg/notifications/shoutrrr.go @@ -1,7 +1,10 @@ package notifications import ( + "bytes" "fmt" + "text/template" + "github.com/containrrr/shoutrrr" "github.com/containrrr/shoutrrr/pkg/router" t "github.com/containrrr/watchtower/pkg/types" @@ -10,7 +13,8 @@ import ( ) const ( - shoutrrrType = "shoutrrr" + shoutrrrDefaultTemplate = "{{range .}}{{.Message}}{{println}}{{end}}" + shoutrrrType = "shoutrrr" ) // Implements Notifier, logrus.Hook @@ -19,6 +23,7 @@ type shoutrrrTypeNotifier struct { Router *router.ServiceRouter entries []*log.Entry logLevels []log.Level + template *template.Template } func newShoutrrrNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifier { @@ -31,6 +36,7 @@ func newShoutrrrNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Noti Urls: urls, Router: r, logLevels: acceptedLogLevels, + template: getShoutrrrTemplate(c), } log.AddHook(n) @@ -39,13 +45,10 @@ func newShoutrrrNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Noti } func (e *shoutrrrTypeNotifier) buildMessage(entries []*log.Entry) string { - body := "" - for _, entry := range entries { - body += entry.Time.Format("2006-01-02 15:04:05") + " (" + entry.Level.String() + "): " + entry.Message + "\r\n" - // We don't use fields in watchtower, so don't bother sending them. - } + var body bytes.Buffer + e.template.Execute(&body, entries) - return body + return body.String() } func (e *shoutrrrTypeNotifier) sendEntries(entries []*log.Entry) { @@ -91,3 +94,35 @@ func (e *shoutrrrTypeNotifier) Fire(entry *log.Entry) error { } return nil } + +func getShoutrrrTemplate(c *cobra.Command) *template.Template { + var tpl *template.Template + + flags := c.PersistentFlags() + + tplString, err := flags.GetString("notification-template") + + // If we succeed in getting a non-empty template configuration + // try to parse the template string. + if tplString != "" && err == nil { + tpl, err = template.New("").Parse(tplString) + } + + // In case of errors (either from parsing the template string + // or from getting the template configuration) log an error + // message about this and the fact that we'll use the default + // template instead. + if err != nil { + log.Errorf("Could not use configured notification template: %s. Using default template", err) + } + + // If we had an error (either from parsing the template string + // or from getting the template configuration) or we a + // template wasn't configured (the empty template string) + // fallback to using the default template. + if err != nil || tplString == "" { + tpl = template.Must(template.New("").Parse(shoutrrrDefaultTemplate)) + } + + return tpl +} diff --git a/pkg/notifications/shoutrrr_test.go b/pkg/notifications/shoutrrr_test.go new file mode 100644 index 0000000..9795c2a --- /dev/null +++ b/pkg/notifications/shoutrrr_test.go @@ -0,0 +1,80 @@ +package notifications + +import ( + "testing" + "text/template" + + "github.com/containrrr/watchtower/internal/flags" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/stretchr/testify/require" +) + +func TestShoutrrrDefaultTemplate(t *testing.T) { + cmd := new(cobra.Command) + + shoutrrr := &shoutrrrTypeNotifier{ + template: getShoutrrrTemplate(cmd), + } + + entries := []*log.Entry{ + { + Message: "foo bar", + }, + } + + s := shoutrrr.buildMessage(entries) + + require.Equal(t, "foo bar\n", s) +} + +func TestShoutrrrTemplate(t *testing.T) { + cmd := new(cobra.Command) + flags.RegisterNotificationFlags(cmd) + err := cmd.ParseFlags([]string{"--notification-template={{range .}}{{.Level}}: {{.Message}}{{println}}{{end}}"}) + + require.NoError(t, err) + + shoutrrr := &shoutrrrTypeNotifier{ + template: getShoutrrrTemplate(cmd), + } + + entries := []*log.Entry{ + { + Level: log.InfoLevel, + Message: "foo bar", + }, + } + + s := shoutrrr.buildMessage(entries) + + require.Equal(t, "info: foo bar\n", s) +} + +func TestShoutrrrInvalidTemplateUsesTemplate(t *testing.T) { + cmd := new(cobra.Command) + + flags.RegisterNotificationFlags(cmd) + err := cmd.ParseFlags([]string{"--notification-template={{"}) + + require.NoError(t, err) + + shoutrrr := &shoutrrrTypeNotifier{ + template: getShoutrrrTemplate(cmd), + } + + shoutrrrDefault := &shoutrrrTypeNotifier{ + template: template.Must(template.New("").Parse(shoutrrrDefaultTemplate)), + } + + entries := []*log.Entry{ + { + Message: "foo bar", + }, + } + + s := shoutrrr.buildMessage(entries) + sd := shoutrrrDefault.buildMessage(entries) + + require.Equal(t, sd, s) +}