C#学写了一个程序记录日志的方法(Log类)

1.错误和警告信息单独生产文本进行记录;

2.日志到一定内存阈值可以打包压缩,单独存储起来,修改字段MaxLogFileSizeForCompress的值即可;

3.Log类调用举例:Log.Txt(JB.信息,"日志记录内容","通道1");

cs 复制代码
using System;
using System.IO;
using System.Windows.Forms;
using ICSharpCode.SharpZipLib.Zip;

namespace _程序日志
{
    // 定义日志级别枚举,虽然取消了日志级别过滤功能,但保留该枚举方便后续可能的扩展
    public enum JB
    {
        错误,  //ERROR,代表程序运行过程中出现的错误情况,通常是需要重点关注和排查问题的日志级别
        警告, //WARN,用于表示程序运行时出现的一些可能会引发问题的异常情况,但不像错误那样严重,起到提醒作用
        信息, //INFO,一般记录程序正常运行过程中的一些常规信息,用于记录流程、状态等内容
        调试  //DEBUG,主要用于开发阶段,帮助开发者调试程序,输出更详细的程序执行过程信息
    }

    public class Log
    {
        // 日志文件基础路径,可通过配置等方式修改,这里指定了默认的基础路径,后续所有日志文件都会基于此路径进一步构建具体的文件路径
        private static string _logFilePath = "D:\\Logs\\程序日志";
        // 日志文件大小阈值,这里可按需调整,单位为字节,当日志文件达到这个大小后,会触发相应的文件压缩等处理逻辑
        private static long MaxLogFileSizeForCompress = 100 * 1024 * 1024;
        // 存放压缩日志文件的文件夹路径,可按需配置,用于存放经过压缩处理后的日志文件,便于节省磁盘空间以及对历史日志进行归档管理
        private static string CompressFolderPath = "D:\\Logs\\压缩日志";

        // 用于标记当前正在写入的日志文件是文本A还是文本B,初始化为文本A,通过这个标记来实现类似双缓冲的机制,方便在文件切换、压缩等操作时保证日志记录的连续性
        private static bool isUsingTextA = true;
        // 记录当前正在使用的日志文件路径(文本A或文本B),会根据程序运行过程中的实际情况动态更新,指向当前真正进行日志写入操作的文件路径
        private static string currentLogFilePath;
        // 记录文本A的文件路径,专门用于记录文本A对应的文件完整路径,方便后续针对文本A进行一些特定操作,比如检查文件大小等
        private static string textAFilePath;
        // 新增用于记录错误日志的文件路径(文本A和文本B),在发现有"错误"级别日志时,对应文本A的错误日志文件路径会存储在这里,方便后续操作
        private static string errorTextAFilePath;
        // 对应文本B的错误日志文件路径,与isUsingTextA标记配合,当切换到文本B进行日志记录时,错误日志也会相应切换写入到这个文件路径对应的文件中(如果需要的话)
        private static string errorTextBFilePath;
        // 新增用于记录警告日志的文件路径(文本A和文本B),功能类似上述错误日志文件路径,用于存放"警告"级别日志对应的文件路径
        private static string warnTextAFilePath;
        // 文本B的警告日志文件路径,用于在相应阶段存储"警告"级别日志信息
        private static string warnTextBFilePath;

        // 辅助方法:确保文件夹存在
        private static void EnsureFolderExists(string folderPath)
        {
            // 检查指定的文件夹路径对应的文件夹是否已经存在
            if (!Directory.Exists(folderPath))
            {
                try
                {
                    // 如果文件夹不存在,则尝试创建该文件夹
                    Directory.CreateDirectory(folderPath);
                }
                catch (IOException ex)
                {
                    // 如果在创建文件夹过程中出现IOException异常(比如权限不足、磁盘已满等原因导致无法创建文件夹)
                    // 这里通过消息框向用户展示创建文件夹失败的异常信息,方便用户知晓问题所在
                    MessageBox.Show($"创建文件夹 {folderPath} 失败,异常信息: {ex.Message}");
                    // 重新抛出异常,让调用这个方法的上层代码知道创建文件夹出现了问题,以便进行进一步的处理,比如终止程序或者尝试其他恢复操作
                    throw;
                }
            }
        }

