在项目中发生了错误时我们都会打印 Error 级别的日志,但是即使有日志采集,在对发生 Error 时的告警通知和信息采集都不一定能快速且完善,目前对日志的告警也只是限于一些指标上的阈值告警,对于一些偶发或者非必现的 Error 其实我们还是很难及时发现。
使用 Sentry 可以解决这类痛点,在可能产生 Error 的地方我们除了打印 Error 日志,同时还会对严重的 Error 事件进行 Sentry 上报,Sentry 会记录详细的相关 issue 并告警通知。
在 Golang 的项目中我对易用性和性能及流行程度的综合考虑下选择使用 zap 作为日志组件来打印日志。
项目的 Error 日志必然是散落到各个角落,对于新写的代码,你可以在打 Error 的时候在加上一行 Sentry 的上报,对于已有的项目你可能需要全局搜索 Error 日志的打印再去人工添加代码,这样的操作显然是合适的。
经过对 zap源码阅读 可以知道 zap 支持大多数其他日志组件都支持的 Hooks 回调和自定义 Core 的方式添加日志打印后的额外操作。那么我们可以使用这个机制,在打印日志时检测日志级别判断符合我们需要的 Error 以上级别的错误都自动上报的到 Sentry。
在 zap 的 Hooks 中有一个限制,就是我们在 zap 的 logger 中通常会添加一些 Fields 来固定打印一些额外的信息,比如 requestid,用户当前请求的 id,服务进程 id 等。
在 Hooks 的回调实现中,阅读源码会发现回调函数中不支持获取这些 Fields,只涉及对 Entry 的读取,Entry 即一条日志的基本信息包括Level、Time、LoggerName、 Message、Caller、Stack 等信息, 而 Fields 中的信息也都是我们所需要的,所以这里我不使用 Hooks 的方法来实现自动上报,而是使用 zap 的 Core 机制。
如果不需要 Fields 信息也可以使用 Hooks 来实现。Hook 函数签名为:func(Entry) error
, 即接收一个 zap 的 Entry 作为参数,你可以在这个函数里面做你需要完成的任务后返回 error 即可,然后将 Hook 函数注册到 Logger 上即可。Hook 函数最终也是通过实现一个自定义 Core 的方式来把这些注册的 Hooks 都执行。
这里贴一下 Hooks 在 zap 中的用法:
package main
import (
"fmt"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
// DemoHook 用于生成一个示例Hook函数,Hook函数实现当前日志大于Error级别时打印Message的长度
// 这里使用函数的方法返回一个Hook函数的好处时外层函数可以根据需要传入一些参数供Hook函数使用
func DemoHook() func(zapcore.Entry) error {
return func(e zapcore.Entry) error {
if e.Level < zapcore.ErrorLevel {
return nil
}
fmt.Printf("message length:%d\n", len(e.Message))
return nil
}
}
func main() {
logger := zap.NewExample() // 生成一个logger
logger = logger.WithOptions(zap.Hooks(DemoHook())) // 将回调函数注册到logger生成一个新的logger
logger.Info("123456789") // 不打印长度信息
logger.Error("123456789") // 打印长度信息
}
执行结果:
{"level":"info","msg":"123456789"}
{"level":"error","msg":"123456789"}
message length:9
可以看到定义 Hook 函数的时候无法接收到 Fields 的相关参数,所以需要 Fields 参数时,需要自定义Core。
zap 在执行最终日志打印的时候会将其 logger 中的所有 core 中的 Write 方法都遍历执行,因此我们只需要在自定义的 Core 中实现我们自己的操作即可,在这里需要实现对 Sentry 客户端的调用上报信息到 Sentry,最后将 core 添加到 logger 中即可。
在 zap 中 core 是一个接口,所以自定义 core 需要实现其定义的 5 个方法,相对于 Hooks 的实现较为复杂。
以下是一个 Sentry Core 的实现和使用示例:
// Package main is a demo for zapcore for sentry capture error
package main
import (
"errors"
"fmt"
"time"
"github.com/getsentry/sentry-go"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
// 将zap的Level转换为sentry的Level
func sentryLevel(lvl zapcore.Level) sentry.Level {
switch lvl {
case zapcore.DebugLevel:
return sentry.LevelDebug
case zapcore.InfoLevel:
return sentry.LevelInfo
case zapcore.WarnLevel:
return sentry.LevelWarning
case zapcore.ErrorLevel:
return sentry.LevelError
case zapcore.DPanicLevel:
return sentry.LevelFatal
case zapcore.PanicLevel:
return sentry.LevelFatal
case zapcore.FatalLevel:
return sentry.LevelFatal
default:
return sentry.LevelFatal
}
}
// SentryCoreConfig 定义 Sentry Core 的配置参数.
type SentryCoreConfig struct {
Tags map[string]string
DisableStacktrace bool
Level zapcore.Level
FlushTimeout time.Duration
Hub *sentry.Hub
}
// sentryCore sentrycore的Core结构体,用于实现Core接口
type sentryCore struct {
client *sentry.Client // sentry客户端
cfg *SentryCoreConfig // core配置
zapcore.LevelEnabler // LevelEnabler接口
flushTimeout time.Duration // sentry上报的flush时间
fields map[string]interface{} // 保存Fields
}
// With接口方法的实际实现,对传入fields进行设置日志打印时的打印解析方式并添加到已有的fields中
func (c *sentryCore) with(fs []zapcore.Field) *sentryCore {
// Copy our map.
m := make(map[string]interface{}, len(c.fields))
for k, v := range c.fields {
m[k] = v
}
// Add fields to an in-memory encoder.
enc := zapcore.NewMapObjectEncoder()
for _, f := range fs {
f.AddTo(enc)
}
// Merge the two maps.
for k, v := range enc.Fields {
m[k] = v
}
return &sentryCore{
client: c.client,
cfg: c.cfg,
fields: m,
LevelEnabler: c.LevelEnabler,
}
}
// With 实现Core接口的With方法
func (c *sentryCore) With(fs []zapcore.Field) zapcore.Core {
return c.with(fs)
}
// Check 实现Core接口的Check方法,只有大于在core配置中的的Level才会被打印
func (c *sentryCore) Check(ent zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry {
if c.cfg.Level.Enabled(ent.Level) {
return ce.AddCore(ent, c)
}
return ce
}
// Write 实现Core接口的Write方法,对sentry进行上报,Fields作为Extra信息上报
func (c *sentryCore) Write(ent zapcore.Entry, fs []zapcore.Field) error {
clone := c.with(fs)
event := sentry.NewEvent()
event.Message = ent.Message
event.Timestamp = ent.Time.Unix()
event.Level = sentryLevel(ent.Level)
event.Platform = "demo"
event.Extra = clone.fields
event.Tags = c.cfg.Tags
if !c.cfg.DisableStacktrace {
trace := sentry.NewStacktrace()
if trace != nil {
event.Exception = []sentry.Exception{{
Type: ent.Message,
Value: ent.Caller.TrimmedPath(),
Stacktrace: trace,
}}
}
}
hub := c.cfg.Hub
if hub == nil {
hub = sentry.CurrentHub()
}
_ = c.client.CaptureEvent(event, nil, hub.Scope())
if ent.Level > zapcore.ErrorLevel {
c.client.Flush(c.flushTimeout)
}
return nil
}
// Sync 实现Core接口的Sync方法
func (c *sentryCore) Sync() error {
c.client.Flush(c.flushTimeout)
return nil
}
// NewSentryCore 生成Core对象
func NewSentryCore(cfg SentryCoreConfig, sentryClient *sentry.Client) zapcore.Core {
core := sentryCore{
client: sentryClient,
cfg: &cfg,
LevelEnabler: cfg.Level,
flushTimeout: 3 * time.Second,
fields: make(map[string]interface{}),
}
if cfg.FlushTimeout > 0 {
core.flushTimeout = cfg.FlushTimeout
}
return &core
}
// 生成SentryCore对象并添加到Logger中
func main() {
// 默认logger
logger := zap.NewExample()
// sentrycore配置
cfg := SentryCoreConfig{
Level: zap.ErrorLevel,
Tags: map[string]string{
"source": "demo",
},
}
// 生成sentry客户端
sentryClient, err := sentry.NewClient(sentry.ClientOptions{
Dsn: "https://abc:xyz@sentry.host.com/id",
Debug: true,
AttachStacktrace: true,
})
if err != nil {
fmt.Println(err)
}
// 生成sentryCore
sCore := NewSentryCore(cfg, sentryClient)
// 添加sentryCore到默认logger产生新的logger,使用该logger即可自动上报sentry
logger = logger.WithOptions(zap.WrapCore(func(core zapcore.Core) zapcore.Core {
return zapcore.NewTee(core, sCore)
}))
logger.Info("info log")
logger.Error("this log will be auto captured by sentry", zap.String("f1", "v1"), zap.Error(errors.New("this ia an error")))
time.Sleep(2 * time.Second) // sleep避免程序结束太快而导致上报失败
}
运行结果:
[Sentry] 2020/03/15 17:46:10 Integration installed: ContextifyFrames
[Sentry] 2020/03/15 17:46:10 Integration installed: Environment
[Sentry] 2020/03/15 17:46:10 Integration installed: Modules
[Sentry] 2020/03/15 17:46:10 Integration installed: IgnoreErrors
{"level":"info","msg":"info log"}
{"level":"error","msg":"this log will be auto captured by sentry","f1":"v1","error":"this ia an error"}
[Sentry] 2020/03/15 17:46:10 Sending error event [81e966c6573e4134b3d8b842ea0fb738] to sentry.host.com project: id
sentry页面信息: