From 47aa47e3f63baed82e1924e2cb9ec32b626893eb Mon Sep 17 00:00:00 2001 From: xiekeyang Date: Fri, 17 Apr 2015 20:19:20 +0800 Subject: [PATCH] Feature: Web Panic Reporting via hooks This PR is for issue of "email after registry webapp panic" #41, improving my previous design (closed). It use self setting up hooks, to catch panic in web application. And, send email in hooks handle directly, to no use new http server and handler. Signed-off-by: xiekeyang --- cmd/registry/config.yml | 18 ++++++++-- configuration/configuration.go | 45 ++++++++++++++++++++++++ configuration/configuration_test.go | 1 + docs/configuration.md | 36 ++++++++++++++++++++ registry/handlers/app.go | 27 +++++++++++++++ registry/handlers/hooks.go | 53 +++++++++++++++++++++++++++++ registry/handlers/mail.go | 45 ++++++++++++++++++++++++ 7 files changed, 223 insertions(+), 2 deletions(-) create mode 100644 registry/handlers/hooks.go create mode 100644 registry/handlers/mail.go diff --git a/cmd/registry/config.yml b/cmd/registry/config.yml index 2e41c64d8..6d41cc8f2 100644 --- a/cmd/registry/config.yml +++ b/cmd/registry/config.yml @@ -4,6 +4,20 @@ log: fields: service: registry environment: development + hooks: + - type: mail + disabled: true + levels: + - panic + options: + smtp: + addr: mail.example.com:25 + username: mailuser + password: password + insecure: true + from: sender@example.com + to: + - errors@example.com storage: cache: blobdescriptor: redis @@ -41,5 +55,5 @@ notifications: timeout: 1s threshold: 10 backoff: 1s - disabled: true - \ No newline at end of file + disabled: true + diff --git a/configuration/configuration.go b/configuration/configuration.go index 31496e6ef..3d86df550 100644 --- a/configuration/configuration.go +++ b/configuration/configuration.go @@ -29,6 +29,10 @@ type Configuration struct { // Fields allows users to specify static string fields to include in // the logger context. Fields map[string]interface{} `yaml:"fields,omitempty"` + + // Hooks allows users to configurate the log hooks, to enabling the + // sequent handling behavior, when defined levels of log message emit. + Hooks []LogHook `yaml:"hooks,omitempty"` } // Loglevel is the level at which registry operations are logged. This is @@ -126,6 +130,47 @@ type Configuration struct { } `yaml:"redis,omitempty"` } +// LogHook is composed of hook Level and Type. +// After hooks configuration, it can execute the next handling automatically, +// when defined levels of log message emitted. +// Example: hook can sending an email notification when error log happens in app. +type LogHook struct { + // Disable lets user select to enable hook or not. + Disabled bool `yaml:"disabled,omitempty"` + + // Type allows user to select which type of hook handler they want. + Type string `yaml:"type,omitempty"` + + // Levels set which levels of log message will let hook executed. + Levels []string `yaml:"levels,omitempty"` + + // MailOptions allows user to configurate email parameters. + MailOptions MailOptions `yaml:"options,omitempty"` +} + +// MailOptions provides the configuration sections to user, for specific handler. +type MailOptions struct { + SMTP struct { + // Addr defines smtp host address + Addr string `yaml:"addr,omitempty"` + + // Username defines user name to smtp host + Username string `yaml:"username,omitempty"` + + // Password defines password of login user + Password string `yaml:"password,omitempty"` + + // Insecure defines if smtp login skips the secure cerification. + Insecure bool `yaml:"insecure,omitempty"` + } `yaml:"smtp,omitempty"` + + // From defines mail sending address + From string `yaml:"from,omitempty"` + + // To defines mail receiving address + To []string `yaml:"to,omitempty"` +} + // v0_1Configuration is a Version 0.1 Configuration struct // This is currently aliased to Configuration, as it is the current version type v0_1Configuration Configuration diff --git a/configuration/configuration_test.go b/configuration/configuration_test.go index cc7427dca..24076e2cf 100644 --- a/configuration/configuration_test.go +++ b/configuration/configuration_test.go @@ -20,6 +20,7 @@ var configStruct = Configuration{ Level Loglevel `yaml:"level"` Formatter string `yaml:"formatter,omitempty"` Fields map[string]interface{} `yaml:"fields,omitempty"` + Hooks []LogHook `yaml:"hooks,omitempty"` }{ Fields: map[string]interface{}{"environment": "test"}, }, diff --git a/docs/configuration.md b/docs/configuration.md index 85f7948e6..1caf66fd6 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -25,6 +25,20 @@ log: fields: service: registry environment: staging + hooks: + - type: mail + disabled: true + levels: + - panic + options: + smtp: + addr: mail.example.com:25 + username: mailuser + password: password + insecure: true + from: sender@example.com + to: + - errors@example.com loglevel: debug # deprecated: use "log" storage: filesystem: @@ -229,6 +243,28 @@ log: +## hooks + + +```yaml +hooks: + - type: mail + levels: + - panic + options: + smtp: + addr: smtp.sendhost.com:25 + username: sendername + password: password + insecure: true + from: name@sendhost.com + to: + - name@receivehost.com +``` + +The `hooks` subsection configures the logging hooks' behavior. This subsection +includes a sequence handler which you can use for sending mail, for example. +Refer to `loglevel` to configure the level of messages printed. ## loglevel diff --git a/registry/handlers/app.go b/registry/handlers/app.go index 1d58e9454..77e3a956c 100644 --- a/registry/handlers/app.go +++ b/registry/handlers/app.go @@ -9,6 +9,7 @@ import ( "os" "time" + log "github.com/Sirupsen/logrus" "github.com/docker/distribution" "github.com/docker/distribution/configuration" ctxu "github.com/docker/distribution/context" @@ -101,6 +102,7 @@ func NewApp(ctx context.Context, configuration configuration.Configuration) *App app.configureEvents(&configuration) app.configureRedis(&configuration) + app.configureLogHook(&configuration) // configure storage caches if cc, ok := configuration.Storage["cache"]; ok { @@ -291,6 +293,31 @@ func (app *App) configureRedis(configuration *configuration.Configuration) { })) } +// configureLogHook prepares logging hook parameters. +func (app *App) configureLogHook(configuration *configuration.Configuration) { + logger := ctxu.GetLogger(app).(*log.Entry).Logger + for _, configHook := range configuration.Log.Hooks { + if !configHook.Disabled { + switch configHook.Type { + case "mail": + hook := &logHook{} + hook.LevelsParam = configHook.Levels + hook.Mail = &mailer{ + Addr: configHook.MailOptions.SMTP.Addr, + Username: configHook.MailOptions.SMTP.Username, + Password: configHook.MailOptions.SMTP.Password, + Insecure: configHook.MailOptions.SMTP.Insecure, + From: configHook.MailOptions.From, + To: configHook.MailOptions.To, + } + logger.Hooks.Add(hook) + default: + } + } + } + app.Context = ctxu.WithLogger(app.Context, logger) +} + func (app *App) ServeHTTP(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() // ensure that request body is always closed. diff --git a/registry/handlers/hooks.go b/registry/handlers/hooks.go new file mode 100644 index 000000000..7bbab4f8a --- /dev/null +++ b/registry/handlers/hooks.go @@ -0,0 +1,53 @@ +package handlers + +import ( + "bytes" + "errors" + "fmt" + "strings" + "text/template" + + "github.com/Sirupsen/logrus" +) + +// logHook is for hooking Panic in web application +type logHook struct { + LevelsParam []string + Mail *mailer +} + +// Fire forwards an error to LogHook +func (hook *logHook) Fire(entry *logrus.Entry) error { + addr := strings.Split(hook.Mail.Addr, ":") + if len(addr) != 2 { + return errors.New("Invalid Mail Address") + } + host := addr[0] + subject := fmt.Sprintf("[%s] %s: %s", entry.Level, host, entry.Message) + + html := ` + {{.Message}} + + {{range $key, $value := .Data}} + {{$key}}: {{$value}} + {{end}} + ` + b := bytes.NewBuffer(make([]byte, 0)) + t := template.Must(template.New("mail body").Parse(html)) + if err := t.Execute(b, entry); err != nil { + return err + } + body := fmt.Sprintf("%s", b) + + return hook.Mail.sendMail(subject, body) +} + +// Levels contains hook levels to be catched +func (hook *logHook) Levels() []logrus.Level { + levels := []logrus.Level{} + for _, v := range hook.LevelsParam { + lv, _ := logrus.ParseLevel(v) + levels = append(levels, lv) + } + return levels +} diff --git a/registry/handlers/mail.go b/registry/handlers/mail.go new file mode 100644 index 000000000..39244909d --- /dev/null +++ b/registry/handlers/mail.go @@ -0,0 +1,45 @@ +package handlers + +import ( + "errors" + "net/smtp" + "strings" +) + +// mailer provides fields of email configuration for sending. +type mailer struct { + Addr, Username, Password, From string + Insecure bool + To []string +} + +// sendMail allows users to send email, only if mail parameters is configured correctly. +func (mail *mailer) sendMail(subject, message string) error { + addr := strings.Split(mail.Addr, ":") + if len(addr) != 2 { + return errors.New("Invalid Mail Address") + } + host := addr[0] + msg := []byte("To:" + strings.Join(mail.To, ";") + + "\r\nFrom: " + mail.From + + "\r\nSubject: " + subject + + "\r\nContent-Type: text/plain\r\n\r\n" + + message) + auth := smtp.PlainAuth( + "", + mail.Username, + mail.Password, + host, + ) + err := smtp.SendMail( + mail.Addr, + auth, + mail.From, + mail.To, + []byte(msg), + ) + if err != nil { + return err + } + return nil +}