        /// <summary>
        /// 保存txt文档,即实现日志记录功能,是整个日志记录模块的核心方法,接收日志级别、要保存的内容以及通道等参数来决定如何将日志信息保存到对应的文件中
        /// </summary>
        /// <param name="jb">日志级别(目前未使用级别过滤功能),通过传入不同的日志级别来区分不同重要程度的日志信息,方便后续查看和分析日志时筛选不同级别的内容</param>
        /// <param name="zhi">保存内容,即具体要记录到日志文件中的文本信息,描述了程序运行过程中发生的相关事件、状态等内容</param>
        /// <param name="tongdao">通道,可用于对日志进行分类,比如按照不同的功能模块、业务流程等划分不同的通道,便于对日志进行归类查看和管理</param>
        public static void Txt(Enum jb, string zhi, string tongdao)
        {
            string basePath = _logFilePath;
            string year = DateTime.Now.ToString("yyyy-MM");//年月日文件夹,根据当前时间获取年份和月份信息,用于构建日志文件所在的年月文件夹路径,便于按时间对日志进行分类存储
            string passageway = tongdao;//通道文件夹,使用传入的通道参数作为文件夹名称,用于进一步细分日志文件的存储位置

            // 构建日志文件所在文件夹路径,将基础路径、通道文件夹和年月文件夹路径组合起来,形成完整的日志文件所在文件夹的路径
            string logFolderPath = Path.Combine(basePath, passageway, year);
            EnsureFolderExists(logFolderPath);

            // 拼接完整日志文件路径,文件名格式保持不变(这里以简单的日期格式为例,你可按需调整),以当前日期作为文件名(格式为yyyy-MM-dd.txt),方便按天查看和管理日志文件
            string filename = DateTime.Now.ToString("yyyy-MM-dd") + ".txt";
            if (isUsingTextA)
            {
                textAFilePath = Path.Combine(logFolderPath, filename);
                currentLogFilePath = textAFilePath;
                // 同时确定错误和警告日志对应的文本A文件路径,在原文件名基础上添加相应前缀(Error_和WARN_),用于区分不同级别的专用日志文件
                errorTextAFilePath = Path.Combine(logFolderPath, "Error_" + filename);
                warnTextAFilePath = Path.Combine(logFolderPath, "WARN_" + filename);
            }
            else
            {
                currentLogFilePath = Path.Combine(logFolderPath, filename);
                // 同时确定错误和警告日志对应的文本B文件路径,同样添加相应前缀,以对应文本B的情况
                errorTextBFilePath = Path.Combine(logFolderPath, "Error_" + filename);
                warnTextBFilePath = Path.Combine(logFolderPath, "WARN_" + filename);
            }

            bool fileExists = File.Exists(currentLogFilePath);

            // 先处理文件已存在且未超过阈值的情况,直接打开并追加内容,这种情况下无需进行复杂的文件切换或压缩等操作,直接在现有文件末尾追加新的日志信息即可
            if (fileExists)
            {
                long fileSize = new FileInfo(currentLogFilePath).Length;
                if (fileSize < MaxLogFileSizeForCompress)
                {
                    WriteToExistingFile(jb, zhi, currentLogFilePath);
                    // 根据日志级别额外处理错误和警告日志写入对应的专用文件,调用该方法来处理将"错误"和"警告"级别日志分别写入对应的专用文件的操作
                    HandleSpecialLogLevel(jb, zhi);
                    return;
                }
            }

            // 如果文件不存在或者已超过阈值,执行创建文件或切换文件等逻辑,比如首次创建日志文件或者当前文件已满需要切换到新文件进行日志记录等情况
            FileStream fs = null;
            try
            {
                fs = fileExists ? File.Open(currentLogFilePath, FileMode.Append) : File.Create(currentLogFilePath);
                WriteToFileAndHandleSize(jb, zhi, fs, currentLogFilePath);
            }
            catch (IOException ex)
            {
                MessageBox.Show($"打开或创建日志文件 {currentLogFilePath} 失败,异常信息: {ex.Message}");
                throw; // 重新抛出异常,避免程序继续执行可能导致的数据不一致等问题,确保上层代码知道日志文件操作出现了异常
            }
            finally
            {
                if (fs != null)
                {
                    fs.Close();
                }
            }
        }

        /// <summary>
        /// 根据日志级别处理错误和警告日志写入对应的专用文件,该方法根据传入的日志级别判断是"错误"还是"警告"级别,然后分别进行对应的文件写入操作
        /// </summary>
        private static void HandleSpecialLogLevel(Enum jb, string zhi)
        {
            if (jb.ToString() == "错误")
            {
                string currentErrorFilePath = isUsingTextA ? errorTextAFilePath : errorTextBFilePath;
                EnsureErrorFileExists(currentErrorFilePath);
                WriteToSpecialFile(currentErrorFilePath, zhi);
            }
            else if (jb.ToString() == "警告")
            {
                string currentWarnFilePath = isUsingTextA ? warnTextAFilePath : warnTextBFilePath;
                EnsureWarnFileExists(currentWarnFilePath);
                WriteToSpecialFile(currentWarnFilePath, zhi);
            }
        }

