C# 如何设计一个好用的日志库?【架构篇】 您所在的位置:网站首页 日语翻译注音的应用软件 C# 如何设计一个好用的日志库?【架构篇】

C# 如何设计一个好用的日志库?【架构篇】

2023-04-27 23:22| 来源: 网络整理| 查看: 265

本文介绍了 C# 应用程序记录日志的三种方法,从简到繁逐步深入,并在最后一部分介绍了一个很好用的日志查看工具 TextAnalysisTool。 〇、前言

相信你在实际工作期间经常遇到或听到这样的说法:

  “我现在加一下日志,等会儿你再操作下。”

  “只有在程序出问题以后才会知道打一个好的日志有多么重要。”

可见日志的记录是日常开发的必备技能。

记录日志的必要性:

  当业务比较复杂时,在关键代码附件添加合适的日志是非常重要的,这样可以出现异常后,有章可循,较快速的在不停服的情况下,定位问题并解决。特别是在项目组中,人员较多,若没有统一的日志记录规范,查找系统问题原因就更加费时费力。

记录日志的三种实现:

当业务比较简单,性能要求不高,只是单纯的记录程序的运行是否正常。此时就可以参考本文第一种实现,仅一种级别的文本记录。 当业务复杂较复杂,对性能有一定要求时,可以根据实际情况,参考本文的第二、第三种实现。 当业务非常复杂,必然运行的效率就要求比较高,如何即让程序稳定高效的运行,又能合理记录程序运行状态成为关键。高效的的日志操作可以参考本文的第三种实现。 一、日志的简单记录

如下,为简单的记录开发人员预输出的文本内容,其内容为自定义,输出的时间格式和固定标识需相同。

此方法的性能当然是最差的,针对同一个日志文件,需要独占访问,当同时出现多个记录需求时,会出现排队的情况,导致系统出现卡顿。当然,可以采用多目标文件的方式来提高性能表现,若业务较复杂,还是推荐使用后两种方式。

日志内容测试结果:

