Adding basic (but flexible) notification system which hooks into logrus.

This only adds e-mail notifications, but others could be easily done. In
many cases, adding another existing logrus hook will be sufficient.
pull/106/head
Rodrigo Damazio Bovendorp 7 years ago
parent 717ca9fcaf
commit 5adb143f62

@ -1,7 +1,6 @@
package main // import "github.com/v2tec/watchtower" package main // import "github.com/v2tec/watchtower"
import ( import (
"fmt"
"os" "os"
"os/signal" "os/signal"
"syscall" "syscall"
@ -14,6 +13,7 @@ import (
"github.com/urfave/cli" "github.com/urfave/cli"
"github.com/v2tec/watchtower/actions" "github.com/v2tec/watchtower/actions"
"github.com/v2tec/watchtower/container" "github.com/v2tec/watchtower/container"
"github.com/v2tec/watchtower/notifications"
) )
// DockerAPIMinVersion is the version of the docker API, which is minimally required by // DockerAPIMinVersion is the version of the docker API, which is minimally required by
@ -27,6 +27,7 @@ var (
scheduleSpec string scheduleSpec string
cleanup bool cleanup bool
noRestart bool noRestart bool
notifier *notifications.Notifier
) )
func init() { func init() {
@ -82,6 +83,37 @@ func main() {
Name: "debug", Name: "debug",
Usage: "enable debug mode with verbose logging", Usage: "enable debug mode with verbose logging",
}, },
cli.StringSliceFlag{
Name: "notifications",
Value: &cli.StringSlice{},
Usage: "notification types to send (valid: email)",
EnvVar: "WATCHTOWER_NOTIFICATIONS",
},
cli.StringFlag{
Name: "notification-email-from",
Usage: "Address to send notification e-mails from",
EnvVar: "WATCHTOWER_NOTIFICATION_EMAIL_FROM",
},
cli.StringFlag{
Name: "notification-email-to",
Usage: "Address to send notification e-mails to",
EnvVar: "WATCHTOWER_NOTIFICATION_EMAIL_TO",
},
cli.StringFlag{
Name: "notification-email-server",
Usage: "SMTP server to send notification e-mails through",
EnvVar: "WATCHTOWER_NOTIFICATION_EMAIL_SERVER",
},
cli.StringFlag{
Name: "notification-email-server-user",
Usage: "SMTP server user for sending notifications",
EnvVar: "WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER",
},
cli.StringFlag{
Name: "notification-email-server-password",
Usage: "SMTP server password for sending notifications",
EnvVar: "WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD",
},
} }
if err := app.Run(os.Args); err != nil { if err := app.Run(os.Args); err != nil {
@ -115,6 +147,8 @@ func before(c *cli.Context) error {
} }
client = container.NewClient(!c.GlobalBool("no-pull")) client = container.NewClient(!c.GlobalBool("no-pull"))
notifier = notifications.NewNotifier(c)
return nil return nil
} }
@ -135,9 +169,11 @@ func start(c *cli.Context) error {
select { select {
case v := <-tryLockSem: case v := <-tryLockSem:
defer func() { tryLockSem <- v }() defer func() { tryLockSem <- v }()
notifier.StartNotification()
if err := actions.Update(client, names, cleanup, noRestart); err != nil { if err := actions.Update(client, names, cleanup, noRestart); err != nil {
fmt.Println(err) log.Println(err)
} }
notifier.SendNotification()
default: default:
log.Debug("Skipped another update already running.") log.Debug("Skipped another update already running.")
} }

@ -0,0 +1,115 @@
package notifications
import (
"encoding/base64"
"fmt"
"net/smtp"
"os"
log "github.com/Sirupsen/logrus"
"github.com/urfave/cli"
)
const (
emailType = "email"
)
// Implements typeNotifier, logrus.Hook
// The default logrus email integration would have several issues:
// - It would send one email per log output
// - It would only send errors
// We work around that by holding on to log entries until the update cycle is done.
type emailTypeNotifier struct {
From, To string
Server, User, Password string
entries []*log.Entry
}
func newEmailNotifier(c *cli.Context) typeNotifier {
n := &emailTypeNotifier{
From: c.GlobalString("notification-email-from"),
To: c.GlobalString("notification-email-to"),
Server: c.GlobalString("notification-email-server"),
User: c.GlobalString("notification-email-server-user"),
Password: c.GlobalString("notification-email-server-password"),
}
log.AddHook(n)
return n
}
func (e *emailTypeNotifier) buildMessage(entries []*log.Entry) []byte {
emailSubject := "Watchtower updates"
if hostname, err := os.Hostname(); err == nil {
emailSubject += " on " + hostname
}
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.
}
header := make(map[string]string)
header["From"] = e.From
header["To"] = e.To
header["Subject"] = emailSubject
header["MIME-Version"] = "1.0"
header["Content-Type"] = "text/plain; charset=\"utf-8\""
header["Content-Transfer-Encoding"] = "base64"
message := ""
for k, v := range header {
message += fmt.Sprintf("%s: %s\r\n", k, v)
}
message += "\r\n" + base64.StdEncoding.EncodeToString([]byte(body))
return []byte(message)
}
func (e *emailTypeNotifier) sendEntries(entries []*log.Entry) {
// Do the sending in a separate goroutine so we don't block the main process.
msg := e.buildMessage(entries)
go func() {
auth := smtp.PlainAuth("", e.User, e.Password, e.Server)
err := smtp.SendMail(e.Server+":25", auth, e.From, []string{e.To}, msg)
if err != nil {
// Use fmt so it doesn't trigger another email.
fmt.Println("Failed to send notification email: ", err)
}
}()
}
func (e *emailTypeNotifier) StartNotification() {
if e.entries == nil {
e.entries = make([]*log.Entry, 0, 10)
}
}
func (e *emailTypeNotifier) SendNotification() {
if e.entries != nil {
e.sendEntries(e.entries)
e.entries = nil
}
}
func (e *emailTypeNotifier) Levels() []log.Level {
// TODO: Make this configurable.
return []log.Level{
log.PanicLevel,
log.FatalLevel,
log.ErrorLevel,
log.WarnLevel,
log.InfoLevel,
}
}
func (e *emailTypeNotifier) Fire(entry *log.Entry) error {
if e.entries != nil {
e.entries = append(e.entries, entry)
} else {
// Log output generated outside a cycle is sent immediately.
e.sendEntries([]*log.Entry{entry})
}
return nil
}

@ -0,0 +1,46 @@
package notifications
import (
log "github.com/Sirupsen/logrus"
"github.com/urfave/cli"
)
type typeNotifier interface {
StartNotification()
SendNotification()
}
type Notifier struct {
types []typeNotifier
}
func NewNotifier(c *cli.Context) *Notifier {
n := &Notifier{}
// Parse types and create notifiers.
types := c.GlobalStringSlice("notifications")
for _, t := range types {
var tn typeNotifier
switch t {
case emailType:
tn = newEmailNotifier(c)
default:
log.Fatalf("Unknown notification type %q", t)
}
n.types = append(n.types, tn)
}
return n
}
func (n *Notifier) StartNotification() {
for _, t := range n.types {
t.StartNotification()
}
}
func (n *Notifier) SendNotification() {
for _, t := range n.types {
t.SendNotification()
}
}
Loading…
Cancel
Save