        /// <summary>
        /// 确保错误日志文件存在,如果不存在则创建,该方法用于保证在写入"错误"级别日志到对应的专用文件时,文件是存在的,避免出现写入失败的情况
        /// </summary>
        private static void EnsureErrorFileExists(string errorFilePath)
        {
            if (!File.Exists(errorFilePath))
            {
                try
                {
                    // 使用File.Create创建文件,创建后需要手动关闭文件流,这里直接调用Close方法关闭
                    File.Create(errorFilePath).Close();
                }
                catch (IOException ex)
                {
                    MessageBox.Show($"创建错误日志文件 {errorFilePath} 失败,异常信息: {ex.Message}");
                    throw;
                }
            }
        }

        /// <summary>
        /// 确保警告日志文件存在,如果不存在则创建,与EnsureErrorFileExists类似,只是针对"警告"级别日志对应的专用文件进行存在性检查和创建操作
        /// </summary>
        private static void EnsureWarnFileExists(string warnFilePath)
        {
            if (!File.Exists(warnFilePath))
            {
                try
                {
                    File.Create(warnFilePath).Close();
                }
                catch (IOException ex)
                {
                    MessageBox.Show($"创建警告日志文件 {warnFilePath} 失败,异常信息: {ex.Message}");
                    throw;
                }
            }
        }

        /// <summary>
        /// 向专用文件写入日志内容,负责将具体的日志内容按照一定格式写入到指定的专用文件(错误或警告日志文件)中
        /// </summary>
        private static void WriteToSpecialFile(string specialFilePath, string zhi)
        {
            FileStream fs = null;
            try
            {
                // 以追加模式打开指定的专用文件,以便在文件末尾添加新的日志内容
                fs = File.Open(specialFilePath, FileMode.Append);
                using (StreamWriter sw = new StreamWriter(fs))
                {
                    // 将写入位置定位到文件末尾,确保新的日志内容追加在已有内容之后
                    sw.BaseStream.Seek(0, SeekOrigin.End);
                    sw.Write($"操作时间:{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}  内容:{zhi}\n");
                    sw.Flush();
                }
            }
            catch (IOException ex)
            {
                MessageBox.Show($"打开或创建特殊日志文件 {specialFilePath} 失败,异常信息: {ex.Message}");
                throw;
            }
            finally
            {
                if (fs != null)
                {
                    fs.Close();
                }
            }
        }

        /// <summary>
        /// 向已存在且未超过阈值的文件中写入内容,用于向已经存在且文件大小未超过设定阈值的普通日志文件中追加新的日志信息,按照一定格式写入日志内容并进行必要的异常处理
        /// </summary>
        private static void WriteToExistingFile(Enum jb, string zhi, string currentLogFilePath)
        {
            FileStream fs = null;
            try
            {
                // 以追加模式打开当前的普通日志文件,准备写入新的日志内容
                fs = File.Open(currentLogFilePath, FileMode.Append);
                using (StreamWriter sw = new StreamWriter(fs))
                {
                    // 将写入位置定位到文件末尾,使得新日志信息添加在文件已有内容的后面
                    sw.BaseStream.Seek(0, SeekOrigin.End);

                    // 获取当前日志级别字符串表示(虽然取消了日志级别过滤功能,但这里保留获取级别字符串表示的代码),用于在日志内容中记录当前日志的级别信息
                    string zt = jb.ToString();
                    sw.Write("[{0}] 操作时间:" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + "  内容:{1}\n", zt, zhi);
                    sw.Flush();
                }
            }
            catch (IOException ex)
            {
                MessageBox.Show($"写入日志到文件 {currentLogFilePath} 失败,异常信息: {ex.Message}");
                throw; // 重新抛出异常,保证数据完整性,不丢失此次写入数据的异常情况,确保日志记录的可靠性
            }
            finally
            {
                if (fs != null)
                {
                    fs.Close();
                }
            }
        }

        /// <summary>
        /// 向文件写入内容,并处理文件大小超过阈值的情况,在向普通日志文件写入内容后,检查文件大小是否超过阈值,如果超过则触发文件压缩、切换等相关处理逻辑
        /// </summary>
        private static void WriteToFileAndHandleSize(Enum jb, string zhi, FileStream fs, string currentLogFilePath)
        {
            using (StreamWriter sw = new StreamWriter(fs))
            {
                // 将写入位置定位到文件末尾,确保日志内容按顺序追加到文件末尾
                sw.BaseStream.Seek(0, SeekOrigin.End);

                // 获取当前日志级别字符串表示(虽然取消了日志级别过滤功能,但这里保留获取级别字符串表示的代码),用于在日志记录中体现日志级别信息
                string zt = jb.ToString();
                sw.Write("[{0}] 操作时间:" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + "  内容:{1}\n", zt, zhi);
                sw.Flush();
            }

            // 获取文件大小(如果文件存在的话),只针对文本A进行大小检查,根据当前是否正在使用文本A以及文件是否存在来决定是否检查文件大小并进行后续处理
            if (isUsingTextA && File.Exists(currentLogFilePath))
            {
                long fileSize = new FileInfo(currentLogFilePath).Length;
                if (fileSize >= MaxLogFileSizeForCompress)
                {
                    HandleFileCompressionAndSwitch(currentLogFilePath);
                }
            }
        }

