cs
/*
# 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();
}
}
修改一下:
cs
/*
# encoding: utf-8
# 版权所有 2026 ©涂聚文有限公司™ ®
# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
# 描述: Algorithm
# 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 : LogLevel.cs
*/
using System;
using System.Collections.Generic;
using System.Text;
namespace CSharpAlgorithms.common
{
/// <summary>
/// 日志级别
/// </summary>
public enum LogLevel
{
Off = 0,
Error = 1,
Info = 2,
Debug = 3
}
}
/*
# encoding: utf-8
# 版权所有 2026 ©涂聚文有限公司™ ®
# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
# 描述: Algorithm
# 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 : AlertCallbackDelegate.cs
*/
using System;
using System.Collections.Generic;
using System.Text;
namespace CSharpAlgorithms.common
{
/// <summary>
/// 告警回调委托
/// </summary>
public delegate void AlertCallbackDelegate(string eventType, string message, Dictionary<string, object> ext);
}
/*
# encoding: utf-8
# 版权所有 2026 ©涂聚文有限公司™ ®
# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
# 描述: Algorithm
# 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 : DesensitizeDelegate.cs
*/
using System;
using System.Collections.Generic;
using System.Text;
namespace CSharpAlgorithms.common
{
/// <summary>
/// 脱敏委托:级别、模块、原始消息 -> 脱敏后消息
/// </summary>
public delegate string DesensitizeDelegate(string level, string module, string msg);
}
/*
# encoding: utf-8
# 版权所有 2026 ©涂聚文有限公司™ ®
# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
# 描述: Algorithm
# 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 : AlertEvent.cs
*/
using System;
using System.Collections.Generic;
using System.Text;
namespace CSharpAlgorithms.common
{
/// <summary>
/// 告警常量
/// </summary>
public static class AlertEvent
{
public const string DiskOverLimit = "DISK_OVER_LIMIT";
public const string QueueBlocked = "QUEUE_BLOCKED";
}
}
/*
# encoding: utf-8
# 版权所有 2026 ©涂聚文有限公司™ ®
# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
# 描述: Algorithm
# 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 : LogItem.cs
*/
using System;
using System.Collections.Generic;
using System.Text;
namespace CSharpAlgorithms.common
{
/// <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;
}
}
/*
# encoding: utf-8
# 版权所有 2026 ©涂聚文有限公司™ ®
# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
# 描述: Algorithm
# 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 : Log.cs
*/
using System;
using System.Collections.Generic;
using System.Text;
namespace CSharpAlgorithms.common
{
/// <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();
}
}
/*
# encoding: utf-8
# 版权所有 2026 ©涂聚文有限公司™ ®
# 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
# 描述: Algorithm
# 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>
/// .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>();
/// <summary>
///
/// </summary>
private readonly Dictionary<string, double> _levelDiskLimit = new()
{
{ "debug", 150 },
{ "info", 250 },
{ "error", 100 }
};
/// <summary>
///
/// </summary>
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
}
}
还需要完善下。
调用:
cs
// 脱敏注册
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();