Skip to content

Commit cd0ec88

Browse files
authored
fix(notifications): default templates and logic (#1010)
* fix(notifications): default templates and logic * fix multi-entry report notifs and add test * add tests for log queueing
1 parent fc31c6e commit cd0ec88

File tree

5 files changed

+197
-59
lines changed

5 files changed

+197
-59
lines changed

cmd/root.go

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cmd
33
import (
44
"github.com/containrrr/watchtower/internal/meta"
55
"math"
6+
"net/http"
67
"os"
78
"os/signal"
89
"strconv"
@@ -197,7 +198,7 @@ func Run(c *cobra.Command, names []string) {
197198
httpAPI.RegisterHandler(metricsHandler.Path, metricsHandler.Handle)
198199
}
199200

200-
if err := httpAPI.Start(enableUpdateAPI && !unblockHTTPAPI); err != nil {
201+
if err := httpAPI.Start(enableUpdateAPI && !unblockHTTPAPI); err != nil && err != http.ErrServerClosed {
201202
log.Error("failed to start API", err)
202203
}
203204

@@ -259,24 +260,43 @@ func formatDuration(d time.Duration) string {
259260
}
260261

261262
func writeStartupMessage(c *cobra.Command, sched time.Time, filtering string) {
262-
if noStartupMessage, _ := c.PersistentFlags().GetBool("no-startup-message"); !noStartupMessage {
263-
schedMessage := "Running a one time update."
264-
if !sched.IsZero() {
265-
until := formatDuration(time.Until(sched))
266-
schedMessage = "Scheduling first run: " + sched.Format("2006-01-02 15:04:05 -0700 MST") +
267-
"\nNote that the first check will be performed in " + until
268-
}
269-
270-
notifs := "Using no notifications"
271-
notifierNames := notifier.GetNames()
272-
if len(notifierNames) > 0 {
273-
notifs = "Using notifications: " + strings.Join(notifierNames, ", ")
274-
}
275-
276-
log.Info("Watchtower ", meta.Version, "\n", notifs, "\n", filtering, "\n", schedMessage)
277-
if log.IsLevelEnabled(log.TraceLevel) {
278-
log.Warn("trace level enabled: log will include sensitive information as credentials and tokens")
279-
}
263+
noStartupMessage, _ := c.PersistentFlags().GetBool("no-startup-message")
264+
265+
var startupLog *log.Entry
266+
if noStartupMessage {
267+
startupLog = notifications.LocalLog
268+
} else {
269+
startupLog = log.NewEntry(log.StandardLogger())
270+
// Batch up startup messages to send them as a single notification
271+
notifier.StartNotification()
272+
}
273+
274+
startupLog.Info("Watchtower ", meta.Version)
275+
276+
notifierNames := notifier.GetNames()
277+
if len(notifierNames) > 0 {
278+
startupLog.Info("Using notifications: " + strings.Join(notifierNames, ", "))
279+
} else {
280+
startupLog.Info("Using no notifications")
281+
}
282+
283+
startupLog.Info(filtering)
284+
285+
if !sched.IsZero() {
286+
until := formatDuration(time.Until(sched))
287+
startupLog.Info("Scheduling first run: " + sched.Format("2006-01-02 15:04:05 -0700 MST"))
288+
startupLog.Info("Note that the first check will be performed in " + until)
289+
} else {
290+
startupLog.Info("Running a one time update.")
291+
}
292+
293+
if !noStartupMessage {
294+
// Send the queued up startup messages, not including the trace warning below (to make sure it's noticed)
295+
notifier.SendNotification(nil)
296+
}
297+
298+
if log.IsLevelEnabled(log.TraceLevel) {
299+
startupLog.Warn("Trace level enabled: log will include sensitive information as credentials and tokens")
280300
}
281301
}
282302

pkg/notifications/notifications_suite_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package notifications_test
22

33
import (
4+
"github.com/onsi/gomega/format"
45
"testing"
56

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

1011
func TestNotifications(t *testing.T) {
1112
RegisterFailHandler(Fail)
13+
format.CharactersAroundMismatchToInclude = 20
1214
RunSpecs(t, "Notifications Suite")
1315
}

pkg/notifications/notifier.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ func NewNotifier(c *cobra.Command) ty.Notifier {
3030

3131
urls = AppendLegacyUrls(urls, c)
3232

33-
return newShoutrrrNotifier(tplString, acceptedLogLevels, !reportTemplate, urls...)
33+
title := GetTitle(c)
34+
return newShoutrrrNotifier(tplString, acceptedLogLevels, !reportTemplate, title, urls...)
3435
}
3536

3637
// AppendLegacyUrls creates shoutrrr equivalent URLs from legacy notification flags

pkg/notifications/shoutrrr.go

Lines changed: 55 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package notifications
22

33
import (
44
"bytes"
5-
"fmt"
65
stdlog "log"
76
"strings"
87
"text/template"
@@ -13,23 +12,33 @@ import (
1312
log "github.com/sirupsen/logrus"
1413
)
1514

15+
// LocalLog is a logrus logger that does not send entries as notifications
16+
var LocalLog = log.WithField("notify", "no")
17+
1618
const (
1719
shoutrrrDefaultLegacyTemplate = "{{range .}}{{.Message}}{{println}}{{end}}"
18-
shoutrrrDefaultTemplate = `{{- with .Report -}}
20+
shoutrrrDefaultTemplate = `
21+
{{- if .Report -}}
22+
{{- with .Report -}}
23+
{{- if ( or .Updated .Failed ) -}}
1924
{{len .Scanned}} Scanned, {{len .Updated}} Updated, {{len .Failed}} Failed
20-
{{range .Updated -}}
25+
{{- range .Updated}}
2126
- {{.Name}} ({{.ImageName}}): {{.CurrentImageID.ShortID}} updated to {{.LatestImageID.ShortID}}
22-
{{end -}}
23-
{{range .Fresh -}}
27+
{{- end -}}
28+
{{- range .Fresh}}
2429
- {{.Name}} ({{.ImageName}}): {{.State}}
25-
{{end -}}
26-
{{range .Skipped -}}
30+
{{- end -}}
31+
{{- range .Skipped}}
2732
- {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}}
28-
{{end -}}
29-
{{range .Failed -}}
33+
{{- end -}}
34+
{{- range .Failed}}
3035
- {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}}
31-
{{end -}}
32-
{{end -}}`
36+
{{- end -}}
37+
{{- end -}}
38+
{{- end -}}
39+
{{- else -}}
40+
{{range .Entries -}}{{.Message}}{{"\n"}}{{- end -}}
41+
{{- end -}}`
3342
shoutrrrType = "shoutrrr"
3443
)
3544

@@ -47,6 +56,7 @@ type shoutrrrTypeNotifier struct {
4756
messages chan string
4857
done chan bool
4958
legacyTemplate bool
59+
params *types.Params
5060
}
5161

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

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

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

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

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

103115
func sendNotifications(n *shoutrrrTypeNotifier) {
104116
for msg := range n.messages {
105-
errs := n.Router.Send(msg, nil)
117+
errs := n.Router.Send(msg, n.params)
106118

107119
for i, err := range errs {
108120
if err != nil {
109121
scheme := GetScheme(n.Urls[i])
110122
// Use fmt so it doesn't trigger another notification.
111-
fmt.Printf("Failed to send shoutrrr notification (#%d, %s): %v\n", i, scheme, err)
123+
LocalLog.WithFields(log.Fields{
124+
"service": scheme,
125+
"index": i,
126+
}).WithError(err).Error("Failed to send shoutrrr notification")
112127
}
113128
}
114129
}
115130

116131
n.done <- true
117132
}
118133

119-
func (n *shoutrrrTypeNotifier) buildMessage(data Data) string {
134+
func (n *shoutrrrTypeNotifier) buildMessage(data Data) (string, error) {
120135
var body bytes.Buffer
121136
var templateData interface{} = data
122137
if n.legacyTemplate {
123138
templateData = data.Entries
124139
}
125140
if err := n.template.Execute(&body, templateData); err != nil {
126-
fmt.Printf("Failed to execute Shoutrrrr template: %s\n", err.Error())
141+
return "", err
127142
}
128143

129-
return body.String()
144+
return body.String(), nil
130145
}
131146

132147
func (n *shoutrrrTypeNotifier) sendEntries(entries []*log.Entry, report t.Report) {
133-
msg := n.buildMessage(Data{entries, report})
148+
msg, err := n.buildMessage(Data{entries, report})
149+
150+
if msg == "" {
151+
// Log in go func in case we entered from Fire to avoid stalling
152+
go func() {
153+
if err != nil {
154+
LocalLog.WithError(err).Fatal("Notification template error")
155+
} else {
156+
LocalLog.Info("Skipping notification due to empty message")
157+
}
158+
}()
159+
return
160+
}
134161
n.messages <- msg
135162
}
136163

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

171+
// SendNotification sends the queued up messages as a notification
143172
func (n *shoutrrrTypeNotifier) SendNotification(report t.Report) {
144-
//if n.entries == nil || len(n.entries) <= 0 {
145-
// return
146-
//}
147-
148173
n.sendEntries(n.entries, report)
149174
n.entries = nil
150175
}
151176

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

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

158184
_ = <-n.done
159185
}
160186

187+
// Levels return what log levels trigger notifications
161188
func (n *shoutrrrTypeNotifier) Levels() []log.Level {
162189
return n.logLevels
163190
}
164191

192+
// Fire is the hook that logrus calls on a new log message
165193
func (n *shoutrrrTypeNotifier) Fire(entry *log.Entry) error {
194+
if entry.Data["notify"] == "no" {
195+
// Skip logging if explicitly tagged as non-notify
196+
return nil
197+
}
166198
if n.entries != nil {
167199
n.entries = append(n.entries, entry)
168200
} else {

0 commit comments

Comments
 (0)