【日志包】go语言如何设计日志包 - 基于zap封装适合自己的日志包
文章目录
- 前言
- 一、自己设计log包的重要性
- 二、日志包的基本需求
- 1. 全局logger和传递参数的logger的用法
- 2. 日志包的基本需求
- logger最基本的功能
- 3. 日志debug、info、error等级别的使用场景
- 日志打印的实践经验
- 三、生产环境中的日志系统架构
- 四、自定义log包
- 1. 自定义log的options
- 2. 自定义log接口
- 调用
- 五、使用基于zap封装的自定义log包
前言
在 Go 语言项目中自己设计日志包是非常重要的,原因如下:
-
提高代码可读性和可维护性:良好的日志设计可以让代码更加易读和易于维护。日志可以帮助开发人员理解代码的运行过程,方便调试和错误排查。
-
支持调试和错误排查:日志可以帮助开发人员跟踪代码的执行路径,从而更容易发现潜在的问题和错误。通过在不同的位置记录不同的日志信息,可以更精确地定位问题所在。
-
支持性能分析和优化:日志可以记录代码执行的时间和资源使用情况,从而帮助开发人员进行性能分析和优化。例如,可以记录代码中每个函数的执行时间和调用次数,以及内存使用情况等信息。
-
支持安全审计:日志可以记录系统中的操作行为和事件,从而帮助开发人员进行安全审计和漏洞分析。例如,可以记录用户登录和操作行为等信息,以便跟踪恶意行为或异常情况。
总之,自己设计日志包可以帮助开发人员更好地理解和管理代码,提高代码质量和效率。
一、自己设计log包的重要性
开发,debug,故障排查,数据分析,监控告警,保存现场
我们需要设计一个优秀的日志包,如果我们要扩展就比较麻烦,1.基于zap封装,2.自己实现3.改zap的源码
- 是否可以替换后期我们想要替换成另一个日志框架
- 我们要考虑扩展性,log打印的时候是否支持打印当前的goroutine的id是否支持打印当前的context
- 我们给大家提供的日志包,还能支持集成tracing(open-telemetry, metrics,logging),就可以集成jaeger
- 是否每个日志打印都能知道这个日志是哪个请求的
封装日志包很重要!最好是自己封装
gorm,go-redis、我们自己业务代码
二、日志包的基本需求
Kratos的日志处理:https://go-kratos.dev/docs/component/log
1. 全局logger和传递参数的logger的用法
- 全局 logger
全局 logger 的设计思想是在整个应用程序中都可以方便地使用同一个 logger,避免了在不同的代码段中都要创建 logger 的麻烦。这个 logger 通常是在程序启动时初始化,并通过包级别的变量暴露出来,以便其他代码使用。
全局 logger 的优点是简单易用,可以方便地在整个应用程序中记录日志,但缺点是不能很好地控制日志输出的格式、级别和目标。
- 传递参数的 logger
传递参数的 logger 的设计思想是通过将 logger 作为参数传递给需要记录日志的函数,让函数可以控制日志的格式、级别和目标。这个 logger 可以是标准库的 log 包中的 logger,也可以是自己定义的 logger。
传递参数的 logger 的优点是可以更灵活地控制日志输出,但缺点是需要在每个函数调用时都传递 logger,代码可能会变得更复杂。
总的来说,全局 logger 更适合简单的应用程序,而传递参数的 logger 更适合复杂的应用程序,其中需要更灵活地控制日志输出的方式。
2. 日志包的基本需求
logger最基本的功能
- 日志基本debug、 info、warn、error . fatal、panic
- 打印方式2020-12-02T01:16:18+08:00 INF0 example.go:11 std log json (zap)
- 日志是否支持轮转、单文件不能太大,压缩,切割
- 日志包是否支持hook,gorm
其他的需求:
是否支持颜色显示是否兼容表中的Log
error打印到error文件,info打印到info文件
error能否发送到其他的监控软件,统计一个metrics错误指标error是否能支持发送到jaeger
其他需求:
高性能
并发安全
插件化:错误告警,发邮件 sentry
参数控制
我会使用基于zap封装
3. 日志debug、info、error等级别的使用场景
log使用经验:
- if分支的时候可以打印日志
- 写操作要尽量写日志 gorm,要记录数据
- for循环打印日志的时候要慎重,for+上万次
- 错误产生的原始位置打印日志 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包
自定义 log 包可以根据具体需求提供更加灵活、定制化的日志输出方式,提高了日志的可用性和可维护性。
1. 自定义log的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(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
}
}
// 就可以自定义检查规则
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
}
// 可以自己将options具体的列映射到flog的字段上
func (o *Options) AddFloags(fs pflag.FlagSet) *Options {
fs.StringVar(&o.Level, flagLevel, o.Level, "log level")
return o
}
2. 自定义log接口
它提供了日志记录的功能。包中的主要结构是 zapLogger,它实现了 Logger 接口。它包含了一些方法,如 Debug、DebugC、Debugf、DebugfC、DebugW 和 DebugWC,可以用来记录不同级别的日志消息。同时,它还提供了一个全局默认的 logger 和初始化函数 Init,以便用户可以方便地记录日志。在包的顶部还定义了一些别名类型,如 Field 和 Logger。它们与 zap 包中的类型相同,以方便使用 zap 包的其他功能。
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 Debug(msg string) {
defaultLogger.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 = New(NewOptions())
mu sync.Mutex
)
func Logs() *zapLogger {
return defaultLogger
}
func New(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 = New(opt)
}
调用
使用了一个自定义的日志库NewGo/log,在main函数中调用了log.Init()和log.Debug()函数。
log.Init()函数用于初始化日志库,接受一个log.Options类型的参数作为配置。在本例中,调用了log.NewOptions()函数创建一个默认的配置,然后传递给log.Init()函数。
log.Debug()函数用于输出调试信息。在本例中,输出了一条hello的调试信息。
package main
import "NewGo/log"
func main() {
log.Init(log.NewOptions())
log.Debug("hello")
/*
我们自己封装了一个options,用于隔开zap.config
日志初始化,Init(options),
整个过程中调用法看不到zap的信息,
*/
}
五、使用基于zap封装的自定义log包
这个逻辑和上述自定义的差不多
链接:https://pan.baidu.com/s/1GiCW8basUso5LLWv7eJ0sA?pwd=1234
提取码:1234