文章目录
- 原生Logger
-
- [原生 log 的缺点](#原生 log 的缺点)
- zap日志框架
-
- [zap 的核心结构](#zap 的核心结构)
-
- [创建 Logger 的方法](#创建 Logger 的方法)
- [Logger 组件解析](#Logger 组件解析)
- 搭配gin使用
原生Logger
Go 标准库中的 log
提供基本的日志记录能力,包括:
- 打印日志到控制台。
- 设置输出目的地。
- 设置日志前缀。
- 控制日志时间、文件等输出格式。
基础的控制台输出:
go
package main
import (
"log"
)
func main() {
log.Println("This is a standard log message")
log.Printf("Hello, %s!", "world")
}
go
2025/01/06 22:21:12 This is a standard log message
2025/01/06 22:21:12 Hello, world!
设置日志前缀:
通过 SetPrefix
添加固定的前缀到日志中:
go
log.SetPrefix("[INFO] ")
log.Println("This is an informational log")
go
[INFO] 2025/01/06 22:23:21 This is an informational log
设置输出目标:
默认情况下,日志输出到 os.Stderr
。可以通过 SetOutput
修改输出目标(如文件):
go
package main
import (
"log"
"os"
)
func main() {
file, err := os.OpenFile("app.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
if err != nil {
log.Fatalf("Failed to open log file: %v", err)
}
defer file.Close()
log.SetOutput(file)
log.SetPrefix("[LOG-INFO]")
log.Println("This message is logged to a file")
}
控制日志格式:
log.SetFlags
设置日志内容格式:
log.Ldate
:日期(形如2009/01/23
)。log.Ltime
:时间(形如01:23:45
)。log.Lmicroseconds
:时间戳微秒部分。log.Llongfile
:完整文件路径和行号。log.Lshortfile
:简短文件名和行号。
go
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
log.Println("Log with date, time, and file info")
go
2025/01/06 22:28:10 main.go:9: Log with date, time, and file info
原生 log 的缺点
- 日志级别缺失 :
- 无法区分
DEBUG
,INFO
,WARN
,ERROR
等级别的日志。 - 开发人员需要通过手动拼接前缀区分级别。
- 无法区分
- 结构化日志支持不足 :
- 原生
log
只能输出字符串,无法记录结构化数据(如 JSON 格式)。
- 原生
- 性能问题 :
- 对于高并发场景,原生
log
缺少优化机制。
- 对于高并发场景,原生
- 缺乏扩展性 :
- 难以满足多输出目标(如同时写入控制台和文件)的需求。
- 不支持自定义日志格式和字段。
- 线程安全 :
- 原生
log
的SetOutput
是全局的,可能被其他模块影响。
- 原生
zap日志框架
为了弥补原生 log
的缺陷,可以逐步引入 zap
这种性能优越的日志框架。
第一步:支持日志级别
使用 zap
的预定义方法(zap.NewProduction
或 zap.NewDevelopment
)快速实现日志级别功能:
go
package main
import (
"go.uber.org/zap"
)
func main() {
logger, _ := zap.NewDevelopment() // 或 zap.NewProduction()
defer logger.Sync()
logger.Debug("This is a DEBUG message")
logger.Info("This is an INFO message")
logger.Warn("This is a WARN message")
logger.Error("This is an ERROR message")
}
go
2025-01-06T22:44:28.401+0800 DEBUG go_database/main.go:11 This is a DEBUG message
2025-01-06T22:44:28.419+0800 INFO go_database/main.go:12 This is an INFO message
2025-01-06T22:44:28.419+0800 WARN go_database/main.go:13 This is a WARN message
main.main
/Users/hezhe/Documents/Go_Works/my_Golang/go_database/main.go:13
runtime.main
/usr/local/go/src/runtime/proc.go:272
2025-01-06T22:44:28.419+0800 ERROR go_database/main.go:14 This is an ERROR message
main.main
/Users/hezhe/Documents/Go_Works/my_Golang/go_database/main.go:14
runtime.main
/usr/local/go/src/runtime/proc.go:272
Process finished with the exit code 0
第二步:引入结构化日志
zap
支持通过 Field
的形式记录结构化日志,方便分析和查询:
go
logger.Info("User logged in",
zap.String("username", "admin"),
zap.Int("user_id", 42),
zap.Bool("active", true),
)
go
2025-01-06T22:47:28.778+0800 INFO go_database/main.go:11 User logged in {"username": "admin", "user_id": 42, "active": true}
第三步:支持多输出目标
zap
可同时写入多个输出目标,如控制台和文件:
go
package main
import (
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"os"
)
func main() {
//consoleEncoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig())
fileEncoder := zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())
logFile, _ := os.Create("app.log")
writeSyncer := zapcore.NewMultiWriteSyncer(
zapcore.AddSync(os.Stdout),
zapcore.AddSync(logFile),
)
core := zapcore.NewCore(fileEncoder, writeSyncer, zapcore.InfoLevel)
logger := zap.New(core)
defer logger.Sync()
logger.Info("Log to both console and file")
}
zap 的核心结构
zap
的核心由以下几个部分组成:
Logger
:主日志记录器,用于写入日志。Core
:日志的核心组件,定义日志的输出目标、格式和日志级别。Encoder
:日志格式化器,决定日志的最终表现形式(如 JSON 或控制台格式)。Sync
:用于确保日志缓冲区内容被写入。Field
:结构化日志的键值对,用于附加额外的上下文信息。
创建 Logger 的方法
1. 通过预定义方法创建
zap
提供了两个预定义的 Logger
:
zap.NewProduction()
:适合生产环境的日志配置。zap.NewDevelopment()
:适合开发环境,带有更加友好的输出格式。
go
// 创建生产环境的 Logger
logger, _ := zap.NewProduction()
// 创建开发环境的 Logger
devLogger, _ := zap.NewDevelopment()
2. 自定义创建 Logger
通过 zap.Config
自定义日志配置,灵活设置日志格式、级别、输出位置等。
go
config := zap.Config{
Level: zap.NewAtomicLevelAt(zap.InfoLevel), // 日志级别
Development: false, // 是否是开发模式
Encoding: "json", // 日志格式:json 或 console
OutputPaths: []string{"stdout", "./logs/app.log"}, // 输出位置
EncoderConfig: zapcore.EncoderConfig{
TimeKey: "timestamp",
LevelKey: "level",
NameKey: "logger",
CallerKey: "caller",
MessageKey: "message",
StacktraceKey: "stacktrace",
LineEnding: zapcore.DefaultLineEnding,
EncodeLevel: zapcore.CapitalLevelEncoder, // INFO -> INFO
EncodeTime: zapcore.ISO8601TimeEncoder, // 时间格式化
EncodeDuration: zapcore.StringDurationEncoder,
EncodeCaller: zapcore.ShortCallerEncoder, // caller 压缩显示
},
}
logger, _ := config.Build()
Logger 组件解析
1. 日志级别
日志级别决定了记录哪些重要程度的日志。zap
支持以下级别(从低到高):
DebugLevel
:调试信息,开发调试时使用。InfoLevel
:常规信息,生产环境常用。WarnLevel
:警告信息,表示潜在问题。ErrorLevel
:错误信息。DPanicLevel
:开发者恐慌级别,开发模式下会触发 panic。PanicLevel
:触发 panic。FatalLevel
:触发程序退出。
设置级别时,低于指定级别的日志不会输出。
go
atomicLevel := zap.NewAtomicLevel()
atomicLevel.SetLevel(zap.WarnLevel) // 只记录 WARN 及以上日志
2. Encoder(日志格式)
Encoder
决定了日志的输出格式:
- JSON 编码器:结构化输出,适合生产环境。
- Console 编码器:人类可读格式,适合开发调试。
go
encoder := zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()) // JSON 格式
consoleEncoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig()) // 控制台格式
3. 输出位置
通过 zapcore.AddSync
设置日志写入目标:
os.Stdout
:标准输出。- 文件路径:将日志写入文件。
go
file, _ := os.Create("./logs/app.log")
writeSyncer := zapcore.AddSync(file)
4. Core(核心组件)
Core
是日志记录的核心模块,负责将日志写入指定的目标位置。
go
core := zapcore.NewCore(
zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), // 编码器
zapcore.AddSync(os.Stdout), // 输出位置
zapcore.InfoLevel, // 日志级别
)
logger := zap.New(core)
5. 增加 Caller 信息
显示日志记录点(代码文件和行号),有助于定位问题。
go
logger := zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1))
zap.AddCaller()
:启用调用信息。
zap.AddCallerSkip(n)
:跳过调用栈的层级,避免日志打印到框架内部。
搭配gin使用
目录结构:
go
├── app.log
├── config
│ └── config.go
├── config.json
├── go.mod
├── go.sum
├── logger
│ └── logger.go
└── main.go
config/config.go
负责加载和解析配置文件(如 config.json
),并提供统一的配置管理。
go
package config
import (
"encoding/json"
"os"
)
// Config 整个项目的配置
type Config struct {
Mode string `json:"mode"` // 运行模式,例如 debug 或 release
Port int `json:"port"` // 服务端口号
*LogConfig `json:"log"` // 日志相关配置
}
// LogConfig 日志配置
type LogConfig struct {
Level string `json:"level"` // 日志级别,debug、info 等
Filename string `json:"filename"` // 日志文件路径
MaxSize int `json:"maxsize"` // 日志文件最大大小(MB)
MaxAge int `json:"max_age"` // 日志保留天数
MaxBackups int `json:"max_backups"` // 日志文件最大备份数
}
// Conf 全局配置变量
var Conf *Config
// Init 初始化配置;从指定文件加载配置文件
func Init(filePath string) error {
file, err := os.ReadFile(filePath) // 代替 ioutil.ReadFile
if err != nil {
return err
}
Conf = &Config{}
if err := json.Unmarshal(file, Conf); err != nil {
return err
}
return nil
}
config.json
go
{
"mode": "debug",
"port": 8180,
"log": {
"level": "debug",
"filename": "app.log",
"maxsize": 200,
"max_age": 7,
"max_backups": 10
}
}
logger.go
go
package logger
import (
"gin_demo/config"
"net"
"net/http"
"net/http/httputil"
"os"
"runtime/debug"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/natefinch/lumberjack"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
var lg *zap.Logger // 全局 Logger 实例
// InitLogger 初始化 Logger 配置
// 参数:cfg 是从配置文件读取的日志相关配置
// 作用:设置日志文件位置、日志级别、日志格式等
func InitLogger(cfg *config.LogConfig) (err error) {
// 创建日志写入器(文件配置)
writeSyncer := getLogWriter(cfg.Filename, cfg.MaxSize, cfg.MaxBackups, cfg.MaxAge)
// 创建日志格式化器(日志编码器)
encoder := getEncoder()
// 设置日志级别
var l = new(zapcore.Level)
err = l.UnmarshalText([]byte(cfg.Level)) // 从文本解析日志级别
if err != nil {
return
}
// 创建日志核心配置
core := zapcore.NewCore(encoder, writeSyncer, l)
// 创建 Logger 实例,启用调用者信息记录
lg = zap.New(core, zap.AddCaller())
// 替换全局 Logger 实例
zap.ReplaceGlobals(lg) // 使用 zap.L() 即可调用全局 Logger
return
}
// getEncoder 获取日志编码器
// 返回 JSON 格式的日志编码器,包含时间、调用栈、日志级别等信息
func getEncoder() zapcore.Encoder {
encoderConfig := zap.NewProductionEncoderConfig() // 创建生产环境配置
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder // 时间格式化为 ISO8601
encoderConfig.TimeKey = "time" // 定义时间字段键名
encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder // 日志级别为大写形式
encoderConfig.EncodeDuration = zapcore.SecondsDurationEncoder // 时间间隔格式化为秒
encoderConfig.EncodeCaller = zapcore.ShortCallerEncoder // 调用栈简化为短路径
return zapcore.NewJSONEncoder(encoderConfig) // 返回 JSON 编码器
}
// getLogWriter 获取日志写入器
// 返回基于 lumberjack 实现的日志切割器(日志归档功能)
// 参数:filename 日志文件路径;maxSize 单个日志文件最大大小(MB);maxBackup 最大备份数;maxAge 日志保留时间(天)
func getLogWriter(filename string, maxSize, maxBackup, maxAge int) zapcore.WriteSyncer {
lumberJackLogger := &lumberjack.Logger{
Filename: filename,
MaxSize: maxSize,
MaxBackups: maxBackup,
MaxAge: maxAge,
}
return zapcore.AddSync(lumberJackLogger)
}
// GinLogger 接收 Gin 框架默认的日志输出
// 作用:替换 Gin 框架的默认日志中间件,将请求信息记录到日志文件
func GinLogger() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now() // 请求开始时间
path := c.Request.URL.Path // 请求路径
query := c.Request.URL.RawQuery // 查询字符串
c.Next() // 处理请求
cost := time.Since(start) // 请求耗时
lg.Info(path,
zap.Int("status", c.Writer.Status()), // HTTP 状态码
zap.String("method", c.Request.Method), // 请求方法
zap.String("path", path), // 请求路径
zap.String("query", query), // 查询参数
zap.String("ip", c.ClientIP()), // 客户端 IP
zap.String("user-agent", c.Request.UserAgent()), // 客户端 UA
zap.String("errors", c.Errors.ByType(gin.ErrorTypePrivate).String()), // 请求错误
zap.Duration("cost", cost), // 请求耗时
)
}
}
// GinRecovery 捕获 Gin 框架中可能的 panic,并记录到日志文件
// 参数:stack 表示是否记录调用栈信息
// 作用:防止 panic 导致服务崩溃,同时将详细错误信息记录到日志
func GinRecovery(stack bool) gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil { // 捕获 panic
// 检查是否是断开的连接
var brokenPipe bool
if ne, ok := err.(*net.OpError); ok {
if se, ok := ne.Err.(*os.SyscallError); ok {
if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
brokenPipe = true
}
}
}
// 打印 HTTP 请求内容
httpRequest, _ := httputil.DumpRequest(c.Request, false)
if brokenPipe {
lg.Error(c.Request.URL.Path,
zap.Any("error", err),
zap.String("request", string(httpRequest)),
)
c.Error(err.(error)) // nolint: errcheck
c.Abort()
return
}
// 记录 panic 信息到日志
if stack {
lg.Error("[Recovery from panic]",
zap.Any("error", err),
zap.String("request", string(httpRequest)),
zap.String("stack", string(debug.Stack())), // 调用栈信息
)
} else {
lg.Error("[Recovery from panic]",
zap.Any("error", err),
zap.String("request", string(httpRequest)),
)
}
c.AbortWithStatus(http.StatusInternalServerError) // 返回 500 错误
}
}()
c.Next() // 继续处理请求
}
}
InitLogger
:初始化日志,配置日志级别、文件归档等信息。
getEncoder
:设置日志格式为 JSON,并包含时间、调用栈、日志级别等字段。
getLogWriter
:设置日志文件的切割和归档功能。
GinLogger
:拦截 Gin 框架的日志输出,记录 HTTP 请求信息。
GinRecovery
:捕获可能的 panic,并记录详细错误日志,避免服务崩溃。
main.go
go
package main
import (
"fmt"
"gin_demo/config"
"gin_demo/logger"
"net/http"
"os"
"go.uber.org/zap"
"github.com/gin-gonic/gin"
)
func main() {
// 默认配置文件路径
configFilePath := "config.json"
if len(os.Args) > 1 {
configFilePath = os.Args[1]
}
// 加载配置
if err := config.Init(configFilePath); err != nil {
fmt.Printf("Failed to load config: %v\n", err)
os.Exit(1)
}
// 初始化日志
if err := logger.InitLogger(config.Conf.LogConfig); err != nil {
fmt.Printf("Logger initialization failed: %v\n", err)
os.Exit(1)
}
defer zap.L().Sync() // 确保日志缓冲区被清空
// 设置 Gin 模式
gin.SetMode(config.Conf.Mode)
// 创建 Gin 实例
r := gin.New()
// 注册日志中间件
r.Use(logger.GinLogger(), logger.GinRecovery(true))
// 路由
r.GET("/hello", func(c *gin.Context) {
name := "q1mi"
age := 18
zap.L().Info("Hello function called",
zap.String("user", name),
zap.Int("age", age),
zap.String("path", c.FullPath()),
)
c.String(http.StatusOK, "Hello, world!")
})
// 启动 HTTP 服务
if config.Conf.Port <= 0 || config.Conf.Port > 65535 {
zap.L().Fatal("Invalid port number", zap.Int("port", config.Conf.Port))
}
addr := fmt.Sprintf(":%v", config.Conf.Port)
zap.L().Info("Starting server", zap.String("address", addr))
if err := r.Run(addr); err != nil {
zap.L().Fatal("Failed to start server", zap.Error(err))
}
}
- 运行,生产
app.log
go
{"level":"INFO","time":"2025-01-07T10:47:19.263+0800","caller":"gin_demo/main.go:62","msg":"Starting server","address":":8180"}
{"level":"INFO","time":"2025-01-07T10:48:04.499+0800","caller":"gin_demo/main.go:48","msg":"Hello function called","user":"q1mi","age":18,"path":"/hello"}
{"level":"INFO","time":"2025-01-07T10:48:04.500+0800","caller":"logger/logger.go:85","msg":"/hello","status":200,"method":"GET","path":"/hello","query":"","ip":"::1","user-agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36","errors":"","cost":0.000132625}
{"level":"INFO","time":"2025-01-07T10:48:04.613+0800","caller":"logger/logger.go:85","msg":"/favicon.ico","status":404,"method":"GET","path":"/favicon.ico","query":"","ip":"::1","user-agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36","errors":"","cost":0.000000708}