go:loghelper

发布时间:2026/8/1 1:48:54
go:loghelper /* # 版权所有 2026 ©涂聚文有限公司™ ® # 许可信息查看言語成了邀功盡責的功臣還需要行爲每日來值班嗎 # 描述 # Author : geovindu,Geovin Du 涂聚文. # IDE : goLang 2024.3.6 go 26.2 # os : windows 10 # database : mysql 9.0 sql server 2019, postgreSQL 17.0 Oracle 21c Neo4j # Datetime : 2026/7/31 21:01 # User : geovindu # Product : GoLand # Project : goalgorithms # File : loghelper.go */ package loghelper import ( archive/zip context encoding/json fmt io math/rand os path/filepath regexp runtime sort strings sync sync/atomic time ) // LogLevel 日志级别 大写导出 type LogLevel int const ( LogLevelOff LogLevel iota LogLevelError LogLevelInfo LogLevelDebug ) // 告警常量 const ( AlertEventDiskOverLimit DISK_OVER_LIMIT AlertEventQueueBlocked QUEUE_BLOCKED ) // DesensitizeFunc 日志脱敏回调定义 type DesensitizeFunc func(level string, module string, msg string) string // AlertCallbackFunc 系统告警回调定义 type AlertCallbackFunc func(eventType string, message string, ext map[string]any) // LogItem 日志队列单元 type LogItem struct { level string module string textData string jsonData map[string]any stackTrace string } // LogHelper 日志核心结构体 type LogHelper struct { logRoot string defaultModule string keepDays int maxFileSizeMB int64 queueMaxSize int queueAlertThreshold int batchWriteCount int enableConsole bool enableJSONFormat bool compressBackupLog bool maxDiskTotalMB float64 levelDiskLimit map[string]float64 globalSamplingRate float64 levelSamplingRate map[string]float64 diskCheckIntervalSec int timeFormatTemplate string minLogLevel LogLevel desensitizeCallback DesensitizeFunc // 脱敏回调 alertCallback AlertCallbackFunc // 告警回调 logQueue chan *LogItem stopCtx context.Context stopCancel context.CancelFunc wg sync.WaitGroup fileLock sync.Mutex lastCleanDay time.Time lastDiskCheckTs int64 pid int colorMap map[string]string } var ( singleton *LogHelper once sync.Once // 随机数全局 rngOnce sync.Once rng *rand.Rand ) // 随机数初始化 func initRng() { seed : time.Now().UnixNano() src : rand.NewSource(seed) rng rand.New(src) } func randFloat() float64 { rngOnce.Do(initRng) return rng.Float64() } // getGoroutineName 获取当前协程名称 func getGoroutineName() string { buf : make([]byte, 64) n : runtime.Stack(buf, false) stackStr : string(buf[:n]) lines : strings.Split(stackStr, \n) if len(lines) 0 { firstLine : lines[0] idx : strings.Index(firstLine, [) if idx 0 { return strings.TrimSpace(firstLine[:idx]) } return strings.TrimSpace(firstLine) } return main } // NewLogHelper 获取日志单例 func NewLogHelper() *LogHelper { once.Do(func() { ctx, cancel : context.WithCancel(context.Background()) singleton LogHelper{ logRoot: filepath.Join(getWorkDir(), Logs), defaultModule: default, keepDays: 7, maxFileSizeMB: 10, queueMaxSize: 10000, queueAlertThreshold: 7000, batchWriteCount: 10, enableConsole: true, enableJSONFormat: false, compressBackupLog: true, maxDiskTotalMB: 500, levelDiskLimit: map[string]float64{ debug: 150, info: 250, error: 100, }, globalSamplingRate: 1.0, levelSamplingRate: map[string]float64{ debug: 1.0, info: 1.0, error: 1.0, }, diskCheckIntervalSec: 60, timeFormatTemplate: 2006-01-02 15:04:05, minLogLevel: LogLevelDebug, logQueue: make(chan *LogItem, 10000), stopCtx: ctx, stopCancel: cancel, pid: os.Getpid(), } singleton.colorMap map[string]string{ DEBUG: \033[34m, INFO: \033[32m, ERROR: \033[31m, RESET: \033[0m, } // Windows关闭ANSI颜色防止控制台空白 if runtime.GOOS windows { singleton.colorMap[DEBUG] singleton.colorMap[INFO] singleton.colorMap[ERROR] singleton.colorMap[RESET] } _ os.MkdirAll(singleton.logRoot, 0755) singleton.startWriterLoop() }) return singleton } // Logger 全局导出单例外部调用入口 var Logger NewLogHelper() func getWorkDir() string { wd, err : os.Getwd() if err ! nil { return . } return wd } var safeModuleReg regexp.MustCompile([^\w\-]) // SafeModuleName 模块名清洗 func (lh *LogHelper) SafeModuleName(module string) string { module strings.TrimSpace(module) if module { return lh.defaultModule } return safeModuleReg.ReplaceAllString(module, _) } // GetModuleLogDir 获取模块日志目录 func (lh *LogHelper) GetModuleLogDir(module string) string { mod : lh.SafeModuleName(module) return filepath.Join(lh.logRoot, mod) } // 两个关键注册方法完整保留 // RegisterDesensitize 注册日志脱敏回调 func (lh *LogHelper) RegisterDesensitize(fn DesensitizeFunc) { lh.desensitizeCallback fn } // RegisterAlertCallback 注册告警回调 func (lh *LogHelper) RegisterAlertCallback(fn AlertCallbackFunc) { lh.alertCallback fn } // // TriggerAlert 内部触发告警 func (lh *LogHelper) TriggerAlert(eventType string, msg string, ext map[string]any) { if lh.alertCallback nil { return } defer func() { recover() }() lh.alertCallback(eventType, msg, ext) } // SetLogLevel 设置日志级别 func (lh *LogHelper) SetLogLevel(level LogLevel) { lh.minLogLevel level } // SwitchJSONFormat 开启/关闭JSON格式化 func (lh *LogHelper) SwitchJSONFormat(enable bool) { lh.enableJSONFormat enable } // SwitchConsole 开启/关闭控制台打印 func (lh *LogHelper) SwitchConsole(enable bool) { lh.enableConsole enable } // SetSamplingRate 设置级别采样率 func (lh *LogHelper) SetSamplingRate(level string, rate float64) { if rate 0 { rate 0 } if rate 1 { rate 1 } lh.levelSamplingRate[strings.ToLower(level)] rate } // NeedSample 判断是否需要打印本条日志 func (lh *LogHelper) NeedSample(level string) bool { lv : strings.ToLower(level) lvRate : lh.levelSamplingRate[lv] finalRate : lh.globalSamplingRate * lvRate if finalRate 1.0 { return true } return randFloat() finalRate } // getDirTotalSizeMB 统计文件夹大小 func (lh *LogHelper) getDirTotalSizeMB(folder string, filterPrefix string) float64 { var total int64 _ filepath.Walk(folder, func(path string, info os.FileInfo, err error) error { if err ! nil || info.IsDir() { return nil } base : filepath.Base(path) if !strings.HasSuffix(base, .log) !strings.HasSuffix(base, .zip) { return nil } if filterPrefix ! !strings.HasPrefix(base, filterPrefix_) { return nil } total info.Size() return nil }) return float64(total) / 1024 / 1024 } // getSortedLogFiles 获取按修改时间升序日志文件 func (lh *LogHelper) getSortedLogFiles(folder string, filterPrefix string) []string { var list []struct { mtime int64 path string } _ filepath.Walk(folder, func(path string, info os.FileInfo, err error) error { if err ! nil || info.IsDir() { return nil } base : filepath.Base(path) if !strings.HasSuffix(base, .log) !strings.HasSuffix(base, .zip) { return nil } if filterPrefix ! !strings.HasPrefix(base, filterPrefix_) { return nil } list append(list, struct { mtime int64 path string }{info.ModTime().Unix(), path}) return nil }) sort.Slice(list, func(i, j int) bool { return list[i].mtime list[j].mtime }) res : make([]string, 0, len(list)) for _, v : range list { res append(res, v.path) } return res } // tryFreeDiskSpace 磁盘清理 func (lh *LogHelper) tryFreeDiskSpace(level string) bool { for { var usage float64 var limit float64 if level ! { usage lh.getDirTotalSizeMB(lh.logRoot, level) limit lh.levelDiskLimit[level] if usage limit { return true } } else { usage lh.getDirTotalSizeMB(lh.logRoot, ) limit lh.maxDiskTotalMB if usage limit { return true } } files : lh.getSortedLogFiles(lh.logRoot, level) if len(files) 0 { return false } deleted : false for _, fp : range files { if strings.HasSuffix(fp, .zip) { if err : os.Remove(fp); err nil { fmt.Fprintf(os.Stderr, [LogHelper CLEAN] 删除老旧压缩包%s\n, filepath.Base(fp)) deleted true break } } } if deleted { continue } for _, fp : range files { if strings.HasSuffix(fp, .log) { if err : os.Remove(fp); err nil { fmt.Fprintf(os.Stderr, [LogHelper CLEAN] 删除老旧日志%s\n, filepath.Base(fp)) deleted true break } } } if !deleted { return false } } } // CheckDiskLimit 磁盘超限检测 func (lh *LogHelper) CheckDiskLimit(level string) bool { nowTs : time.Now().Unix() last : atomic.LoadInt64(lh.lastDiskCheckTs) if nowTs-last int64(lh.diskCheckIntervalSec) { return false } atomic.StoreInt64(lh.lastDiskCheckTs, nowTs) lv : strings.ToLower(level) lvUsage : lh.getDirTotalSizeMB(lh.logRoot, lv) lvLimit : lh.levelDiskLimit[lv] totalUsage : lh.getDirTotalSizeMB(lh.logRoot, ) over : false if lvUsage lvLimit { lh.TriggerAlert(AlertEventDiskOverLimit, fmt.Sprintf(日志【%s】分级磁盘占用超限(%.2fMB/%.2fMB)开始清理, level, lvUsage, lvLimit), map[string]any{ level: level, usage_mb: lvUsage, limit_mb: lvLimit, total_usage: totalUsage, }) if !lh.tryFreeDiskSpace(lv) { over true } } else if totalUsage lh.maxDiskTotalMB { lh.TriggerAlert(AlertEventDiskOverLimit, fmt.Sprintf(日志总磁盘占用超限(%.2fMB/%.2fMB)开始清理, totalUsage, lh.maxDiskTotalMB), map[string]any{ usage_mb: totalUsage, limit_mb: lh.maxDiskTotalMB, }) if !lh.tryFreeDiskSpace() { over true } } if over { fmt.Fprintf(os.Stderr, [LogHelper FATAL] 磁盘清理后依旧超限暂停持久化【%s】日志\n, level) } return over } // cleanOldLogs 清理过期日志 func (lh *LogHelper) cleanOldLogs() { expire : time.Now().AddDate(0, 0, -lh.keepDays) _ filepath.Walk(lh.logRoot, func(path string, info os.FileInfo, err error) error { if err ! nil || info.IsDir() { return nil } base : filepath.Base(path) if !strings.HasSuffix(base, .log) !strings.HasSuffix(base, .zip) { return nil } if info.ModTime().Before(expire) { _ os.Remove(path) } return nil }) } // getLogPath 获取日志文件路径 func (lh *LogHelper) getLogPath(level, module string) string { modDir : lh.GetModuleLogDir(module) _ os.MkdirAll(modDir, 0755) dateStr : time.Now().Format(20060102) return filepath.Join(modDir, strings.ToLower(level)_dateStr.log) } // splitLogFile 日志切割 func (lh *LogHelper) splitLogFile(logPath string) { lh.fileLock.Lock() defer lh.fileLock.Unlock() stat, err : os.Stat(logPath) if err ! nil { return } if stat.Size() lh.maxFileSizeMB*1024*1024 { return } base : strings.TrimSuffix(logPath, .log) bakPath : fmt.Sprintf(%s_%s.log, base, time.Now().Format(150405)) _ os.Rename(logPath, bakPath) if lh.compressBackupLog { go lh.compressFile(bakPath) } } // compressFile 压缩备份日志 func (lh *LogHelper) compressFile(src string) { if _, err : os.Stat(src); err ! nil { return } zipPath : strings.TrimSuffix(src, .log) .zip fw, err : os.Create(zipPath) if err ! nil { return } defer fw.Close() zw : zip.NewWriter(fw) defer zw.Close() fIn, err : os.Open(src) if err ! nil { return } defer fIn.Close() w, err : zw.Create(filepath.Base(src)) if err ! nil { return } _, _ io.Copy(w, fIn) _ os.Remove(src) } // getCallerInfo 获取调用位置 func getCallerInfo(skip int) (fileName, funcName string, line int) { pc, file, lineNo, ok : runtime.Caller(skip) if !ok { return unknown, unknown, 0 } fileName filepath.Base(file) fn : runtime.FuncForPC(pc) if fn ! nil { funcName fn.Name() } return fileName, funcName, lineNo } // GetStackTrace 安全获取异常堆栈 func GetStackTrace(err error, skip int) string { if err nil { return } buf : make([]byte, 4096) n : runtime.Stack(buf, false) return fmt.Sprintf(错误信息%v\n调用堆栈\n%s, err, string(buf[:n])) } // buildLogData 组装日志文本/JSON func (lh *LogHelper) buildLogData(level, module, msg, stack string) (string, map[string]any) { modSafe : lh.SafeModuleName(module) // 脱敏回调自动执行 if lh.desensitizeCallback ! nil { defer func() { recover() }() msg lh.desensitizeCallback(level, modSafe, msg) } now : time.Now() timeStr : now.Format(lh.timeFormatTemplate) timestamp : float64(now.UnixNano()) / 1e9 goroutineName : getGoroutineName() fileName, funcName, lineNo : getCallerInfo(4) if lh.enableJSONFormat { jsonMap : map[string]any{ time: timeStr, timestamp: timestamp, level: level, module: modSafe, pid: lh.pid, thread: goroutineName, file: fileName, function: funcName, line: lineNo, message: msg, stack_trace: stack, } return , jsonMap } text : fmt.Sprintf([%s] [%s] [MODULE:%s] [PID:%d] [G:%s] [%s·%d行·%s] %s, timeStr, level, modSafe, lh.pid, goroutineName, funcName, lineNo, fileName, msg) if stack ! { text \n stack } text \n return text, nil } // EnqueueLog 日志入队 func (lh *LogHelper) EnqueueLog(level, module, msg, stack string) { if !lh.NeedSample(level) { return } textData, jsonData : lh.buildLogData(level, module, msg, stack) // 控制台输出独立执行不受队列阻塞 if lh.enableConsole { var consoleStr string if jsonData ! nil { buf, err : json.MarshalIndent(jsonData, , ) if err ! nil { consoleStr strings.TrimSuffix(textData, \n) } else { consoleStr string(buf) } } else { consoleStr strings.TrimSuffix(textData, \n) } color : lh.colorMap[level] reset : lh.colorMap[RESET] fmt.Printf(%s%s%s\n, color, consoleStr, reset) } item : LogItem{ level: strings.ToLower(level), module: module, textData: textData, jsonData: jsonData, stackTrace: stack, } // 非阻塞入队队列满丢弃 select { case lh.logQueue - item: default: fmt.Fprintln(os.Stderr, [LogHelper WARN] 队列已满丢弃持久化日志) } } // startWriterLoop 后台持久化协程 func (lh *LogHelper) startWriterLoop() { lh.wg.Add(1) go func() { defer lh.wg.Done() buffer : make(map[string][]*LogItem) ticker : time.NewTicker(800 * time.Millisecond) defer ticker.Stop() for { select { case -lh.stopCtx.Done(): lh.FlushAllBuffer(buffer) return case item : -lh.logQueue: key : fmt.Sprintf(%s|%s, item.level, item.module) buffer[key] append(buffer[key], item) if len(buffer[key]) lh.batchWriteCount { lh.FlushBuffer(buffer, key) } case -ticker.C: if len(buffer) 0 { lh.FlushAllBuffer(buffer) } } } }() } // FlushBuffer 刷新单组缓存写入文件 func (lh *LogHelper) FlushBuffer(buffer map[string][]*LogItem, key string) { items, ok : buffer[key] if !ok || len(items) 0 { return } parts : strings.Split(key, |) level, module : parts[0], parts[1] if lh.CheckDiskLimit(level) { delete(buffer, key) return } logPath : lh.getLogPath(level, module) lh.splitLogFile(logPath) var writeBuilder strings.Builder for _, it : range items { if it.jsonData ! nil { buf, err : json.Marshal(it.jsonData) if err nil { writeBuilder.Write(buf) writeBuilder.WriteByte(\n) } } else { writeBuilder.WriteString(it.textData) } } f, err : os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err ! nil { fmt.Fprintf(os.Stderr, [LogHelper] 写入失败 level:%s module:%s err:%v\n, level, module, err) delete(buffer, key) return } _, _ f.WriteString(writeBuilder.String()) _ f.Close() delete(buffer, key) nowDay : time.Now().Truncate(24 * time.Hour) if !nowDay.Equal(lh.lastCleanDay) { lh.cleanOldLogs() lh.lastCleanDay nowDay } } // FlushAllBuffer 刷新全部缓存 func (lh *LogHelper) FlushAllBuffer(buffer map[string][]*LogItem) { for k : range buffer { lh.FlushBuffer(buffer, k) } } // Debug 普通调试日志 func (lh *LogHelper) Debug(msg string) { if lh.minLogLevel LogLevelDebug { return } lh.EnqueueLog(DEBUG, lh.defaultModule, msg, ) } // Info 普通信息日志 func (lh *LogHelper) Info(msg string) { if lh.minLogLevel LogLevelInfo { return } lh.EnqueueLog(INFO, lh.defaultModule, msg, ) } // Error 普通错误日志 func (lh *LogHelper) Error(msg string, err error) { if lh.minLogLevel LogLevelError { return } stack : if err ! nil { stack GetStackTrace(err, 4) } lh.EnqueueLog(ERROR, lh.defaultModule, msg, stack) } // DebugMod 模块调试日志 func (lh *LogHelper) DebugMod(module string, msg string) { if lh.minLogLevel LogLevelDebug { return } lh.EnqueueLog(DEBUG, module, msg, ) } // InfoMod 模块信息日志 func (lh *LogHelper) InfoMod(module string, msg string) { if lh.minLogLevel LogLevelInfo { return } lh.EnqueueLog(INFO, module, msg, ) } // ErrorMod 模块错误日志 func (lh *LogHelper) ErrorMod(module string, msg string, err error) { if lh.minLogLevel LogLevelError { return } stack : if err ! nil { stack GetStackTrace(err, 4) } lh.EnqueueLog(ERROR, module, msg, stack) } // Shutdown 优雅关闭日志 func (lh *LogHelper) Shutdown() { lh.stopCancel() lh.wg.Wait() } // 全局快捷导出函数 func Debug(msg string) { Logger.Debug(msg) } func Info(msg string) { Logger.Info(msg) } func Error(msg string, err error) { Logger.Error(msg, err) } func DebugMod(module string, msg string) { Logger.DebugMod(module, msg) } func InfoMod(module string, msg string) { Logger.InfoMod(module, msg) } func ErrorMod(module string, msg string, err error) { Logger.ErrorMod(module, msg, err) } func Shutdown() { Logger.Shutdown() }调用// 1. 注册脱敏回调 mobileReg : regexp.MustCompile(1[3-9]\d{9}) loghelper.Logger.RegisterDesensitize(func(level, module, msg string) string { return mobileReg.ReplaceAllStringFunc(msg, func(s string) string { return s[:3] **** s[7:] }) }) // 2. 注册告警回调 loghelper.Logger.RegisterAlertCallback(func(eventType, msg string, ext map[string]any) { fmt.Printf(\n【ALERT】%s | %s | %v\n, eventType, msg, ext) }) // 3. 设置采样率debug 20%采样 loghelper.Logger.SetSamplingRate(debug, 0.2) // 旧接口默认模块 default loghelper.Info(默认模块启动手机号13800138000) // 新接口按业务模块隔离日志 loghelper.InfoMod(user_service, 用户注册手机号13900139000) loghelper.DebugMod(order_service, 订单创建请求) errTest : fmt.Errorf(div zero) loghelper.ErrorMod(pay_service, 支付异常, errTest) // 开启JSON格式 loghelper.Logger.SwitchJSONFormat(true) //SwitchJSONFormat(true) loghelper.InfoMod(goods_service, 商品库存查询) fmt.Println(按回车退出...) var in string fmt.Scanln(in) loghelper.Shutdown()Go语言实现的日志工具包loghelper主要功能包括多级日志记录DEBUG/INFO/ERROR模块化日志隔离日志脱敏处理支持手机号等敏感信息掩码磁盘空间监控与自动清理日志采样率控制支持JSON格式输出异步队列写入告警回调机制核心特性线程安全的单例模式支持按日期、大小自动分割日志文件可配置日志保留天数支持压缩归档旧日志包含调用堆栈信息协程安全的写入队列使用示例展示了如何注册脱敏回调、告警回调以及通过不同模块记录日志。该工具适用于需要结构化日志、敏感信息保护和资源监控的中大型项目。