/* # 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, Dictionarystring, 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 Dictionarystring, object? JsonData { get; set; } public string StackTrace { get; set; } string.Empty; } /// summary /// .NET10 高性能日志核心类修复空引用异常 /// /summary public sealed class LogHelper { #region 单例 严格顺序初始化 private static readonly LazyLogHelper _lazyInstance new LazyLogHelper(() new LogHelper(), LazyThreadSafetyMode.ExecutionAndPublication); public static LogHelper Instance _lazyInstance.Value; // 【强制提前实例化】所有成员字段直接初始化杜绝null private readonly ConcurrentQueueLogItem _logQueue new ConcurrentQueueLogItem(); private readonly CancellationTokenSource _cts new CancellationTokenSource(); private readonly object _fileLock new object(); private readonly Random _random new Random(); private readonly Dictionarystring, ConsoleColor _colorMap new Dictionarystring, ConsoleColor(); private readonly Dictionarystring, double _levelDiskLimit new() { { debug, 150 }, { info, 250 }, { error, 100 } }; private readonly Dictionarystring, 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, Dictionarystring, 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 Liststring GetSortedLogFiles(string folder, string? filterPrefix) { var list new List(long mtime, string path)(); if (!Directory.Exists(folder)) return new Liststring(); 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 Dictionarystring, 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 Dictionarystring, 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, Dictionarystring, 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 Dictionarystring, 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 Dictionarystring, ListLogItem buffer new Dictionarystring, ListLogItem(); 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 ListLogItem(); 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(Dictionarystring, ListLogItem 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(Dictionarystring, ListLogItem 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();