一、Kratos 日志设计理念
kratos 日志库相关的官方文档:日志 | Kratos
Kratos的日志库主要有如下特性:
- Logger用于对接各种日志库或日志平台,可以用现成的或者自己实现
- Helper是在您的项目代码中实际需要调用的,用于在业务代码里打日志
- Filter用于对输出日志进行过滤或魔改(通常用于日志脱敏)
- Valuer用于绑定一些全局的固定值或动态值(比如时间戳、traceID或者实例id之类的东西)到输出日志中
Logger统一了日志的接入方式,Helper接口统一的日志库的调用方式。
二、Kratos 使用标准内置日志库的方法
1、打印日志到控制台的步骤
①导入kratos日志包
import "github.com/go-kratos/kratos/v2/log"
//若导入失败,则需先获取包
go get github.com/go-kratos/kratos/v2/log
②使用 kratos 内置标准输出创建日志对象
logger := log.NewStdLogger(os.Stdout)
③用 Log 方法打印日志,需要传入日志级别
logger.Log(log.LevelInfo, "msg", "logger.Log 打印的日志")
④为了简化写日志时需传入的参数,kratos 用 Helper 对 Logger 进行了包装,建议用新建 helper 对象写日志,方法如下:
//对 logger 进行包装,简化写日志时需传入的参数
h := log.NewHelper(logger)
//写入信息级别的日志
h.Info("使用 kratos 内置标准输出记录的日志")
//写入错误级别的日志
h.Errorf("用户名【%s】不存在", "张三")
⑤可通过 log.With() 方法绑定全局字段到 Vauler,用来打印全局信息到日志中
//通过 log.With 方法绑定全局字段到 Vauler,用来打印全局信息到日志中
logger = log.With(logger,
"ts", log.DefaultTimestamp,
"caller", log.DefaultCaller,
"trace_id", tracing.TraceID(),
"span_id", tracing.SpanID(),
)
h = log.NewHelper(logger)
h.Info("绑定了全局信息到日志中")
⑥用 log.NewFilter() 方法对日志输出进行过滤
// 日志过滤,显示特定级别的日志,或对敏感信息脱敏
h = log.NewHelper(
log.NewFilter(logger,
// 按等级过滤
//log.FilterLevel(log.LevelError),
// 按key遮蔽
log.FilterKey("username"),
// 按value遮蔽
log.FilterValue("hello"),
),
)
h.Warn("warn log")
h.Infow("password", "123456")
//日志中 kratos 会变为 ***
h.Infow("username", "kratos")
//日志中 hello 会变为 ***
h.Info("hello")
效果演示:
完整代码:
package main
import (
"os"
"github.com/go-kratos/kratos/v2"
"github.com/go-kratos/kratos/v2/log"
"github.com/go-kratos/kratos/v2/middleware/tracing"
"github.com/go-kratos/kratos/v2/transport/http"
)
func main() {
//使用 kratos 内置标准输出创建日志对象
logger := log.NewStdLogger(os.Stdout)
// 用 Log 方法打印日志,需要传入日志级别,不建议使用这种方式,而建议使用 helper 打印日志
logger.Log(log.LevelInfo, "msg", "logger.Log 打印的日志")
//对 logger 进行包装,简化写日志时需传入的参数
h := log.NewHelper(logger)
//写入信息级别的日志
h.Info("使用 kratos 内置标准输出记录的日志")
//写入错误级别的日志
h.Errorf("用户名【%s】不存在", "张三")
// 通过 log.With 方法绑定全局字段到 Vauler,用来打印全局信息到日志中
logger = log.With(logger,
"ts", log.DefaultTimestamp,
"caller", log.DefaultCaller,
"trace_id", tracing.TraceID(),
"span_id", tracing.SpanID(),
)
h = log.NewHelper(logger)
h.Info("绑定了全局信息到日志中")
// 日志过滤,显示特定级别的日志,或对敏感信息脱敏
h = log.NewHelper(
log.NewFilter(logger,
// 按等级过滤
//log.FilterLevel(log.LevelError),
// 按key遮蔽
log.FilterKey("username"),
// 按value遮蔽
log.FilterValue("hello"),
),
)
h.Warn("warn log")
h.Infow("password", "123456")
//日志中 kratos 会变为 ***
h.Infow("username", "kratos")
//日志中 hello 会变为 ***
h.Info("hello")
//创建 kratos http server 及 app
httpSrv := http.NewServer(
http.Address(":8080"),
)
app := kratos.New(
kratos.Name("测试log"),
kratos.Server(
httpSrv,
),
)
if err := app.Run(); err != nil {
log.Fatal(err)
}
}
2、将日志写入到本地文件的方法
① 通过 os.OpenFile 新建文件,获取到 os.File 对象
f, err := os.OpenFile("../test.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
return
}
② 通过 log.NewStdLogger(w io.Writer) 新建 logger 时,传入上述 os.File 对象 f
//输出到控制台,传入os.Stdout;输出到文件,则传文件指针
logger := log.NewStdLogger(f)
注意:os.File 实现了 io.Writer 接口的 Write(p []byte) (n int, err error) 方法,所以能将 f 作为输入参数传入 log.NewStdLogger() 函数
③用 log.NewHelper 包装 logger,并打印日志
h1 := log.NewHelper(logger1)
h1.Info("输出到日志文件中的日志信息")
效果演示:
代码实现:
package main
import (
"os"
"github.com/go-kratos/kratos/v2"
"github.com/go-kratos/kratos/v2/log"
"github.com/go-kratos/kratos/v2/transport/http"
)
func main() {
// 通过 log.NewStdLogger(w io.Writer) 中的 io.Writer 设置日志输出方式
// 将日志输出到 test.log 文件
f, err := os.OpenFile("../test.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
return
}
logger1 := log.NewStdLogger(f)
h1 := log.NewHelper(logger1)
h1.Info("输出到日志文件中的日志信息")
//创建 kratos http server 及 app
httpSrv := http.NewServer(
http.Address(":8080"),
)
app := kratos.New(
kratos.Name("测试log"),
kratos.Server(
httpSrv,
),
)
if err := app.Run(); err != nil {
log.Fatal(err)
}
}
三、 zap 日志库的使用方法
1、zap简介
Zap 是 uber 用 go 语言实现的超快、结构化、分级的日志记录库,性能极高,比其他结构化日志记录包快4-10倍。日志记录机构化,以JSON等格式输出。
有关详细介绍见:zap package - go.uber.org/zap - Go Packages
2、使用方法
①安装 zap 包
go get -u go.uber.org/zap
②导入 zap 包
import "go.uber.org/zap"
③用 NewProduction 等方法创建 zap 日志对象
// 创建 zap 日志对象
zaplogger, _ := zap.NewProduction()
defer logger.Sync()
④调用 Info、Error 等方法打印日志
//使用 zap 写日志
zaplogger.Info("failed to fetch URL",
zap.String("url", "http://dddd.com"),
zap.Int("attempt", 3),
zap.Duration("backoff", time.Second),
)
效果演示:
完整代码:
package main
import (
"time"
kzap "github.com/go-kratos/kratos/contrib/log/zap/v2"
"github.com/go-kratos/kratos/v2"
"github.com/go-kratos/kratos/v2/log"
"github.com/go-kratos/kratos/v2/transport/http"
"go.uber.org/zap"
)
func main() {
// 创建 zap 日志对象,并打印日志
zaplogger, _ := zap.NewProduction()
defer zaplogger.Sync()
//使用 zap 写日志
zaplogger.Info("failed to fetch URL",
zap.String("url", "http://dddd.com"),
zap.Int("attempt", 3),
zap.Duration("backoff", time.Second))
httpSrv := http.NewServer(
http.Address(":8080"),
)
app := kratos.New(
kratos.Name("测试log"),
kratos.Server(
httpSrv,
),
)
if err := app.Run(); err != nil {
log.Fatal(err)
}
}
⑤如果需要将日志输出到文件,则使用如下方法:
package main
import (
"os"
kzap "github.com/go-kratos/kratos/contrib/log/zap/v2"
"github.com/go-kratos/kratos/v2"
"github.com/go-kratos/kratos/v2/log"
"github.com/go-kratos/kratos/v2/transport/http"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
func main() {
// 配置Encoder
encoder := zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())
// 配置 WriteSyncer,将日志写入文件
f, err := os.OpenFile("zaptest.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
panic(err)
}
defer f.Close()
writeSyncer := zapcore.AddSync(f)
// 设置LogLevel
level := zap.NewAtomicLevelAt(zapcore.InfoLevel)
// 创建Core
core := zapcore.NewCore(encoder, writeSyncer, level)
// 构建Logger
zaplogger := zap.New(core)
logger := kzap.NewLogger(zaplogger)
// 将 zap 日志对象适配到 kratos 的日志库,以便直接使用 kratos 的日志方法
h := log.NewHelper(logger)
h.Info("msg", "kratos适配的zap, 写入日志到文件")
httpSrv := http.NewServer(
http.Address(":8080"),
)
app := kratos.New(
kratos.Name("测试log"),
kratos.Server(
httpSrv,
),
)
if err := app.Run(); err != nil {
log.Fatal(err)
}
}
效果演示:
四、Kratos 适配 zap 日志库的方法
kratos 在 contrib/log 实现好了一些插件,用于适配目前常用的日志库。
- std 标准输出,Kratos内置
- fluent 输出到fluentd
- zap 适配了uber的zap日志库
- aliyun 输出到阿里云日志
使用方法:
①安装 kratos 的 zap 适配包
go get github.com/go-kratos/kratos/contrib/log/zap/v2
②导入 zap 适配包
import (
kzap "github.com/go-kratos/kratos/contrib/log/zap/v2"
)
③创建 zap 日志对象
// 创建 zap 日志对象,并打印日志
zaplogger, _ := zap.NewProduction()
defer zaplogger.Sync()
④调用函数 NewLogger(zlog *zap.Logger) 将创建的 zap 日志对象适配到 kratos 的日志操作对象
// 将 zap 日志对象适配到 kratos 的日志库,以便直接使用 kratos 的日志方法
logger := kzap.NewLogger(zaplogger)
⑤调用函数 log.NewHelper 将 logger 进行包装,简化日志写入操作
h := log.NewHelper(logger)
h.Info("kratos适配的zap日志库")
效果演示:
代码实现:
package main
import (
kzap "github.com/go-kratos/kratos/contrib/log/zap/v2"
"github.com/go-kratos/kratos/v2"
"github.com/go-kratos/kratos/v2/log"
"github.com/go-kratos/kratos/v2/transport/http"
"go.uber.org/zap"
)
func main() {
// 创建 zap 日志对象,并打印日志
zaplogger, _ := zap.NewProduction()
defer zaplogger.Sync()
// 将 zap 日志对象适配到 kratos 的日志库,以便直接使用 kratos 的日志方法
logger := kzap.NewLogger(zaplogger)
h := log.NewHelper(logger)
h.Info("kratos适配的zap日志库")
httpSrv := http.NewServer(
http.Address(":8080"),
)
app := kratos.New(
kratos.Name("测试log"),
kratos.Server(
httpSrv,
),
)
if err := app.Run(); err != nil {
log.Fatal(err)
}
}
五、Kratos 适配第三方日志库的实现原理
kratos 底层日志接口 Logger,可用于快速适配各种日志库到框架中来,仅需要实现一个最简单的Log方法。
// Logger is a logger interface.
type Logger interface {
Log(level Level, keyvals ...interface{}) error
}
以适配 uber 的 zap 日志库为例,kratos 在 contrib/log/zap/zap.go 中定义了新的日志结构体 Logger,并实现了方法 Log(level log.Level, keyvals ...interface{}) error。其源码如下:
package zap
import (
"fmt"
"go.uber.org/zap"
"github.com/go-kratos/kratos/v2/log"
)
var _ log.Logger = (*Logger)(nil)
type Logger struct {
log *zap.Logger
msgKey string
}
type Option func(*Logger)
// WithMessageKey with message key.
func WithMessageKey(key string) Option {
return func(l *Logger) {
l.msgKey = key
}
}
func NewLogger(zlog *zap.Logger) *Logger {
return &Logger{
log: zlog,
msgKey: log.DefaultMessageKey,
}
}
func (l *Logger) Log(level log.Level, keyvals ...interface{}) error {
var (
msg = ""
keylen = len(keyvals)
)
if keylen == 0 || keylen%2 != 0 {
l.log.Warn(fmt.Sprint("Keyvalues must appear in pairs: ", keyvals))
return nil
}
data := make([]zap.Field, 0, (keylen/2)+1)
for i := 0; i < keylen; i += 2 {
if keyvals[i].(string) == l.msgKey {
msg, _ = keyvals[i+1].(string)
continue
}
data = append(data, zap.Any(fmt.Sprint(keyvals[i]), keyvals[i+1]))
}
switch level {
case log.LevelDebug:
l.log.Debug(msg, data...)
case log.LevelInfo:
l.log.Info(msg, data...)
case log.LevelWarn:
l.log.Warn(msg, data...)
case log.LevelError:
l.log.Error(msg, data...)
case log.LevelFatal:
l.log.Fatal(msg, data...)
}
return nil
}
func (l *Logger) Sync() error {
return l.log.Sync()
}
func (l *Logger) Close() error {
return l.Sync()
}