什么是日志
所谓日志(Log)是指系统所指定对象的某些操作和其操作结果按时间有序的集合。log文件就是日志文件,log文件记录了系统和系统的用户之间交互的信息,是自动捕获人与系统终端之间交互的类型、内容或时间的数据收集方法;
日志是用来记录,用户操作,系统状态,错误信息等等内容的文件,是一个软件系统的重要组成部分。一个良好的日志规范,对于系统运行状态的分析,以及线上问题的解决具有重大的意义。
在开发软件打印日志时,需要注意一些问题,举例可能不全,可以自行百度相关文章或查看文章底部文献
重要功能日志尽可能的完善。
不要随意打印无用的日志,过多无用的日志会增加分析日志的难度。
日志要区分等级 如 debug,warn,info,error 等。
捕获到未处理错误时最好打印错误堆栈信息
Go 语言常用的日志库
Go 语言标准库中就为我们提供了一个日志库
log
,除了这个以外还有很多日志库,如
logrus
,
glog
,
logx
,
Uber
的
zap
等等,我们今天主要介绍一下
zap
,文章后面还会使用
zap
实现
kratos
的
log
配置项丰富
多种日志级别
支持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 打印日志
如何在日志中使用ctx
在日志实现时如果需要用的 ctx,可以使用 WithContext()方法,把ctx 传入,然后干自己想干的事。
func TraceID() Valuer {
return func(ctx context.Context) interface{} {
if span := trace.SpanContextFromContext(ctx); span.HasTraceID() {
return span.TraceID().String()
return ""
logger := DefaultLogger
ctx := context.Background()
var trace = TraceID()
logger = With(logger,"traceId",trace)
WithContext(ctx,logger).Log(LevelInfo,"print trace_id")
用 Zap 实现 kratos 的日志系统
实现的代码十分简单,仅有不到100 行代码,写的不是很好,仅供大家参考
package logger
import (
"fmt"
"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","baozi","from","hulunbeier")
defer logger.Sync()
关于 log 库的讨论 issue
Uber 的日志库 Zap uber/zap
日志割接库 lumberjack
基于 zap 的日志demo log example