326 lines
8.0 KiB
Go
326 lines
8.0 KiB
Go
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
|
||
}
|
||
}
|