Go
/*
# 版权所有 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()
}
调用:
Go
// 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格式输出
- 异步队列写入
- 告警回调机制
核心特性:
- 线程安全的单例模式
- 支持按日期、大小自动分割日志文件
- 可配置日志保留天数
- 支持压缩归档旧日志
- 包含调用堆栈信息
- 协程安全的写入队列
使用示例展示了如何注册脱敏回调、告警回调,以及通过不同模块记录日志。该工具适用于需要结构化日志、敏感信息保护和资源监控的中大型项目。