CSharp: LogHelper
2026/7/31 23:48:17 网站建设 项目流程
/* # encoding: utf-8 # 版权所有 2026 ©涂聚文有限公司™ ® # 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎 # 描述: 日志 # Author : geovindu,Geovin Du 涂聚文. # IDE : vs2026 c# .net 10 # os : windows 10 # database : mysql 9.0 sql server 2019, postgreSQL 17.0 Oracle 21c Neo4j # Datetime : 2026/07/05 23:16 # User : geovindu # Product : Visual Studio 2026 # Project : CSharpAlgorithms # File : LogHelper.cs */ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Text.Json; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using System.Diagnostics; using System.IO.Compression; namespace CSharpAlgorithms.common { /// <summary> /// 日志级别 /// </summary> public enum LogLevel { Off = 0, Error = 1, Info = 2, Debug = 3 } /// <summary> /// 脱敏委托:级别、模块、原始消息 -> 脱敏后消息 /// </summary> public delegate string DesensitizeDelegate(string level, string module, string msg); /// <summary> /// 告警回调委托 /// </summary> public delegate void AlertCallbackDelegate(string eventType, string message, Dictionary<string, object> ext); /// <summary> /// 告警常量 /// </summary> public static class AlertEvent { public const string DiskOverLimit = "DISK_OVER_LIMIT"; public const string QueueBlocked = "QUEUE_BLOCKED"; } /// <summary> /// 日志队列单元 /// </summary> internal sealed class LogItem { public string Level { get; set; } = string.Empty; public string Module { get; set; } = string.Empty; public string TextData { get; set; } = string.Empty; public Dictionary<string, object>? JsonData { get; set; } public string StackTrace { get; set; } = string.Empty; } /// <summary> /// .NET10 高性能日志核心类,修复空引用异常 /// </summary> public sealed class LogHelper { #region 单例 严格顺序初始化 private static readonly Lazy<LogHelper> _lazyInstance = new Lazy<LogHelper>(() => new LogHelper(), LazyThreadSafetyMode.ExecutionAndPublication); public static LogHelper Instance => _lazyInstance.Value; // 【强制提前实例化】所有成员字段直接初始化,杜绝null private readonly ConcurrentQueue<LogItem> _logQueue = new ConcurrentQueue<LogItem>(); private readonly CancellationTokenSource _cts = new CancellationTokenSource(); private readonly object _fileLock = new object(); private readonly Random _random = new Random(); private readonly Dictionary<string, ConsoleColor> _colorMap = new Dictionary<string, ConsoleColor>(); private readonly Dictionary<string, double> _levelDiskLimit = new() { { "debug", 150 }, { "info", 250 }, { "error", 100 } }; private readonly Dictionary<string, double> _levelSamplingRate = new() { { "debug", 1.0 }, { "info", 1.0 }, { "error", 1.0 } }; private Task? _writeTask; private DateTime _lastCleanDay = DateTime.MinValue; private long _lastDiskCheckTs; private readonly int _pid = Environment.ProcessId; #region 配置属性 public string LogRoot { get; private set; } public string DefaultModule { get; set; } = "default"; public int KeepDays { get; set; } = 7; public long MaxFileSizeMb { get; set; } = 10; public int QueueMaxSize { get; set; } = 10000; public int QueueAlertThreshold { get; set; } = 7000; public int BatchWriteCount { get; set; } = 10; public bool EnableConsole { get; set; } = true; public bool EnableJsonFormat { get; set; } = false; public bool CompressBackupLog { get; set; } = true; public double MaxDiskTotalMb { get; set; } = 500; public double GlobalSamplingRate { get; set; } = 1.0; public int DiskCheckIntervalSec { get; set; } = 60; public string TimeFormat { get; set; } = "yyyy-MM-dd HH:mm:ss"; public LogLevel MinLogLevel { get; set; } = LogLevel.Debug; // 回调 public DesensitizeDelegate? DesensitizeCallback { get; private set; } public AlertCallbackDelegate? AlertCallback { get; private set; } #endregion // 私有构造:仅赋值、初始化,最后再启动线程 private LogHelper() { LogRoot = Path.Combine(Directory.GetCurrentDirectory(), "Logs"); Directory.CreateDirectory(LogRoot); // 颜色初始化 _colorMap["DEBUG"] = ConsoleColor.Blue; _colorMap["INFO"] = ConsoleColor.Green; _colorMap["ERROR"] = ConsoleColor.Red; // Windows控制台编码兼容 if (OperatingSystem.IsWindows() && !Console.IsOutputRedirected) { try { Console.OutputEncoding = Encoding.UTF8; } catch { EnableConsole = true; } } // 【关键点】所有变量初始化完毕,最后再启动后台写入线程 StartBackgroundWriter(); } #endregion #region 回调注册 /// <summary> /// 注册日志脱敏回调 /// </summary> public void RegisterDesensitize(DesensitizeDelegate func) { DesensitizeCallback = func; } /// <summary> /// 注册告警回调 /// </summary> public void RegisterAlertCallback(AlertCallbackDelegate func) { AlertCallback = func; } /// <summary> /// 触发告警 /// </summary> private void TriggerAlert(string eventType, string msg, Dictionary<string, object> ext) { if (AlertCallback == null) return; try { AlertCallback.Invoke(eventType, msg, ext); } catch { } } #endregion #region 对外配置API public void SetLogLevel(LogLevel level) => MinLogLevel = level; public void SwitchJsonFormat(bool enable) => EnableJsonFormat = enable; public void SwitchConsole(bool enable) => EnableConsole = enable; /// <summary> /// 设置单级别采样率 0~1 /// </summary> public void SetSamplingRate(string level, double rate) { rate = Math.Clamp(rate, 0, 1); string lv = level.ToLower(); if (_levelSamplingRate.ContainsKey(lv)) _levelSamplingRate[lv] = rate; } /// <summary> /// 判断本条日志是否采样放行 /// </summary> private bool NeedSample(string level) { string lv = level.ToLower(); double lvRate = _levelSamplingRate.TryGetValue(lv, out var val) ? val : 1; double final = GlobalSamplingRate * lvRate; if (final >= 1) return true; return _random.NextDouble() <= final; } #endregion #region 工具静态/私有方法 /// <summary> /// 模块名清洗 /// </summary> private string SafeModuleName(string module) { if (string.IsNullOrWhiteSpace(module)) return DefaultModule; return Regex.Replace(module.Trim(), @"[^\w\-]", "_"); } private string GetModuleLogDir(string module) { string mod = SafeModuleName(module); string dir = Path.Combine(LogRoot, mod); Directory.CreateDirectory(dir); return dir; } private double GetDirTotalMb(string folder, string? filterPrefix = null) { if (!Directory.Exists(folder)) return 0; long totalBytes = 0; var files = Directory.GetFiles(folder, "*.*", SearchOption.TopDirectoryOnly); foreach (var file in files) { string name = Path.GetFileName(file); if (!name.EndsWith(".log") && !name.EndsWith(".zip")) continue; if (filterPrefix != null && !name.StartsWith(filterPrefix + "_")) continue; totalBytes += new FileInfo(file).Length; } return totalBytes / 1024.0 / 1024.0; } private List<string> GetSortedLogFiles(string folder, string? filterPrefix) { var list = new List<(long mtime, string path)>(); if (!Directory.Exists(folder)) return new List<string>(); foreach (var file in Directory.GetFiles(folder)) { string name = Path.GetFileName(file); if (!name.EndsWith(".log") && !name.EndsWith(".zip")) continue; if (filterPrefix != null && !name.StartsWith(filterPrefix + "_")) continue; list.Add((new FileInfo(file).LastWriteTimeUtc.Ticks, file)); } return list.OrderBy(x => x.mtime).Select(x => x.path).ToList(); } private bool TryFreeDiskSpace(string? level) { while (true) { double usage, limit; if (!string.IsNullOrEmpty(level)) { usage = GetDirTotalMb(LogRoot, level.ToLower()); limit = _levelDiskLimit[level.ToLower()]; if (usage < limit) return true; } else { usage = GetDirTotalMb(LogRoot); limit = MaxDiskTotalMb; if (usage < limit) return true; } var files = GetSortedLogFiles(LogRoot, level?.ToLower()); if (files.Count == 0) return false; bool deleted = false; foreach (var fp in files.Where(x => x.EndsWith(".zip"))) { try { File.Delete(fp); Console.WriteLine($"[LogHelper CLEAN] 删除老旧压缩包:{Path.GetFileName(fp)}"); deleted = true; break; } catch { } } if (deleted) continue; foreach (var fp in files.Where(x => x.EndsWith(".log"))) { try { File.Delete(fp); Console.WriteLine($"[LogHelper CLEAN] 删除老旧日志:{Path.GetFileName(fp)}"); deleted = true; break; } catch { } } if (!deleted) return false; } } private bool CheckDiskLimit(string level) { long nowTs = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); long last = Interlocked.Read(ref _lastDiskCheckTs); if (nowTs - last < DiskCheckIntervalSec) return false; Interlocked.Exchange(ref _lastDiskCheckTs, nowTs); string lv = level.ToLower(); double lvUsage = GetDirTotalMb(LogRoot, lv); double lvLimit = _levelDiskLimit[lv]; double totalUsage = GetDirTotalMb(LogRoot); bool over = false; if (lvUsage >= lvLimit) { var ext = new Dictionary<string, object>() { {"level", level}, {"usage_mb", lvUsage}, {"limit_mb", lvLimit}, {"total_usage", totalUsage} }; TriggerAlert(AlertEvent.DiskOverLimit, $"日志【{level}】分级磁盘占用超限({lvUsage:F2}MB/{lvLimit:F2}MB),开始清理", ext); if (!TryFreeDiskSpace(lv)) over = true; } else if (totalUsage >= MaxDiskTotalMb) { var ext = new Dictionary<string, object>() { {"usage_mb", totalUsage}, {"limit_mb", MaxDiskTotalMb} }; TriggerAlert(AlertEvent.DiskOverLimit, $"日志总磁盘占用超限({totalUsage:F2}MB/{MaxDiskTotalMb:F2}MB),开始清理", ext); if (!TryFreeDiskSpace(null)) over = true; } if (over) Console.WriteLine($"[LogHelper FATAL] 磁盘清理后依旧超限,暂停持久化【{level}】日志"); return over; } private void CleanExpireLogs() { DateTime expire = DateTime.Now.AddDays(-KeepDays); foreach (var file in Directory.GetFiles(LogRoot, "*.*", SearchOption.AllDirectories)) { string name = Path.GetFileName(file); if (!name.EndsWith(".log") && !name.EndsWith(".zip")) continue; var fi = new FileInfo(file); if (fi.LastWriteTime < expire) { try { File.Delete(file); } catch { } } } } private string GetLogPath(string level, string module) { string dir = GetModuleLogDir(module); string date = DateTime.Now.ToString("yyyyMMdd"); return Path.Combine(dir, $"{level.ToLower()}_{date}.log"); } private void SplitLogFile(string logPath) { lock (_fileLock) { if (!File.Exists(logPath)) return; var fi = new FileInfo(logPath); long maxByte = MaxFileSizeMb * 1024 * 1024; if (fi.Length < maxByte) return; string basePath = Path.ChangeExtension(logPath, null); string bakPath = $"{basePath}_{DateTime.Now:HHmmss}.log"; File.Move(logPath, bakPath); if (CompressBackupLog) _ = Task.Run(() => CompressFile(bakPath)); } } private void CompressFile(string srcPath) { if (!File.Exists(srcPath)) return; string zipPath = Path.ChangeExtension(srcPath, ".zip"); try { using var fs = new FileStream(zipPath, FileMode.Create); using var zip = new ZipArchive(fs, ZipArchiveMode.Create); var entry = zip.CreateEntry(Path.GetFileName(srcPath)); using var entryStream = entry.Open(); using var srcStream = File.OpenRead(srcPath); srcStream.CopyTo(entryStream); File.Delete(srcPath); } catch { } } private static (string fileName, string funcName, int line) GetCallerInfo(int skipFrames) { var stack = new StackTrace(skipFrames, true); var frame = stack.GetFrame(0); if (frame == null) return ("unknown", "unknown", 0); string file = Path.GetFileName(frame.GetFileName() ?? "unknown"); string func = frame.GetMethod()?.Name ?? "unknown"; int line = frame.GetFileLineNumber(); return (file, func, line); } private static string GetThreadName() { string name = Thread.CurrentThread.Name; return string.IsNullOrWhiteSpace(name) ? $"Thread_{Thread.CurrentThread.ManagedThreadId}" : name; } private (string text, Dictionary<string, object>? json) BuildLogData(string level, string module, string msg, string stack) { string modSafe = SafeModuleName(module); if (DesensitizeCallback != null) { try { msg = DesensitizeCallback.Invoke(level, modSafe, msg); } catch { } } var now = DateTime.Now; string timeStr = now.ToString(TimeFormat); double timestamp = now.Ticks / 10000000.0; string threadName = GetThreadName(); var (file, func, lineNo) = GetCallerInfo(4); if (EnableJsonFormat) { var jsonObj = new Dictionary<string, object>() { ["time"] = timeStr, ["timestamp"] = timestamp, ["level"] = level, ["module"] = modSafe, ["pid"] = _pid, ["thread"] = threadName, ["file"] = file, ["function"] = func, ["line"] = lineNo, ["message"] = msg, ["stack_trace"] = stack }; return (string.Empty, jsonObj); } StringBuilder sb = new(); sb.Append($"[{timeStr}] [{level}] [MODULE:{modSafe}] [PID:{_pid}] [T:{threadName}] [{func}·{lineNo}行·{file}] {msg}"); if (!string.IsNullOrEmpty(stack)) sb.AppendLine().Append(stack); sb.AppendLine(); return (sb.ToString(), null); } #endregion #region 队列、写入逻辑(修复buffer空引用) private void EnqueueLog(string level, string module, string msg, string stack) { if (level == "DEBUG" && MinLogLevel < LogLevel.Debug) return; if (level == "INFO" && MinLogLevel < LogLevel.Info) return; if (level == "ERROR" && MinLogLevel < LogLevel.Error) return; if (!NeedSample(level)) return; var (text, json) = BuildLogData(level, module, msg, stack); if (EnableConsole) { string consoleStr; if (json != null) { consoleStr = JsonSerializer.Serialize(json, new JsonSerializerOptions { WriteIndented = true }); } else { consoleStr = text.TrimEnd('\n', '\r'); } if (_colorMap.TryGetValue(level, out var color)) { Console.ForegroundColor = color; Console.WriteLine(consoleStr); Console.ResetColor(); } else { Console.WriteLine(consoleStr); } } var item = new LogItem() { Level = level.ToLower(), Module = module, TextData = text, JsonData = json, StackTrace = stack }; if (_logQueue.Count < QueueMaxSize) _logQueue.Enqueue(item); else Console.WriteLine("[LogHelper WARN] 队列已满,丢弃持久化日志"); } private void StartBackgroundWriter() { _writeTask = Task.Run(async () => { // 【修复】循环内每次直接初始化字典,杜绝null Dictionary<string, List<LogItem>> buffer = new Dictionary<string, List<LogItem>>(); var ticker = new PeriodicTimer(TimeSpan.FromMilliseconds(800)); while (!_cts.Token.IsCancellationRequested) { try { while (_logQueue.TryDequeue(out var item)) { string key = $"{item.Level}|{item.Module}"; // 安全判断,不会空引用 if (!buffer.ContainsKey(key)) buffer[key] = new List<LogItem>(); buffer[key].Add(item); if (buffer[key].Count >= BatchWriteCount) FlushBuffer(buffer, key); } if (await ticker.WaitForNextTickAsync(_cts.Token)) FlushAllBuffer(buffer); } catch { } } FlushAllBuffer(buffer); }, _cts.Token); } private void FlushBuffer(Dictionary<string, List<LogItem>> buffer, string key) { if (!buffer.TryGetValue(key, out var items) || items.Count == 0) return; string[] parts = key.Split('|'); string level = parts[0]; string module = parts[1]; if (CheckDiskLimit(level)) { buffer.Remove(key); return; } string logPath = GetLogPath(level, module); SplitLogFile(logPath); StringBuilder writeSb = new StringBuilder(); foreach (var it in items) { if (it.JsonData != null) { string jsonLine = JsonSerializer.Serialize(it.JsonData); writeSb.AppendLine(jsonLine); } else { writeSb.Append(it.TextData); } } try { File.AppendAllText(logPath, writeSb.ToString(), Encoding.UTF8); } catch (Exception ex) { Console.WriteLine($"[LogHelper] 写入失败 level:{level} module:{module} err:{ex.Message}"); } buffer.Remove(key); DateTime today = DateTime.Now.Date; if (_lastCleanDay != today) { CleanExpireLogs(); _lastCleanDay = today; } } private void FlushAllBuffer(Dictionary<string, List<LogItem>> buffer) { var keys = buffer.Keys.ToList(); foreach (var k in keys) FlushBuffer(buffer, k); } #endregion #region 对外日志快捷方法 public void Debug(string msg) => EnqueueLog("DEBUG", DefaultModule, msg, string.Empty); public void Info(string msg) => EnqueueLog("INFO", DefaultModule, msg, string.Empty); public void Error(string msg, Exception? ex = null) { string stack = ex == null ? string.Empty : ex.ToString(); EnqueueLog("ERROR", DefaultModule, msg, stack); } public void DebugMod(string module, string msg) => EnqueueLog("DEBUG", module, msg, string.Empty); public void InfoMod(string module, string msg) => EnqueueLog("INFO", module, msg, string.Empty); public void ErrorMod(string module, string msg, Exception? ex = null) { string stack = ex == null ? string.Empty : ex.ToString(); EnqueueLog("ERROR", module, msg, stack); } /// <summary> /// 优雅关闭 /// </summary> public async Task ShutdownAsync() { _cts.Cancel(); if (_writeTask != null) await _writeTask; } #endregion } /// <summary> /// 全局静态快捷入口 /// </summary> public static class Log { public static void Debug(string msg) => LogHelper.Instance.Debug(msg); public static void Info(string msg) => LogHelper.Instance.Info(msg); public static void Error(string msg, Exception? ex = null) => LogHelper.Instance.Error(msg, ex); public static void DebugMod(string module, string msg) => LogHelper.Instance.DebugMod(module, msg); public static void InfoMod(string module, string msg) => LogHelper.Instance.InfoMod(module, msg); public static void ErrorMod(string module, string msg, Exception? ex = null) => LogHelper.Instance.ErrorMod(module, msg, ex); public static void RegisterDesensitize(DesensitizeDelegate func) => LogHelper.Instance.RegisterDesensitize(func); public static void RegisterAlertCallback(AlertCallbackDelegate func) => LogHelper.Instance.RegisterAlertCallback(func); public static void SetSamplingRate(string level, double rate) => LogHelper.Instance.SetSamplingRate(level, rate); public static void SwitchJsonFormat(bool enable) => LogHelper.Instance.SwitchJsonFormat(enable); public static void SwitchConsole(bool enable) => LogHelper.Instance.SwitchConsole(enable); public static Task ShutdownAsync() => LogHelper.Instance.ShutdownAsync(); } }

调用:

// 脱敏注册 Log.RegisterDesensitize((level, module, msg) => { return Regex.Replace(msg, @"1[3-9]\d{9}", "1*******"); }); // 告警注册 Log.RegisterAlertCallback((type, msg, ext) => { Console.WriteLine($"\n【告警】{type}:{msg}"); }); Log.SetSamplingRate("debug", 0.2); Log.Debug("手机号13900001111测试脱敏日志"); Log.DebugMod("Server", "手机号13900001111测试脱敏日志"); Log.Info("系统启动成功"); try { int i = 0; int k = 1 / i; } catch (Exception e) { Log.Error("计算异常", e); } // 程序退出等待落地 // await Log.ShutdownAsync();

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询