为什么要使用日志
- 记录用户操作,猜测用户行为
- 记录bug
日志级别介绍
Debug:表示调试信息,用于记录系统运行过程中的详细信息,适合在开发和调试阶段使用。
Info:表示普通信息,记录系统运行过程中的一般性信息,适用于生产环境和日常监控。
Warning:表示警告信息,记录系统运行过程中的警告信息,用于提醒可能的潜在问题。
Error:表示错误信息,记录系统运行过程中的错误信息,对于诊断问题非常有用。
Fatal:表示关键的致命错误,比Error级别更严重,通常表示系统无法继续正常运行。
Panic:表示紧急情况,通常表示系统处于紧急状态,需要立即处理。
go日志系统简单对比
- go原生log
- gin框架日志
- logrus
- zap
下面是具体对比
-
毋庸置疑,zap库无论是Golang在项目中 还是生产中都极其优秀的一个数据库,而且他是当今Go最快的日志库 性能最高的日志库。
-
虽然 logrus 已经不维护 且不更新,但是个人感觉logrus比zap 要好用很多,不是说性能,是使用的简易程度而言。
logrus相比较zap 没有完整的日志库级别 但是比起自带的logger还是要丰富很多
一共有七种日志库级别 Trace, Debug, Info, Warning, Error, Fatal, Panic。
性能:相别zap 略逊一筹
而他的优缺点则更为明显:
优点在一开始也提及了 就是使用非常简单。
缺点也更加显而易见 性能一般,日志库等级不丰富(其实七个日库等级 一般而言也够用。)
log库
简介
golang内置了log包,实现简单的日志服务。通过调用log包的函数,可以实现简单的日志打印功能。
- Go语言自带的标准库中的 log 包提供了基本的日志功能,包括不同级别的日志输出(如通过调用 log.Fatal, log.Panic, log.Printf 等)。
- log包本身提供直接的并发安全日志写入功能(写入数据时加了锁,可以自己看看代码)
- 不支持日志切割、多级日志处理等功能,如果要支持需要其他日志组件配合使用
- log下面的slog包支持日志格式化输出,包括json 等
日志级别与函数
log包中有3个系列的日志打印函数,分别print系列、panic系列、fatal系列。
函数系列 | 作用 |
---|---|
单纯打印日志 | |
panic | 打印日志,抛出panic异常 |
fatal | 打印日志,强制结束程序(os.Exit(1)),defer函数不会执行 |
简单示例
func main() {
defer fmt.Println("panic退出前处理")
log.Println("println日志")
log.Panic("panic日志")
log.Fatal("程序退出日志")
}
结果示例(实际结果不是这样的哦,因为panic,fatal会影响程序的执行):
2020/06/02 11:04:17 println日志
2020/06/02 11:04:17 panic日志
2020/06/02 11:04:17 panic退出前处理
2020/06/02 11:04:17 程序退出日志
较全示例
package main
import (
"bytes"
"fmt"
"log"
"os"
"log/slog"
)
func main() {
//输出到文件
logFile, err := os.OpenFile("./logs/app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
fmt.Println("open log file failed, err:", err)
return
}
log.SetOutput(logFile)
log.Println("standard logger")
//设置输出格式
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
log.Println("with micro")
//设置输出格式
log.SetFlags(log.LstdFlags | log.Lshortfile)
log.Println("with file/line")
//输出到标准输出
mylog := log.New(os.Stdout, "my:", log.LstdFlags)
mylog.Println("from mylog")
mylog.SetPrefix("ohmy:")
mylog.Println("from mylog")
//设置缓冲区
var buf bytes.Buffer
buflog := log.New(&buf, "buf:", log.LstdFlags)
buflog.Println("hello")
fmt.Print("from buflog:", buf.String())
//设置json格式
jsonHandler := slog.NewJSONHandler(os.Stderr, nil)
myslog := slog.New(jsonHandler)
myslog.Info("hi there")//slog的方法
myslog.Info("hello again", "key", "val", "age", 25)
}
log配置
标准log配置
默认情况下log只会打印出时间,但是实际情况下我们可能还需要获取文件名,行号等信息,log包提供给我们定制的接口。
log包提供两个标准log配置的相关方法:
func Flags() int // 返回标准log输出配置
func SetFlags(flag int) // 设置标准log输出配置
flag参数如下
const (
// 控制输出日志信息的细节,不能控制输出的顺序和格式。
// 输出的日志在每一项后会有一个冒号分隔:例如2009/01/23 01:23:23.123123 /a/b/c/d.go:23: message
Ldate = 1 << iota // 1 日期:2009/01/23
Ltime // 2 时间:01:23:23
Lmicroseconds // 4 微秒级别的时间:01:23:23.123123(用于增强Ltime位)
Llongfile // 8 文件全路径名+行号: /a/b/c/d.go:23
Lshortfile // 16 文件名+行号:d.go:23(会覆盖掉Llongfile)
LUTC // 32 使用UTC时间
Lmsgprefix // 64 move the "prefix" from the beginning of the line to before the message
LstdFlags = Ldate | Ltime // 3 标准logger的初始值
)
注意这种二进制的设计很巧妙 二进制按位|或后 可以实现 多参数的效果
标准日志配置示例
func init() {
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
}
func main() {
log.Println("println日志")
}
日志前缀配置
log包提供两个日志前缀配置的相关函数:
func Prefix() string // 返回日志的前缀配置
func SetPrefix(prefix string) // 设置日志前缀
日志前缀配置实例
func init() {
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
log.SetPrefix("success")
}
func main() {
log.Println("println日志")
}
日志输出位置配置
前面介绍的都是将日志输出到控制台上,golang
的log
包还支持将日志输出到文件中。
log包提供了func SetOutput(w io.Writer)
函数,将日志输出到文件中。
日志输出位置配置
func init() {
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
logFile, err := os.OpenFile("./c.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
log.Panic("打开日志文件异常")
}
log.SetOutput(logFile)
}
func main() {
log.Println("println日志")
}
结果:日志输出到当前目录下c.log文件中
自定义logger
log包为我们提供了内置函数,让我们能自定义logger。从效果上来看,就是将前面的的标准日志配置、日志前缀配置、日志输出位置配置整合到一个函数中,使日志配置不在那么繁琐。
log包中提供了func New(out io.Writer, prefix string, flag int) *Logger
函数来实现自定义logger。
var Mylogger *log.Logger
func init() {
logFile, err := os.OpenFile("./c.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
log.Panic("打开日志文件异常")
}
Mylogger = log.New(logFile, "success", log.Ldate | log.Ltime | log.Lshortfile)
}
func main() {
Mylogger .Println("自定义logger")
}
按照级别输出日志
此处的分级非之前的print、fatal、panic 这几个会影响程序功能 此处的分级是print的细化
我们自定义
在Go语言中,标准库log提供了记录日志的功能。为了处理不同的日志级别,你可以定义一个日志级别的类型,并为每个级别提供一个函数来记录该级别的日志。以下是一个简单的例子:
package main
import (
"log"
"os"
)
// 定义日志级别
type LogLevel int
const (
DEBUG LogLevel = iota
INFO
WARNING
ERROR
)
// 日志配置
type LoggerConfig struct {
Level LogLevel
}
// 日志记录器
type Logger struct {
config LoggerConfig
}
// 创建一个新的日志记录器
func NewLogger(config LoggerConfig) *Logger {
return &Logger{config: config}
}
// 根据日志级别记录日志
func (l *Logger) log(level LogLevel, msg string) {
if l.config.Level <= level {
switch level {
case DEBUG:
log.Println("DEBUG:", msg)
case INFO:
log.Println("INFO:", msg)
case WARNING:
log.Println("WARNING:", msg)
case ERROR:
log.Println("ERROR:", msg)
}
}
}
// 调用日志记录器的方法记录不同级别的日志
func main() {
logger := NewLogger(LoggerConfig{Level: DEBUG})
logger.log(DEBUG, "This is a debug message")
logger.log(INFO, "This is an info message")
logger.log(WARNING, "This is a warning message")
logger.log(ERROR, "This is an error message")
}
在这个例子中,我们定义了一个Logger结构体和一个LoggerConfig结构体来配置日志记录器。log函数根据日志级别来决定是否记录日志。在main函数中,我们创建了一个日志记录器并调用它的log方法来记录不同级别的日志。通过更改LoggerConfig中的Level字段,你可以控制记录哪些级别的日志。
使用log/slog包
slog 日志包是 Go 语言中的一个结构化日志库,旨在提供一个简单而强大的日志系统。因为标准日志库 log 过于简陋,社区中经常有人吐槽,Go 官方也承认了这一点,于是 Go 团队成员 Jonathan Amsterdam 操刀设计了新的日志库 slog,其放在 log/slog 目录中。
slog 设计之初大量参考了社区中现有日志包方案,相比于 log,主要解决了两个问题:
- log 不支持日志级别。
- log 日志不是结构化的。
这两个问题都能在 slog 中得到解决,本文就来带大家详解 slog 用法及设计。
https://zhuanlan.zhihu.com/p/705096082
Gin的日志模块
Gin框架默认使用的是标准库的log包,将日志输出到控制台。可以通过gin.Default()方法来创建一个带有默认中间件的路由引擎。
定义
// 使用Gin.Default自带一个日志中间件
engine:= gin.Default()
// Logger()的源码就是
func Default() *Engine {
debugPrintWARNINGDefault()
engine := New()
engine.Use(Logger(), Recovery())
return engine
}
上面是default 默认的,我们也可以自定义
自定义日志输出 如果想要自定义日志的输出方式,可以通过gin.New()方法来创建一个不带默认中间件的路由引擎,并使用gin.Logger()方法设置自定义的日志中间件。
engine := gin.New()
engine.Use(gin.Logger())
这个logger实际上就是标准库的logger
所以基本都一样 只是多了一些gin的属性封装
日志基础使用示例(和log一样)
使用日志 在实际项目中,可以在处理请求的函数中使用日志记录相关信息。
func handler(c *gin.Context) {
log.Println("Handling request...")//直接写log就好 gin已经封装好这个变量了
c.JSON(http.StatusOK, gin.H{"message": "Hello, world!"})
}
以上代码在处理请求的函数中使用了log包的Println函数记录了一条信息。
日志格式化 Gin框架中提供了方便的方法来格式化日志的输出。可以使用log包的Printf函数来格式化日志信息。
func handler(c *gin.Context) {
log.Printf("Handling request: %s", c.Request.URL.Path)
c.JSON(http.StatusOK, gin.H{"message": "Hello, world!"})
}
以上代码使用Printf函数格式化了日志输出,打印了请求的URL路径。
将日志输出到文件(和原生log有点不一样)
gin设置属性
日志文件 除了输出到控制台,还可以将日志输出到文件中。
//gin.DefaultWriter = io.MultiWriter(f) //将日志写入文件,但是控制台不显示
//gin.DefaultWriter = io.MultiWriter(f, os.Stdout)//同时将日志写入文件和控制台
func main() {
// 输出到文件
f, _ := os.Create("gin.log")
//gin.DefaultWriter = io.MultiWriter(f)
// 如果需要同时将日志写入文件和控制台,请使用以下代码。
gin.DefaultWriter = io.MultiWriter(f, os.Stdout)
router := gin.Default()
router.GET("/", func(c *gin.Context) {
c.JSON(200, gin.H{"msg": "/"})
})
router.Run()
}
以上代码将日志输出到名为"logfile.log"的文件中。
或者我们自定义一个log的中间件
还有一种方式,可以做一个中间件,用来将日志写入文件,并且控制台也显示:
这就和log几乎完全一样了
使用log包的SetOutput函数将日志输出到指定的文件。
func main() {
router := gin.Default()
router.GET("/", handler)
router.Run(":8000")
}
func handler(c *gin.Context) {
file, _ := os.OpenFile("logfile.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
log.SetOutput(file)
log.Printf("Handling request: %s", c.Request.URL.Path)
c.JSON(http.StatusOK, gin.H{"message": "Hello, world!"})
}
定义路由显示格式(和原生的不一样)
启动gin,它会显示所有的路由,默认格式如下
[GIN-debug] GET / --> main.main.func1 (3 handlers)
[GIN-debug] GET /hello --> main.main.func2 (3 handlers)
我们也可以进行修改,自定义这个输出格式:
gin.DebugPrintRouteFunc
是Gin框架提供的一个全局变量,用于自定义路由信息的调试输出格式和行为。该变量是一个函数类型,声明如下:
type DebugPrintRouteFunc func(httpMethod, absolutePath, handlerName string, nuHandlers int)
该函数类型接收以下参数:
httpMethod:HTTP方法,表示请求使用了哪种HTTP方法(GET、POST、PUT、DELETE等)。
absolutePath:请求路径,包括了路由组前缀和被路由匹配的路径。
handlerName:处理函数的名称,用于标识该路由绑定的处理函数。
nuHandlers:处理函数的数量,即路由绑定的处理函数个数。
用户可以通过定义一个自定义的DebugPrintRouteFunc函数,并将其赋值给
gin.DebugPrintRouteFunc变量来定制网站路由信息的输出。
func main() {
gin.DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string, nuHandlers int) {
fmt.Printf("[小智] %v - url:%v --> handlerName:%v (%v handlers)\n", httpMethod, absolutePath, handlerName, nuHandlers)
}
router := gin.Default()
router.GET("/", func(c *gin.Context) {
c.JSON(200, gin.H{"msg": "/"})
})
router.GET("/hello", func(c *gin.Context) {
c.JSON(200, gin.H{"msg": "/"})
})
router.Run()
}
输出结果:
[小智] GET - url:/ --> handlerName:main.main.func2 (3 handlers)
[小智] GET - url:/hello --> handlerName:main.main.func3 (3 handlers)
修改系统日志级别
这几种模式是gin框架本身的输出 而非我们自己去打印的日志
如果要实现用户分级日志 可能得类似之前用slog或者其他的日志系统
比如下面这个就是gin源码 (debug.go文件) 可以看出 这个并未暴露出去给我们使用
func debugPrintError(err error) {
if err != nil && IsDebugging() {
fmt.Fprintf(DefaultErrorWriter, "[GIN-debug] [ERROR] %v\n", err)
}
}
所以 它针对的是比如控制台这些
Gin框架提供了三种模式
- gin.DebugMode:启用DEBUG级别日志,显示所有日志信息。
- gin.ReleaseMode:启用INFO级别日志,仅显示INFO、WARN和ERROR级别的日志信息。
- gin.TestMode:禁用日志,不显示任何日志信息。
可以通过gin.DebugMode、gin.ReleaseMode和gin.TestMode方法设置不同的日志级别。
// 设置为DEBUG级别日志
gin.SetMode(gin.DebugMode)
// 设置为INFO级别日志
gin.SetMode(gin.ReleaseMode)
// 禁用日志
gin.SetMode(gin.TestMode)
我这里选择了一个设置,再次运行,下面的内容就少了很多,特别是设置INFO之后,完美~~
修改日志格式
如果觉得gin输出的不好看,我们可以自定义
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
func LoggerWithFormatter(params gin.LogFormatterParams) string {
return fmt.Sprintf(
"[ 小智 ] %s | %d | \t %s | %s | %s \t %s\n",
params.TimeStamp.Format("2006/01/02 - 15:04:05"),
params.StatusCode, // 状态码
params.ClientIP, // 客户端ip
params.Latency, // 请求耗时
params.Method, // 请求方法
params.Path, // 路径
)
}
func main() {
router := gin.New()
router.Use(gin.LoggerWithFormatter(LoggerWithFormatter))
router.Run()
}
也可以这样
func LoggerWithFormatter(params gin.LogFormatterParams) string {
return fmt.Sprintf(
"[ 小智 ] %s | %d | \t %s | %s | %s \t %s\n",
params.TimeStamp.Format("2006/01/02 - 15:04:05"),
params.StatusCode,
params.ClientIP,
params.Latency,
params.Method,
params.Path,
)
}
func main() {
router := gin.New()
router.Use(
gin.LoggerWithConfig(
gin.LoggerConfig{Formatter: LoggerWithFormatter},
),
)
router.Run()
}
我们可以输出有颜色的log
func LoggerWithFormatter(params gin.LogFormatterParams) string {
var statusColor, methodColor, resetColor string
statusColor = params.StatusCodeColor()
methodColor = params.MethodColor()
resetColor = params.ResetColor()
return fmt.Sprintf(
"[ 小智 ] %s | %s %d %s | \t %s | %s | %s %-7s %s \t %s\n",
params.TimeStamp.Format("2006/01/02 - 15:04:05"),
statusColor, params.StatusCode, resetColor,
params.ClientIP,
params.Latency,
methodColor, params.Method, resetColor,
params.Path,
)
}
Logrus
Logrus 是一个非常流行的第三方日志库,它具有丰富的功能和高度可配置性。
Logrus 支持结构化日志、多种输出格式(JSON、文本等)、多种日志级别(Trace, Debug, Info, Warn, Error, Fatal)以及自定义钩子(Hook)等功能。
Logrus 默认情况下其 API 是线程安全的,通过内部互斥锁来保证并发写入的安全性。不过可以通过SetNoLock()方法关闭这个安全锁。
对于需要同时输出到多个目的地的情况(比如mongodb、通知群等等),可以使用logrus Hooks
,若要自定义更多功能,比如添加请求ID等上下文信息,可以使用logrus.WithFields
Logrus 日志写入是通过标准库io.Writer
接口来间接实现的。当你将一个实现了并发安全的io.Writer对象设置给 Logrus 时,就可以将日志输出定位到这个writer
(这个配合其他日志组件的关键)
Logrus 可以与 lumberjack
或file-rotatelogs
库配合使用,实现日志切割。
Logrus 中,Hook是一个可以被添加到logger实例上的接口,它允许开发者在日志记录发生时执行自定义的行为,比如将日志发送到远程服务、写入特殊文件或其他任何需要与日志事件关联的操作
下面是一个简单示例:
package main
import (
"fmt"
"os"
"github.com/sirupsen/logrus"
)
func init() {
//设置std日志级别为 Info,低于这个级别的日志将不会被输出
logrus.SetLevel(logrus.InfoLevel)
//添加hook处理逻辑
logrus.AddHook(&CustomHook{})
//设置输出格式为json
logrus.SetFormatter(&logrus.JSONFormatter{})
//设置输出格式为text
//logrus.SetFormatter(&logrus.TextFormatter{})
//这个方法用于创建新的logger实例,可以用于多logger实例的使用,默认全局是std内置的logger实例,
//logrus.New()
//设置是否显示调用者信息,当设置为 true 时,
//logrus 在输出日志时会自动包含调用日志方法的文件名和行号信息
logrus.SetReportCaller(true)
//在logrus库中,SetBufferPool()方法用于设置一个缓冲池
//,这对于提高日志写入效率和减少内存分配开销是非常有帮助的。
//缓冲池可以复用已分配的内存空间,避免频繁地创建和销毁小对象
//注意:这里要你自己实现 type BufferPool interface 接口中的方法才行
//logrus.SetBufferPool(userpool)
/**
默认情况下其 API 是线程安全的,通过内部互斥锁来保证并发写入的安全性,
但如果你想关闭这种线程安全模式以提高性能,
可以重新New一个实列,关闭对当前 logger 的互斥锁保护,
这意味着,在没有锁的情况下并发调用日志方法可能会导致数据混乱或丢失
*/
logrus.New().SetNoLock()
//设置输出到控制台,默认是os.Stderr,这里可以扩展日志输出到日志组件
//这里如果是设置的writer组件时线程安全的,上面那个就可以关闭,自己可以去看源码。。。
logrus.SetOutput(os.Stdout)
}
// 自定义hook 实现type Hook interface 接口的两个方法
type CustomHook struct{}
func (h *CustomHook) Levels() []logrus.Level {
// 返回该hook要处理的日志级别列表,用于控制该hook处理哪些级别的日志
return []logrus.Level{
logrus.InfoLevel,
logrus.WarnLevel,
logrus.ErrorLevel,
}
}
func (h *CustomHook) Fire(entry *logrus.Entry) error {
// 当日志级别匹配并且有新的日志条目产生时,这个方法会被调用
// 在这里你可以实现自己的逻辑,例如发送邮件、保存到数据库等
message := entry.Message
level := entry.Level.String()
// 假设我们只是简单地将日志信息输出到控制台
fmt.Printf("Hook received log: Level=%s, Message=%s\n", level, message)
return nil
}
func main() {
logrus.Info("Application started")
// 结构化日志示例,用于输出特定字段信息
logrus.WithFields(logrus.Fields{
"animal": "walrus",
"size": 10,
}).Info("A group of walrus emerges from the ocean")
// 如果需要写入文件,可以创建自定义的 io.Writer 对象,并设置给 logrus
// 注意:这里仅作示例,实际操作时需确保该 writer 实现了并发安全
logFile, err := os.OpenFile("logs/app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err == nil {
defer logFile.Close()
logrus.SetOutput(logFile) // 将日志输出重定向到文件
} else {
logrus.Warn("Failed to log to file, using default stderr")
}
}
more
https://blog.csdn.net/Zuko_chen/article/details/130212672
Zap
Zap 是 Uber 开发的高性能日志库,以其快速、灵活和强大的特性受到广泛认可。
Zap 提供了优化过的编码器和性能极高的日志记录方式,支持结构化日志(支持添加结构化字段到日志消息中),并且有多个日志级别(Debug、Info、Warn、Error 和 Panic),适合对日志处理速度要求较高的场景,是专门为高并发设计的日志库。
根据具体需求调整日志格式、编码器以及日志级别等参数。同时,如需进行日志切割,通常会配合外部工具或库(例如 lumberjack)来处理文件滚动。对于更复杂的需求,还可以自定义 Encoder 和 Core,以适应特定的结构化日志格式。
每个 goroutine 都可以直接调用http://logger.Info()来记录日志,而无需担心并发问题。Zap 内部会处理好这些并发请求,并将其高效地写入到日志目的地(如文件、网络等)
zapcore 是 zap 库的核心组件,它提供了更低级别的日志处理功能,包括日志编码器(Encoder)、核心处理器(Core)和日志级别(Level)等基础构建块
编码器(Encoder)负责将日志数据结构化为JSON、console或其他格式
核心处理器(Core)是实际执行日志写入的地方,它可以配置输出到控制台、文件或其他同步器(Syncer)实现的任何地方。
zapcore 允许开发者精细地控制日志的输出格式、压缩、存储位置、日志级别过滤等功能
示例1:
package main
import (
"errors"
"fmt"
"time"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
func setupLoggerzap2() (*zap.Logger, error) {
// 设置开发模式(生产环境应使用 zap.NewProductionConfig)
//测试环境:zap.NewDevelopmentConfig()
cfg := zap.NewProductionConfig()
//添加fields
cfg.InitialFields = map[string]interface{}{"app": "myapp", "ID": "12345"}
//设置日志级别
cfg.Level = zap.NewAtomicLevelAt(zap.DebugLevel)
//设置是否显示调用者信息
cfg.DisableCaller = true
//设置是否显示堆栈信息
cfg.DisableStacktrace = false
//设置是否开启采样 默认都是100
cfg.Sampling = &zap.SamplingConfig{
Initial: 100, // 初始采样率
Thereafter: 10, // 后续采样率
}
// 定义 Sampling Hook
cfg.Sampling.Hook = func(entry zapcore.Entry, samplingDecision zapcore.SamplingDecision) {
// entry 参数包含了当前的日志条目信息,如级别、时间戳、消息等。
// samplingDecision 参数表示 Zap 根据采样配置计算出的采样结果,可以是zapcore.Sample或zapcore.Discard。
fmt.Println("hook", entry)
switch samplingDecision {
case zapcore.LogSampled:
// 当 Zap 决定记录这条日志时,你可以在这里执行一些附加操作,比如发送通知、记录到其他系统等。
case zapcore.LogDropped:
// 当 Zap 决定丢弃这条日志时,你也可以在此处执行相应的操作,比如记录采样丢弃统计、替代日志方案等。
}
// 注意:在这个 Hook 中不能改变采样决策的结果,即无法强制 Zap 记录或丢弃这条日志。
}
//设置编码器 json or console, 或是第三方自定义的编码器
cfg.Encoding = "console"
cfg.EncoderConfig.EncodeCaller = zapcore.ShortCallerEncoder
cfg.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
cfg.EncoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
//cfg.EncoderConfig.EncodeDuration = zapcore.StringDurationEncoder
cfg.EncoderConfig.EncodeName = zapcore.FullNameEncoder
//设置输出路径
cfg.OutputPaths = []string{"./logs/zap_log.log", "stdout"}
//设置错误输出路径
cfg.ErrorOutputPaths = []string{"./logs/zap_err.log", "stderr"}
// 创建 logger 实例
logger, err := cfg.Build()
//logger, err = zap.NewProduction()
if err != nil {
return nil, err
}
return logger, nil
}
func main() {
logger, err := setupLoggerzap2()
if err != nil {
fmt.Println("err:", err)
panic(err)
}
// 在程序退出时确保所有日志都已写入
defer logger.Sync()
//logger.Core().Enabled(zapcore.DebugLevel)
logger.Info("Application started.")
logger.Error("Application stopped.", zap.Int("exit code", 1))
logger.Warn("Application stopped.", zap.Int("exit code", 1))
//logger.Fatal("Application stopped.", zap.Int("exit code", 1))
logger.Debug("Application stopped.", zap.Int("exit code", 1))
logger.Error("has error...", zap.Error(errors.New("error...............")))
logger.Error("This is an error message.", zap.String("key", "value"))
//logger.Panic("This is a panic message.", zap.String("key", "value"))
time.Sleep(time.Second * 5)
}
示例2:
package main
import (
"fmt"
"os"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
func setupLoggerzap() (*zap.Logger, error) {
// 首先定义或配置编码器
encoderConfig := zap.NewProductionEncoderConfig()
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder // 设置时间格式
// 创建一个JSON编码器实例
encoder := zapcore.NewJSONEncoder(encoderConfig)
// 创建日志输出目的地,例如一个文件
fileWriter, err := os.OpenFile("logs/app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
return nil, err
}
errorFilewriter, err2 := os.OpenFile("logs/app-error.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err2 != nil {
return nil, err2
}
// 将文件转换为同步器
writeSyncer := zapcore.AddSync(fileWriter)
//日志级别控制
levelFn := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
return lvl >= zapcore.DebugLevel
})
// 创建zapcore.Core实例,指定编码器、同步器和日志级别
core := zapcore.NewCore(encoder, writeSyncer, levelFn)
//添加字段信息
core = core.With([]zapcore.Field{zap.String("key111", "value1234"),
zapcore.Field{Key: "key345", Type: zapcore.Int64Type, String: "value678"}})
hook := func(e zapcore.Entry) error {
// do something with the entry
fmt.Println("hook", e)
return nil
}
// 使用该核心实例创建zap.Logger
// 可选地添加调用者信息
logger := zap.New(core,
zap.AddCaller(), // 可选地添加调用者信息
zap.AddStacktrace(zapcore.ErrorLevel), // 可选地添加堆栈跟踪
zap.Fields(zap.String("service.name", "my-service")), // 可选地添加字段
zap.ErrorOutput(zapcore.Lock(errorFilewriter)), // 可选地添加错误输出
zap.Hooks(hook), // 可选地添加钩子
)
return logger, nil
}
func main() {
logger, err := setupLoggerzap()
if err != nil {
panic(err)
}
defer logger.Sync()
// 在多个goroutine中共享并使用logger
for i := 0; i < 10; i++ {
go func(i int) {
logger.Error("message", zap.Int("goroutine_id", i))
}(i)
}
// 阻塞主线程,等待goroutines完成
select {}
}
更多可以看 https://www.jb51.net/jiaoben/3172623xm.htm 这个很好
全部变量和zap.L()的区别
zap.L() 和直接使用全局 logger 变量在功能上非常相似,它们都提供了一种在应用程序任何地方记录日志的方法。不过,它们之间还是有一些细微的差别,这些差别主要体现在使用场景和设计哲学上。
zap.L() 的特点
全局访问点:zap.L() 提供了一个全局的访问点,这意味着无需通过参数传递或依赖注入,就可以在任何地方获取到同一个 Logger 实例。这减少了代码的复杂性,因为你不需要在函数签名中包含 logger 参数。
隐式依赖:由于 zap.L() 是全局可访问的,它引入了一个隐式的依赖。这可能在某些情况下会导致代码的可测试性降低,因为你不能轻易地替换日志记录器实例进行单元测试。
初始化时机:zap.L() 依赖于全局状态的初始化。你需要确保在程序的任何地方使用 zap.L() 之前,已经通过 zap.ReplaceGlobals() 正确地初始化了全局 Logger。
直接使用全局 logger 变量的特点
显式依赖:通过全局变量直接访问 logger,这种方式使得日志记录器的依赖更加明显。这有助于代码的可读性和可维护性,因为全局变量的作用域和目的都很明确。
易于测试:由于 logger 是一个全局变量,你可以通过修改这个变量来控制测试中的日志记录行为,或者在测试前将其设置为一个不进行实际日志记录的假对象。
灵活性:如果你的应用程序在不同的组件或模块中需要不同的日志记录器配置,使用全局变量可以让你更灵活地为每个组件或模块创建和管理不同的 logger 实例。
示例对比
为了更直观地展示两者的区别,我们可以通过一个简单的例子来对比:
使用 zap.L()
package main
import (
"go.uber.org/zap"
)
func init() {
var err error
zap.ReplaceGlobals(zap.NewProduction())
}
func main() {
zap.L().Info("程序开始")
doWork()
}
func doWork() {
zap.L().Warn("执行工作")
}
使用全局变量
package main
import (
"go.uber.org/zap"
)
var globalLogger *zap.Logger
func init() {
var err error
globalLogger, err = zap.NewProduction()
if err != nil {
panic(err)
}
}
func main() {
globalLogger.Info("程序开始")
doWork()
}
func doWork() {
globalLogger.Warn("执行工作")
}
在这两个例子中,日志记录的行为是相同的,但是它们的设计哲学和使用模式有所不同。选择哪一种方式,取决于你的个人偏好、项目需求以及对代码可维护性和可测试性的考量。
zerolog
zerolog 是一个强调零分配(zero-allocation)的日志库,设计时考虑了性能因素,支持 JSON 格式输出和多种日志级别。(仓库地址)
zerolog 默认情况下并不支持日志切割功能,若需要实现日志文件滚动,可以结合其他库如file-rotatelogs或者自定义逻辑来实现日志文件的定期切换和清理。同时可以通过zerolog.SyncWriter来配置线程安全性。
零内存分配:zerolog 通过预先分配缓冲区以及避免在每次写入时进行字符串拼接来减少内存分配。它采用了一种延迟格式化的策略,在构建日志消息的过程中尽可能地复用内存,最后一次性写入到输出流。
结构化日志:zerolog 主张使用结构化日志而非传统的文本日志,因此它的日志是以 JSON 格式输出的,这使得日志可以方便地被机器解析和索引,有利于日志分析工具和日志管理系统。
事件栈:zerolog 使用事件栈的概念来构建日志消息。每个日志条目是一个事件,可以通过 .Fields()、.Str()、.Int() 等方法添加多个键值对,并在最后通过 .Msg() 方法附加上最终的消息内容。
高效处理:为了优化性能,zerolog 设计了许多机制来减少 CPU 和内存开销,例如使用编解码器直接操作字节缓冲而不是字符串,以及通过钩子函数定制日志记录行为等。
示例:
package main
import (
"fmt"
"os"
"time"
"github.com/rs/zerolog"
)
type MyHook struct{}
func (s *MyHook) Run(e *zerolog.Event, level zerolog.Level, message string) {
// do something
fmt.Println("hook", message)
}
func main() {
zerolog.SetGlobalLevel(zerolog.InfoLevel)
zerolog.CallerFieldName = "caller"
zerolog.ErrorFieldName = "error"
zerolog.ErrorStackFieldName = "stack"
zerolog.LevelErrorValue = "error"
//zerolog.SyncWriter(os.Stderr) //线程安全writer
fileWriter, err := os.OpenFile("logs/app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
return
}
// 创建一个新的日志器实例并添加hook,level
fileLog := zerolog.New(fileWriter)
// 自定义hook,如写入文件
fileLog = fileLog.Hook(&MyHook{})
// 输出到控制台
consoleLog := zerolog.New(os.Stdout)
//到多个输出
mlog := zerolog.MultiLevelWriter(fileLog, consoleLog)
logger := zerolog.New(mlog).With().Timestamp().Caller().Stack().Strs("tags", []string{"tag1", "tag2"}).Str("app", "myapp").Logger()
logger.Info().Msg("Application started")
// 记录带有字段的日志
logger.Info().
Str("event", "startup").
Dur("elapsed", time.Duration(12345)).
Msg("Server started")
// 使用嵌套字段
logger.Info().Fields(map[string]interface{}{
"user": "John Doe",
"action": "logged in",
}).Msg("User logged in")
for i := 0; i < 10; i++ {
go func(v int) {
logger.Error().Str("error", "something bad happened").Msg(fmt.Sprintf("error %d", v))
}(i)
}
time.Sleep(time.Second * 5)
}
日志切分清理支持
file-rotatelogs
rotatelogs 是一个 Go 语言的日志文件切割库,它允许你根据时间或文件大小自动滚动和归档日志文件。使用 rotatelogs 可以与各种日志库(如 logrus、zap 等)结合,实现日志文件的定时或按大小切割功能,以及旧日志文件的清理策略。在每天结束时,rotatelogs将会自动创建新的日志文件,并按照指定的规则清理旧的日志文件。同时,rotatelogs内部对写入操作进行了线程安全处理,在多 goroutine 下并发写入也是安全的。仓库地址(file-rotatelogs)
配置示例:
package main
import (
"fmt"
"log"
"time"
rotatelogs "github.com/lestrrat-go/file-rotatelogs"
)
type MyHandler struct {
}
func (h *MyHandler) Handle(e rotatelogs.Event) {
fmt.Println(e)
}
func main() {
//日志writer配置
writer, err := rotatelogs.New(
"logs/app-%Y%m%d.log", // 按日期进行日志文件命名
rotatelogs.WithLinkName("app.log"), // 创建一个指向最新日志文件的软链接
rotatelogs.WithMaxAge(7*24*time.Hour), // 文件最大保存时间
rotatelogs.WithRotationTime(24*time.Hour), // 每天进行日志切换
//rotatelogs.WithRotationCount(10), // 日志文件最大保存个数
rotatelogs.WithLocation(time.Local), // 本地时间
rotatelogs.ForceNewFile(), // 重新打开文件
rotatelogs.WithClock(rotatelogs.Local), // 本地时间
//rotatelogs.WithHandler(&MyHandler{}), //事件处理
)
if err != nil {
fmt.Println(err)
}
//设置输出,对于其他日志框架也是这样使用
log.SetOutput(writer)
log.Println("hello world")
}
lumberjack
Lumberjack 是一个 Go 语言编写的日志滚动(rotation)库,并不是一个完整的日志库,而是一个实现了日志切割和滚动策略的包,它可以自动根据文件大小和时间来切割日志文件。它可以与标准库log或其他第三方日志库(如 Logrus、Zap 等)结合使用,以实现日志文件的滚动和清理。
lumberjack库内部对写入操作进行了线程安全处理,所以在并发环境下直接共享这个 logger 实例并同时写入日志是安全的。
配置示例:
package main
import (
"fmt"
"log"
"time"
"gopkg.in/natefinch/lumberjack.v2"
)
func main() {
// 设置日志输出为 lumberjack.Writer,它会自动处理日志切割,
//并且这个写入是线程安全的,用了锁进行了控制
fileWriter := &lumberjack.Logger{
Filename: "logs/app_lumberjack_test.log", // 日志文件名
MaxSize: 100, // 每个日志文件的最大大小(MB)
MaxAge: 7, // 文件保留天数
MaxBackups: 5, // 最多保留多少个旧的日志文件
LocalTime: true, // 是否使用本地时间
Compress: false, // 是否压缩旧日志文件
}
defer fileWriter.Close()
// 将 lumberjack.Writer 设置给 log,其他日志也是相同配置
log.SetOutput(fileWriter)
for i := 0; i < 12; i++ {
go log.Println(fmt.Sprintf("test log: %d", i))
}
time.Sleep(time.Second * 3)
}