【日志包】go语言如何设计日志包 - 基于zap封装适合自己的日志包


前言

在 Go 语言项目中自己设计日志包是非常重要的,原因如下:

  1. 提高代码可读性和可维护性:良好的日志设计可以让代码更加易读和易于维护。日志可以帮助开发人员理解代码的运行过程,方便调试和错误排查。

  2. 支持调试和错误排查:日志可以帮助开发人员跟踪代码的执行路径,从而更容易发现潜在的问题和错误。通过在不同的位置记录不同的日志信息,可以更精确地定位问题所在。

  3. 支持性能分析和优化:日志可以记录代码执行的时间和资源使用情况,从而帮助开发人员进行性能分析和优化。例如,可以记录代码中每个函数的执行时间和调用次数,以及内存使用情况等信息。

  4. 支持安全审计:日志可以记录系统中的操作行为和事件,从而帮助开发人员进行安全审计和漏洞分析。例如,可以记录用户登录和操作行为等信息,以便跟踪恶意行为或异常情况。

总之,自己设计日志包可以帮助开发人员更好地理解和管理代码,提高代码质量和效率。


一、自己设计log包的重要性

开发,debug,故障排查,数据分析,监控告警,保存现场
我们需要设计一个优秀的日志包,如果我们要扩展就比较麻烦,1.基于zap封装,2.自己实现3.改zap的源码

  1. 是否可以替换后期我们想要替换成另一个日志框架
  2. 我们要考虑扩展性,log打印的时候是否支持打印当前的goroutine的id是否支持打印当前的context
  3. 我们给大家提供的日志包,还能支持集成tracing(open-telemetry, metrics,logging),就可以集成jaeger
  4. 是否每个日志打印都能知道这个日志是哪个请求的

封装日志包很重要!最好是自己封装

gorm,go-redis、我们自己业务代码

二、日志包的基本需求

Kratos的日志处理:https://go-kratos.dev/docs/component/log

1. 全局logger和传递参数的logger的用法

  1. 全局 logger

全局 logger 的设计思想是在整个应用程序中都可以方便地使用同一个 logger,避免了在不同的代码段中都要创建 logger 的麻烦。这个 logger 通常是在程序启动时初始化,并通过包级别的变量暴露出来,以便其他代码使用。

全局 logger 的优点是简单易用,可以方便地在整个应用程序中记录日志,但缺点是不能很好地控制日志输出的格式、级别和目标。

  1. 传递参数的 logger

传递参数的 logger 的设计思想是通过将 logger 作为参数传递给需要记录日志的函数,让函数可以控制日志的格式、级别和目标。这个 logger 可以是标准库的 log 包中的 logger,也可以是自己定义的 logger。

传递参数的 logger 的优点是可以更灵活地控制日志输出,但缺点是需要在每个函数调用时都传递 logger,代码可能会变得更复杂。

总的来说,全局 logger 更适合简单的应用程序,而传递参数的 logger 更适合复杂的应用程序,其中需要更灵活地控制日志输出的方式。

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. 日志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:
最高级别错误,当你使用这个方法的时候你心里应该清楚,这个错误不应该被原谅,就应该导致程序挂掉

日志打印的实践经验

写日志的注意事项

  1. 日志中不能记录敏感数据,密码、token等
  2. 日志打印的时候音量写清楚错误的原因 log.Warnf(“[getDB] init database:%v”,err)
  3. 如果可以,每一条日志尽量和请求的id关联起来
  4. info和error不要乱用,很常见 - 要注意

实践

  1. 好的日志不可能一开始就设计的很好,这是一个演进的过程,日志打印要重视
  2. 日志不是越多越好,越少越好,关键信息要打印
  3. 日志要兼容本地打印
  4. 能否支持动态调整日志级别(能不能拿到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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Jzin

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值