Files
29WxOrder/WxOrder.Framework/Helpers/NLogger.cs
T
2026-10-20 17:03:23 +08:00

275 lines
6.7 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.IO;
using System.Text;
namespace WxOrder.Framework.Helpers
{
/// <summary> 日志消息级别。 </summary>
public enum LogMessageLevel
{
Trace = 0,
Debug = 1,
Info = 2,
Warn = 3,
Error = 4,
Fatal = 5,
None = 6
}
/// <summary>
/// 调试输出桥接。默认输出到 System.Diagnostics.Debug,
/// 可在外部重新赋值 DebugPrint 以接管控件台/UI 输出。
/// </summary>
public static class DebugLog
{
/// <summary> 调试输出回调。默认写到 Debug 输出窗口。 </summary>
public static Action<string>? DebugPrint { get; set; }
= msg => System.Diagnostics.Debug.WriteLine(msg);
}
/// <summary>
/// 默认日志逻辑类(不依赖任何第三方日志库)。
/// 日志文件按日期分目录存放,并在跨天时自动切换:
/// {logDirectory}/yyyy-MM/yyyy-MM-dd.log,例如 log/2022-12/2022-12-12.log
/// </summary>
public sealed class NLogger : IDisposable, ILogger
{
private const string DefaultName = "App";
private static readonly object _lock = new object();
private static NLogger? _instance;
/// <summary>
/// 同一进程内所有实例共用的写入锁。
/// 多个实例(不同 Name)会写入同一个按日期命名的文件,需要串行化写入。
/// </summary>
private static readonly object _fileLock = new object();
/// <summary> 获取 NLogger 的唯一单例实例(线程安全)。 </summary>
public static NLogger Instance
{
get
{
if (_instance == null)
{
lock (_lock)
{
if (_instance == null)
_instance = new NLogger(DefaultName);
}
}
return _instance;
}
}
/// <summary> 静态工厂:根据名称创建日志实例。 </summary>
public static NLogger Create(string name, string? logDirectory = null)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("日志名称不能为空。", nameof(name));
return new NLogger(name, logDirectory);
}
/// <summary> 静态工厂:创建使用默认名称的日志实例。 </summary>
public static NLogger Create() => new NLogger(DefaultName);
/// <summary> 日志名称(写入每行日志中,用于区分来源,不再决定文件名)。 </summary>
public string Name { get; }
public bool IsDebugEnabled { get; set; } = false;
public bool IsDebugWriteLineEnabled { get; set; } = false;
public LogMessageLevel MinimumLevel { get; set; } = LogMessageLevel.Trace;
/// <summary> 日志根目录。 </summary>
public string LogDirectory { get; }
private string? _currentFilePath;
private StreamWriter? _writer;
private bool _disposed;
private NLogger(string name, string? logDirectory = null)
{
Name = name;
LogDirectory = logDirectory ?? Path.Combine(AppContext.BaseDirectory, "log");
// 文件在第一次写入时按当前日期惰性创建,构造阶段不打开文件。
}
#region 日志方法
public void Trace(string message, Exception? exception = null)
=> Execute(LogMessageLevel.Trace, message, exception);
public void Debug(string message, Exception? exception = null)
=> Execute(LogMessageLevel.Debug, message, exception);
public void Info(string message, Exception? exception = null)
=> Execute(LogMessageLevel.Info, message, exception);
public void Warn(string message, Exception? exception = null)
=> Execute(LogMessageLevel.Warn, message, exception);
public void Error(string message, Exception? exception = null)
=> Execute(LogMessageLevel.Error, message, exception);
public void Fatal(string message, Exception? exception = null)
=> Execute(LogMessageLevel.Fatal, message, exception);
public void Log(string message, LogMessageLevel level, Exception? exception = null)
=> Execute(level, message, exception);
#endregion
#region 内部逻辑
private bool CheckEnabled(LogMessageLevel level)
{
if (level == LogMessageLevel.None)
return false;
if (level < MinimumLevel)
return false;
if (level == LogMessageLevel.Trace || level == LogMessageLevel.Debug)
return IsDebugEnabled;
return true;
}
private void Execute(LogMessageLevel level, string message, Exception? exception)
{
if (CheckEnabled(level))
{
WriteToFile(level, message, exception);
}
if (IsDebugWriteLineEnabled)
{
string logOutput = "[" + level + "] " + message;
if (exception != null)
{
logOutput += Environment.NewLine + exception;
}
Action<string>? handler = DebugLog.DebugPrint;
if (handler != null)
{
handler(logOutput);
}
}
}
/// <summary> 根据日期生成日志文件路径:{根目录}/yyyy-MM/yyyy-MM-dd.log </summary>
private string GetLogFilePath(DateTime time)
{
return Path.Combine(
LogDirectory,
time.ToString("yyyy-MM"),
time.ToString("yyyy-MM-dd") + ".log");
}
/// <summary> 写入当天的日志文件,跨天时自动切换到新文件。 </summary>
private void WriteToFile(LogMessageLevel level, string message, Exception? exception)
{
DateTime now = DateTime.Now;
var sb = new StringBuilder();
sb.Append('[')
.Append(now.ToString("yyyy-MM-dd HH:mm:ss.fff"))
.Append("] [")
.Append(level)
.Append("] [")
.Append(Name)
.Append("] ")
.AppendLine(message);
if (exception != null)
{
sb.AppendLine(exception.ToString());
}
string line = sb.ToString();
lock (_fileLock)
{
if (_disposed) return;
try
{
EnsureWriter(GetLogFilePath(now));
_writer!.Write(line);
_writer.Flush();
}
catch
{
// 日志写入失败不应影响主流程,这里吞掉异常。
// 重置写入器,下次写入时重新尝试打开文件。
CloseWriter();
}
}
}
/// <summary> 确保写入器指向指定路径;路径变化(跨天/跨月)时关闭旧文件并打开新文件。 </summary>
private void EnsureWriter(string path)
{
if (_writer != null && string.Equals(_currentFilePath, path, StringComparison.Ordinal))
return;
CloseWriter();
string? dir = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(dir))
{
Directory.CreateDirectory(dir);
}
var fs = new FileStream(
path,
FileMode.Append,
FileAccess.Write,
FileShare.ReadWrite);
_writer = new StreamWriter(fs, new UTF8Encoding(false))
{
AutoFlush = false
};
_currentFilePath = path;
}
private void CloseWriter()
{
try
{
if (_writer != null)
{
_writer.Flush();
_writer.Dispose();
}
}
catch { /* ignore */ }
finally
{
_writer = null;
_currentFilePath = null;
}
}
#endregion
#region IDisposable
public void Dispose()
{
lock (_fileLock)
{
if (_disposed) return;
_disposed = true;
CloseWriter();
}
}
#endregion
}
}