在项目中,我们通常会记录 Error 级别的日志,以便于追踪和排查问题。然而,依赖于日志采集的方式,在面对偶发性或非必现的错误时,我们依然很难及时发现和响应。尽管目前的告警机制主要基于指标的阈值设置,对于一些不易察觉的错误仍然存在盲点。
使用 Sentry 可以有效解决这些问题。通过在可能产生错误的代码块中,不仅记录 Error 日志,还能将严重的错误事件上报至 Sentry。Sentry 会详细记录相关问题并提供实时告警通知,从而大幅提高错误监控和处理的效率。
在 Golang 项目中,选择 zap 作为日志组件,综合考虑了易用性、性能以及流行程度。zap 能够高效地记录日志,并通过其丰富的功能满足各种需求。
错误日志的分散管理
项目中的 Error 日志往往散落在各个模块。对于新编写的代码,可以在打印错误的同时,添加 Sentry 的上报逻辑。而对于现有项目,则需要全局搜索 Error 日志的打印位置,逐一手动添加代码,这种方法显然不够高效。因此,如何实现日志的自动上报成为了一个关键问题。
通过阅读 zap 源码,可以发现 zap 支持大多数日志组件所具有的 Hooks 回调和自定义 Core 的机制,便于在日志打印后添加额外的操作。可以利用这一机制,设置在打印 Error 及以上级别的日志时,自动上报至 Sentry。
Zap 的 Hooks 和 Core
zap 的 Hooks 中存在一个限制:在回调函数中无法获取 Fields(额外信息),只涉及 Entry 的读取。Entry 只包含了日志的基本信息,如 Level、Time、LoggerName、Message、Caller 和 Stack 等。因此,使用 zap 的 Core 机制更为合适,允许我们在记录日志时自动处理这些额外信息。
相关阅读推荐:深入解析 Golang Zap Logger 的源码与日志打印流程
示例:Zap Hooks 的使用
以下是一个简单的 Hook 示例,用于记录消息长度:
package main
import (
"fmt"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
// DemoHook 生成示例 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.WithOptions(zap.Hooks(DemoHook()))
logger.Info("123456789") // 不打印长度信息
logger.Error("123456789") // 打印长度信息
}
执行结果:
{"level":"info","msg":"123456789"}
{"level":"error","msg":"123456789"}
message length:9
可以看到,定义 Hook 函数时无法接收 Fields 参数,因此在需要 Fields 的情况下,应该实现自定义 Core。
实现 Sentry Core
自定义 Core 需要实现 zapcore.Core 接口的五个方法。下面是一个 Sentry Core 的实现示例:
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
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 实现 Core 接口
type sentryCore struct {
client *sentry.Client
cfg *SentryCoreConfig
zapcore.LevelEnabler
flushTimeout time.Duration
fields map[string]interface{}
}
// With 实现 Core 接口的 With 方法
func (c *sentryCore) With(fs []zapcore.Field) zapcore.Core {
// 处理 Fields
return &sentryCore{client: c.client, cfg: c.cfg, fields: mergeFields(c.fields, fs)}
}
// Check 和 Write 方法实现
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
}
func (c *sentryCore) Write(ent zapcore.Entry, fs []zapcore.Field) error {
clone := c.With(fs)
event := createSentryEvent(ent, clone.fields, c.cfg.Tags)
return c.client.CaptureEvent(event, nil, c.cfg.Hub)
}
// NewSentryCore 生成 Core 对象
func NewSentryCore(cfg SentryCoreConfig, sentryClient *sentry.Client) zapcore.Core {
return &sentryCore{
client: sentryClient,
cfg: &cfg,
LevelEnabler: cfg.Level,
flushTimeout: cfg.FlushTimeout,
fields: make(map[string]interface{}),
}
}
// main 函数
func main() {
logger := zap.NewExample()
sentryClient, _ := sentry.NewClient(sentry.ClientOptions{Dsn: "your_sentry_dsn"})
cfg := SentryCoreConfig{Level: zap.ErrorLevel, Tags: map[string]string{"source": "demo"}}
sCore := NewSentryCore(cfg, sentryClient)
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 is an error")))
time.Sleep(2 * time.Second)
}
结论
通过将 Sentry 与 zap 日志结合使用,可以有效提升 Golang 项目的错误监控能力。自动上报机制不仅减少了开发者的工作量,还能实时捕捉重要错误,确保系统的稳定性。