go:loghelper

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,主要功能包括:

  1. 多级日志记录(DEBUG/INFO/ERROR)
  2. 模块化日志隔离
  3. 日志脱敏处理(支持手机号等敏感信息掩码)
  4. 磁盘空间监控与自动清理
  5. 日志采样率控制
  6. 支持JSON格式输出
  7. 异步队列写入
  8. 告警回调机制

核心特性:

  • 线程安全的单例模式
  • 支持按日期、大小自动分割日志文件
  • 可配置日志保留天数
  • 支持压缩归档旧日志
  • 包含调用堆栈信息
  • 协程安全的写入队列

使用示例展示了如何注册脱敏回调、告警回调,以及通过不同模块记录日志。该工具适用于需要结构化日志、敏感信息保护和资源监控的中大型项目。

相关推荐
听雨入夜2 小时前
zero.zhang
开发语言·python
江屿风2 小时前
【C++笔记】【二叉搜索树】流食般投喂
开发语言·数据结构·c++·笔记
1001101_QIA2 小时前
VRChat 插件开发方式
开发语言·vr
小满zs2 小时前
Go语言第四章(类型转换)
后端·go
会编程的土豆3 小时前
GoWeb 处理请求详解:请求行、请求头、请求参数与给客户端响应
开发语言·http·golang
人间凡尔赛4 小时前
告别冷启动!WebAssembly + Spin 实战:Serverless 延迟从 1 秒降到 1 毫秒
后端·云原生·serverless·webassembly·spin
陈随易10 小时前
bm2,MoonBit实现的pm2替代品
前端·后端·程序员
name好难取诶12 小时前
PHP 静态分析工具实战 PHPStan 和 Psalm 完全指南
android·开发语言·php
都叫我大帅哥12 小时前
Spring Data JPA 查询之道:方法命名与示例查询完全指南
java·后端·spring