十三.日志包设计
一.前言
在 Go 语言项目中自己设计日志包是非常重要的,原因如下:
提高代码可读性和可维护性:良好的日志设计可以让代码更加易读和易于维护。日志可以帮助开发人员理解代码的运行过程,方便调试和错误排查。
支持调试和错误排查:日志可以帮助开发人员跟踪代码的执行路径,从而更容易发现潜在的问题和错误。通过在不同的位置记录不同的日志信息,可以更精确地定位问题所在。
支持性能分析和优化:日志可以记录代码执行的时间和资源使用情况,从而帮助开发人员进行性能分析和优化。例如,可以记录代码中每个函数的执行时间和调用次数,以及内存使用情况等信息。
支持安全审计:日志可以记录系统中的操作行为和事件,从而帮助开发人员进行安全审计和漏洞分析。例如,可以记录用户登录和操作行为等信息,以便跟踪恶意行为或异常情况。
总之,自己设计日志包可以帮助开发人员更好地理解和管理代码,提高代码质量和效率。
二.自己设计log包的重要性
开发,debug,故障排查,数据分析,监控告警,保存现场
我们需要设计一个优秀的日志包,如果我们要扩展就比较麻烦,1.基于zap封装,2.自己实现3.改zap的源码
1.是否可以替换后期我们想要替换成另一个日志框架
2.我们要考虑扩展性,log打印的时候是否支持打印当前的goroutine的id是否支持打印当前的context
3.我们给大家提供的日志包,还能支持集成tracing(open-telemetry, metrics,logging),就可以集成jaeger
4.是否每个日志打印都能知道这个日志是哪个请求的
封装日志包很重要!最好是自己封装
gorm,go-redis、我们自己业务代码
三.日志包的基本需求
3.1. 全局logger和传递参数的logger的用法
1.全局的Logger
全局 logger 的设计思想是在整个应用程序中都可以方便地使用同一个 logger,避免了在不同的代码段中都要创建 logger 的麻烦。这个 logger 通常是在程序启动时初始化,并通过包级别的变量暴露出来,以便其他代码使用。
全局 logger 的优点是简单易用,可以方便地在整个应用程序中记录日志,但缺点是不能很好地控制日志输出的格式、级别和目标。
2.传递参数的logger
传递参数的 logger 的设计思想是通过将 logger 作为参数传递给需要记录日志的函数,让函数可以控制日志的格式、级别和目标。这个 logger 可以是标准库的 log 包中的 logger,也可以是自己定义的 logger。
传递参数的 logger 的优点是可以更灵活地控制日志输出,但缺点是需要在每个函数调用时都传递 logger,代码可能会变得更复杂。
3.2日志包的基本需求
logger最基本的功能
1.日志基本debug、 info、warn、error . fatal、panic
2.打印方式2020-12-02T01:16:18+08:00 INF0 example.go:11 std log json (zap)
3.日志是否支持轮转、单文件不能太大,压缩,切割
4.日志包是否支持hook,gorm
其他的需求:
是否支持颜色显示是否兼容表中的Log
error打印到error文件,info打印到info文件
error能否发送到其他的监控软件,统计一个metrics错误指标error是否能支持发送到jaeger
其他需求:
高性能
并发安全
插件化:错误告警,发邮件 sentry
参数控制
我会使用基于zap封装
3.3日志debug、info、error等级别的使用场景
log使用经验:
1.if分支的时候可以打印日志
2.写操作要尽量写日志 gorm,要记录数据
3.for循环打印日志的时候要慎重,for+上万次
4.错误产生的原始位置打印日志 A(这里打印行不行)->B->C(error,应该在此处打印日志) 这样做比较保险,所有error一律采用记录stack 同时采用fail fast
debug:
我们为了方便排查错误很多时候会在很多地方使用debug,debug往往很多,上了生产如果开启debvug会导致性能受影响,在上线的时候尽量关闭到debug
info:
关键的地方打印一些信息,这些信息数据可以交给大数据进行分析,info量来说相对比较适中。如果你发现了你的info使用量特别大,你就该考虑是不是可以换成debug
warn(警告):
warn往往不会导致一个请求失败,但是我们还是应该关注的一些数据,
比如:服务端页面要求请求1才是第一页,结果客户端传递的是a,这时,我正常返回 但是打印一次warn,如果有大量的warn,这时我们就能知道 应该是一种爬虫行为
error:
这就是程序失败,我们的函数没有做好错误兼容,由于业务运行过程中的bug,请求第三方资源,创建数据库记录,这种错误一定要关注
panic:
panic会导致整个系统直接挂掉,我们一开始项目启动的时候会链接数据库,可以使用panic去结束掉程序,panic是可以被recover住的
有一些情况 比如slice越界 2/0,业务中遇到这种panic你的程序挂了 这就要命了
Fatal:
最高级别错误,当你使用这个方法的时候你心里应该清楚,这个错误不应该被原谅,就应该导致程序挂掉
日志打印的实践经验
写日志的注意事项
日志中不能记录敏感数据,密码、token等
日志打印的时候音量写清楚错误的原因 log.Warnf(“[getDB] init database:%v”,err)
如果可以,每一条日志尽量和请求的id关联起来
info和error不要乱用,很常见 - 要注意
实践
好的日志不可能一开始就设计的很好,这是一个演进的过程,日志打印要重视
日志不是越多越好,越少越好,关键信息要打印
日志要兼容本地打印
能否支持动态调整日志级别(能不能拿到nacos中?)
四.生产环境中的日志系统架构
五.自定义log包
5.1自定义options
package log
import (
"fmt"
"github.com/spf13/pflag"
"go.uber.org/zap/zapcore"
"strings"
)
const (
FORAMT_CONSOLE = "console"
FORAMT_JSON = "json"
OUTPUT_STD = "stdout"
OUTPUT_STD_ERR = "stderr"
flagLevel = "log.level"
)
type Options struct {
OutputPaths []string `json:"output-paths" mapstructure:"output-paths"` //输出文件
ErrorOutputPaths []string `json:"error-output-paths" mapstructure:"error-output-paths"` //err输出文件
Level string `json:"level" mapstructure:"level"` //日志级别
Format string `json:"format" mapstructure:"format"` //日志打印格式
Name string `json:"name" mapstructure:"name"` //名称
}
type Option func(o *Options)
func NewOptions() *Options {
return nil
}
func New(opts ...Option) *Options {
options := &Options{
Level: zapcore.InfoLevel.String(),
Format: FORAMT_CONSOLE,
OutputPaths: []string{OUTPUT_STD},
ErrorOutputPaths: []string{OUTPUT_STD_ERR},
}
for _, opt := range opts {
opt(options)
}
return options
}
func WithLevel(level string) Option {
return func(o *Options) {
o.Level = level
}
}
// Validate 就可以自定义检查规则
func (o *Options) Validate() []error {
var errs []error
format := strings.ToLower(o.Format)
if format != FORAMT_CONSOLE && format != FORAMT_JSON {
errs = append(errs, fmt.Errorf("not supppor format %s", o.Format))
}
return errs
}
// AddFloags 可以自己将options具体的列映射到flog的字段上
func (o *Options) AddFloags(fs pflag.FlagSet) *Options {
fs.StringVar(&o.Level, flagLevel, o.Level, "log level")
return o
}
5.2自定义log接口
package log
import (
"context"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"sync"
)
type Field = zapcore.Field
type Logger interface {
Debug(msg string)
DebugC(context context.Context, msg string)
Debugf(format string, args ...interface{})
DebugfC(context context.Context, format string, args ...interface{})
DebugW(msg string, keysAndValues ...interface{})
DebugWC(context context.Context, msg string, keysAndValues ...interface{})
}
var _ Logger = &zapLogger{}
type zapLogger struct {
zapLogger *zap.Logger
}
func (z *zapLogger) Debug(msg string) {
z.zapLogger.Debug(msg)
}
func (z *zapLogger) DebugC(context context.Context, msg string) {
//TODO implement me
panic("implement me")
}
func (z *zapLogger) Debugf(format string, args ...interface{}) {
//TODO implement me
panic("implement me")
}
func (z *zapLogger) DebugfC(context context.Context, format string, args ...interface{}) {
//TODO implement me
panic("implement me")
}
func (z *zapLogger) DebugW(msg string, keysAndValues ...interface{}) {
//TODO implement me
panic("implement me")
}
func (z *zapLogger) DebugWC(context context.Context, msg string, keysAndValues ...interface{}) {
//TODO implement me
panic("implement me")
}
var (
defaultLogger = NewLog(NewOptions())
mu sync.Mutex
)
func Logs() *zapLogger {
return defaultLogger
}
func Debug(msg string) {
defaultLogger.Debug(msg)
}
func NewLog(opts *Options) *zapLogger {
if opts == nil {
opts = NewOptions()
}
//实例化zap
var zapLevel zapcore.Level
if err := zapLevel.UnmarshalText([]byte(opts.Level)); err != nil {
zapLevel = zapcore.InfoLevel
}
loggerConfig := zap.Config{
Level: zap.NewAtomicLevelAt(zapLevel),
}
l, err := loggerConfig.Build(zap.AddStacktrace(zapcore.PanicLevel))
if err != nil {
panic(err)
}
logger := &zapLogger{
zapLogger: l.Named(opts.Name),
}
return logger
}
func Init(opt *Options) {
//看起来没有问题,并发问题,因为我们后面可能希望我们这个全局logger是动态的
mu.Lock()
defer mu.Unlock()
defaultLogger = NewLog(opt)
}
//调用日志
package main
import "GoStart/log"
func main() {
//初始化日志
log.Init(log.NewOptions())
log.Debug("hello")
/*
我们自己封装了一个options,用于隔开zap.config
日志初始化,Init(options),
整个过程中调用法看不到zap的信息,
*/
}