2024-01-22 18:05:51 +08:00
|
|
|
|
package loggerx
|
|
|
|
|
|
|
2024-06-20 23:59:13 +08:00
|
|
|
|
import (
|
2026-09-13 21:53:37 +08:00
|
|
|
|
"compress/gzip"
|
2024-06-20 23:59:13 +08:00
|
|
|
|
"io"
|
|
|
|
|
|
"os"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
)
|
2024-04-15 23:26:55 +08:00
|
|
|
|
|
2024-01-22 18:05:51 +08:00
|
|
|
|
type loggerOption struct {
|
2024-01-23 00:12:08 +08:00
|
|
|
|
prefix string // 日志前缀
|
2026-09-14 00:14:30 +08:00
|
|
|
|
format Format // json / text
|
2024-01-23 00:12:08 +08:00
|
|
|
|
dir string // 文件目录
|
|
|
|
|
|
isGinLog bool
|
|
|
|
|
|
isGid bool
|
2024-07-09 15:36:11 +08:00
|
|
|
|
isPrintFile bool
|
2026-09-14 00:14:30 +08:00
|
|
|
|
asGlobalLog bool // 是否接管全局 stdlib log(默认 false)
|
2024-11-22 20:08:25 +08:00
|
|
|
|
writeType writeType // 是否异步罗盘
|
2024-06-20 23:59:13 +08:00
|
|
|
|
traceField string // trace字段
|
|
|
|
|
|
errorToInfo bool // 错误日志是否写入info日志
|
|
|
|
|
|
days int // 日志保存天数
|
|
|
|
|
|
drivers []io.Writer // 文件落盘驱动器
|
|
|
|
|
|
fileSplit FileSplit // 文件切割规则
|
2026-09-13 21:53:37 +08:00
|
|
|
|
sizeSplit int // 根据文件大小切割(字节,<=0 不切割)
|
|
|
|
|
|
compress bool // 归档文件是否压缩为 .gz
|
|
|
|
|
|
compressLvl int // gzip 压缩级别
|
|
|
|
|
|
flushEvery time.Duration // 定时刷盘间隔,0 表示不开启
|
2026-09-14 00:14:30 +08:00
|
|
|
|
minLevel Level // 最低输出级别
|
|
|
|
|
|
onError func(error) // 内部错误回调
|
2024-06-20 23:59:13 +08:00
|
|
|
|
timeZone *time.Location // 时区
|
2025-01-15 15:29:45 +08:00
|
|
|
|
escapeHTML bool
|
2025-06-10 15:41:44 +08:00
|
|
|
|
expandData map[string]string // 扩展字段
|
2024-01-22 18:05:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-11-22 20:08:25 +08:00
|
|
|
|
type writeType uint8
|
|
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
|
// 0.默认(同步) 1.指定同步 2.指定异步
|
|
|
|
|
|
writeTypeDefault writeType = iota
|
|
|
|
|
|
writeTypeSync
|
|
|
|
|
|
writeTypeAsync
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2024-01-22 18:05:51 +08:00
|
|
|
|
func defaultOptions() loggerOption {
|
|
|
|
|
|
return loggerOption{
|
2026-09-14 00:14:30 +08:00
|
|
|
|
isGinLog: false, // 默认不接管 gin 写手:非 Gin 服务不该被改全局状态
|
2024-07-09 15:36:11 +08:00
|
|
|
|
isGid: true,
|
|
|
|
|
|
isPrintFile: true,
|
2024-11-22 20:08:25 +08:00
|
|
|
|
writeType: writeTypeDefault, // 默认同步
|
2026-09-14 00:14:30 +08:00
|
|
|
|
format: FormatJSON,
|
2024-07-09 15:36:11 +08:00
|
|
|
|
dir: "./log",
|
|
|
|
|
|
traceField: "trace_id",
|
|
|
|
|
|
days: 7,
|
|
|
|
|
|
fileSplit: FileSplitTimeE,
|
2026-09-13 21:53:37 +08:00
|
|
|
|
compress: true,
|
|
|
|
|
|
compressLvl: gzip.BestSpeed,
|
2024-07-09 15:36:11 +08:00
|
|
|
|
timeZone: time.Local,
|
2025-01-15 15:29:45 +08:00
|
|
|
|
escapeHTML: true,
|
2025-06-10 15:41:44 +08:00
|
|
|
|
expandData: make(map[string]string),
|
2024-01-22 18:05:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type Option func(*loggerOption)
|
|
|
|
|
|
|
2024-01-23 00:12:08 +08:00
|
|
|
|
// trace字段
|
|
|
|
|
|
func SetTraceField(traceField string) Option {
|
|
|
|
|
|
return func(o *loggerOption) {
|
|
|
|
|
|
o.traceField = traceField
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-23 19:17:40 +08:00
|
|
|
|
// 1.info 2.error 3.debug 4.warn 5.fatal
|
|
|
|
|
|
|
2025-06-10 15:41:44 +08:00
|
|
|
|
// 附加字段
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-11-22 20:08:25 +08:00
|
|
|
|
// 是否异步写入
|
|
|
|
|
|
func SetWriteAsync() Option {
|
|
|
|
|
|
return func(o *loggerOption) {
|
|
|
|
|
|
o.writeType = writeTypeAsync
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-06-20 23:59:13 +08:00
|
|
|
|
// 打印到控制台
|
|
|
|
|
|
func SetToConsole() Option {
|
|
|
|
|
|
return func(o *loggerOption) {
|
|
|
|
|
|
o.drivers = append(o.drivers, os.Stdout)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-01-23 00:12:08 +08:00
|
|
|
|
// 错误日志是否写入info日志
|
|
|
|
|
|
func SetErrorToInfo() Option {
|
|
|
|
|
|
return func(o *loggerOption) {
|
|
|
|
|
|
o.errorToInfo = true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-14 00:14:30 +08:00
|
|
|
|
// 日志前缀
|
|
|
|
|
|
// 会加在每行日志最前面(JSON 与 text 都生效),便于多实例共用一个目录时区分来源
|
2024-01-22 18:05:51 +08:00
|
|
|
|
func SetPrefix(prefix string) Option {
|
|
|
|
|
|
return func(o *loggerOption) {
|
|
|
|
|
|
o.prefix = prefix
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-14 00:14:30 +08:00
|
|
|
|
// 输出格式
|
|
|
|
|
|
type Format string
|
|
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
|
// FormatJSON 每行一条 JSON(默认,推荐给采集端)
|
|
|
|
|
|
FormatJSON Format = "json"
|
|
|
|
|
|
// FormatText 每行一条紧凑文本,适合人直接看
|
|
|
|
|
|
FormatText Format = "text"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// 日志格式(默认 FormatJSON)
|
|
|
|
|
|
// 传非法值时保持原值,不会静默切到别的格式
|
|
|
|
|
|
func SetFormat(format Format) Option {
|
2024-01-22 18:05:51 +08:00
|
|
|
|
return func(o *loggerOption) {
|
2026-09-14 00:14:30 +08:00
|
|
|
|
if format == FormatJSON || format == FormatText {
|
|
|
|
|
|
o.format = format
|
|
|
|
|
|
}
|
2024-01-22 18:05:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-07-09 15:36:11 +08:00
|
|
|
|
// 设置是否打印到文件
|
|
|
|
|
|
func SetPrintFile(print bool) Option {
|
|
|
|
|
|
return func(o *loggerOption) {
|
|
|
|
|
|
o.isPrintFile = print
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-01-22 18:05:51 +08:00
|
|
|
|
// 是否保存gin的日志
|
2024-04-15 23:40:55 +08:00
|
|
|
|
func SetGinLog(open bool) Option {
|
2024-01-22 18:05:51 +08:00
|
|
|
|
return func(o *loggerOption) {
|
2024-04-15 23:40:55 +08:00
|
|
|
|
o.isGinLog = open
|
2024-01-22 18:05:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-14 00:14:30 +08:00
|
|
|
|
// 日志级别。级别低于设定值的日志会被直接丢弃,不落盘
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-01-23 00:12:08 +08:00
|
|
|
|
// 文件路径
|
|
|
|
|
|
func SetDir(dir string) Option {
|
2024-01-22 18:05:51 +08:00
|
|
|
|
return func(o *loggerOption) {
|
2024-03-13 16:39:42 +08:00
|
|
|
|
if dir != "" {
|
|
|
|
|
|
o.dir = dir
|
|
|
|
|
|
}
|
2024-01-22 18:05:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 保存goroutine的ID信息
|
2024-04-15 23:40:55 +08:00
|
|
|
|
func SetGID(open bool) Option {
|
2024-01-22 18:05:51 +08:00
|
|
|
|
return func(o *loggerOption) {
|
2024-04-15 23:40:55 +08:00
|
|
|
|
o.isGid = open
|
2024-01-22 18:05:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-01-23 13:37:51 +08:00
|
|
|
|
// 日志保存天数
|
|
|
|
|
|
func SetDays(days int) Option {
|
|
|
|
|
|
return func(o *loggerOption) {
|
|
|
|
|
|
o.days = days
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-04-15 23:26:55 +08:00
|
|
|
|
// 设置时区
|
2024-06-20 23:59:13 +08:00
|
|
|
|
func SetTimeZone(loc *time.Location) Option {
|
2024-04-15 23:26:55 +08:00
|
|
|
|
return func(o *loggerOption) {
|
2024-06-20 23:59:13 +08:00
|
|
|
|
o.timeZone = loc
|
2024-04-15 23:26:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 文件额外的驱动
|
|
|
|
|
|
func SetExtraDriver(ds ...io.Writer) Option {
|
|
|
|
|
|
return func(o *loggerOption) {
|
2024-04-21 12:23:26 +08:00
|
|
|
|
for _, d := range ds {
|
|
|
|
|
|
if d != nil {
|
|
|
|
|
|
o.drivers = append(o.drivers, d)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2024-04-15 23:26:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-01-22 18:05:51 +08:00
|
|
|
|
// 文件切割规则
|
2024-01-23 00:12:08 +08:00
|
|
|
|
// 1.文件大小
|
|
|
|
|
|
// 2.时间A(年/月/日/时)
|
|
|
|
|
|
// 3.时间B(年/月-日)
|
|
|
|
|
|
// 4.时间C(年-月-日-时)
|
|
|
|
|
|
// 5.时间D(年-月-日)
|
2024-05-08 16:33:58 +08:00
|
|
|
|
func SetFileSplit(split FileSplit) Option {
|
|
|
|
|
|
return func(o *loggerOption) {
|
|
|
|
|
|
o.fileSplit = split
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2024-01-23 00:12:08 +08:00
|
|
|
|
|
2024-05-08 16:33:58 +08:00
|
|
|
|
type FileSplit string
|
|
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
|
FileSplitNone FileSplit = "none" // 不切割
|
|
|
|
|
|
FileSplitTimeA FileSplit = "timeA" // (年/月/日/时)
|
|
|
|
|
|
FileSplitTimeB FileSplit = "timeB" // (年/月/日)
|
|
|
|
|
|
FileSplitTimeC FileSplit = "timeC" // (年/月-日)
|
|
|
|
|
|
FileSplitTimeD FileSplit = "timeD" // (年-月-日-时)
|
|
|
|
|
|
FileSplitTimeE FileSplit = "timeE" // (年-月-日)
|
|
|
|
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-09-13 21:53:37 +08:00
|
|
|
|
// 根据文件大小切割
|
|
|
|
|
|
// m 为单个文件的大小上限(字节);<=0 表示不按大小切割
|
|
|
|
|
|
// 超过上限时当前文件会被改名归档(并可选压缩),随后写入新文件
|
2024-05-08 16:33:58 +08:00
|
|
|
|
func SetSizeSplit(m int) Option {
|
|
|
|
|
|
return func(o *loggerOption) {
|
|
|
|
|
|
o.sizeSplit = m
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-01-15 15:29:45 +08:00
|
|
|
|
|
2026-09-13 21:53:37 +08:00
|
|
|
|
// 归档文件是否压缩成 .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
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-01-15 15:29:45 +08:00
|
|
|
|
func SetEscapeHTML(b bool) Option {
|
|
|
|
|
|
return func(o *loggerOption) {
|
|
|
|
|
|
o.escapeHTML = b
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|