package loggerx import ( "compress/gzip" "io" "os" "time" ) type loggerOption struct { prefix string // 日志前缀 format Format // json / text dir string // 文件目录 isGinLog bool isGid bool isPrintFile bool asGlobalLog bool // 是否接管全局 stdlib log(默认 false) writeType writeType // 是否异步罗盘 traceField string // trace字段 errorToInfo bool // 错误日志是否写入info日志 days int // 日志保存天数 drivers []io.Writer // 文件落盘驱动器 fileSplit FileSplit // 文件切割规则 sizeSplit int // 根据文件大小切割(字节,<=0 不切割) compress bool // 归档文件是否压缩为 .gz compressLvl int // gzip 压缩级别 flushEvery time.Duration // 定时刷盘间隔,0 表示不开启 minLevel Level // 最低输出级别 onError func(error) // 内部错误回调 timeZone *time.Location // 时区 escapeHTML bool expandData map[string]string // 扩展字段 } type writeType uint8 const ( // 0.默认(同步) 1.指定同步 2.指定异步 writeTypeDefault writeType = iota writeTypeSync writeTypeAsync ) func defaultOptions() loggerOption { return loggerOption{ isGinLog: false, // 默认不接管 gin 写手:非 Gin 服务不该被改全局状态 isGid: true, isPrintFile: true, writeType: writeTypeDefault, // 默认同步 format: FormatJSON, dir: "./log", traceField: "trace_id", days: 7, fileSplit: FileSplitTimeE, compress: true, compressLvl: gzip.BestSpeed, timeZone: time.Local, escapeHTML: true, expandData: make(map[string]string), } } type Option func(*loggerOption) // trace字段 func SetTraceField(traceField string) Option { return func(o *loggerOption) { o.traceField = traceField } } // 1.info 2.error 3.debug 4.warn 5.fatal // 附加字段 func SetExpandData(key string, value string) Option { return func(o *loggerOption) { if o.expandData == nil { o.expandData = make(map[string]string) } o.expandData[key] = value } } // 是否异步写入 func SetWriteAsync() Option { return func(o *loggerOption) { o.writeType = writeTypeAsync } } // 打印到控制台 func SetToConsole() Option { return func(o *loggerOption) { o.drivers = append(o.drivers, os.Stdout) } } // 错误日志是否写入info日志 func SetErrorToInfo() Option { return func(o *loggerOption) { o.errorToInfo = true } } // 日志前缀 // 会加在每行日志最前面(JSON 与 text 都生效),便于多实例共用一个目录时区分来源 func SetPrefix(prefix string) Option { return func(o *loggerOption) { o.prefix = prefix } } // 输出格式 type Format string const ( // FormatJSON 每行一条 JSON(默认,推荐给采集端) FormatJSON Format = "json" // FormatText 每行一条紧凑文本,适合人直接看 FormatText Format = "text" ) // 日志格式(默认 FormatJSON) // 传非法值时保持原值,不会静默切到别的格式 func SetFormat(format Format) Option { return func(o *loggerOption) { if format == FormatJSON || format == FormatText { o.format = format } } } // 设置是否打印到文件 func SetPrintFile(print bool) Option { return func(o *loggerOption) { o.isPrintFile = print } } // 是否保存gin的日志 func SetGinLog(open bool) Option { return func(o *loggerOption) { o.isGinLog = open } } // 日志级别。级别低于设定值的日志会被直接丢弃,不落盘 type Level uint8 const ( LevelDebug Level = iota LevelInfo LevelWarn LevelError // LevelOff 关闭所有级别(连 error 也不写) LevelOff ) // String 便于打印 func (lv Level) String() string { switch lv { case LevelDebug: return "debug" case LevelInfo: return "info" case LevelWarn: return "warn" case LevelError: return "error" case LevelOff: return "off" default: return "unknown" } } // SetMinLevel 设置最低输出级别(默认 LevelDebug,即全部输出) // // 生产环境通常设成 LevelInfo:这样代码里的 Debug 调用不会白算一遍 // JSON、也不会落盘占空间 func SetMinLevel(lv Level) Option { return func(o *loggerOption) { o.minLevel = lv } } // SetErrorHandler 注册内部错误处理回调(默认 nil) // // 用于把「日志库自身的故障」暴露给运维:磁盘满、句柄失效、归档压缩失败、 // 定时刷盘失败等。不注册时这些错误只体现在返回值里,而调用方通常忽略返回值, // 问题就完全不可见了。 // // 建议接到告警通道或一个「不会失败」的输出(例如 stderr): // // loggerx.NewLogger(ctx, loggerx.SetErrorHandler(func(err error) { // fmt.Fprintln(os.Stderr, "loggerx:", err) // })) // // 回调在写日志的调用栈上同步执行,务必保持轻量, // 且不要在里面再调用本实例的日志方法(可能递归) func SetErrorHandler(fn func(error)) Option { return func(o *loggerOption) { o.onError = fn } } // 是否把全局标准库 log 的输出接管到本实例(默认 false,不接管) // // 默认不接管是刻意的:日志库在 import / NewLogger 时静默劫持宿主全局 log // 会带来两个生产事故场景 —— 多个实例时后建的把先建的顶掉; // Close 之后全局 log 仍指向已关闭实例,之后的日志被静默丢弃。 // 确需接管(例如老代码大量使用 log.Printf)时再显式打开。 func SetAsGlobalLog() Option { return func(o *loggerOption) { o.asGlobalLog = true } } // 文件路径 func SetDir(dir string) Option { return func(o *loggerOption) { if dir != "" { o.dir = dir } } } // 保存goroutine的ID信息 func SetGID(open bool) Option { return func(o *loggerOption) { o.isGid = open } } // 日志保存天数 func SetDays(days int) Option { return func(o *loggerOption) { o.days = days } } // 设置时区 func SetTimeZone(loc *time.Location) Option { return func(o *loggerOption) { o.timeZone = loc } } // 文件额外的驱动 func SetExtraDriver(ds ...io.Writer) Option { return func(o *loggerOption) { for _, d := range ds { if d != nil { o.drivers = append(o.drivers, d) } } } } // 文件切割规则 // 1.文件大小 // 2.时间A(年/月/日/时) // 3.时间B(年/月-日) // 4.时间C(年-月-日-时) // 5.时间D(年-月-日) func SetFileSplit(split FileSplit) Option { return func(o *loggerOption) { o.fileSplit = split } } type FileSplit string const ( FileSplitNone FileSplit = "none" // 不切割 FileSplitTimeA FileSplit = "timeA" // (年/月/日/时) FileSplitTimeB FileSplit = "timeB" // (年/月/日) FileSplitTimeC FileSplit = "timeC" // (年/月-日) FileSplitTimeD FileSplit = "timeD" // (年-月-日-时) FileSplitTimeE FileSplit = "timeE" // (年-月-日) ) // 根据文件大小切割 // m 为单个文件的大小上限(字节);<=0 表示不按大小切割 // 超过上限时当前文件会被改名归档(并可选压缩),随后写入新文件 func SetSizeSplit(m int) Option { return func(o *loggerOption) { o.sizeSplit = m } } // 归档文件是否压缩成 .gz(默认开启) // 压缩在后台协程完成,不阻塞写入 func SetCompress(open bool) Option { return func(o *loggerOption) { o.compress = open } } // 压缩级别 1~9,数字越大压缩率越高、CPU 开销越大,默认 gzip.BestSpeed func SetCompressLevel(level int) Option { return func(o *loggerOption) { if level >= gzip.HuffmanOnly && level <= gzip.BestCompression { o.compressLvl = level } } } // 定时刷盘间隔(默认 0 = 不开启) // 开启后会按间隔把内存缓冲刷到磁盘,把「进程崩溃时可能丢失的数据量」 // 从「最多 32KB 缓冲」压缩到「一个间隔内产生的日志量」 func SetFlushInterval(d time.Duration) Option { return func(o *loggerOption) { if d > 0 { o.flushEvery = d } } } func SetEscapeHTML(b bool) Option { return func(o *loggerOption) { o.escapeHTML = b } }