初学golang不久,在尝试一个golang的基础组件封装,想着完成一个Java的Logback日志框架的粗略功能的组件
由于是自学且刚刚开始,对golang的很多语法及开发模式不是非常了解,只是为实现如标题所示一样的功能,可以说是不择手段
所以,不管是日志配置文件的配置方式,还是代码的实现逻辑都还有很多需要改善的地方,该示例也只能算作一个随笔一个记录
该文章编写时间为: 2023年12月11日
日志组件选择
- 由于日志是使用配置方式,选择.ini格式的配置文件,对应则使用:github.com/go-ini/ini@v1.67.0 组件进行读取
- 实现日志要实现分类存储,选择go.uber.org/zap@v1.26.0 日志组件
- 实现日志按容量大小切割,选择github.com/natefinch/lumberjack@v2.0.0+incompatible 日志切割组件
- 实现日志过滤功能,比如某些日志文件只记录某些go文件指定级别的日志输出,也是使用Zap日志组件进行实现,重写了
Check
方法
日志配置文件说明
配置文件在当前示例中使用config.ini配置文件,可自行修改配置文件名称,对应的也要修改源码中读取的配置文件名称
配置中主要有四大配置项: logging, Logging, Encode, LogRule
- logging 为日志读取和初始化开始的地方,这里配置了终端输出的日志级别、终端日志输出格式、日志文件保存目录、日志文件的配置,示例配置:
[logging]
# 终端日志级别从小到大为: Debug, Info, Warn, Error, DPanic, Panic, Fatal
# 日志级别配置
# :Info -> 表示小于等于Info级别
# Info: -> 表示大于等于Info级别
# Info -> 表示等于Info级别
# 为空则默认配置为 Debug:
ConsoleLevel = Debug:
# 终端日志格式配置,默认为内置配置,否则必须申明
ConsoleEncode = consoleEncode
# 日志保存目录
# 默认在与应用平级目录下
LogPath = /logs
# 日志文件的配置,可不配置,也可配置多个,但是配置了就必须要在配置文件中找到指定的配置项,日志加载时会进行验证
Logging = stdout, stderr, stdBus
- Logging 为日志文件的配置,配置了日志文件的名称,日志文件记录的日志级别,日志文件要过滤的GO文件日志,日志文件切割规则和日志内容格式,示例配置:
[stdout]
# 日志文件名称
# 必须申明
# 必须以".log"结尾,只能包含大小写字母、数字、符号"-"和"_"
# 路径为 LogPath/LogFile
LogFile = stdout.log
# 日志级别从小到大为: Debug, Info, Warn, Error, DPanic, Panic, Fatal
# 日志级别配置
# :Info -> 表示小于等于Info级别
# Info: -> 表示大于等于Info级别
# Info -> 表示等于Info级别
# 为空则默认配置为 Debug:
LogLevel = Info
#日志过滤,多个配置,以英文逗号分隔:
# Service -> 表示只记录Service.go文件的日志
# Service: -> 表示以Service开头的Go文件的日志
# :Service -> 表示以Service结尾的Go文件的日志
# :Service: -> 表示包含Service关键字的Go文件的日志
# !Service -> 表示不记录Service.go文件的日志
# !Service: -> 表示不以Service开头的Go文件的日志
# !:Service -> 表示不以Service结尾的Go文件的日志
# !:Service: -> 表示不包含Service关键字的Go文件的日志
# !Ser:vice -> 表示不记录Ser:vice.Go文件日志,中间的 "!" 或 ":" 将当作文件名称处理
# "Service" 这个Go文件名称根据项目中实际需求自行修改,不是固定的只能过滤Service关键字的Go文件名称,这里只是用来做为示例
Filters =
# 日志文件切割规则
# "fileRule"这个名称可以自定义,要保证配置文件中必须有对应名称的配置项
# 这里只是把"fileRule"这个字符串用作示例,并且在下面的示例中可以找到fileRule的配置
# 也可以不写配置名称,将使用默认的切割规则(其实就是示例配置的切割规则),
LogRule = fileRule
# 日志格式配置
# "fileEncode"这个名称可以自定义,要保证配置文件必须有对应名称的配置项
# 这里只是把"fileEncode"这个字符串用作示例,并且在下面的示例中可以找到fileEncode的配置
# 也可以不写日志格式配置名称,将使用默认的日志格式配置(其实就是示例配置的格式配置)
LogEncode = fileEncode
-
Encode 为日志内容的格式配置,为Zap对日志内容的配置,
[fileEncode]
在上面的[stdout]
中配置引用,配置示例:
[fileEncode]
# 日志时间格式
# EpochTimeEncoder -> 时间戳
# EpochMillisTimeEncoder -> 到毫秒
# EpochNanosTimeEncoder -> 到纳秒
# ISO8601TimeEncoder -> UTC时间
# RFC3339TimeEncoder -> RFC3339格式
# RFC3339NanoTimeEncoder -> RFC3339格式到纳秒
# 否则为指定的格式内容,其中YYYY,MM,dd,HH,mm,ss,SSS为重要的格式,其他的可以自定义
# 默认为 ISO8601TimeEncoder
EncodeTime = YYYY-MM-dd HH:mm:ss.SSS
# 默认为Time
TimeKey = Time
# 日志级别字符串的格式:
# CapitalLevelEncoder -> 大写不带颜色
# CapitalColorLevelEncoder -> 大写带颜色
# LowercaseLevelEncoder -> 小写不带颜色
# LowercaseColorLevelEncoder -> 小写带颜色
# 默认为 CapitalLevelEncoder
EncodeLevel = CapitalLevelEncoder
# 日志对于时间类型数据的格式化
# SecondsDurationEncoder -> 格式化到秒
# NanosDurationEncoder -> 格式化到纳秒
# MillisDurationEncoder -> 格式化到毫秒
# StringDurationEncoder -> 使用内置的时间格式化,与EncodeTime没有任何关系
# 默认为 SecondsDurationEncoder
EncodeDuration = SecondsDurationEncoder
# 打印日志的文件和行数配置
# ShortCallerEncoder -> 使用短名称,格式为package/file.go:line
# FullCallerEncoder -> 使用完整的名称,格式为 path/to/package/file.go:line
# 默认为 ShortCallerEncoder
EncodeCaller = ShortCallerEncoder
# 日志内容结构配置
# Console -> 使用ConsoleEncoder
# JSON -> 使用JSONEncoder
# 默认为 Console
Encode = Console
-
LogRule为
lumberjack
的日志切割配置,[fileRule]
在上面的[stdout]
配置中引用,示例如下:
[fileRule]
# 单个日志文件最大体积,单位为MB,超过该大小会进行日志切割
# 为空默认为100MB
MaxSize = 100
# 日志保留天数
# 默认为 10
MaxAge = 10
# 切割的日志保留的数量
# 默认为 10
MaxBackups = 10
# 确定用于格式化备份文件中的时间戳的时间是否为计算机的本地时间
# 默认为 false,使用UTC时间用作备份文件名称
LocalTime = true
# 备份日志文件是否启用压缩
# 默认为 false,不压缩
Compress = true
必须说明:
-
[logging]
这个名称不可自定义,这个是内置的读取名称,如果要进行修改,则需要对源码进行修改 - [logging] > Logging的值,根据实际需要进行自定义,但是这些自定义的名称必须要在配置文件中能找到对应的配置
- [logging] > Logging > Filters的值, 根据实际的需求进行一系列自定义Go文件名称过滤
- [logging] > Logging > LogRule的值,根据实际需要自定义名称,但是定义的名称必须要在配置文件中找到对应的配置,可复用
- [logging] > Logging > LogEncode的值,根据实际需要自定义名称,但是定义的名称必须要在配置文件中找到对应的配置,可复用
日志配置的加载及功能实现源码
没有注释,自行理解吧~~
在项目启动过程中,在适当的时候调用该go文件的InitLogging()
方法进行日志文件解析和功能初始化
package logging
import (
"fmt"
"github.com/go-ini/ini"
"github.com/natefinch/lumberjack"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"os"
"path/filepath"
"regexp"
"runtime"
"strings"
"time"
)
type customCore struct {
zapcore.LevelEnabler
enc zapcore.Encoder
out zapcore.WriteSyncer
filter *[]string
}
type logging struct {
logPath string
consoleLevel string
consoleEncode *logEncoder
loggingMap *map[string]*logSdt
}
type logSdt struct {
logFile string
logLevel string
filters *[]string
logRule *logRule
logEncode *logEncoder
}
type logRule struct {
maxSize int
maxAge int
maxBackups int
localTime bool
compress bool
}
type logEncoder struct {
encodeTime string
timeKey string
encodeLevel string
encodeDuration string
encodeCaller string
encode string
}
var logLevelStrArr = []string{"Debug", "Debug:", "Info", ":Info", "Info:", "Warn", ":Warn", "Warn:", "Error", ":Error", "Error:", ":DPanic", "DPanic:", "DPanic", "Panic", ":Panic", "Panic:", "Fatal", ":Fatal"}
var encodeDurationStrArr = []string{"SecondsDurationEncoder", "NanosDurationEncoder", "MillisDurationEncoder", "StringDurationEncoder"}
var encodeLevelStrArr = []string{"CapitalLevelEncoder", "CapitalColorLevelEncoder", "LowercaseLevelEncoder", "LowercaseColorLevelEncoder"}
var encodeCallerStrArr = []string{"ShortCallerEncoder", "FullCallerEncoder"}
var encodeStrArr = []string{"Console", "JSON"}
func InitLogging() {
fmt.Println("Start Init Logger ...")
loadLogging(loadLoggingConfig())
}
func loadLogging(loggingCfg *logging) {
var cores []zapcore.Core
// console
consoleWriter := zapcore.AddSync(os.Stdout)
consoleEncoder := logEncode(loggingCfg.consoleEncode)
var consoleFilter []string
consoleCore := newCore(consoleEncoder, &consoleWriter, getLogLevel(loggingCfg.consoleLevel), &consoleFilter)
cores = append(cores, consoleCore)
// logFiles
logPath := loggingCfg.logPath
loggingMap := *loggingCfg.loggingMap
for key := range loggingMap {
std := loggingMap[key]
stdWriter := logWriter(logPath, std)
stdEncoder := logEncode(std.logEncode)
stdCore := newCore(stdEncoder, &stdWriter, getLogLevel(std.logLevel), std.filters)
cores = append(cores, stdCore)
}
core := zapcore.NewTee(cores[:]...)
logger := zap.New(core, zap.AddCaller())
defer logger.Sync()
zap.ReplaceGlobals(logger)
}
func logWriter(logPath string, sdt *logSdt) zapcore.WriteSyncer {
logRule := sdt.logRule
logger := &lumberjack.Logger{
Filename: filepath.Join(logPath, sdt.logFile),
MaxSize: logRule.maxSize,
MaxAge: logRule.maxAge,
MaxBackups: logRule.maxBackups,
LocalTime: logRule.localTime,
Compress: logRule.compress,
}
return zapcore.AddSync(logger)
}
func logEncode(cfg *logEncoder) *zapcore.Encoder {
config := zap.NewProductionEncoderConfig()
config.EncodeTime = logEncodeTime(cfg.encodeTime)
config.TimeKey = cfg.timeKey
config.EncodeLevel = logEncodeLevel(cfg.encodeLevel)
config.EncodeDuration = logEncodeDuration(cfg.encodeDuration)
config.EncodeCaller = logCallerEncoder(cfg.encodeCaller)
var encoder zapcore.Encoder
if strings.EqualFold(cfg.encode, "JSON") {
encoder = zapcore.NewJSONEncoder(config)
} else {
encoder = zapcore.NewConsoleEncoder(config)
}
return &encoder
}
func logCallerEncoder(encodeCaller string) func(caller zapcore.EntryCaller, enc zapcore.PrimitiveArrayEncoder) {
switch encodeCaller {
case "ShortCallerEncoder":
return zapcore.ShortCallerEncoder
case "FullCallerEncoder":
return zapcore.FullCallerEncoder
default:
return zapcore.ShortCallerEncoder
}
}
func logEncodeDuration(encodeDuration string) func(d time.Duration, enc zapcore.PrimitiveArrayEncoder) {
switch encodeDuration {
case "SecondsDurationEncoder":
return zapcore.SecondsDurationEncoder
case "NanosDurationEncoder":
return zapcore.NanosDurationEncoder
case "MillisDurationEncoder":
return zapcore.MillisDurationEncoder
case "StringDurationEncoder":
return zapcore.StringDurationEncoder
default:
return zapcore.SecondsDurationEncoder
}
}
func logEncodeLevel(encodeLevel string) func(l zapcore.Level, enc zapcore.PrimitiveArrayEncoder) {
switch encodeLevel {
case "CapitalLevelEncoder":
return zapcore.CapitalLevelEncoder
case "CapitalColorLevelEncoder":
return zapcore.CapitalColorLevelEncoder
case "LowercaseLevelEncoder":
return zapcore.LowercaseLevelEncoder
case "LowercaseColorLevelEncoder":
return zapcore.LowercaseColorLevelEncoder
default:
return zapcore.CapitalLevelEncoder
}
}
func logEncodeTime(encodeTime string) func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
switch encodeTime {
case "EpochTimeEncoder":
return zapcore.EpochTimeEncoder
case "EpochMillisTimeEncoder":
return zapcore.EpochMillisTimeEncoder
case "EpochNanosTimeEncoder":
return zapcore.EpochNanosTimeEncoder
case "ISO8601TimeEncoder":
return zapcore.ISO8601TimeEncoder
case "RFC3339TimeEncoder":
return zapcore.RFC3339TimeEncoder
case "RFC3339NanoTimeEncoder":
return zapcore.RFC3339NanoTimeEncoder
default:
return customEncodeTime(encodeTime)
}
}
func customEncodeTime(format string) func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
// YYYY,MM,dd,HH,mm,ss,SSS
_format := strings.ReplaceAll(format, "YYYY", "2006")
_format = strings.ReplaceAll(_format, "MM", "01")
_format = strings.ReplaceAll(_format, "dd", "02")
_format = strings.ReplaceAll(_format, "HH", "15")
_format = strings.ReplaceAll(_format, "mm", "04")
_format = strings.ReplaceAll(_format, "ss", "05")
_format = strings.ReplaceAll(_format, "SSS", "000")
return func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
enc.AppendString(t.Format(_format))
}
}
func getLogLevel(l string) zap.LevelEnablerFunc {
return zap.LevelEnablerFunc(func(level zapcore.Level) bool {
switch l {
case "Debug":
return level == zapcore.DebugLevel
case "Debug:":
return level >= zapcore.DebugLevel
case "Info":
return level == zapcore.InfoLevel
case ":Info":
return level <= zapcore.InfoLevel
case "Info:":
return level >= zapcore.InfoLevel
case "Warn":
return level == zapcore.WarnLevel
case ":Warn":
return level <= zapcore.WarnLevel
case "Warn:":
return level >= zapcore.WarnLevel
case "Error":
return level == zapcore.ErrorLevel
case ":Error":
return level <= zapcore.ErrorLevel
case "Error:":
return level >= zapcore.ErrorLevel
case "DPanic":
return level == zapcore.DPanicLevel
case ":DPanic":
return level <= zapcore.DPanicLevel
case "DPanic:":
return level >= zapcore.DPanicLevel
case "Panic":
return level == zapcore.PanicLevel
case ":Panic":
return level <= zapcore.PanicLevel
case "Panic:":
return level >= zapcore.PanicLevel
case "Fatal":
return level == zapcore.FatalLevel
case ":Fatal":
return level <= zapcore.FatalLevel
default:
return level >= zapcore.DebugLevel
}
})
}
func loadLoggingConfig() *logging {
cfg, err := ini.Load("config.ini")
if err != nil {
zap.L().Error("Failed to load config", zap.Error(err))
panic(err)
}
logPath := cfg.Section("logging").Key("LogPath").MustString(".")
if len(strings.TrimSpace(logPath)) == 0 {
logPath = "."
}
logPathClean := filepath.Clean(logPath)
consoleLevel := cfg.Section("logging").Key("ConsoleLevel").MustString("Debug:")
if !verifyStr(logLevelStrArr, consoleLevel) {
panic(fmt.Errorf("logging.ConsoleLevel -> %s Not Support", consoleLevel))
}
clsEncode := cfg.Section("logging").Key("ConsoleEncode").String()
clsEncodeCfg := loggingEncodeCfg(cfg, clsEncode)
loggingArr := cfg.Section("logging").Key("Logging").Strings(",")
loggingMap := make(map[string]*logSdt)
for _, v := range loggingArr {
loggingMap[v] = logMap(cfg, v)
}
return &logging{
logPath: logPathClean,
consoleLevel: consoleLevel,
consoleEncode: clsEncodeCfg,
loggingMap: &loggingMap,
}
}
func logMap(cfg *ini.File, pk string) *logSdt {
if !cfg.HasSection(pk) {
panic(fmt.Errorf("logging.Logging -> %s Not Found ", pk))
}
pkSection := cfg.Section(pk)
logFile := pkSection.Key("LogFile").String()
logFileClean := filepath.Clean(logFile)
filePattern := "^[a-zA-Z0-9\\_-]+.log$"
matched, _ := regexp.MatchString(filePattern, logFileClean)
if !matched {
panic(fmt.Errorf("%s.LogFile -> %s Invalied", pk, logFile))
}
logLevel := pkSection.Key("LogLevel").MustString("Debug:")
if !verifyStr(logLevelStrArr, logLevel) {
panic(fmt.Errorf("%s.LogLevel -> %s Not Support", pk, logLevel))
}
filters := pkSection.Key("Filters").Strings(",")
logRuleStr := pkSection.Key("LogRule").String()
logRuleCfg := loggingRuleCfg(cfg, logRuleStr)
logEncodeStr := pkSection.Key("LogEncode").String()
logEncoderCfg := loggingEncodeCfg(cfg, logEncodeStr)
return &logSdt{
logFile: logFileClean,
logLevel: logLevel,
filters: &filters,
logRule: logRuleCfg,
logEncode: logEncoderCfg,
}
}
func loggingEncodeCfg(cfg *ini.File, pk string) *logEncoder {
if len(strings.TrimSpace(pk)) == 0 {
return &logEncoder{
encodeTime: "ISO8601TimeEncoder",
timeKey: "Time",
encodeLevel: "CapitalLevelEncoder",
encodeDuration: "SecondsDurationEncoder",
encodeCaller: "ShortCallerEncoder",
encode: "Console",
}
}
if !cfg.HasSection(pk) {
panic(fmt.Errorf("config %s Not Found", pk))
}
pkSection := cfg.Section(pk)
encodeLevel := pkSection.Key("EncodeLevel").MustString("CapitalLevelEncoder")
if !verifyStr(encodeLevelStrArr, encodeLevel) {
panic(fmt.Errorf("%s.EncodeLevel -> %s Not Support", pk, encodeLevel))
}
encodeDuration := pkSection.Key("EncodeDuration").MustString("SecondsDurationEncoder")
if !verifyStr(encodeDurationStrArr, encodeDuration) {
panic(fmt.Errorf("%s.EncodeDuration -> %s Not Support", pk, encodeDuration))
}
encodeCaller := pkSection.Key("EncodeCaller").MustString("ShortCallerEncoder")
if !verifyStr(encodeCallerStrArr, encodeCaller) {
panic(fmt.Errorf("%s.EncodeCaller -> %s Not Support", pk, encodeCaller))
}
encode := pkSection.Key("Encode").MustString("Console")
if !verifyStr(encodeStrArr, encode) {
panic(fmt.Errorf("%s.Encode -> %s Not Support", pk, encode))
}
return &logEncoder{
encodeTime: pkSection.Key("EncodeTime").MustString("ISO8601TimeEncoder"),
timeKey: pkSection.Key("TimeKey").MustString("Time"),
encodeLevel: encodeLevel,
encodeDuration: encodeDuration,
encodeCaller: encodeCaller,
encode: encode,
}
}
func loggingRuleCfg(cfg *ini.File, pk string) *logRule {
if len(strings.TrimSpace(pk)) == 0 {
return &logRule{
maxSize: 100,
maxAge: 10,
maxBackups: 10,
localTime: false,
compress: false,
}
}
if !cfg.HasSection(pk) {
panic(fmt.Errorf("config %s Not Found", pk))
}
pkSection := cfg.Section(pk)
return &logRule{
maxSize: pkSection.Key("MaxSize").MustInt(100),
maxAge: pkSection.Key("MaxAge").MustInt(10),
maxBackups: pkSection.Key("MaxBackups").MustInt(10),
localTime: pkSection.Key("LocalTime").MustBool(false),
compress: pkSection.Key("Compress").MustBool(false),
}
}
func verifyStr(arr []string, str string) bool {
for _, v := range arr {
if strings.EqualFold(v, str) {
return true
}
}
return false
}
func (c customCore) With(fields []zapcore.Field) zapcore.Core {
clone := c.clone()
addFields(clone.enc, fields)
return clone
}
func (c customCore) Check(ent zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry {
_, file, _, ok := runtime.Caller(4)
if !ok {
return nil
}
fileName := file[strings.LastIndex(file, "/")+1 : strings.LastIndex(file, ".")]
flag := false
if c.filter == nil || len(*c.filter) == 0 {
flag = true
}
for _, str := range *c.filter {
if len(strings.TrimSpace(str)) == 0 {
continue
}
keyWord := trimFilter(str)
if strings.HasPrefix(str, "!:") {
if strings.HasSuffix(str, ":") {
// !:aa:
flag = !strings.Contains(fileName, keyWord)
} else {
// !:aa
flag = !strings.HasSuffix(fileName, keyWord)
}
} else if strings.HasPrefix(str, "!") {
if strings.HasSuffix(str, ":") {
// !aa:
flag = !strings.HasPrefix(fileName, keyWord)
} else {
// !aa
flag = !strings.EqualFold(fileName, keyWord)
}
} else if strings.HasPrefix(str, ":") {
if strings.HasSuffix(str, ":") {
// :aa:
flag = strings.Contains(fileName, keyWord)
} else {
// :aa
flag = strings.HasSuffix(fileName, keyWord)
}
} else if strings.HasSuffix(str, ":") {
// aa:
flag = strings.HasPrefix(fileName, keyWord)
} else {
// aa
flag = strings.EqualFold(fileName, keyWord)
}
if flag {
break
}
}
if c.Enabled(ent.Level) && flag {
return ce.AddCore(ent, c)
}
return ce
}
func trimFilter(str string) string {
replace := ""
if strings.HasPrefix(str, "!:") {
replace = str[2:]
} else if strings.HasPrefix(str, ":") {
replace = str[1:]
}
if strings.HasSuffix(replace, ":") {
replace = replace[:len(replace)-1]
}
if len(strings.TrimSpace(replace)) == 0 {
panic(fmt.Errorf("Logging Filter -> %s Parttern Is Invalid ", str))
}
return replace
}
func (c customCore) Write(ent zapcore.Entry, fields []zapcore.Field) error {
buf, err := c.enc.EncodeEntry(ent, fields)
if err != nil {
return err
}
_, err = c.out.Write(buf.Bytes())
buf.Free()
if err != nil {
return err
}
if ent.Level > zapcore.ErrorLevel {
_ = c.Sync()
}
return nil
}
func (c customCore) Sync() error {
return c.out.Sync()
}
func newCore(enc *zapcore.Encoder, ws *zapcore.WriteSyncer, enab zapcore.LevelEnabler, filter *[]string) zapcore.Core {
return &customCore{
LevelEnabler: enab,
enc: *enc,
out: *ws,
filter: filter,
}
}
func addFields(enc zapcore.ObjectEncoder, fields []zapcore.Field) {
for i := range fields {
fields[i].AddTo(enc)
}
}
func (c *customCore) clone() *customCore {
return &customCore{
LevelEnabler: c.LevelEnabler,
enc: c.enc.Clone(),
out: c.out,
filter: c.filter,
}
}
在项目中,需要打印日志的地方的使用:
import (
"go.uber.org/zap"
)
func TestLog() {
zap.L().Info("test Info")
zap.L().Error("test error")
zap.L().Debug("test debug")
...
}