C# 如何设计一个好用的日志库?架构篇
Posted 橙子家
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C# 如何设计一个好用的日志库?架构篇相关的知识,希望对你有一定的参考价值。
〇、前言
相信你在实际工作期间经常遇到或听到这样的说法:
“我现在加一下日志,等会儿你再操作下。”
“只有在程序出问题以后才会知道打一个好的日志有多么重要。”
可见日志的记录是日常开发的必备技能。
记录日志的必要性:
当业务比较复杂时,在关键代码附件添加合适的日志是非常重要的,这样可以出现异常后,有章可循,较快速的在不停服的情况下,定位问题并解决。特别是在项目组中,人员较多,若没有统一的日志记录规范,查找系统问题原因就更加费时费力。
记录日志的三种实现:
- 当业务比较简单,性能要求不高,只是单纯的记录程序的运行是否正常。此时就可以参考本文第一种实现,仅一种级别的文本记录。
- 当业务复杂较复杂,对性能有一定要求时,可以根据实际情况,参考本文的第二、第三种实现。
- 当业务非常复杂,必然运行的效率就要求比较高,如何即让程序稳定高效的运行,又能合理记录程序运行状态成为关键。高效的的日志操作可以参考本文的第三种实现。
一、日志的简单记录
如下,为简单的记录开发人员预输出的文本内容,其内容为自定义,输出的时间格式和固定标识需相同。
此方法的性能当然是最差的,针对同一个日志文件,需要独占访问,当同时出现多个记录需求时,会出现排队的情况,导致系统出现卡顿。当然,可以采用多目标文件的方式来提高性能表现,若业务较复杂,还是推荐使用后两种方式。
日志内容测试结果:
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
/// <summary>
/// 记录日志
/// </summary>
/// <param name="projectname">项目名称</param>
/// <param name="controllername">控制器名称</param>
/// <param name="strlog">日志内容</param>
public static void WriteLogFunStr(string projectname, string controllername, string strlog)
string sFilePath = $"C:\\\\Logs\\\\projectname\\\\controllername\\\\DateTime.Now.ToString("yyyyMM")"; // 根据项目名称等创建文件夹
string sFileName = $"logDateTime.Now.ToString("dd").log";
sFileName = sFilePath + "\\\\" + sFileName; // 文件的绝对路径
if (!Directory.Exists(sFilePath)) // 验证路径是否存在
Directory.CreateDirectory(sFilePath); // 不存在则创建
FileStream fs;
StreamWriter sw;
if (File.Exists(sFileName)) // 验证文件是否存在,有则追加,无则创建
fs = new FileStream(sFileName, FileMode.Append, FileAccess.Write);
else
fs = new FileStream(sFileName, FileMode.Create, FileAccess.Write);
sw = new StreamWriter(fs);
sw.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH-mm-ss") + " --- " + strlog);
sw.Close();
fs.Close();
二、通过开源库 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;
/// <summary>
/// 日志文件根目录
/// </summary>
public static string rootpath = "C:\\\\Log";
/// <summary>
/// 单日志文件存储
/// </summary>
/// <param name="projectname"></param>
/// <param name="opername">日志文件名</param>
public static void HslComLog(string projectname, string opername)
logNet = new LogNetSingle($"rootpath\\\\projectname\\\\opername.txt");
logNet.SetMessageDegree(HslMessageDegree.DEBUG); // 默认存储最低级别为 DEBUG
/// <summary>
/// 限定日志文件大小
/// </summary>
/// <param name="projectname"></param>
/// <param name="opername">日志上级文件夹名</param>
/// <param name="logfilesize">日志文件大小(单位:M) 1~20,默认 5</param>
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
/// <summary>
/// 按照日期存储
/// </summary>
/// <param name="projectname"></param>
/// <param name="opername">日志上级文件夹名</param>
/// <param name="recodemode">传入枚举类型(TimeType),值范围:Minute、Hour、Day、Month、Season、Year</param>
public static void HslComLogByDate(string projectname, string opername, GenerateMode generateMode = GenerateMode.ByEveryDay)
logNet = new LogNetDateTime($"rootpath\\\\projectname\\\\opername", generateMode); // 按每天
logNet.SetMessageDegree(HslMessageDegree.DEBUG); // 默认存储最低级别为 DEBUG
/// <summary>
/// 按照文件或日期存储
/// </summary>
/// <param name="projectname"></param>
/// <param name="opername">日志上级文件夹名</param>
/// <param name="generateMode">传入枚举类型 GenerateMode</param>
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
/// <summary>
/// 单独配置日志级别
/// </summary>
/// <param name="messageDegree">默认 DEBUG</param>
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]";
/// <summary>
/// 消息级别
/// </summary>
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<T> 都是线程安全的,可以从多个线程并发使用。
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:单次日志的存储大小(单位是KB),超过配置,会 archiveFileName 中创建新的日志;
- archiveNumbering:Sequence(排序),Rolling(滚动);
- concurrentWrites:支持多个并发一起写文件,提高文件写入性能;
- keepFileOpen:为了提高文件写入性能,避免每次写入文件都开关文件;
- autoFlush:为了提高日志写入性能,不必每次写入日志都直接写入到硬盘;
- createDirs:若设置的日志文件夹不存在,则自动创建文件夹,true:创建;false:不创建。
其中,layout 属性的标记变量($xxx)解析可以参考以下代码:
点击展开 查看标记释义
$cached - 将缓存应用于另一个布局输出。
$db-null - 为数据库呈现 DbNull
$exception - 通过调用记录器方法之一提供的异常信息
$level - 日志级别(例如错误、调试)或级别序号(数字)
$literal - 字符串 literal。(文本) - 用于转义括号
$logger - 记录器名称。GetLogger, GetCurrentClassLogger 等
$message - (格式化的)日志消息。
$newline - 换行符文字。
$object-path - 呈现对象的(嵌套)属性
$onexception - 仅在为日志消息定义了异常时才输出内部布局。
$onhasproperties - 仅当事件属性包含在日志事件中时才输出内部布局。
$var - 渲染变量
// 调用站点和堆栈跟踪
$callite - 调用站点(类名、方法名和源信息)
$callite-filename - 调用站点源文件名。
$callsite-linenumber - 呼叫站点源行编号。
$stacktrace - Render the Stack trace
// 条件
$when - 仅在满足指定条件时输出内部布局。
$whenempty - 当内部布局生成空结果时输出备用布局。
// 上下文信息
$activity - 从 System.Diagnostics.Activity.Current NLog.DiagnosticSource External 捕获跟踪上下文
$activityid - 将 System.Diagnostics 跟踪关联 ID 放入日志中。
$all-event-properties - 记录所有事件上下文数据。
$event-context - 记录事件属性数据 - 替换为 $事件属性
$event-properties - 记录事件属性数据 - 重命名 $事件-上下文
$gdc - 全局诊断上下文项。用于保存每个应用程序实例值的字典结构。
$install-context - 安装参数(传递给 InstallNLogConfig)。
$mdc - 映射的诊断上下文 - 线程本地结构。
$mdlc - 异步映射诊断上下文 - 作用域内上下文的线程本地结构。MDC 的异步版本。
$ndc - 嵌套诊断上下文 - 线程本地结构。
$ndlc - 异步嵌套诊断上下文 - 线程本地结构。
// 计数器
$counter - 计数器值(在每次布局呈现时增加)
$guid - 全局唯一标识符(GUID)。
$sequenceid - 日志序列 ID
// 日期和时间
$date - 当前日期和时间。
$longdate - 日期和时间采用长而可排序的格式"yyyy-MM-dd HH:mm:ss.ffff"。
$qpc - 高精度计时器,基于从 QueryPerformanceCounter 返回的值。
$shortdate - 可排序格式为 yyyy-MM-dd 的短日期。
$ticks - 当前日期和时间的分笔报价值。
$time - 24 小时可排序格式的时间 HH:mm:ss.mmm。
// 编码和字符串转换
$json-encode - 使用 JSON 规则转义另一个布局的输出。
$left - 文本的剩余部分
$lowercase - 将另一个布局输出的结果转换为小写。
$norawvalue - 防止将另一个布局呈现器的输出视为原始值
$pad - 将填充应用于另一个布局输出。
$replace - 将另一个布局输出中的字符串替换为另一个字符串。使用正则表达式可选
$replace-newlines - 将换行符替换为另一个字符串。
$right - 文本的右侧部分
$rot13 - 使用 ROT-13 解码"加密"的文本。
$substring - 文本的子字符串
$trim-whitespace - 从另一个布局呈现器的结果中修剪空格。
$uppercase - 将另一个布局输出的结果转换为大写。
$url-encode - 对另一个布局输出的结果进行编码,以便与 URL 一起使用。
$wrapline - 以指定的行长度换行另一个布局输出的结果。
$xml-encode - 将另一个布局输出的结果转换为符合 XML 标准。
// 环境和配置文件
$appsetting - 来自 .config 文件 NLog.Extended 的应用程序配置设置
$configsetting - 来自 appsettings.json 的值或 ASP.NET Core & .NET Core NLog.Extensions.LoggingNLog.Extensions.HostingNLog.Web.AspNetCore
$environment - 环境变量。(例如 PATH、OSVersion)
$environment-user - 用户标识信息(用户名)。
$registry - 来自 Windows 注册表的值。
// 文件和目录
$basedir - 当前应用程序域的基目录。
$currentdir - 应用程序的当前工作目录。
$dir-separator - 操作系统相关目录分隔符。
$file-contents - 呈现指定文件的内容。
$filesystem-normalize - 通过将文件名中不允许使用的字符替换为安全字符来筛选它们。
$nlogdir - NLog.dll所在的目录。
$processdir - 应用程序的可执行进程目录。
$specialfolder - 系统特殊文件夹路径(包括"我的文档"、"我的音乐"、"程序文件"、"桌面"等)。
$tempdir - 一个临时目录。
// 身份
$identity - 线程标识信息(名称和身份验证信息)。
$windows-identity - Thread Windows identity information (username)
$windows-identity - Thread Windows identity information (username) Nlog.WindowsIdentity
// 集成
$gelf - 将 LogEvents 转换为 GELF 格式以发送到 Graylog NLog.GelfLayout External
$log4jxmlevent - XML 事件描述与 log4j、Chainsaw 和 NLogViewer 兼容。
// 进程、线程和程序集
$appdomain - 当前应用域。
$assembly-version - 默认应用程序域中可执行文件的版本。
$gc - 有关垃圾回收器的信息。
$hostname - 运行进程的计算机的主机名。
$local-ip - 来自网络接口的本地 IP 地址。
$machinename - 运行进程的计算机名称。
$performancecounter - 性能计数器。
$processid - 当前进程的标识符。
$processinfo - 有关正在运行的进程的信息,例如 StartTime、PagedMemorySize
$processname - 当前进程的名称。
$processtime - 格式为 HH:mm:ss.mmm 的处理时间。
$threadid - 当前线程的标识符。
$threadname - 当前线程的名称。
// 银光
$document-uri - 承载当前 Silverlight 应用程序的 HTML 页面的 URI。
$sl-appinfo - 有关 Silverlight 应用程序的信息。
// 网络、ASP.NET 和 ASP.NET 核心
$aspnet-appbasepath - ASP.NET Application base path (Content Root) NLog.WebNLog.Web.AspNetCore
$aspnet-application - ASP.NET Application variable. NLog.Web
$aspnet-environment - ASP.NET Environment name NLog.Web.AspNetCore
$aspnet-item - ASP.NET \'HttpContext\' item variable. NLog.WebNLog.Web.AspNetCore
$aspnet-mvc-action - ASP.NET MVC Action Name from routing parameters NLog.WebNLog.Web.AspNetCore
$aspnet-mvc-controller - ASP.NET MVC Controller Name from routing parameters NLog.WebNLog.Web.AspNetCore
$aspnet-request - ASP.NET Request variable. NLog.WebNLog.Web.AspNetCore
$aspnet-request-contenttype - ASP.NET Content-Type header (Ex. application/json) NLog.Web.AspNetCore
$aspnet-request-cookie - ASP.NET Request cookie content. NLog.WebNLog.Web.AspNetCore
$aspnet-request-form - ASP.NET Request form content. NLog.WebNLog.Web.AspNetCore
$aspnet-request-headers - ASP.NET Header key/value pairs. NLog.Web.Web.AspNetCore
$aspnet-request-host - ASP.NET Request host. NLog.WebNLog.Web.AspNetCore
$aspnet-request-ip - Client IP. NLog.WebNLog.Web.AspNetCore
$aspnet-request-method - ASP.NET Request method (GET, POST etc). NLog.WebNLog.Web.AspNetCore
$aspnet-request-posted-body - ASP.NET posted body / payload NLog.WebNLog.Web.AspNetCore
$aspnet-request-querystring - ASP.NET Request querystring. NLog.WebNLog.Web.AspNetCore
$aspnet-request-referrer - ASP.NET Request referrer. NLog.WebNLog.Web.AspNetCore
$aspnet-request-routeparameters - ASP.NET Request route parameters. NLog.WebNLog.Web.AspNetCore
$aspnet-request-url - ASP.NET Request URL. NLog.WebNLog.Web.AspNetCore
$aspnet-request-useragent - ASP.NET Request useragent. NLog.WebNLog.Web.AspNetCore
$aspnet-response-statuscode - ASP.NET Response status code content. NLog.WebNLog.Web.AspNetCore
$aspnet-session - ASP.NET Session variable. NLog.WebNLog.Web.AspNetCore
$aspnet-sessionid - ASP.NET Session ID variable. NLog.WebNLog.Web.AspNetCore
$aspnet-traceidentifier - ASP.NET trace identifier NLog.WebNLog.Web.AspNetCore
$aspnet-user-authtype - ASP.NET User auth. NLog.WebNLog.Web.AspNetCore
$aspnet-user-claim - ASP.NET User Claims 授权值 NLog.Web.AspNetCore
$aspnet-user-identity - ASP.NET User variable. NLog.WebNLog.Web.AspNetCore
$aspnet-user-isauthenticated - ASP.NET User authenticated? NLog.WebNLog.Web.AspNetCore
$aspnet-webrootpath - ASP.NET Web root path (wwwroot) NLog.WebNLog.Web.AspNetCore
$iis-site-name - IIS site name. NLog.WebNLog.Web.AspNetCore
//参考: https://www.cnblogs.com/zmy2020/p/15936886.html
2/5 rules(必须有) - 定义日志路由规则
rules 下只有一种节点 logger(可同时配置多个),其属性释义如下:
- name:logger 名称,若为 * 则表示适用于所有日志,?:匹配单个字符;
- minlevel:表示记录的最低日志级别,只有大于等于该日志级别才会被记录;
- maxlevel:记录的最高级别;
- level:单极记录,只记录一个级别日志;
- levels:同时记录多个级别的日志,用逗号分隔;
- writeTo:和 target 节点的 name 属性值匹配,一个 rules 对应一个 target;
- enabled:通过值为 false 禁用规则,而不用删除;
- ruleName:规则标识符,允许使用 Configuration.FindRuleByName 和进行规则查找 Configuration.RemoveRuleByName,在 NLog 4.6.4 中引入。
3/5 variables - 声明变量的值
variable 元素定义了配置文件中需要用到的变量,一般用来表示复杂或者重复的表达式(例如文件名)。变量需要先定义后使用,否则配置文件将初始化失败。
- name:变量名;
- value:变量值。
定义变量之后,可以通过 $my_name
语法来使用。
4/5 extensions - 定义要加载的 NLog 扩展项 *.dll 文件
extensions 节点可以添加额外的 NLog 元包或自定义功能,assembly 属性指定的被包含程序集不带后缀 .dll 。示例如下:
<nlog>
<extensions>
<add assembly="MyAssembly" />
</extensions>
<targets>
<target name="a1" type="MyFirst" host="localhost" />
</targets>
<rules>
<logger name="*" minLevel="Info" appendTo="a1" />
</rules>
</nlog>
NLog 4.0 之后,与 NLog.dll
同目录下名如 NLog*.dll
的程序集(如:NLog.CustomTarget.dll
)会被自动加载。
5/5 includes - 指定当前配置文件包含多个子配置文件
通过 $ 语法可以使用环境变量,下例展示包含一个名为当前机器名的配置文件。
<nlog>
...
<include file="$machinename.config" />
...
</nlog>
NLog 4.4.2 之后可以使用通配符 *
指定多个文件。例如:<include file="nlog-*.config" />
。
示例配置:
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.nlog-project.org/schemas/NLog.xsd NLog.xsd"
autoReload="true"
throwExceptions="false"
internalLogLevel="Off" internalLogFile="c:\\temp\\nlog-internal.log">
<variable name="appName" value="ConsoleAppDemo"/>
<targets>
<target name="logconsole" xsi:type="Console"
layout="$longdate [$uppercase:$level] $callsite($callsite-filename:includeSourcePath=False:$callsite-linenumber) - $message $exception:format=ToString"
/>
<target name="logfile"
xsi:type="File"
fileName="$basedir/logs/$appName-$shortdate.log"
layout="$longdate [$uppercase:$level] $callsite($callsite-filename:includeSourcePath=False:$callsite-linenumber) - $message $exception:format=ToString"
maxArchiveFiles="999"
archiveFileName="$basedir/logs/$appName-$shortdate-$###.log"
createDirs="true"
archiveAboveSize="102400"
archiveEvery="Day"
encoding="UTF-8"
/>
</targets>
<rules>
<logger name="*" minlevel="Debug" writeTo="logfile" />
</rules>
</nlog>
参考:完善 .Net Core 项目 — NLog入门 (日志组件)
第二种方式:嵌入程序配置文件
NLog 配置信息可以嵌入在 .net 应用程序自身的配置文件中,例如 *.exe.config 或者 *.web.config 中,需要使用 configSections 节点配置,如下 XML 代码,再将其他配置填入 nlog 节点即可。
nlog 节点内的内容,参考前边‘第一种方式’。
<configuration>
<configSections>
<section name="nlog" type="NLog.Config.ConfigSectionHandler, NLog"/>
</configSections>
<nlog>
......
</nlog>
</configuration>
2. 测试代码
static void Main(string[] args)
try
LoggerHelper._.Info($"完成");
LoggerHelper._.Debug($"Debug完成");
LoggerHelper._.Error($"Error完成");
throw (new Exception());
catch (Exception ex)
LoggerHelper._.Error(ex.Message);
// 输出日志
2023-04-04 17:14:45.6651 [INFO] YOKAVerse.Net.Log.LoggerHelper.Info(Logger.cs:40) - 完成
2023-04-04 17:14:46.7303 [DEBUG] YOKAVerse.Net.Log.LoggerHelper.Debug(Logger.cs:28) - Debug完成
2023-04-04 17:14:47.2924 [ERROR] YOKAVerse.Net.Log.LoggerHelper.Error(Logger.cs:76) - Error完成
2023-04-04 17:14:49.5869 [ERROR] YOKAVerse.Net.Log.LoggerHelper.Error(Logger.cs:76) - Exception of type \'System.Exception\' was thrown.
3. 日志记录类
以下代码对 NLog 进行了封装,将日志记录先存在线程安全的队列里,以避免调用写入文件时 I/O 的耗时操作拖垮应用程序。
队列有两个,一个是操作队列-concurrentQueue_operation,一个是助手队列-concurrentQueue_assistant,程序中的日志记录需求直接写入助手队列,避免影响程序频繁写入造成的系统等待。当操作队列中的记录处理完成后,再将助手队列的记录转至操作队列,继续进行比较耗时的写入操作。
当然这种方法在提高系统响应速度的同时,也存在一个弊端,就是在程序崩溃而异常退出时,可能造成积压在队列中的日志记录未全部完成落地,导致日志内容丢失。所以使用时还请权衡利弊,慎重使用。
public class LoggerHelper
/// <summary>
/// 实例化nLog,即为获取配置文件相关信息(获取以当前正在初始化的类命名的记录器)
/// </summary>
private readonly NLog.Logger logger = LogManager.GetCurrentClassLogger();
private static LoggerHelper _obj;
/// <summary>
/// 辅助队列
/// </summary>
private static ConcurrentQueue<LogModel> concurrentQueue_assistant = new ConcurrentQueue<LogModel>();
/// <summary>
/// 操作队列
/// </summary>
private static ConcurrentQueue<LogModel> concurrentQueue_operation = new ConcurrentQueue<LogModel>();
private static string lockobj_assistant = string.Empty;
private static string lockobj_operation = string.Empty;
public static LoggerHelper LHR
get => _obj ?? (_obj = new LoggerHelper());
set => _obj = value;
public LoggerHelper()
InitializeTask();
private static LogModel logModel_init = null;
/// <summary>
/// 初始化后台线程
/// </summary>
private void InitializeTask()
if (logModel_init == null)
logModel_init = new LogModel();
Thread t = new Thread(new ThreadStart(LogOperation));
t.IsBackground = false;
t.Start();
/// <summary>
/// 记录日志
/// </summary>
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<LogModel>(); // 注意此处不可用 .Clear() 因为 ConcurrentQueue<T> 为引用类型
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);
/// <summary>
/// 加入队列前,根据日志级别统一验证
/// </summary>
/// <param name="logModel"></param>
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);
/// <summary>
/// Trace,追踪,非常详细的日志,该日志等级通常仅在开发过程中被使用
/// </summary>
/// <param name="msg"></param>
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 );
/// <summary>
/// Debug,调试,详尽信息次于 Trace,在生产环境中通常不启用
/// </summary>
/// <param name="msg"></param>
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 );
/// <summary>
/// Info,信息,通常在生产环境中通常启用
/// </summary>
/// <param name="msg"></param>
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 );
/// <summary>
/// Warn,警告,通常用于非关键问题,这些问题可以恢复,或者是暂时的故障
/// </summary>
/// <param name="msg"></param>
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 );
/// <summary>
/// Error,错误,多数情况下记录Exceptions(异常)信息
/// </summary>
/// <param name="msg"></param>
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 );
/// <summary>
/// Fatal,致命错误,非常严重的错误
/// </summary>
/// <param name="msg"></param>
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;
/// <summary>
/// NLog 日志等级
/// </summary>
public enum NLogLevel
Trace,
Debug,
Info,
Warn,
Error,
Fatal
参考:C# 超高速高性能写日志 代码开源 .net core 中的那些常用的日志框架(NLog篇)
四、日志查看器
作为一名研发人员,高效率的日志分析是必须的,当然好的工具也是前提条件。
要想高效分析日志,有几个问题需要解决:
- 快速定位,在海量日志信息中快速定位目标行;
- 高亮显示,以不同颜色显示目标行,以便分类提高辨识度;
- 只显示有用的行。
在日常开发使用最多的莫过于 NotePad++ 了,尽管其可以通过 “搜索-标记/标记所有-使用格式1/2/3/4/5”的操作来实现以上的前两点,但是操作较繁琐,当日志行数比较多时,也无法仅显示标记行,从而造成效率低下。
当然,对于普通的业务量不太高的日志记录,NotePad++ 足以满足使用。
下面介绍一个非常简单实用的开源日志查看工具 TextAnalysisTool.NET。
- 官网地址:http://textanalysistool.github.io/
- 用法地址:http://textanalysistool.github.io/TextAnalysisTool.NET.txt
1. 下载应用程序包
下载完成后,如下图打开最新版的应用程序:
2. 分析的日志文件
按照“File -> Open”选择要打开的日志文件。
双击任意行,便会跳出“Add Filter”窗口:(Text 默认为鼠标焦点行的内容)
可以通过修改“Text Color”和“Background”来指定查询结果的文本和行底色,达到高亮显示目的。
其他选项:Description:描述;Excluding:排除,不包含;Case-sensitive:大小写敏感;Regular-expression:按照正则表达式查询。
如下图示例,查询三个语句,标志为不同的行底色效果:
若想只显示查询目标所在的行,可以如下图鼠标操作,也可使用快捷键 Ctrl+H,取消时重复操作即可。
本文来自博客园,作者:橙子家,微信号:zfy1070491745,有任何疑问欢迎沟通,一起成长。
转载本文请注明原文链接:https://www.cnblogs.com/czzj/p/JGP_MyLog.html
C# 如何使用使用不同 CPU 架构的库?
【中文标题】C# 如何使用使用不同 CPU 架构的库?【英文标题】:C# How can I use libraries which use a different CPU architecture? 【发布时间】:2019-09-18 21:01:10 【问题描述】:我目前正在使用 C# 开发一个独立的应用程序,但我遇到了一个我找不到解决方案的问题。实际上,我的脚本使用了两个库(由外部公司提供),但是,一个用于 64 位系统,另一个用于 32 位系统。所以这是我的问题,一旦我尝试同时使用这两个库,我总是会得到一个错误:
System.BadImageFormatException
我已经尝试为“Any CPU”、“x64”和“x86”构建我的项目,但我总是遇到同样的问题。
所以我想知道是否有可能解决这个问题以便能够在同一个脚本中使用这两个库,或者是否有其他技术可以达到相同的结果?
提前谢谢你, 克莱门特
【问题讨论】:
您必须为这两个进程启动不同的进程并将各自的程序集加载到每个进程中,然后找出两者之间的通信。 无法将 32 位和 64 位程序集加载到同一进程中。 另一种方法是联系供应商,看看是否有办法让他们提供所有这些程序集的 32 位或 64 位版本,这样您就不会必须跳过箍来加载它们。 不幸的是,提供这些库的公司最近关闭了......如果我开始在 A 脚本(64 位)中处理第一个库,然后使用 B 脚本(86 位),结果可能是决定性的? 可能,尝试通过 TCP 实现客户端-服务器的 DLL-as-a-service 方法。服务器为您的库运行 32 位包装器,您向他询问操作。手动、gRPC、SignalR、WCF 等 可能,重复:***.com/questions/26598558/… 【参考方案1】:如果 libraries 使用第三方标准 windows dlls,这将更加困难。
但是,只需反编译该库,然后重建它在您选择的bitness中。
注意 1:注意这可能会侵犯许可协议
注意 2 :一些反编译器可能会有点失败
【讨论】:
如果你无权访问源代码,必须反编译,你很可能违反了许可协议。 @TsahiAsher 鉴于 OP 说供应商公司最近关闭,这有关系吗? @NetMage IANAL,但一家关闭的公司并不意味着它的所有版权都无效。例如,它可能已出售其专利以偿还债务。 @TsahiAsher 没错,但你说的是许可协议。我想这取决于它是停业还是被购买或出售,以及在这些情况下协议的内容。但如果它倒闭了,谁来执行这些条款? @NetMage 公平点。不过,这很可能侵犯了版权。以上是关于C# 如何设计一个好用的日志库?架构篇的主要内容,如果未能解决你的问题,请参考以下文章