阿小信大人的头像
Life is short (You need Python) Bruce Eckel

在zap中集成Sentry自动上报Error事件2020-03-15 17:59

在项目中发生了错误时我们都会打印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:              "http://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页面信息: 截屏2020-03-15 17 48 56

如果您觉得从我的分享中得到了帮助,并且希望我的博客持续发展下去,请点击支付宝捐赠,谢谢!

若非特别声明,文章均为阿小信的个人笔记,转载请注明出处。文章如有侵权内容,请联系我,我会及时删除。

#Golang#  
分享到:
阅读[352] 评论[0]

下一篇:已经是最后一篇

你可能也感兴趣的文章推荐

本文最近访客

网友66.*.*.73[火星]2020-05-31 12:40
网友66.*.*.77[Mountain View]2020-05-31 12:19
网友46.*.*.150[火星]2020-05-31 11:56
网友46.*.*.135[火星]2020-05-31 11:49

发表评论