        /// <summary>
        /// 处理文件压缩以及切换使用的文件(从文本A切换到文本B),当文本A对应的日志文件大小达到阈值后,执行文件压缩操作,将文本A文件压缩保存到指定的压缩文件夹中,然后删除原文本A文件,并切换到使用文本B进行后续日志记录
        /// </summary>
        private static void HandleFileCompressionAndSwitch(string currentLogFilePath)
        {
            // 确保压缩文件夹存在,如果不存在则创建,为即将进行的文件压缩操作准备好存放压缩文件的目标文件夹
            EnsureFolderExists(CompressFolderPath);

            // 生成压缩文件的文件名(文本A的压缩文件名,带上时分秒信息),在原日志文件名基础上添加当前时分秒信息,使得压缩文件名具有唯一性,便于区分不同时间压缩的文件
            string zipFileName = Path.GetFileNameWithoutExtension(currentLogFilePath) + "_" + DateTime.Now.ToString("HH-mm-ss") + ".zip";
            string zipFilePath = Path.Combine(CompressFolderPath, zipFileName);

            try
            {
                // 使用SharpZipLib进行文件压缩(文本A),创建输出的压缩文件流,然后基于此流创建ZipOutputStream对象用于实际的压缩操作,并设置较高的压缩级别(0 - 9,9为最高压缩比,可按需调整)以获得较好的压缩效果
                using (FileStream fsOut = File.Create(zipFilePath))
                {
                    using (ZipOutputStream zipStream = new ZipOutputStream(fsOut))
                    {
                        zipStream.SetLevel(9);

                        ZipEntry entry = new ZipEntry(Path.GetFileName(currentLogFilePath));
                        zipStream.PutNextEntry(entry);

                        using (FileStream fsIn = File.OpenRead(currentLogFilePath))
                        {
                            byte[] buffer = new byte[4096];
                            int sourceBytes;
                            do
                            {
                                sourceBytes = fsIn.Read(buffer, 0, buffer.Length);
                                zipStream.Write(buffer, 0, sourceBytes);
                            } while (sourceBytes > 0);
                        }
                        zipStream.CloseEntry();
                    }
                }
            }
            catch (IOException ex)
            {
                MessageBox.Show($"压缩日志文件 {currentLogFilePath} 失败,异常信息: {ex.Message}");
                throw; // 重新抛出异常,避免掩盖压缩失败问题,确保上层代码知道压缩操作出现了异常情况
            }

            try
            {
                // 删除原日志文件(文本A)(可根据实际需求考虑是否备份等其他操作)
                File.Delete(currentLogFilePath);
            }
            catch (IOException ex)
            {
                MessageBox.Show($"删除原日志文件 {currentLogFilePath} 失败,异常信息: {ex.Message}");
                throw; // 重新抛出异常,防止后续出现文件冲突等问题
            }

            // 切换到使用文本B进行后续写入
            isUsingTextA = false;
        }
    }
}
相关推荐
shaoweijava4 分钟前
私人诊所管理系统(源码+数据库+报告)
java·开发语言·数据库·spring boot·mysql·mybatis
知识分享小能手5 分钟前
Java学习教程,从入门到精通,Java Stack(堆栈)语法知识点及语法知识点(58)
java·大数据·开发语言·学习·intellij-idea·java后端·java开发
YiSLWLL20 分钟前
使用rust语言创建python模块(pyo3+maturin)
开发语言·python·rust
Clown9528 分钟前
go-zero(十三)使用MapReduce并发
开发语言·golang·mapreduce
YAy1731 分钟前
FastJson反序列化学习-01
java·开发语言·安全·web安全·网络安全
梦夏夜32 分钟前
go 跨平台打包
开发语言·后端·golang
光头man41 分钟前
【C语言初阶(七)】操作符详解
c语言·开发语言
caifox44 分钟前
C# 探险之旅:第三十八节 - 类型class之Base与This关键字大冒险
开发语言·c#
caifox1 小时前
C# 探险之旅:第二十二节 - 字符串:字符串的奇妙魔法乐园
开发语言·c#
123yhy传奇1 小时前
【学习总结|DAY018】Java异常、泛型、集合框架
java·开发语言