作者:@daemon365
本文为作者原创,转载请注明出处:https://www.cnblogs.com/daemon365/p/15160319.html
什么是日志
所谓日志(Log)是指系统所指定对象的某些操作和其操作结果按时间有序的集合。log文件就是日志文件,log文件记录了系统和系统的用户之间交互的信息,是自动捕获人与系统终端之间交互的类型、内容或时间的数据收集方法。
日志是用来记录,用户操作,系统状态,错误信息等等内容的文件,是一个软件系统的重要组成部分。一个良好的日志规范,对于系统运行状态的分析,以及线上问题的解决具有重大的意义。
日志规范
在开发软件打印日志时,需要注意一些问题,举例可能不全,可以自行百度相关文章或查看文章底部文献:
- 重要功能日志尽可能的完善。
- 不要随意打印无用的日志,过多无用的日志会增加分析日志的难度。
- 日志要区分等级 如 debug,warn,info,error 等。
- 捕获到未处理错误时最好打印错误堆栈信息
Go 语言常用的日志库
Go 语言标准库中就为我们提供了一个日志库 log,除了这个以外还有很多日志库,如 logrus,glog,logx,Uber 的 zap 等等,例如 zap 就有很多的优点:
- 高性能
- 配置项丰富
- 多种日志级别
- 支持Hook
- 丰富的工具包
- 提供了sugar log
- 多种日志打印格式
- ...
简单使用
| package main |
| |
| import ( |
| "errors" |
| "go.uber.org/zap" |
| ) |
| |
| var logger *zap.Logger |
| |
| func init() { |
| logger, _ = zap.NewProduction() |
| } |
| func main() { |
| logger.Error( |
| "My name is baobao", |
| zap.String("from", "Hulun Buir"), |
| zap.Error(errors.New("no good"))) |
| |
| logger.Info("Worked in the Ministry of national development of China!", |
| zap.String("key", "eat🍚"), |
| zap.String("key", "sleep😴")) |
| defer logger.Sync() |
| } |
Kratos 日志库原理解析
在私下与 Tony老师 沟通时关于日志库的实现理念时,Tony老师 说:由于目前日志库非常多并且好用,在 Kratos 的日志中,主要考虑以下几个问题:
- 统一日志接口设计
- 组织结构化日志
- 并且需要有友好的日志级别使用
- 支持多输出源对接需求,如log-agent 或者 3rd 日志库
kratos 的日志库,不强制具体实现方式,只提供适配器,用户可以自行实现日志功能,只需要实现kratos/log 的 Logger interface 即可接入自己喜欢的日志系统。
kratos 的日志库,在设计阶段,参考了很多优秀的开源项目和大厂的日志系统实现,经历了多次改动后才呈现给大家。
log库的组成
kratos 的 log 库主要由以下几个文件组成
- level.go 定义日志级别
- log.go 日志核心
- helper.go log的helper
- value.go 实现动态值
源码分析
kratos 的 log 库中, 核心部分就是 log.go 代码非常简洁,符合 kratos 的设计理念。 log.go 中声明了 Logger interface,用户只需要实现接口,即可引入自己的日志实现,主要代码如下:
log.go
| package log |
| |
| import ( |
| "context" |
| "log" |
| ) |
| |
| var ( |
| |
| DefaultLogger Logger = NewStdLogger(log.Writer()) |
| ) |
| |
| |
| type Logger interface { |
| Log(level Level, keyvals ...interface{}) error |
| } |
| |
| type logger struct { |
| logs []Logger |
| prefix []interface{} |
| hasValuer bool |
| ctx context.Context |
| } |
| |
| func (c *logger) Log(level Level, keyvals ...interface{}) error { |
| kvs := make([]interface{}, 0, len(c.prefix)+len(keyvals)) |
| kvs = append(kvs, c.prefix...) |
| |
| if c.hasValuer { |
| |
| bindValues(c.ctx, kvs) |
| } |
| kvs = append(kvs, keyvals...) |
| |
| for _, l := range c.logs { |
| if err := l.Log(level, kvs...); err != nil { |
| return err |
| } |
| } |
| return nil |
| } |
| |
| |
| func With(l Logger, kv ...interface{}) Logger { |
| |
| if c, ok := l.(*logger); ok { |
| |
| kvs := make([]interface{}, 0, len(c.prefix)+len(kv)) |
| |
| kvs = append(kvs, kv...) |
| kvs = append(kvs, c.prefix...) |
| |
| return &logger{ |
| logs: c.logs, |
| prefix: kvs, |
| hasValuer: containsValuer(kvs), |
| ctx: c.ctx, |
| } |
| } |
| return &logger{logs: []Logger{l}, prefix: kv, hasValuer: containsValuer(kv)} |
| } |
| |
| |
| func WithContext(ctx context.Context, l Logger) Logger { |
| if c, ok := l.(*logger); ok { |
| return &logger{ |
| logs: c.logs, |
| prefix: c.prefix, |
| hasValuer: c.hasValuer, |
| ctx: ctx, |
| } |
| } |
| return &logger{logs: []Logger{l}, ctx: ctx} |
| } |
| |
| |
| func MultiLogger(logs ...Logger) Logger { |
| return &logger{logs: logs} |
| } |
| |
value.go
| |
| func Value(ctx context.Context, v interface{}) interface{} { |
| if v, ok := v.(Valuer); ok { |
| return v(ctx) |
| } |
| return v |
| } |
| |
| |
| |
| |
| func bindValues(ctx context.Context, keyvals []interface{}) { |
| for i := 1; i < len(keyvals); i += 2 { |
| if v, ok := keyvals[i].(Valuer); ok { |
| keyvals[i] = v(ctx) |
| } |
| } |
| } |
| |
| |
| func containsValuer(keyvals []interface{}) bool { |
| for i := 1; i < len(keyvals); i += 2 { |
| if _, ok := keyvals[i].(Valuer); ok { |
| return true |
| } |
| } |
| return false |
| } |
helper.go
| package log |
| |
| import ( |
| "context" |
| "fmt" |
| ) |
| |
| |
| type Helper struct { |
| logger Logger |
| } |
| |
| |
| func NewHelper(logger Logger) *Helper { |
| return &Helper{ |
| logger: logger, |
| } |
| } |
| |
| |
| func (h *Helper) WithContext(ctx context.Context) *Helper { |
| return &Helper{ |
| logger: WithContext(ctx, h.logger), |
| } |
| } |
| |
| func (h *Helper) Log(level Level, keyvals ...interface{}) { |
| h.logger.Log(level, keyvals...) |
| } |
| |
| func (h *Helper) Debug(a ...interface{}) { |
| h.logger.Log(LevelDebug, "msg", fmt.Sprint(a...)) |
| } |
| |
| func (h *Helper) Debugf(format string, a ...interface{}) { |
| h.logger.Log(LevelDebug, "msg", fmt.Sprintf(format, a...)) |
| } |
| |
| |
| |
通过单元测试了解调用逻辑
| func TestInfo(t *testing.T) { |
| logger := DefaultLogger |
| logger = With(logger, "ts", DefaultTimestamp, "caller", DefaultCaller) |
| logger.Log(LevelInfo, "key1", "value1") |
| } |
- 单测中首先声明了一个 logger ,用的默认的 DefaultLogger
- 调用 log.go 中的 With() 函数, 传入了 logger ,和两个动态值, DefaultTimestamp 和 DefaultCaller。
- With方法被调用,判断是否能将参数 l 类型转换成 *logger
- 如果可以转换,将传入的KV,赋值给 logger.prefix 上,然后调用 value.go 中的 containsValuer() 判断传入的KV中是否存在 Valuer类型的值,将结果赋值给 context.hasValuer,最后返回 Logger 对象
- 否则则直接返回一个 &logger{logs: []Logger{l}, prefix: kv, hasValuer: containsValuer(kv)}
- 然后打印日志时,logger struct 的 Log 方法被调用
- Log() 方法首先预分配了 keyvals 的空间,然后判断 hasValuer,如果为 true,则调用 valuer.go 中的 bindValuer() 并传入了 ctx 然后获取 valuer 的值
if v, ok := v.(Valuer); ok { return v() }
8.最后遍历 logger.logs 打印日志
使用方法
使用 Logger 打印日志
| logger := log.DefaultLogger |
| logger.Log(LevelInfo, "key1", "value1") |
使用 Helper 打印日志
| log := log.NewHelper(DefaultLogger) |
| log.Debug("test debug") |
| log.Info("test info") |
| log.Warn("test warn") |
| log.Error("test error") |
使用 valuer
| logger := DefaultLogger |
| logger = With(logger, "ts", DefaultTimestamp, "caller", DefaultCaller) |
| logger.Log(LevelInfo, "msg", "helloworld") |
同时打印多个 logger
| out := log.NewStdLogger(os.Stdout) |
| err := log.NewStdLogger(os.Stderr) |
| l := log.With(MultiLogger(out, err)) |
| l.Log(LevelInfo, "msg", "test") |
使用 context
| logger := log.With(NewStdLogger(os.Stdout), |
| "trace", Trace(), |
| ) |
| log := log.NewHelper(logger) |
| ctx := context.WithValue(context.Background(), "trace_id", "2233") |
| log.WithContext(ctx).Info("got trace!") |
使用 filter 过滤日志
如果需要过滤日志中某些不应该被打印明文的字段如 password 等,可以通过 log.NewFilter() 来实现过滤功能。
通过 level 过滤日志
| l := log.NewHelper(log.NewFilter(log.DefaultLogger, log.FilterLevel(log.LevelWarn))) |
| l.Log(LevelDebug, "msg1", "te1st debug") |
| l.Debug("test debug") |
| l.Debugf("test %s", "debug") |
| l.Debugw("log", "test debug") |
| l.Warn("warn log") |
通过 key 过滤日志
| l := log.NewHelper(log.NewFilter(log.DefaultLogger, log.FilterKey("password"))) |
| l.Debugw("password", "123456") |
通过 value 过滤日志
| l := log.NewHelper(log.NewFilter(log.DefaultLogger, log.FilterValue("kratos"))) |
| l.Debugw("name", "kratos") |
通过 hook func 过滤日志
| l := log.NewHelper(log.NewFilter(log.DefaultLogger, log.FilterFunc(testFilterFunc))) |
| l.Debug("debug level") |
| l.Infow("password", "123456") |
| func testFilterFunc(level Level, keyvals ...interface{}) bool { |
| if level == LevelWarn { |
| return true |
| } |
| for i := 0; i < len(keyvals); i++ { |
| if keyvals[i] == "password" { |
| keyvals[i+1] = "***" |
| } |
| } |
| return false |
| } |
用 Zap 实现 kratos 的日志接口
实现的代码十分简单,仅有不到100 行代码,仅供大家参考。
实现
| |
| package logger |
| |
| import ( |
| "fmt" |
| "os" |
| |
| "github.com/go-kratos/kratos/v2/log" |
| "go.uber.org/zap" |
| "go.uber.org/zap/zapcore" |
| "gopkg.in/natefinch/lumberjack.v2" |
| ) |
| |
| var _ log.Logger = (*ZapLogger)(nil) |
| |
| |
| type ZapLogger struct { |
| log *zap.Logger |
| Sync func() error |
| } |
| |
| |
| func NewZapLogger(encoder zapcore.EncoderConfig, level zap.AtomicLevel, opts ...zap.Option) *ZapLogger { |
| writeSyncer := getLogWriter() |
| |
| core := zapcore.NewCore( |
| zapcore.NewConsoleEncoder(encoder), |
| zapcore.NewMultiWriteSyncer( |
| zapcore.AddSync(os.Stdout), |
| ), level) |
| |
| zapLogger := zap.New(core, opts...) |
| return &ZapLogger{log: zapLogger, Sync: zapLogger.Sync} |
| } |
| |
| |
| func (l *ZapLogger) Log(level log.Level, keyvals ...interface{}) error { |
| if len(keyvals) == 0 || len(keyvals)%2 != 0{ |
| l.log.Warn(fmt.Sprint("Keyvalues must appear in pairs: ", keyvals)) |
| return nil |
| } |
| |
| var data []zap.Field |
| for i := 0; i < len(keyvals); i += 2 { |
| data = append(data, zap.Any(fmt.Sprint(keyvals[i]), fmt.Sprint(keyvals[i+1]))) |
| } |
| switch level { |
| case log.LevelDebug: |
| l.log.Debug("", data...) |
| case log.LevelInfo: |
| l.log.Info("", data...) |
| case log.LevelWarn: |
| l.log.Warn("", data...) |
| case log.LevelError: |
| l.log.Error("", data...) |
| } |
| return nil |
| } |
| |
| |
| func getLogWriter() zapcore.WriteSyncer { |
| lumberJackLogger := &lumberjack.Logger{ |
| Filename: "./test.log", |
| MaxSize: 10, |
| MaxBackups: 5, |
| MaxAge: 30, |
| Compress: false, |
| } |
| return zapcore.AddSync(lumberJackLogger) |
| } |
| |
使用方法
| |
| package logger |
| |
| import ( |
| "testing" |
| |
| "github.com/go-kratos/kratos/v2/log" |
| "go.uber.org/zap" |
| "go.uber.org/zap/zapcore" |
| ) |
| |
| func TestZapLogger(t *testing.T) { |
| encoder := zapcore.EncoderConfig{ |
| TimeKey: "t", |
| LevelKey: "level", |
| NameKey: "logger", |
| CallerKey: "caller", |
| MessageKey: "msg", |
| StacktraceKey: "stack", |
| EncodeTime: zapcore.ISO8601TimeEncoder, |
| LineEnding: zapcore.DefaultLineEnding, |
| EncodeLevel: zapcore.LowercaseLevelEncoder, |
| EncodeDuration: zapcore.SecondsDurationEncoder, |
| EncodeCaller: zapcore.FullCallerEncoder, |
| } |
| logger := NewZapLogger( |
| encoder, |
| zap.NewAtomicLevelAt(zapcore.DebugLevel), |
| zap.AddStacktrace( |
| zap.NewAtomicLevelAt(zapcore.ErrorLevel)), |
| zap.AddCallerSkip(2), |
| zap.Development(), |
| ) |
| zlog := log.NewHelper(logger) |
| zlog.Infow("name","go 语言进阶") |
| defer logger.Sync() |
| } |
参考文献

【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?