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 <keyangxie@126.com>
This commit is contained in:
parent
1f015478a0
commit
47aa47e3f6
7 changed files with 223 additions and 2 deletions
|
@ -4,6 +4,20 @@ log:
|
||||||
fields:
|
fields:
|
||||||
service: registry
|
service: registry
|
||||||
environment: development
|
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:
|
storage:
|
||||||
cache:
|
cache:
|
||||||
blobdescriptor: redis
|
blobdescriptor: redis
|
||||||
|
@ -41,5 +55,5 @@ notifications:
|
||||||
timeout: 1s
|
timeout: 1s
|
||||||
threshold: 10
|
threshold: 10
|
||||||
backoff: 1s
|
backoff: 1s
|
||||||
disabled: true
|
disabled: true
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,10 @@ type Configuration struct {
|
||||||
// Fields allows users to specify static string fields to include in
|
// Fields allows users to specify static string fields to include in
|
||||||
// the logger context.
|
// the logger context.
|
||||||
Fields map[string]interface{} `yaml:"fields,omitempty"`
|
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
|
// Loglevel is the level at which registry operations are logged. This is
|
||||||
|
@ -126,6 +130,47 @@ type Configuration struct {
|
||||||
} `yaml:"redis,omitempty"`
|
} `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
|
// v0_1Configuration is a Version 0.1 Configuration struct
|
||||||
// This is currently aliased to Configuration, as it is the current version
|
// This is currently aliased to Configuration, as it is the current version
|
||||||
type v0_1Configuration Configuration
|
type v0_1Configuration Configuration
|
||||||
|
|
|
@ -20,6 +20,7 @@ var configStruct = Configuration{
|
||||||
Level Loglevel `yaml:"level"`
|
Level Loglevel `yaml:"level"`
|
||||||
Formatter string `yaml:"formatter,omitempty"`
|
Formatter string `yaml:"formatter,omitempty"`
|
||||||
Fields map[string]interface{} `yaml:"fields,omitempty"`
|
Fields map[string]interface{} `yaml:"fields,omitempty"`
|
||||||
|
Hooks []LogHook `yaml:"hooks,omitempty"`
|
||||||
}{
|
}{
|
||||||
Fields: map[string]interface{}{"environment": "test"},
|
Fields: map[string]interface{}{"environment": "test"},
|
||||||
},
|
},
|
||||||
|
|
|
@ -25,6 +25,20 @@ log:
|
||||||
fields:
|
fields:
|
||||||
service: registry
|
service: registry
|
||||||
environment: staging
|
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"
|
loglevel: debug # deprecated: use "log"
|
||||||
storage:
|
storage:
|
||||||
filesystem:
|
filesystem:
|
||||||
|
@ -229,6 +243,28 @@ log:
|
||||||
</td>
|
</td>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
## 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
|
## loglevel
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
log "github.com/Sirupsen/logrus"
|
||||||
"github.com/docker/distribution"
|
"github.com/docker/distribution"
|
||||||
"github.com/docker/distribution/configuration"
|
"github.com/docker/distribution/configuration"
|
||||||
ctxu "github.com/docker/distribution/context"
|
ctxu "github.com/docker/distribution/context"
|
||||||
|
@ -101,6 +102,7 @@ func NewApp(ctx context.Context, configuration configuration.Configuration) *App
|
||||||
|
|
||||||
app.configureEvents(&configuration)
|
app.configureEvents(&configuration)
|
||||||
app.configureRedis(&configuration)
|
app.configureRedis(&configuration)
|
||||||
|
app.configureLogHook(&configuration)
|
||||||
|
|
||||||
// configure storage caches
|
// configure storage caches
|
||||||
if cc, ok := configuration.Storage["cache"]; ok {
|
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) {
|
func (app *App) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
defer r.Body.Close() // ensure that request body is always closed.
|
defer r.Body.Close() // ensure that request body is always closed.
|
||||||
|
|
||||||
|
|
53
registry/handlers/hooks.go
Normal file
53
registry/handlers/hooks.go
Normal file
|
@ -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
|
||||||
|
}
|
45
registry/handlers/mail.go
Normal file
45
registry/handlers/mail.go
Normal file
|
@ -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
|
||||||
|
}
|
Loading…
Reference in a new issue