Golang日志管理
日志
标准库的log包
Go标准库中有log包,提供了简单的日志功能。
输出 | 格式输出 | 换行输出 | |
---|---|---|---|
log.Print() | log.Printf() | log.Pringln() | 类似fmt.print* |
log.Fatal() | log.Fatalf() | log.Fatalln() | 相当于log.Print* + os.Exit(1) |
log.Panic() | log.Panicf() | log.Panicln() | 相当于log.Print* + panic(0 |
日志输出需要使用日志记录器Logger。
log包提供了一个缺省的Logger即std。std是小写的,包外不可见,所以提供了Default()方法返回std给包外使用。如下:
package main
import (
"log"
)
func main() {
// 这里就是使用缺省的logger对象,可以点进Println查看源码
log.Println("这是缺省的logger")
}
源码如下:
// go源码
// Println calls Output to print to the standard logger.
// Arguments are handled in the manner of fmt.Println.
func Println(v ...any) {
if std.isDiscard.Load() {
return
}
std.Output(2, fmt.Sprintln(v...))
}
// 再点击std可以查看到,这里有一个默认的std,他的输出是os.Stderr标准错误输出到控制台,flag为LstdFlags
var std = New(os.Stderr, "", LstdFlags)
// Default returns the standard logger used by the package-level output functions.
func Default() *Logger { return std }
标准库的flag如下:
// 源码41行左右
// 2009/01/23 01:23:23.123123 /a/b/c/d.go:23: message
const (
Ldate = 1 << iota // 1 当前时区日期: 2009/01/23
Ltime // 2 当前时区时间: 01:23:23
Lmicroseconds // 4 微秒: 01:23:23.123123. assumes Ltime.
Llongfile // 8 绝对路径和行号: /a/b/c/d.go:23
Lshortfile // 16 文件名和行号: d.go:23. overrides Llongfile
LUTC // 32 使用UTC(GMT),而不是本地时区
Lmsgprefix // 64 默认前缀是放在行首的,这个标记为把前缀prefix放到消息message之前
LstdFlags = Ldate | Ltime // 3 initial values for the standard logger 位或,二进制的01 | 10 = 11 =》十进制3
)
上表列出的方法底层都使用std.Output输出日志内容。而std本质上是使用了标准错误输出、无前缀、LstdFlags 标准标记的记录器Logger实例
std使用
// 使用缺省Logger
log.Print("abcde\n")
log.Printf("%s\n","abcd")
log.Println("abcd")
log.Fatal("xyz") // 等价于 log.Print("xyz");os.Exit(1)
log.Panic("xyz") // 等价于 log.Print("xyz");panic()
自定义Logger
Go源码
// multiple goroutines; it guarantees to serialize access to the Writer.
type Logger struct {
mu sync.Mutex // ensures atomic writes; protects the following fields
prefix string // prefix on each line to identify the logger (but see Lmsgprefix)
flag int // properties
out io.Writer // destination for output
buf []byte // for accumulating text to write
isDiscard atomic.Bool // whether out == io.Discard
}
// New creates a new Logger. The out variable sets the
// destination to which log data will be written.
// The prefix appears at the beginning of each generated log line, or
// after the log header if the Lmsgprefix flag is provided.
// The flag argument defines the logging properties.
func New(out io.Writer, prefix string, flag int) *Logger {
l := &Logger{out: out, prefix: prefix, flag: flag}
if out == io.Discard {
l.isDiscard.Store(true)
}
return l
}
如果觉得缺省Logger std不满意,可以New构建一个自定义Logger并指定前缀、Flags。
package main
import (
"log"
"os"
)
func main() {
log.Println("这是缺省的logger")
infologger := log.New(os.Stderr, "Info:", 3|log.Lmsgprefix)
infologger.Print("这是自定义的普通消息")
errLogger := log.New(os.Stderr, "Error:", log.LstdFlags)
errLogger.Fatalln("这是一个自定义的错误消息")
}
log.New 参数详解:
- 1、
out io.Writer
:er结尾说明是一个接口,out应该有自己的类型,同时其类型又实现了Writer的所有方法,所以out也可以看做为io.Writer类型的实例,out主要用于写入设备 - 2、
prefix string
:前缀,用于在日志前面增加一串字符串,可以通过flag参数设置为log.Lmsgprefix
将它设置为在message前面 - 3、
flag int
:确定Logger的工作方式如下:
// 源码41行左右
// 2009/01/23 01:23:23.123123 /a/b/c/d.go:23: message
const ( // 可以做位与、为或
Ldate = 1 << iota // 1 当前时区日期: 2009/01/23
Ltime // 2 当前时区时间: 01:23:23
Lmicroseconds // 4 微秒: 01:23:23.123123. assumes Ltime.
Llongfile // 8 绝对路径和行号: /a/b/c/d.go:23
Lshortfile // 16 文件名和行号: d.go:23. overrides Llongfile
LUTC // 32 使用UTC(GMT),而不是本地时区
Lmsgprefix // 64 默认前缀是放在行首的,这个标记为把前缀prefix放到消息message之前
LstdFlags = Ldate | Ltime // 3 initial values for the standard logger 位或,二进制的01 | 10 = 11 =》十进制3
)
写日志文件
New方法签名func New(out io.Writer, prefix string, flag int) *Logger
中out参数提供Writer接口即可,那么就可以提供一个可写文件对象
package main
import (
"fmt"
"log"
"os"
)
func main() {
// os.Open为只读打开,OpenFile按照指定方式打开文件
file, err := os.OpenFile("C:\\Users\\Administrator\\Desktop\\test.txt",
os.O_APPEND|os.O_WRONLY, // 追加| 只写
777) // 权限为777
if err != nil {
fmt.Println(err)
}
defer file.Close() // 使用结束关闭文件句柄
logger := log.New(file, "Info:", 3)
logger.Print("这是一条测试的日志")
}
ZeroLog
log模块太简陋了,实际使用并不方便。
- logrus有日志级别、Hook机制、日志输出格式,很好用
- zap是Uber的开源高性能日志库
- zerolog更注重开发体验,高性能、有日志级别、链式API,json格式记录,号称0内存分配
安装
go get -u github.com/rs/zerolog/log
缺省Logger
package main
import (
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"os"
"time"
)
func main() {
zerolog.TimeFieldFormat = "2006/01/02 15:04:05 -0700"
log.Print("这是zerolog的默认消息") // 使用全局缺省logger
// 输出{"level":"debug","time":"2023/07/23 13:55:05 +0800","message":"这是zerolog的默认消息"}
}
log.Print 产生debug级别的消息,zerolog源码117行左右
// Print sends a log event using debug level and no extra field.
// Arguments are handled in the manner of fmt.Print.
func Print(v ...interface{}) {
Logger.Debug().CallerSkipFrame(1).Msg(fmt.Sprint(v...))
}
// 源码13行左右,定义了一个全局导出的缺省Logger,使用链式调用,缺省Logger使用标准错误输出
var Logger = zerolog.New(os.Stderr).With().Timestamp().Logger()
log.Print()、log.Printf()方法使用方式和标准库log模块类似
级别
zerolog提供以下级别,tracelevel级别最低-1
// zerolog源码112行
type Level int8
const (
// DebugLevel defines debug log level.
DebugLevel Level = iota 0
// InfoLevel defines info log level.
InfoLevel 1
// WarnLevel defines warn log level.
WarnLevel 2
// ErrorLevel defines error log level.
ErrorLevel 3
// FatalLevel defines fatal log level.
FatalLevel 4
// PanicLevel defines panic log level.
PanicLevel 5
// NoLevel defines an absent log level.
NoLevel 6
// Disabled disables the logger.
Disabled 7
// TraceLevel defines trace log level.
TraceLevel Level = -1
// Values less than TraceLevel are handled as numbers.
)
级别有
- gLevel全局级别
zerolog.SetGlobalLevel(级别数字或常量)
来设置全局级别zerolog.GlobalLevel()
获取当前全局级别
- 每个Logger的级别
- 消息的级别
package main
import (
"fmt"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
func main() {
fmt.Println("全局级别的glevel为:", zerolog.GlobalLevel())
fmt.Println("缺省logger的级别为:", log.Logger.GetLevel())
log1 := log.Level(zerolog.WarnLevel).With().Logger()
fmt.Println("log1的级别为", log1.GetLevel())
fmt.Println("~~~~~~~~~~~~~~~~")
log.Trace().Msg("缺省logger输出trace级别消息") // 可以输出
log.Info().Msg("缺省logger输出info级别消息") // 可以输出
log.Warn().Msg("缺省logger输出warn级别消息") // 可以输出
log.Error().Msg("缺省logger输出error级别消息") // 可以输出
//log.Fatal().Msg("缺省logger输出fatal级别消息")
//log.Panic().Msg("缺省logger输出panic级别消息")
log1.Debug().Msg("log1输出debug级别消息") // 输出不了,因为log1的消息级别为debug,小于log1的级别warn
log1.Error().Msg("log1输出error级别消息") // 可以输出,因为log1的消息级别为error,大于log1的级别warn
}
可以看到,使用缺省logger,全部可以输出日志消息,而log1的24行可以输出,23行不能输出,因为有消息级别和Logger级别
log1的级别为warn,而log1.Debug()输出的消息级别为debug级别0,消息级别小于log1级别,所以消息不能输出。log1.Error()产生error消息,消息级别 大于等于log1级别的warn,因此可以输出消息。
而缺省的Logger级别为trace,任何消息级别都大于等于缺省的logger的级别,因此都可以输出
下面可以调整全局级别看一看
package main
import (
"fmt"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
func main() {
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
fmt.Println("全局级别的glevel为:", zerolog.GlobalLevel())
fmt.Println("缺省logger的级别为:", log.Logger.GetLevel())
log1 := log.Level(zerolog.WarnLevel).With().Logger() // 创建一个子logger
fmt.Println("log1的级别为", log1.GetLevel())
fmt.Println("~~~~~~~~~~~~~~~~")
log.Trace().Msg("缺省logger输出trace级别消息") // 不能输出
log.Info().Msg("缺省logger输出info级别消息") // 不能输出
log.Warn().Msg("缺省logger输出warn级别消息") // 不能输出
log.Error().Msg("缺省logger输出error级别消息") // 可以输出
//log.Fatal().Msg("缺省logger输出fatal级别消息")
//log.Panic().Msg("缺省logger输出panic级别消息")
log1.Warn().Msg("log1输出warn级别消息") // 输出不了,因为log1的消息级别为debug,虽然等于log1的级别warn,但是小于全局的glevel的error级别,所以还是输出不了
log1.Error().Msg("log1输出error级别消息") // 可以输出,因为log1的消息级别为error,大于log1的级别warn,且等于全局的glevel的error级别,所以可以输出
}
// 输出
全局级别的glevel为: error
缺省logger的级别为: trace
log1的级别为 warn
~~~~~~~~~~~~~~~~
{"level":"error","time":"2023-07-23T14:22:39+08:00","message":"缺省logger输出error级别消息"}
{"level":"error","time":"2023-07-23T14:22:39+08:00","message":"log1输出error级别消息"}
缺省logger和log1都只有error级别的输出,说明将glevel调整到error级别后,所有logger输出消息必须大于等于glevel。
特别注意:zerolog.SetGlobalLevel()设置的是全局变量gLevel,它影响所有的Logger
所以我们得出以下公式:
日志消息是否能够输出,应当满足下面的要求:消息级别 >= max(全局glevel,当前logger的level)
zerolog.SetGlobalLevel(zerolog.Disabled)
// zerolog.Disabled 为7,没有消息级别可以大于等于7,相当于禁用所有的Logger,自然不能够输出。
上下文
zerolog是以Json对象格式输出的,还可以自定义一些键值对字段增加到上下文中以输出
package main
import (
"fmt"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
var all = []Person{}
for i := 0; i < 10; i++ {
all = append(all, Person{Name: fmt.Sprintf("name%d", i), Age: i + 1})
}
zerolog.SetGlobalLevel(zerolog.InfoLevel)
log.Info().Bool("success", true).Str("Reason", "File Not Found").Msg("文件没有找到")
log.Info().Str("name", "Tom").Floats32("Scores", []float32{62.3, 99.3, 100}).Send()
fmt.Println("~~~~~~~~~~~~~~~~~~~~~~~~~~")
log.Info().Any("data", all).Send()
log.Info().Interface("data", all).Msg("测试啊啊啊")
}
错误日志
package main
import (
"errors"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix // 自定义time字段时间的格式,TimeFormatUnix时间戳
zerolog.ErrorFieldName = "err" // 修改日志Json中的缺省字段名error
// 错误日志
err := errors.New("自定义错误")
log.Error().Err(err).// err 字段,错误消息内容
Send() // 有错误消息了,message可以省略
//log.Fatal().Err(err).Send()
log.Info().Err(nil).Send()
}
// 输出
{"level":"error","err":"自定义错误","time":1690095612}
{"level":"info","time":1690095612}
全局Logger
// 全局Logger定义如下
var Logger = zerolog.New(os.Stderr).With().Timestamp().Logger()
可以覆盖全局Logger
log.Logger = log.With().Str("time", "2020-01-02 15:04").Logger()
log.Info().Send()
自定义Logger
zerolog.TimeFieldFormat = "2006-01-02 15:04:05"
logger := log.With().
Str("School", "magedu").
Caller(). // 增加日志调用的位置信息字段
Logger() // 返回logger
logger.Info().Send()
log.Info().Send()
// 输出
{"level":"info","School":"magedu","time":"2023-07-23 15:05:23","caller":"C:/Users/Administrator/GolandProjects/awesomeProject/hello.go:29"}
{"level":"info","time":"2023-07-23 15:05:23"}
zerolog.TimeFieldFormat = "2006-01-02 15:04:05"
logger := zerolog.New(os.Stderr).With().Caller().Logger().Level(zerolog.InfoLevel)
fmt.Println(logger.GetLevel())
logger.Info().Send()
logger.Error().Send()
log.Info().Send()
写日志文件
package main
import (
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"os"
)
func main() {
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix // 自定义time字段时间的格式,TimeFormatUnix时间戳
zerolog.ErrorFieldName = "err" // 修改日志Json中的缺省字段名error
file, err := os.OpenFile("fuck.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, os.ModePerm)
if err != nil {
log.Fatal().Err(err).Msg("open file error")
}
defer file.Close()
levelwrite := zerolog.MultiLevelWriter(file, os.Stderr)
logger := zerolog.New(levelwrite).With().Timestamp().Logger()
logger.Info().Msg("日志写入到文件同时在控制台打印")
}
如果只输出到文件可以使用zerolog.New(f).With().Timestamp().Logger()