项目源码
在软件开发和系统运维中,日志是一种不可或缺的工具,它在故障排查、性能分析、问题追踪以及监控报警等方面发挥着重要作用。
log 标准库
golang官方提供的log标准库,支持基础的日志操作,不仅可以将信息输出到控制面板,还能输出到文件,并且可通过不同的日志级别来控制输出的详细程度。
基本使用
package main
import (
"log"
)
func main() {
log.Println("Hello World")
}
运行上述代码后,控制台会有如下输出:
2024/07/16 16:41:43 Hello World
日志写入文件
上边演示了日志的基本使用,但是正式开发中我们一般会把日志写入文件中:
package main
import (
"log"
"fmt"
)
func main() {
// 创建一个用于记录日志的文件
file, err := os.OpenFile("sys.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
if err!= nil {
fmt.Println(err)
return
}
logger := log.New(file, "INFO: ", log.Ldate|log.Ltime|log.Lshortfile)
logger.Println("hello world")
}
运行上述代码后,日志会被写入sys.log文件,并在日志前加上INFO: 前缀。
可以看出,log库使用起来非常简单,但也确实简单得有些过头了,多余的功能一点都没有。一般来说,分布式系统的日志都是以json格式存储的,这样便于日志被统一收录到ELK等日志分析平台。此外,还需要支持多种日志级别,以便于检索、报警,同时也能节省一些存储空间。
zap
安装
go get -u go.uber.org/zap
基本使用
package logger
import (
"go.uber.org/zap"
)
func main() {
logger, _ := zap.NewProduction()
logger.Info("hello world", zap.String("name", "eve"),
zap.Any("user", struct {
Name string
Age int
}{Name: "eve", Age: 18}))
}
运行上述代码后,控制台会输出如下内容:
{"level":"info","ts":1721121236.311571,"caller":"logger/logger.go:27","msg":"hello world","name":"eve","user":{"Name":"eve","Age":18}}
这正是我们想要的日志结构
封装到系统中
新增internal/logger/logger.go文件:
package logger
import (
"go.uber.org/zap"
)
func New() (logger *zap.Logger, err error) {
logger, err = zap.NewProduction()
return
}
修改internal/global/global.go文件:
package global
import (
"eve/internal/config"
"go.uber.org/zap"
"gorm.io/gorm"
)
var (
Config *config.Config
Db *gorm.DB
Logger *zap.Logger
)
修改internal/bootstrap/init文件:
package bootstrap
import (
"eve/internal/config"
"eve/internal/global"
"eve/internal/logger"
"eve/internal/mysql"
"fmt"
"go.uber.org/zap"
)
func init() {
var err error
// 初始化配置文件
global.Config = config.GetConfig()
// 初始化数据库
global.Db = mysql.GetConnection()
// 初始化日志
if global.Logger, err = logger.New(); err != nil {
fmt.Println("初始化日志错误:", err)
return
}
// 下边是测试代码,用完删除
global.Logger.Info("log_init", zap.String("name", "eve"))
}
然后运行项目会发现控制台有日志输出:
running...
{"level":"info","ts":1721203124.044276,"caller":"bootstrap/init.go:28","msg":"log_init","name":"eve"}
...
日志记录到文件中
上边的日志是直接输出在控制台的,现在我们改为记录到文件中
修改config.yaml文件,新增logger对象:
logger:
level: debug # 日志级别
file_path: "/tmp/log/" # 日志存放路径
file_name: "eve.log" # 日志文件名称
max_size: 10 # 以M为单位对日志进行切割
max_age: 30 # 保留旧文件最大天数
max_backups: 5 # 保留旧文件最大份数
compress: true # 是否压缩归档文件
新增internal/config/logger.go文件:
package config
type Logger struct {
Level string `mapstructure:"level"`
FilePath string `mapstructure:"file_path"`
FileName string `mapstructure:"file_name"`
Format string `mapstructure:"format"`
MaxSize int `mapstructure:"max_size"`
MaxAge int `mapstructure:"max_age"`
MaxBackups int `mapstructure:"max_backups"`
Compress bool `mapstructure:"compress"`
}
修改internal/config/config.go文件中Config结构体:
...
// Config 配置文件集合
type Config struct {
App App
Database Database
Logger Logger
}
...
修改internal/logger/logger.go文件:
package logger
import (
"eve/internal/config"
"eve/internal/global"
"eve/internal/tool"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"os"
"strings"
"time"
)
var (
options []zap.Option
conf config.Logger
)
func New() (logger *zap.Logger, err error) {
// 获取配置文件
conf = global.Config.Logger
// 创建日志存放目录
rootDir, _ := tool.GetRootDir()
logDir := rootDir + conf.FilePath
err = os.MkdirAll(logDir, os.ModePerm)
if err != nil {
return
}
// 打开日志文件
file, err := os.OpenFile(logDir+conf.FileName, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
if err != nil {
return
}
loggerConf := genConfig()
loggerConf.EncoderConfig = genEncodeConfig()
core := zapcore.NewCore(
zapcore.NewJSONEncoder(loggerConf.EncoderConfig),
zapcore.AddSync(file),
loggerConf.Level,
)
// 添加调用位置
options = append(options, zap.AddCaller())
logger = zap.New(core, options...)
return
}
// 生成配置
func genConfig() (config zap.Config) {
config = zap.NewProductionConfig()
config.EncoderConfig = genEncodeConfig()
config.Level = zap.NewAtomicLevelAt(getLevel())
return
}
// 生成编码配置
func genEncodeConfig() (c zapcore.EncoderConfig) {
c = zap.NewProductionEncoderConfig()
c.EncodeTime = func(time time.Time, encoder zapcore.PrimitiveArrayEncoder) {
encoder.AppendString(time.Format("2006-01-02 15:04:05.000"))
}
c.EncodeLevel = func(l zapcore.Level, encoder zapcore.PrimitiveArrayEncoder) {
encoder.AppendString(strings.ToUpper(l.String()))
}
c.TimeKey = "time"
return
}
// 配置文件的level转换为zapcore的level
func getLevel() (level zapcore.Level) {
switch conf.Level {
case "debug":
level = zap.DebugLevel
case "info":
level = zap.InfoLevel
case "warn":
level = zap.WarnLevel
case "error":
level = zap.ErrorLevel
case "dpanic":
level = zap.DPanicLevel
case "panic":
level = zap.PanicLevel
case "fatal":
level = zap.FatalLevel
default:
level = zap.InfoLevel
}
return
}
修改完之后运行项目,发现日志已经写入了tmp/log/eve.log文件中了
日志归档切割
经过上边的改动,日志已经封装好了,但是我们发现日志是写在一个文件中,那在生产环境中一直往一个文件中写入日志,服务器磁盘肯定会被撑满,肯定是不行的,所以我们还需要可以做日志切割和自动归档的工具,zap当前是不支持的,这里我们使用lumberjack
安装
go get -u github.com/natefinch/lumberjack@v2
使用
官网可以看到,使用时实例化一个Logger结构体,它支持一些初始化的参数:
// Logger 结构体
type Logger struct {
// Filename 字段用于指定日志文件的名称。如果为空,则默认为 os.TempDir() 目录下的 <processname>-lumberjack.log
Filename string `json:"filename" yaml:"filename"`
// MaxSize 字段指定日志文件的最大大小(以兆字节为单位)。如果达到这个大小,文件将被轮循。默认为 100 兆字节
MaxSize int `json:"maxsize" yaml:"maxsize"`
// MaxAge 字段指定保留旧日志文件的最大天数。根据文件名中编码的时间戳来判断。请注意,一天定义为 24 小时,并可能由于夏令时、闰秒等因素而与日历日不完全对应。默认为不根据年龄删除旧日志文件
MaxAge int `json:"maxage" yaml:"maxage"`
// MaxBackups 字段指定要保留的旧日志文件的最大数量。默认情况下,将保留所有旧日志文件(尽管 MaxAge 仍可能导致它们被删除)
MaxBackups int `json:"maxbackups" yaml:"maxbackups"`
// LocalTime 字段决定格式化备份文件时间戳时是否使用计算机的本地时间。默认是使用 UTC 时间
LocalTime bool `json:"localtime" yaml:"localtime"`
// Compress 字段决定是否使用 gzip 压缩轮转后的日志文件。默认情况下不执行压缩
Compress bool `json:"compress" yaml:"compress"`
}
它是返回一个Writer,我们这里可以用它来替换上边用到的file,修改internal/logger/logger.go文件:
package logger
import (
"eve/internal/config"
"eve/internal/global"
"eve/internal/tool"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"gopkg.in/natefinch/lumberjack.v2"
"os"
"strings"
"time"
)
var (
options []zap.Option
conf config.Logger
)
func New() (logger *zap.Logger, err error) {
// 获取配置文件
conf = global.Config.Logger
// 创建日志存放目录
rootDir, _ := tool.GetRootDir()
logDir := rootDir + conf.FilePath
err = os.MkdirAll(logDir, os.ModePerm)
if err != nil {
return
}
loggerConf := genConfig()
loggerConf.EncoderConfig = genEncodeConfig()
writer, err := genWriteSyncer()
if err != nil {
return nil, err
}
core := zapcore.NewCore(
zapcore.NewJSONEncoder(loggerConf.EncoderConfig),
writer,
loggerConf.Level,
)
// 添加调用位置
options = append(options, zap.AddCaller())
logger = zap.New(core, options...)
return
}
// 生成WriteSyncer
func genWriteSyncer() (writeSyncer zapcore.WriteSyncer, err error) {
// 创建日志存放目录
rootDir, _ := tool.GetRootDir()
logDir := rootDir + conf.FilePath
err = os.MkdirAll(logDir, os.ModePerm)
if err != nil {
return
}
lumberJack := &lumberjack.Logger{
Filename: logDir + conf.FileName,
MaxSize: conf.MaxSize, // megabytes
MaxBackups: conf.MaxBackups,
MaxAge: conf.MaxAge, //days
}
writeSyncer = zapcore.AddSync(lumberJack)
return
}
// 生成配置
func genConfig() (config zap.Config) {
config = zap.NewProductionConfig()
config.EncoderConfig = genEncodeConfig()
config.Level = zap.NewAtomicLevelAt(getLevel())
return
}
// 生成编码配置
func genEncodeConfig() (c zapcore.EncoderConfig) {
c = zap.NewProductionEncoderConfig()
c.EncodeTime = func(time time.Time, encoder zapcore.PrimitiveArrayEncoder) {
encoder.AppendString(time.Format("2006-01-02 15:04:05.000"))
}
c.EncodeLevel = func(l zapcore.Level, encoder zapcore.PrimitiveArrayEncoder) {
encoder.AppendString(strings.ToUpper(l.String()))
}
c.TimeKey = "time"
return
}
// 配置文件的level转换为zapcore的level
func getLevel() (level zapcore.Level) {
switch conf.Level {
case "debug":
level = zap.DebugLevel
case "info":
level = zap.InfoLevel
case "warn":
level = zap.WarnLevel
case "error":
level = zap.ErrorLevel
case "dpanic":
level = zap.DPanicLevel
case "panic":
level = zap.PanicLevel
case "fatal":
level = zap.FatalLevel
default:
level = zap.InfoLevel
}
return
}
测试
修改完之后我们测试一下,修改internal/bootstrap/init文件:
package bootstrap
import (
"eve/internal/config"
"eve/internal/global"
"eve/internal/logger"
"eve/internal/mysql"
"go.uber.org/zap"
)
func init() {
var err error
// 初始化配置文件
global.Config = config.GetConfig()
// 初始化数据库
global.Db = mysql.GetConnection()
// 初始化日志
if global.Logger, err = logger.New(); err != nil {
panic(err)
return
}
// 下边是测试代码,用完删除
for {
global.Logger.Info("info", zap.String("name", "eve"))
global.Logger.Debug("debug", zap.String("name", "eve"))
global.Logger.Error("error", zap.String("name", "eve"))
}
}
运行代码一段时间之后可以看到tmp/log目录下:
➜ eve_api git:(main) ✗ ls -lh tmp/log/
total 115688
-rw-r--r-- 1 lining staff 10M Jul 18 10:38 eve-2024-07-18T02-38-06.278.log
-rw-r--r-- 1 lining staff 10M Jul 18 10:38 eve-2024-07-18T02-38-06.549.log
-rw-r--r-- 1 lining staff 10M Jul 18 10:38 eve-2024-07-18T02-38-06.820.log
-rw-r--r-- 1 lining staff 10M Jul 18 10:38 eve-2024-07-18T02-38-07.095.log
-rw-r--r-- 1 lining staff 10M Jul 18 10:38 eve-2024-07-18T02-38-07.361.log
-rw-r--r-- 1 lining staff 6.5M Jul 18 10:38 eve.log
可以看到下目录下有5个归档文件以及一个当前写入的文件,归档文件大小为配置文件所配置的10M
这样我们的日志就封装好了,业务中可以通过global直接调用使用
总结
- 官方日志库简单使用
- zap安装
- zap初始化
- 日志文件归档切割
commit-hash: 369a678