【GO基础学习】项目日志zap Logger使用

文章目录

  • 原生Logger
    • [原生 log 的缺点](#原生 log 的缺点)
  • zap日志框架
    • [zap 的核心结构](#zap 的核心结构)
      • [创建 Logger 的方法](#创建 Logger 的方法)
      • [Logger 组件解析](#Logger 组件解析)
    • 搭配gin使用

原生Logger

Go 标准库中的 log 提供基本的日志记录能力,包括:

  1. 打印日志到控制台。
  2. 设置输出目的地。
  3. 设置日志前缀。
  4. 控制日志时间、文件等输出格式。

基础的控制台输出:

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 的缺点

  1. 日志级别缺失
    • 无法区分 DEBUG, INFO, WARN, ERROR 等级别的日志。
    • 开发人员需要通过手动拼接前缀区分级别。
  2. 结构化日志支持不足
    • 原生 log 只能输出字符串,无法记录结构化数据(如 JSON 格式)。
  3. 性能问题
    • 对于高并发场景,原生 log 缺少优化机制。
  4. 缺乏扩展性
    • 难以满足多输出目标(如同时写入控制台和文件)的需求。
    • 不支持自定义日志格式和字段。
  5. 线程安全
    • 原生 logSetOutput 是全局的,可能被其他模块影响。

zap日志框架

为了弥补原生 log 的缺陷,可以逐步引入 zap 这种性能优越的日志框架。

第一步:支持日志级别

使用 zap 的预定义方法(zap.NewProductionzap.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 的核心由以下几个部分组成:

  1. Logger:主日志记录器,用于写入日志。
  2. Core:日志的核心组件,定义日志的输出目标、格式和日志级别。
  3. Encoder:日志格式化器,决定日志的最终表现形式(如 JSON 或控制台格式)。
  4. Sync:用于确保日志缓冲区内容被写入。
  5. Field:结构化日志的键值对,用于附加额外的上下文信息。

创建 Logger 的方法

1. 通过预定义方法创建

zap 提供了两个预定义的 Logger

  1. zap.NewProduction():适合生产环境的日志配置。
  2. 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
  1. 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
}
  1. config.json
go 复制代码
{
  "mode": "debug",
  "port": 8180,
  "log": {
    "level": "debug",
    "filename": "app.log",
    "maxsize": 200,
    "max_age": 7,
    "max_backups": 10
  }
}
  1. 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,并记录详细错误日志,避免服务崩溃。

  1. 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))
	}
}
  1. 运行,生产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}

相关推荐
Linux520小飞鱼17 分钟前
T-SQL语言的学习路线
开发语言·后端·golang
黄毛火烧雪下1 小时前
React 深入学习理解
前端·学习·react.js
老王聊主机2 小时前
2025年京东云快速搭建幻兽帕鲁联机服务器教程
运维·服务器·京东云
omegayy3 小时前
KCP解读:拥塞控制
服务器·网络·网络协议·计算机网络·c#·游戏程序·kcp
Tatalaluola4 小时前
【《游戏编程模式》实战04】状态模式实现敌人AI
学习·游戏·unity·c#·状态模式
小青柑-6 小时前
Go语言中的接收器(Receiver)详解
开发语言·后端·golang
张声录17 小时前
【Prometheus】【Blackbox Exporter】深入解析 ProbeTCP 函数:如何实现 Go 中的 TCP/SSL 协议探测
tcp/ip·golang·prometheus
9命怪猫7 小时前
AI大模型-提示工程学习笔记5-零提示
人工智能·笔记·学习·ai·提示工程
C++小厨神8 小时前
Bash语言的计算机基础
开发语言·后端·golang
BinaryBardC8 小时前
Bash语言的软件工程
开发语言·后端·golang