Skip to content

Commit

Permalink
fix(notifications): default templates and logic (#1010)
Browse files Browse the repository at this point in the history
* fix(notifications): default templates and logic
* fix multi-entry report notifs and add test
* add tests for log queueing
  • Loading branch information
piksel committed Sep 19, 2021
1 parent fc31c6e commit cd0ec88
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 59 deletions.
58 changes: 39 additions & 19 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cmd
import (
"github.com/containrrr/watchtower/internal/meta"
"math"
"net/http"
"os"
"os/signal"
"strconv"
Expand Down Expand Up @@ -197,7 +198,7 @@ func Run(c *cobra.Command, names []string) {
httpAPI.RegisterHandler(metricsHandler.Path, metricsHandler.Handle)
}

if err := httpAPI.Start(enableUpdateAPI && !unblockHTTPAPI); err != nil {
if err := httpAPI.Start(enableUpdateAPI && !unblockHTTPAPI); err != nil && err != http.ErrServerClosed {
log.Error("failed to start API", err)
}

Expand Down Expand Up @@ -259,24 +260,43 @@ func formatDuration(d time.Duration) string {
}

func writeStartupMessage(c *cobra.Command, sched time.Time, filtering string) {
if noStartupMessage, _ := c.PersistentFlags().GetBool("no-startup-message"); !noStartupMessage {
schedMessage := "Running a one time update."
if !sched.IsZero() {
until := formatDuration(time.Until(sched))
schedMessage = "Scheduling first run: " + sched.Format("2006-01-02 15:04:05 -0700 MST") +
"\nNote that the first check will be performed in " + until
}

notifs := "Using no notifications"
notifierNames := notifier.GetNames()
if len(notifierNames) > 0 {
notifs = "Using notifications: " + strings.Join(notifierNames, ", ")
}

log.Info("Watchtower ", meta.Version, "\n", notifs, "\n", filtering, "\n", schedMessage)
if log.IsLevelEnabled(log.TraceLevel) {
log.Warn("trace level enabled: log will include sensitive information as credentials and tokens")
}
noStartupMessage, _ := c.PersistentFlags().GetBool("no-startup-message")

var startupLog *log.Entry
if noStartupMessage {
startupLog = notifications.LocalLog
} else {
startupLog = log.NewEntry(log.StandardLogger())
// Batch up startup messages to send them as a single notification
notifier.StartNotification()
}

startupLog.Info("Watchtower ", meta.Version)

notifierNames := notifier.GetNames()
if len(notifierNames) > 0 {
startupLog.Info("Using notifications: " + strings.Join(notifierNames, ", "))
} else {
startupLog.Info("Using no notifications")
}

startupLog.Info(filtering)

if !sched.IsZero() {
until := formatDuration(time.Until(sched))
startupLog.Info("Scheduling first run: " + sched.Format("2006-01-02 15:04:05 -0700 MST"))
startupLog.Info("Note that the first check will be performed in " + until)
} else {
startupLog.Info("Running a one time update.")
}

if !noStartupMessage {
// Send the queued up startup messages, not including the trace warning below (to make sure it's noticed)
notifier.SendNotification(nil)
}

if log.IsLevelEnabled(log.TraceLevel) {
startupLog.Warn("Trace level enabled: log will include sensitive information as credentials and tokens")
}
}

Expand Down
2 changes: 2 additions & 0 deletions pkg/notifications/notifications_suite_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package notifications_test

import (
"github.com/onsi/gomega/format"
"testing"

. "github.com/onsi/ginkgo"
Expand All @@ -9,5 +10,6 @@ import (

func TestNotifications(t *testing.T) {
RegisterFailHandler(Fail)
format.CharactersAroundMismatchToInclude = 20
RunSpecs(t, "Notifications Suite")
}
3 changes: 2 additions & 1 deletion pkg/notifications/notifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ func NewNotifier(c *cobra.Command) ty.Notifier {

urls = AppendLegacyUrls(urls, c)

return newShoutrrrNotifier(tplString, acceptedLogLevels, !reportTemplate, urls...)
title := GetTitle(c)
return newShoutrrrNotifier(tplString, acceptedLogLevels, !reportTemplate, title, urls...)
}

// AppendLegacyUrls creates shoutrrr equivalent URLs from legacy notification flags
Expand Down
78 changes: 55 additions & 23 deletions pkg/notifications/shoutrrr.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package notifications

import (
"bytes"
"fmt"
stdlog "log"
"strings"
"text/template"
Expand All @@ -13,23 +12,33 @@ import (
log "github.com/sirupsen/logrus"
)

// LocalLog is a logrus logger that does not send entries as notifications
var LocalLog = log.WithField("notify", "no")

const (
shoutrrrDefaultLegacyTemplate = "{{range .}}{{.Message}}{{println}}{{end}}"
shoutrrrDefaultTemplate = `{{- with .Report -}}
shoutrrrDefaultTemplate = `
{{- if .Report -}}
{{- with .Report -}}
{{- if ( or .Updated .Failed ) -}}
{{len .Scanned}} Scanned, {{len .Updated}} Updated, {{len .Failed}} Failed
{{range .Updated -}}
{{- range .Updated}}
- {{.Name}} ({{.ImageName}}): {{.CurrentImageID.ShortID}} updated to {{.LatestImageID.ShortID}}
{{end -}}
{{range .Fresh -}}
{{- end -}}
{{- range .Fresh}}
- {{.Name}} ({{.ImageName}}): {{.State}}
{{end -}}
{{range .Skipped -}}
{{- end -}}
{{- range .Skipped}}
- {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}}
{{end -}}
{{range .Failed -}}
{{- end -}}
{{- range .Failed}}
- {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}}
{{end -}}
{{end -}}`
{{- end -}}
{{- end -}}
{{- end -}}
{{- else -}}
{{range .Entries -}}{{.Message}}{{"\n"}}{{- end -}}
{{- end -}}`
shoutrrrType = "shoutrrr"
)

Expand All @@ -47,6 +56,7 @@ type shoutrrrTypeNotifier struct {
messages chan string
done chan bool
legacyTemplate bool
params *types.Params
}

// GetScheme returns the scheme part of a Shoutrrr URL
Expand All @@ -58,6 +68,7 @@ func GetScheme(url string) string {
return url[:schemeEnd]
}

// GetNames returns a list of notification services that has been added
func (n *shoutrrrTypeNotifier) GetNames() []string {
names := make([]string, len(n.Urls))
for i, u := range n.Urls {
Expand All @@ -66,9 +77,10 @@ func (n *shoutrrrTypeNotifier) GetNames() []string {
return names
}

func newShoutrrrNotifier(tplString string, acceptedLogLevels []log.Level, legacy bool, urls ...string) t.Notifier {
func newShoutrrrNotifier(tplString string, acceptedLogLevels []log.Level, legacy bool, title string, urls ...string) t.Notifier {

notifier := createNotifier(urls, acceptedLogLevels, tplString, legacy)
notifier.params = &types.Params{"title": title}
log.AddHook(notifier)

// Do the sending in a separate goroutine so we don't block the main process.
Expand Down Expand Up @@ -102,67 +114,87 @@ func createNotifier(urls []string, levels []log.Level, tplString string, legacy

func sendNotifications(n *shoutrrrTypeNotifier) {
for msg := range n.messages {
errs := n.Router.Send(msg, nil)
errs := n.Router.Send(msg, n.params)

for i, err := range errs {
if err != nil {
scheme := GetScheme(n.Urls[i])
// Use fmt so it doesn't trigger another notification.
fmt.Printf("Failed to send shoutrrr notification (#%d, %s): %v\n", i, scheme, err)
LocalLog.WithFields(log.Fields{
"service": scheme,
"index": i,
}).WithError(err).Error("Failed to send shoutrrr notification")
}
}
}

n.done <- true
}

func (n *shoutrrrTypeNotifier) buildMessage(data Data) string {
func (n *shoutrrrTypeNotifier) buildMessage(data Data) (string, error) {
var body bytes.Buffer
var templateData interface{} = data
if n.legacyTemplate {
templateData = data.Entries
}
if err := n.template.Execute(&body, templateData); err != nil {
fmt.Printf("Failed to execute Shoutrrrr template: %s\n", err.Error())
return "", err
}

return body.String()
return body.String(), nil
}

func (n *shoutrrrTypeNotifier) sendEntries(entries []*log.Entry, report t.Report) {
msg := n.buildMessage(Data{entries, report})
msg, err := n.buildMessage(Data{entries, report})

if msg == "" {
// Log in go func in case we entered from Fire to avoid stalling
go func() {
if err != nil {
LocalLog.WithError(err).Fatal("Notification template error")
} else {
LocalLog.Info("Skipping notification due to empty message")
}
}()
return
}
n.messages <- msg
}

// StartNotification begins queueing up messages to send them as a batch
func (n *shoutrrrTypeNotifier) StartNotification() {
if n.entries == nil {
n.entries = make([]*log.Entry, 0, 10)
}
}

// SendNotification sends the queued up messages as a notification
func (n *shoutrrrTypeNotifier) SendNotification(report t.Report) {
//if n.entries == nil || len(n.entries) <= 0 {
// return
//}

n.sendEntries(n.entries, report)
n.entries = nil
}

// Close prevents further messages from being queued and waits until all the currently queued up messages have been sent
func (n *shoutrrrTypeNotifier) Close() {
close(n.messages)

// Use fmt so it doesn't trigger another notification.
fmt.Println("Waiting for the notification goroutine to finish")
LocalLog.Info("Waiting for the notification goroutine to finish")

_ = <-n.done
}

// Levels return what log levels trigger notifications
func (n *shoutrrrTypeNotifier) Levels() []log.Level {
return n.logLevels
}

// Fire is the hook that logrus calls on a new log message
func (n *shoutrrrTypeNotifier) Fire(entry *log.Entry) error {
if entry.Data["notify"] == "no" {
// Skip logging if explicitly tagged as non-notify
return nil
}
if n.entries != nil {
n.entries = append(n.entries, entry)
} else {
Expand Down
Loading

0 comments on commit cd0ec88

Please sign in to comment.