public static string strlock = string.Empty; static void Main(string[] args) { lock(strlock) // 在同一个日志文件操作范围添加同一个锁,避免多线程操作时因抢占资源而报错 { WriteLogPublic.WriteLogFunStr("Program", "Main", "日志内容1"); // 实际生成的路径:C:\Logs\Program\Main\202304\log07.log // 记录的内容:2023-04-07 11-21-31 --- 日志内容1 } }

 日志类内容:

public class WriteLogPublic { /// /// 记录日志 /// /// 项目名称 /// 控制器名称 /// 日志内容 public static void WriteLogFunStr(string projectname, string controllername, string strlog) { string sfilepath = $"C:\\Logs\\{projectname}\\{controllername}\\{DateTime.Now.ToString("yyyyMM")}"; // 根据项目名称等创建文件夹 string sfilename = $"log{DateTime.Now.ToString("dd")}.log"; sfilename = sfilepath + "\\" + sfilename; // 文件的绝对路径 // if (!Directory.Exists(sfilepath)) // 验证路径是否存在(此句可省略,因为 CreateDirectory 方法创建文件路径前会判断是否存在) Directory.CreateDirectory(sfilepath); // 不存在则创建 using (FileStream fs = new FileStream(sfilename, FileMode.OpenOrCreate, FileAccess.Write)) { using (var sw = new StreamWriter(fs)) { sw.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH-mm-ss") + " --- " + strlog); } } } } 二、通过开源库 HslCommunication 记录不同级别的日志

此方式记录日志,简单高效,可以实现不同级别日志的输出控制,日志选项的配置可以配置在程序的配置文件中,在程序启动时加载即可。

若想实现实时加载,这只能在每次写日志前初始化日志对象,这样估计就影响程序性能了。

日志内容测试结果:

static void Main(string[] args) { // 先初始化配置 HslCommunicationOper HslCommunicationOper.HslComLogCollection("Test.ConsoleApp", "Main", 5, HslCommunication.LogNet.GenerateMode.ByEveryHour); // HslCommunicationOper.HslComLog("Test.ConsoleApp", "Main"); // 单文件 // HslCommunicationOper.HslComLogSize("Test.ConsoleApp", "MainSize", 5); // 增加日志单文件大小配置 // HslCommunicationOper.HslComLogByDate("Test.ConsoleApp", "MainDate", TimeType.Day); // 按照日期分文件保存 HslCommunicationOper.SetMessageDegree(MessageDegree.WARN);//日志级别 // 记录日志 HslCommunicationOper.logNet.WriteDebug("调试信息"); HslCommunicationOper.logNet.WriteInfo("一般信息"); HslCommunicationOper.logNet.WriteWarn("警告信息"); HslCommunicationOper.logNet.WriteError("错误信息"); HslCommunicationOper.logNet.WriteFatal("致命信息"); HslCommunicationOper.logNet.WriteDebug("KeyWord调试信息", "调试信息"); HslCommunicationOper.logNet.WriteInfo("KeyWord一般信息", "一般信息"); HslCommunicationOper.logNet.WriteWarn("KeyWord警告信息", "警告信息"); HslCommunicationOper.logNet.WriteError("KeyWord错误信息", "错误信息"); HslCommunicationOper.logNet.WriteFatal("KeyWord致命信息", "致命信息"); HslCommunicationOper.logNet.WriteException("KeyWord-WriteException", new IndexOutOfRangeException()); HslCommunicationOper.logNet.WriteDebug("调试信息"); HslCommunicationOper.logNet.WriteInfo("一般信息"); HslCommunicationOper.logNet.WriteWarn("警告信息"); HslCommunicationOper.logNet.WriteError("错误信息"); HslCommunicationOper.logNet.WriteFatal("致命信息"); } // 日志输出格式示例: [警告] 2023-04-07 18:22:03.565 Thread:[001] 警告信息 [错误] 2023-04-07 18:22:03.605 Thread:[001] 错误信息 [致命] 2023-04-07 18:22:03.605 Thread:[001] 致命信息 [警告] 2023-04-07 18:22:03.605 Thread:[001] KeyWord警告信息 : 警告信息 [错误] 2023-04-07 18:22:03.605 Thread:[001] KeyWord错误信息 : 错误信息 [致命] 2023-04-07 18:22:03.605 Thread:[001] KeyWord致命信息 : 致命信息 [致命] 2023-04-07 18:22:03.676 Thread:[001] KeyWord-WriteException : 错误信息:Index was outside the bounds of the array. 错误源: 错误堆栈: 错误类型:System.IndexOutOfRangeException 错误方法: /=================================================[ Exception ]================================================/ [警告] 2023-04-07 18:22:03.676 Thread:[001] 警告信息 [错误] 2023-04-07 18:22:03.676 Thread:[001] 错误信息 [致命] 2023-04-07 18:22:03.676 Thread:[001] 致命信息

三个相关日志类:

HslCommunicationOper:操作类; LogNetCollection:扩展类(提供日志文件的大小、生成新文件频率的配置); MessageDegree:消息级别枚举。 public static class HslCommunicationOper { public static ILogNet logNet = null; /// /// 日志文件根目录 /// public static string rootpath = "C:\\Log"; /// /// 单日志文件存储 /// /// /// 日志文件名 public static void HslComLog(string projectname, string opername) { logNet = new LogNetSingle($"{rootpath}\\{projectname}\\{opername}.txt"); logNet.SetMessageDegree(HslMessageDegree.DEBUG); // 默认存储最低级别为 DEBUG } /// /// 限定日志文件大小 /// /// /// 日志上级文件夹名 /// 日志文件大小(单位:M) 1~20,默认 5 public static void HslComLogSize(string projectname, string opername, int logfilesize = 5) { if (logfilesize < 1 || logfilesize > 20) logfilesize = 5; logNet = new LogNetFileSize($"{rootpath}\\{projectname}\\{opername}", logfilesize * 1024 * 1024); // 单位M(5M):5 * 1024 * 1024 logNet.SetMessageDegree(HslMessageDegree.DEBUG); // 默认存储最低级别为 DEBUG } /// /// 按照日期存储 /// /// /// 日志上级文件夹名 /// 传入枚举类型(TimeType),值范围:Minute、Hour、Day、Month、Season、Year public static void HslComLogByDate(string projectname, string opername, GenerateMode generateMode = GenerateMode.ByEveryDay) { logNet = new LogNetDateTime($"{rootpath}\\{projectname}\\{opername}", generateMode); // 按每天 logNet.SetMessageDegree(HslMessageDegree.DEBUG); // 默认存储最低级别为 DEBUG } /// /// 按照文件或日期存储 /// /// /// 日志上级文件夹名 /// 传入枚举类型 GenerateMode public static void HslComLogCollection(string projectname, string opername, int filesize, GenerateMode generateMode = GenerateMode.ByEveryDay) { logNet = new LogNetCollection($"{rootpath}\\{projectname}\\{opername}", filesize * 1024 * 1024, generateMode); logNet.SetMessageDegree(HslMessageDegree.DEBUG); // 默认存储最低级别为 DEBUG } /// /// 单独配置日志级别 /// /// 默认 DEBUG public static void SetMessageDegree(MessageDegree messageDegree = MessageDegree.DEBUG) { switch (messageDegree) { case MessageDegree.DEBUG: logNet.SetMessageDegree(HslMessageDegree.DEBUG); // 所有等级存储 break; case MessageDegree.INFO: logNet.SetMessageDegree(HslMessageDegree.INFO); // 除 DEBUG 外,都存储 break; case MessageDegree.WARN: logNet.SetMessageDegree(HslMessageDegree.WARN); // 除 DEBUG 和 INFO 外,都存储 break; case MessageDegree.ERROR: logNet.SetMessageDegree(HslMessageDegree.ERROR); // 只存储 ERROR 和 FATAL break; case MessageDegree.FATAL: logNet.SetMessageDegree(HslMessageDegree.FATAL); // 只存储 FATAL break; case MessageDegree.None: logNet.SetMessageDegree(HslMessageDegree.None); // 不存储任何等级 break; } } } public class LogNetCollection : LogPathBase, ILogNet, IDisposable { private int fileMaxSize = 10485760; // 默认 10M private int currentFileSize = 0; private GenerateMode generateMode = GenerateMode.ByEveryYear; public LogNetCollection(string filePath, int fileMaxSize = 10485760, GenerateMode generateMode = GenerateMode.ByEveryDay, int fileQuantity = -1) { base.filePath = filePath; this.fileMaxSize = fileMaxSize; this.generateMode = generateMode; controlFileQuantity = fileQuantity; base.LogSaveMode = LogSaveMode.FileFixedSize; if (!string.IsNullOrEmpty(filePath) && !Directory.Exists(filePath)) { Directory.CreateDirectory(filePath); } } protected override string GetFileSaveName() { if (string.IsNullOrEmpty(filePath)) { return string.Empty; } if (string.IsNullOrEmpty(fileName)) { fileName = GetLastAccessFileName(); } if (File.Exists(fileName)) { FileInfo fileInfo = new FileInfo(fileName); if (fileInfo.Length > fileMaxSize) { fileName = GetDefaultFileName(); } else { currentFileSize = (int)fileInfo.Length; } } return fileName; } private string GetLastAccessFileName() { string[] existLogFileNames = GetExistLogFileNames(); foreach (string result in existLogFileNames) { FileInfo fileInfo = new FileInfo(result); if (fileInfo.Length < fileMaxSize) // 判断已创建的日志文件是否达到最大内存 { currentFileSize = (int)fileInfo.Length; return result; } } return GetDefaultFileName(); // 若未创建过,通过指定方式创建 } private string GetDefaultFileName() { switch (generateMode) { case GenerateMode.ByEveryMinute: return Path.Combine(filePath, "Logs_" + DateTime.Now.ToString("yyyyMMdd_HHmm") + ".txt"); case GenerateMode.ByEveryHour: return Path.Combine(filePath, "Logs_" + DateTime.Now.ToString("yyyyMMdd_HH") + ".txt"); case GenerateMode.ByEveryDay: return Path.Combine(filePath, "Logs_" + DateTime.Now.ToString("yyyyMMdd") + ".txt"); case GenerateMode.ByEveryWeek: { GregorianCalendar gregorianCalendar = new GregorianCalendar(); int weekOfYear = gregorianCalendar.GetWeekOfYear(DateTime.Now, CalendarWeekRule.FirstDay, DayOfWeek.Monday); return Path.Combine(filePath, "Logs_" + DateTime.Now.Year + "_W" + weekOfYear + ".txt"); } case GenerateMode.ByEveryMonth: return Path.Combine(filePath, "Logs_" + DateTime.Now.ToString("yyyy_MM") + ".txt"); case GenerateMode.ByEverySeason: return Path.Combine(filePath, "Logs_" + DateTime.Now.Year + "_Q" + (DateTime.Now.Month / 3 + 1) + ".txt"); case GenerateMode.ByEveryYear: return Path.Combine(filePath, "Logs_" + DateTime.Now.Year + ".txt"); default: return string.Empty; } } public override string ToString() { return $"LogNetFileSize[{fileMaxSize}];LogNetDateTime[{generateMode}]"; } } /// /// 消息级别 /// public enum MessageDegree { DEBUG = 1, INFO = 2, WARN = 3, ERROR = 4, FATAL = 5, None = 9 }

  参考:C# 日志记录分级功能使用 按照日期,大小,或是单文件存储

三、通过开源库 NLog 实现通过配置文件配置日志选项

 NLog 是一个基于 .net 平台编写的日志记录类库,我们可以使用 NLog 在应用程序中添加极为完善的跟踪调试代码。

 本文将通过日志框架 Nlog 和 ConcurrentQueue 队列,实现一个高性能的日志库。

 首先,为什么相中了 Nlog ?

NLog 是适用于各个 .net 平台的灵活且免费的日志记录平台。通过 NLog, 可以轻松地写入多个目标(例如:数据库、文件、控制台等), 并可动态更改日志记录配置信息。 NLog 支持结构化和传统日志记录。 NLog 的特点: 高性能、易于使用、易于扩展和灵活配置。

ConcurrentQueue:表示线程安全的先进先出(FIFO)集合。所有公共成员和受保护成员 ConcurrentQueue 都是线程安全的,可以从多个线程并发使用。

1. 配置文件

对于 ASP.NET 应用程序,存在嵌入程序配置文件和单独配置文件两种方式,程序在启动时,会在应用程序主目录下依次查找:web.config(*.exe.config、*.web.config)、web.nlog(*.exe.nlog)、NLog.config。

个人推荐单独文件配置,便于修改和迭代使用。

第一种方式:单独配置文件

  常用名称为 NLog.config。此时需要在根节点 nlog 加上智能感知(Intellisense)的属性配置,详见下文配置文件 XML 代码。

  1/5 targets(必须有) - 定义日志目标/输出

name:是指的输出地方的一个名词(给 rules 调用的); xsi:type:输出文件的类型,File 指的是文件,Console 控制台输出; fileName:输出到目标文件的地址,使用的相对路径,可以自行配置输出的地点。 layout:在最简单的形式中,布局是带有嵌入标记的文本,这些嵌入标记样子例如:${xxxx}; archiveFileName:表示滚动日志存放路径; archiveAboveSize:单次日志的存储大小(单位是Byte),超过配置,会 archiveFileName 中创建新的日志文件; maxArchiveFiles:最多保留日志文件的数量,超过后将最早的日志文件自动清除,若值 _obj = value; } public LoggerHelper() { InitializeTask(); } private static LogModel logModel_init = null; /// /// 初始化后台线程 /// private void InitializeTask() { if (logModel_init == null) { logModel_init = new LogModel(); Thread t = new Thread(new ThreadStart(LogOperation)); t.IsBackground = false; t.Start(); } } /// /// 记录日志 /// private void LogOperation() { while (true) // 线程持续处理 { if (concurrentQueue_assistant.Count > 0 && concurrentQueue_operation.Count == 0) { lock (lockobj_assistant) { concurrentQueue_operation = concurrentQueue_assistant; // 将数据转至操作队列 concurrentQueue_assistant = new ConcurrentQueue(); // 注意此处不可用 .Clear() 因为 ConcurrentQueue 为引用类型 } LogModel logModel; // 取出队列 concurrentQueue_operation 中待写入的日志记录,直至全部记录完成 while (concurrentQueue_operation.Count > 0 && concurrentQueue_operation.TryDequeue(out logModel)) { switch (logModel.type) // 日志类型分流 { case NLogLevel.Trace: if (logModel.exobj != null) logger.Trace(logModel.content); else logger.Trace(logModel.content, logModel.exobj); break; case NLogLevel.Debug: if (logModel.exobj != null) logger.Debug(logModel.content); else logger.Debug(logModel.content, logModel.exobj); break; case NLogLevel.Info: if (logModel.exobj != null) logger.Info(logModel.content, logModel.exobj); else logger.Info(logModel.content); break; case NLogLevel.Error: if (logModel.exobj != null) logger.Error(logModel.content, logModel.exobj); else logger.Error(logModel.content); break; case NLogLevel.Warn: if (logModel.exobj != null) logger.Warn(logModel.content, logModel.exobj); else logger.Warn(logModel.content); break; case NLogLevel.Fatal: if (logModel.exobj != null) logger.Fatal(logModel.content, logModel.exobj); else logger.Fatal(logModel.content); break; default: break; } } } else Thread.Sleep(1000); } } /// /// 加入队列前,根据日志级别统一验证 /// /// public void EnqueueLogModel(LogModel logModel) { if ((logModel.type == NLogLevel.Trace && logger.IsTraceEnabled) || (logModel.type == NLogLevel.Debug && logger.IsDebugEnabled) || (logModel.type == NLogLevel.Info && logger.IsInfoEnabled) || (logModel.type == NLogLevel.Warn && logger.IsWarnEnabled) || (logModel.type == NLogLevel.Error && logger.IsErrorEnabled) || (logModel.type == NLogLevel.Fatal && logger.IsFatalEnabled)) { lock (lockobj_assistant) { concurrentQueue_assistant.Enqueue(logModel); } } } /// /// Trace,追踪,非常详细的日志,该日志等级通常仅在开发过程中被使用 /// /// public void Trace(string logcontent) { EnqueueLogModel(new LogModel() { type = NLogLevel.Trace, content = logcontent }); } public void Trace(string logcontent, Exception exception) { EnqueueLogModel(new LogModel() { type = NLogLevel.Trace, content = logcontent, exobj = exception }); } /// /// Debug,调试,详尽信息次于 Trace,在生产环境中通常不启用 /// /// public void Debug(string logcontent) { EnqueueLogModel(new LogModel() { type = NLogLevel.Debug, content = logcontent }); } public void Debug(string logcontent, Exception exception) { EnqueueLogModel(new LogModel() { type = NLogLevel.Debug, content = logcontent, exobj = exception }); } /// /// Info,信息,通常在生产环境中通常启用 /// /// public void Info(string logcontent) { EnqueueLogModel(new LogModel() { type = NLogLevel.Info, content = logcontent }); } public void Info(string logcontent, Exception exception) { EnqueueLogModel(new LogModel() { type = NLogLevel.Info, content = logcontent, exobj = exception }); } /// /// Warn,警告,通常用于非关键问题,这些问题可以恢复,或者是暂时的故障 /// /// public void Warn(string logcontent) { EnqueueLogModel(new LogModel() { type = NLogLevel.Warn, content = logcontent }); } public void Warn(string logcontent, Exception exception) { EnqueueLogModel(new LogModel() { type = NLogLevel.Warn, content = logcontent, exobj = exception }); } /// /// Error,错误,多数情况下记录Exceptions(异常)信息 /// /// public void Error(string logcontent) { EnqueueLogModel(new LogModel() { type = NLogLevel.Error, content = logcontent }); } public void Error(string logcontent, Exception exception) { EnqueueLogModel(new LogModel() { type = NLogLevel.Error, content = logcontent, exobj = exception }); } /// /// Fatal,致命错误,非常严重的错误 /// /// public void Fatal(string logcontent) { EnqueueLogModel(new LogModel() { type = NLogLevel.Fatal, content = logcontent }); } public void Fatal(string logcontent, Exception exception) { EnqueueLogModel(new LogModel() { type = NLogLevel.Fatal, content = logcontent, exobj = exception }); } } public class LogModel { public NLogLevel type { get; set; } public string content { get; set; } public Exception exobj { get; set; } } /// /// NLog 日志等级 /// public enum NLogLevel { Trace, Debug, Info, Warn, Error, Fatal }

  参考:C# 超高速高性能写日志 代码开源       .net core 中的那些常用的日志框架(NLog篇)

四、日志查看器 TextAnalysisTool.NET

作为一名研发人员,高效率的日志分析是必须的,当然好的工具也是前提条件。

要想高效分析日志,有几个问题需要解决:

快速定位,在海量日志信息中快速定位目标行; 高亮显示,以不同颜色显示目标行,以便分类提高辨识度; 只显示有用的行。

在日常开发使用最多的莫过于 NotePad++ 了,尽管其可以通过 “搜索-标记/标记所有-使用格式1/2/3/4/5”的操作来实现以上的前两点,但是操作较繁琐,当日志行数比较多时,也无法仅显示标记行,从而造成效率低下。

当然,对于普通的业务量不太高的日志记录,NotePad++ 足以满足使用。

下面介绍一个非常简单实用的开源日志查看工具 TextAnalysisTool.NET。

官网地址:http://textanalysistool.github.io/  用法地址:http://textanalysistool.github.io/TextAnalysisTool.NET.txt   1. 下载应用程序包 下载地址:http://github.com/TextAnalysisTool/Releases/raw/master/TextAnalysisTool.NET.zip 

下载完成后,如下图打开最新版的应用程序:

  

2. 分析的日志文件

按照“File -> Open”选择要打开的日志文件。

双击任意行,便会跳出“Add Filter”窗口:(Text 默认为鼠标焦点行的内容)

  

可以通过修改“Text Color”和“Background”来指定查询结果的文本和行底色,达到高亮显示目的。

其他选项:Description:描述;Excluding:排除,不包含;Case-sensitive:大小写敏感;Regular-expression:按照正则表达式查询。

如下图示例,查询三个语句,标志为不同的行底色效果:

  

若想只显示查询目标所在的行,可以如下图鼠标操作,也可使用快捷键 Ctrl+H,取消时重复操作即可。

  

  参考:使用TextAnalysisTool来快速提高你分析文本日志的效率

本文来自博客园,作者:橙子家,微信号:zfy1070491745,有任何疑问欢迎沟通,一起成长。

转载本文请注明原文链接:https://www.cnblogs.com/czzj/p/JGP_MyLog.html



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有