C# 实现简单的日志打印

设计一个线程安全、支持按日期自动分文件、具备自动清理旧日志功能 的通用日志类(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. 代码亮点解析

  1. 单例模式 (Singleton)
    • 保证整个应用程序只有一个 Logger 实例,避免多线程同时创建多个文件句柄导致的"文件被占用"错误。
  2. 线程锁 (Thread Safety)
    • 使用 lock (_fileLock) 确保在多线程环境下(例如你的 ExternalOperationTestItems 中可能有异步任务),写入文件是串行的,不会出现日志内容交错混乱的情况。
  3. 按天分文件 (Rolling by Date)
    • 生成的文件名为 Log_2023-10-01.txt。这样方便你按天归档,查找特定日期的日志非常快,也不会出现单个日志文件过大(几GB)导致无法打开的情况。
  4. 自动清理 (Auto Cleanup)
    • 代码中包含了一个简单的定时器逻辑(或你可以在程序启动时调用),会自动删除超过 7 天(可配置)的旧日志,防止硬盘被日志填满。
  5. Caller Info (调用者信息)
    • 利用 StackTrace,日志中会自动打印出是哪个类(Class)和哪个方法(Method)输出的日志,极大方便了定位问题。
    • 输出示例: [2023-10-01 12:00:00] [Error] [qate_pcbafct_5g.DetectVisionTestItems] : 设备连接失败
相关推荐
绿浪19842 小时前
c# 中结构体 的定义字符串字段(性能优化)
开发语言·c#
房开民2 小时前
可变参数模板
java·开发语言·算法
t***5442 小时前
如何在现代C++中更有效地应用这些模式
java·开发语言·c++
状元岐3 小时前
C#反射从入门到精通
java·javascript·算法
唐青枫3 小时前
C#.NET ObjectPool 深入解析:对象复用、池化策略与使用边界
c#·.net
Victoria.a4 小时前
python基础语法
开发语言·python
a1117764 小时前
Three.js 的前端 WebGL 页面合集(日本 开源项目)
前端·javascript·webgl
Kk.08024 小时前
项目《基于Linux下的mybash命令解释器》(一)
前端·javascript·算法
xiaoyaohou114 小时前
023、数据增强改进(二):自适应数据增强与AutoAugment策略
开发语言·python