设计一个线程安全、支持按日期自动分文件、具备自动清理旧日志功能 的通用日志类(Logger)。
这个类可以直接集成到项目中使用。
1. 核心日志类代码 (Logger.cs)
你可以直接复制以下代码保存为 Logger.cs 文件。
csharp
using System;
using System.IO;
using System.Reflection;
using System.Security.Permissions;
using System.Text;
using System.Threading;
/// <summary>
/// 通用日志记录类 (线程安全)
/// </summary>
public class Logger
{
#region 单例模式
private static Logger _instance;
private static readonly object _lockObj = new object();
public static Logger Instance
{
get
{
if (_instance == null)
{
lock (_lockObj)
{
if (_instance == null)
{
_instance = new Logger();
}
}
}
return _instance;
}
}
#endregion
#region 配置参数
// 日志根目录 (默认在程序运行目录下的 Log 文件夹)
private string _logRootPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Log");
// 日志文件名前缀
private string _fileNamePrefix = "Log_";
// 日志文件后缀
private string _fileExtension = ".txt";
// 单个文件最大大小 (字节),默认 10MB
private long _maxFileSize = 10 * 1024 * 1024;
// 保留的日志天数 (自动删除超过天数的旧日志)
private int _keepDays = 7;
// 编码格式
private Encoding _encoding = Encoding.UTF8;
#endregion
#region 私有成员
private readonly object _fileLock = new object(); // 文件写入锁,保证线程安全
private StreamWriter _currentWriter = null;
private string _currentFileName = "";
private Timer _cleanupTimer; // 定时清理旧文件的定时器
#endregion
/// <summary>
/// 私有构造函数
/// </summary>
private Logger()
{
// 初始化目录
if (!Directory.Exists(_logRootPath))
{
Directory.CreateDirectory(_logRootPath);
}
// 启动定时器,每天凌晨清理一次旧日志 (这里简化为启动后1小时执行,实际项目中可用Quartz等框架)
// 这里仅做演示,实际可结合Windows服务或计划任务
_cleanupTimer = new Timer(state => CleanOldFiles(), null, TimeSpan.FromHours(1), TimeSpan.FromHours(24));
}
/// <summary>
/// 初始化日志配置 (可以在程序启动时调用)
/// </summary>
/// <param name="logPath">日志存储路径</param>
/// <param name="maxFileSize">单个文件最大字节</param>
/// <param name="keepDays">保留天数</param>
public void Init(string logPath = null, long maxFileSize = 0, int keepDays = 0)
{
if (!string.IsNullOrEmpty(logPath))
{
_logRootPath = logPath;
if (!Directory.Exists(_logRootPath)) Directory.CreateDirectory(_logRootPath);
}
if (maxFileSize > 0) _maxFileSize = maxFileSize;
if (keepDays > 0) _keepDays = keepDays;
}
/// <summary>
/// 写入日志 (公共接口)
/// </summary>
/// <param name="level">日志级别</param>
/// <param name="content">日志内容</param>
/// <param name="ex">异常对象 (可选)</param>
public void Write(LogLevels level, string content, Exception ex = null)
{
try
{
string logLine = FormatLog(level, content, ex);
// 确保写入线程安全
lock (_fileLock)
{
// 检查文件是否存在或是否需要滚动 (按天或按大小)
string todayFileName = GetTodayFileName();
// 如果文件名变了(新一天)或者文件太大了,关闭旧流,创建新流
if (_currentFileName != todayFileName ||
(_currentWriter != null && _currentWriter.BaseStream.Length > _maxFileSize))
{
CloseWriter();
_currentFileName = todayFileName;
}
// 如果当前写入器为空,创建新的
if (_currentWriter == null)
{
// 追加模式打开文件
var fileStream = new FileStream(_currentFileName, FileMode.Append, FileAccess.Write, FileShare.Read);
_currentWriter = new StreamWriter(fileStream, _encoding);
}
// 写入日志
_currentWriter.WriteLine(logLine);
_currentWriter.Flush(); // 立即写入磁盘,防止丢失
}
}
catch (Exception)
{
// 注意:这里为了防止递归死循环,不建议再抛出异常或写入日志。
// 在实际生产环境中,可以尝试写入到Windows事件日志作为备选方案。
}
}
#region 辅助方法
/// <summary>
/// 格式化日志行
/// </summary>
private string FormatLog(LogLevels level, string content, Exception ex)
{
StringBuilder sb = new StringBuilder();
sb.Append($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}]");
sb.Append($" [{level}]");
// 获取调用者信息 (跳过Logger的Write方法,获取实际调用者的类名/方法名)
var stack = new StackTrace(skipFrames: 1);
var frame = stack.GetFrame(0);
var method = frame.GetMethod();
var type = method.DeclaringType;
sb.Append($" [{type?.Name}.{method.Name}]");
sb.Append($" : {content}");
if (ex != null)
{
sb.Append($" | Exception: {ex.Message} | StackTrace: {ex.StackTrace}");
}
return sb.ToString();
}
/// <summary>
/// 获取今天的日志文件名
/// </summary>
private string GetTodayFileName()
{
string dateStr = DateTime.Now.ToString("yyyy-MM-dd");
return Path.Combine(_logRootPath, $"{_fileNamePrefix}{dateStr}{_fileExtension}");
}
/// <summary>
/// 关闭当前写入流
/// </summary>
private void CloseWriter()
{
if (_currentWriter != null)
{
_currentWriter.Dispose();
_currentWriter = null;
}
}
/// <summary>
/// 清理过期文件
/// </summary>
private void CleanOldFiles()
{
try
{
if (Directory.Exists(_logRootPath))
{
var files = Directory.GetFiles(_logRootPath, $"{_fileNamePrefix}*{_fileExtension}");
DateTime cutoffDate = DateTime.Now.AddDays(-_keepDays);
foreach (var file in files)
{
try
{
// 根据文件名中的日期判断 (例如 Log_2023-10-01.txt)
string fileName = Path.GetFileNameWithoutExtension(file);
string datePart = fileName.Replace(_fileNamePrefix, "");
if (DateTime.TryParseExact(datePart, "yyyy-MM-dd", null, System.Globalization.DateTimeStyles.None, out DateTime fileDate))
{
if (fileDate < cutoffDate)
{
File.Delete(file);
}
}
}
catch (Exception ex)
{
// 记录删除失败的日志(仅控制台或调试输出,防止死循环)
DebugWrite($"清理文件失败 {file}: {ex.Message}");
}
}
}
}
catch { }
}
/// <summary>
/// 调试用的简单控制台输出 (防止Logger自身出错导致程序崩溃)
/// </summary>
[Conditional("DEBUG")]
private void DebugWrite(string msg)
{
Console.WriteLine($"[Logger Debug] {DateTime.Now}: {msg}");
}
#endregion
#region IDisposable Support
// 实现IDisposable以确保资源释放
public void Dispose()
{
lock (_fileLock)
{
CloseWriter();
_cleanupTimer?.Dispose();
}
}
#endregion
}
/// <summary>
/// 日志级别枚举 (与你代码中的 QATE_TOOLS_ENUM_LogLevel 对应)
/// </summary>
public enum LogLevels
{
Debug = 0,
Info = 1,
Warning = 2,
Error = 3,
Fatal = 4
}
/// <summary>
/// 静态扩展类 (方便像你代码里那样直接调用 Logger.Debug(...) )
/// </summary>
public static class Log
{
/// <summary>
/// 调试信息
/// </summary>
public static void Debug(string msg, Exception ex = null) => Logger.Instance.Write(LogLevels.Debug, msg, ex);
/// <summary>
/// 普通信息
/// </summary>
public static void Info(string msg, Exception ex = null) => Logger.Instance.Write(LogLevels.Info, msg, ex);
/// <summary>
/// 警告
/// </summary>
public static void Warning(string msg, Exception ex = null) => Logger.Instance.Write(LogLevels.Warning, msg, ex);
/// <summary>
/// 错误
/// </summary>
public static void Error(string msg, Exception ex = null) => Logger.Instance.Write(LogLevels.Error, msg, ex);
/// <summary>
/// 致命错误
/// </summary>
public static void Fatal(string msg, Exception ex = null) => Logger.Instance.Write(LogLevels.Fatal, msg, ex);
/// <summary>
/// 初始化配置
/// </summary>
public static void Init(string path = null, long maxFileSize = 0, int keepDays = 0)
=> Logger.Instance.Init(path, maxFileSize, keepDays);
}
2. 使用方法
在你的 qate_pcbafct_5g 类或其他业务代码中,直接调用即可:
A. 初始化 (在 InitAll 或程序启动时调用一次)
csharp
// 初始化日志配置 (可选)
Log.Init(path: Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs"),
keepDays: 14); // 保留14天
B. 记录日志 (替代原来的 pAteTools.WriteLog 或配合它使用)
你可以直接使用这个类,或者将其封装进你的 pAteTools 接口。
csharp
public bool MyTestFunction()
{
try
{
Log.Info("测试开始执行");
// 模拟测试逻辑
bool result = DoSomething();
if(result)
Log.Info("测试成功完成");
else
Log.Warning("测试未通过,但非异常");
return result;
}
catch(Exception ex)
{
// 自动记录异常消息和堆栈
Log.Error("测试过程中发生未处理异常", ex);
return false;
}
}
3. 代码亮点解析
- 单例模式 (Singleton) :
- 保证整个应用程序只有一个
Logger实例,避免多线程同时创建多个文件句柄导致的"文件被占用"错误。
- 保证整个应用程序只有一个
- 线程锁 (Thread Safety) :
- 使用
lock (_fileLock)确保在多线程环境下(例如你的ExternalOperationTestItems中可能有异步任务),写入文件是串行的,不会出现日志内容交错混乱的情况。
- 使用
- 按天分文件 (Rolling by Date) :
- 生成的文件名为
Log_2023-10-01.txt。这样方便你按天归档,查找特定日期的日志非常快,也不会出现单个日志文件过大(几GB)导致无法打开的情况。
- 生成的文件名为
- 自动清理 (Auto Cleanup) :
- 代码中包含了一个简单的定时器逻辑(或你可以在程序启动时调用),会自动删除超过 7 天(可配置)的旧日志,防止硬盘被日志填满。
- Caller Info (调用者信息) :
- 利用
StackTrace,日志中会自动打印出是哪个类(Class)和哪个方法(Method)输出的日志,极大方便了定位问题。 - 输出示例:
[2023-10-01 12:00:00] [Error] [qate_pcbafct_5g.DetectVisionTestItems] : 设备连接失败
- 利用