防御性编码:手搓NLog日志+Seq分布式
关于日志,历时这么多年,我发现很多地方还是没有被普及,仍然采用传统的手搓IO日志的方式,一个IO日志方法被各个业务多次复制,此种现象原因无非以下三点:
① 复制大法好,固步自封,不愿意改变,去接受新鲜事务,全是拿来主义,自己的业务需要时,直接拷贝一份,改个路径名字就接着用
② 没有意识到日志的重要性,线上有问题,直接抛出异常,但是没有文件记录,线上环境常常难以模拟测试,抓瞎靠想象,缺乏必要的日志来提供线索
③ 缺乏统筹意识,大批量日志记录时, 出现阻塞,影响正常流程,造成不可预料的风险
④ 防御性代码,毕竟老板经常降本增笑,流水的人员也没必要如此上心,七天内只有上帝和他自己才能懂的代码才能有稳定的位置
错误示范:如下,是否似曾相识
public static class LogHelper { static object lockLog = new object(); /// 写入日志文件 public static void WriteLog(string fileContent, string fileName, string path) { try { lock (lockLog) { StreamWriter sr; StringBuilder sb = new StringBuilder(); sb.AppendLine(fileContent); string fn = fileName + "_" + DateTime.Now.ToString("yyyyMMdd") + ".txt"; if (!Directory.Exists(path)) { Directory.CreateDirectory(path); } sr = File.AppendText(path + fn); sr.WriteLine($"{DateTime.Now}:{fileContent}\r\n"); sr.Close(); } } catch { } } }
其实很容易改造,只需要站在巨人的肩膀上,如NLog 、Log4Net 等稍微进行改造,加入自定义文件名即可,这里,比如先引用一下 NLog.Web.AspNetCore 包

然后移除默认配置文件NLog.config 的内容,只保留一个 nlog 壳子就好,因为我们后面直接通过代码动态控制(当然这里配置记录级别、规则、路径、格式也可以)
<?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" autoReload="true" throwExceptions="false" internalLogLevel="Off" internalLogFile="c:\temp\nlog-internal.log"> </nlog>
剩下的就是来一个Nlog 的帮助类,加入安全队列 和异步,动态匹配传入的文件名称
public static class NLogHelper { // 日志器缓存:按 logger 名称缓存对应的 ILogger 实例,忽略大小写 private static readonly ConcurrentDictionary<string, ILogger> _cache = new(StringComparer.OrdinalIgnoreCase); // 保护 _cache 之外的全局共享资源(NLog Configuration)的互斥锁 private static readonly object _sync = new(); // 日志根目录,支持 NLog Layout 占位符 ${basedir} private const string LogDir = "${basedir}/Logs"; /// <summary> /// 为指定名称和级别的 logger 配置 FileTarget & AsyncTargetWrapper, /// 并将规则写入 NLog 全局配置。 /// 该方法幂等:重复调用不会重复创建同名 target。 /// </summary> /// <param name="name">Logger 名称(命名空间或自定义名)</param> /// <param name="level">最低日志级别</param> private static void ConfigureLogger(string name, LogLevel level) { // 构造 target 名称,形如 "Error_MyService" var targetName = $"{level.Name}_{name}".TrimEnd('_'); // 获取或新建 NLog 配置对象 var config = LogManager.Configuration ?? new LoggingConfiguration(); // 第一次快速检查:如果已存在该 target 直接返回(无锁路径) if (config.FindTargetByName(targetName) != null) return; // 双检锁,确保并发时只创建一次 lock (_sync) { // 再次确认,防止锁期间被其他线程抢先创建 if (config.FindTargetByName(targetName) != null) return; // 创建文件目标 var target = new FileTarget(targetName) { // 日志文件路径:LogDir/Level/Name/yyyy-MM-dd.log FileName = Path.Combine(LogDir, level.Name, string.IsNullOrEmpty(name) ? "" : name, "${date:format=yyyy-MM-dd}.log"), Layout = "${longdate} ${level} ${message}${newline}", KeepFileOpen = true, // 提升性能,避免频繁打开/关闭文件 ConcurrentWrites = false // 由 AsyncTargetWrapper 串行写,无需并发 }; // 包装为异步目标,防止 I/O 阻塞调用线程 var asyncTarget = new AsyncTargetWrapper(targetName, target) { QueueLimit = 10000, // 队列最大长度 OverflowAction = AsyncTargetWrapperOverflowAction.Block // 队列满时阻塞调用者 }; // 将同步与异步 target 都注册到配置(部分场景可直接引用同步 target) config.AddTarget(target); config.AddTarget(asyncTarget); // 添加规则:名称匹配的 logger 在 >=level 时输出到 asyncTarget config.LoggingRules.Add(new LoggingRule(name, level, asyncTarget)); // 重新应用配置(线程安全) LogManager.Configuration = config; } } /// <summary> /// 获取或创建指定名称和级别的 ILogger。 /// 内部先确保 logger 所需的 target 与规则已就绪,再从缓存返回实例。 /// </summary> /// <param name="name">Logger 名称</param> /// <param name="level">最低日志级别</param> /// <returns>配置好的 ILogger</returns> private static ILogger GetLogger(string name, LogLevel level) { // 确保配置存在(幂等) ConfigureLogger(name, level); // 从缓存中获取或添加 logger 实例 return _cache.GetOrAdd(name, _ => LogManager.GetLogger(name)); } public static void WriteError(string message, string loggerName = "") { GetLogger(loggerName, LogLevel.Error).Error(message); } public static void WriteInfo(string message, string loggerName = "") { GetLogger(loggerName, LogLevel.Info).Info(message); } public static void WriteWarn(string message, string loggerName = "") { GetLogger(loggerName, LogLevel.Warn).Warn(message); } }
已经完成了,直接调用对应的日志方法即可
NLogHelper.WriteInfo($"操作日志,{sign} ", ParamConst.DOORAPP); NLogHelper.WriteWarn($"警告日志,{sign} ", ParamConst.DOORAPP); NLogHelper.WriteError($"错误日志,{sign} ", ParamConst.OtherOpenRoom);
会根据动态输入的文件名,自动生成对应的日志,简单又高效,可以看到,这个操作步骤非常简单,却很实用,起码能保证基本的数据留痕,走到这一步,是否还能更进一步?当然!
我们的系统很多时候是分布式的,排查线上问题时,经常要切到各个服务器上去搜罗文件日志,非常繁琐,不利于统一定位数据,常见的日志系统有 Loki (最轻量)、Filebeat+ES 、Graylog 、Seq ,本文推荐使用Seq,因为他使用起来妙不可言,尤其在查询语法、界面、报警逻辑方面,都是专门为 .NET / 结构化日志设计的。接入十分便捷,几分钟就能搞定,步骤如下:
1. 下载安装Seq (windows \dock 一键操作都很简单)

安装成功后,会以服务的方式,常驻服务器,比如:

2. 参数配置
一般情况下,直接在Nlog.config 里面添加几行seq的参数即可,如下图所示

按照上面的风格,不想走配置路线,当然也是可以直接通过代码来控制,如下图,直接编写一个seq的处理方法
private static void EnsureSeq() { if (_seqInitialized) return; lock (_seqInitSync) { if (_seqInitialized) return; var cfg = LogManager.Configuration ?? new LoggingConfiguration(); // 如果还没注册过 Seq,就加进去 if (cfg.FindTargetByName("Seq") == null) { var seqTarget = new SeqTarget { Name = "Seq", ServerUrl = "http://localhost:5341", // 如端口改过请改这里 ApiKey = "" // 需要时再填 }; // 可选:包一层异步提高吞吐 var asyncSeq = new AsyncTargetWrapper("SeqAsync", seqTarget) { QueueLimit = 10000, OverflowAction = AsyncTargetWrapperOverflowAction.Block }; cfg.AddTarget(seqTarget); cfg.AddTarget(asyncSeq); // 给所有 logger 都发一份到 Seq cfg.LoggingRules.Add(new LoggingRule("*", LogLevel.Debug, asyncSeq)); LogManager.Configuration = cfg; } _seqInitialized = true; } }
在日志帮助类第一次启动时调用即可

3. 运行项目,走个写入日志的方法
NLogHelper.WriteInfo($"记录请求的日志,{obj.sign}", ParamConst.PathKiaser);
4. 线上查看,登录设置的seq地址 http://localhost:5341/ , 账号和密码取自安装时所设置的值,也可以根据需要考虑是否创建个人的 ApiKey

到这里,基本就结束了,可以简单的使用seq上直接对日志进行筛选查看了,而不是傻傻的逐个一台一台服务器上去查看实际记录的情况 ,后面排查线上问题就会方便很多
是不是感觉平时用的日志系统貌似也没有这么多,你的感觉是对的,下一篇 真实性编码:配置路线异步Nlog+seq, 无缝衔接,走配置路线,移除大量的业务代码
本文来自博客园,作者:郎中令,世人皆大笑,举手揶揄之,文未佳,却己创,转载请注明原文链接:https://www.cnblogs.com/Sientuo/p/19048926

浙公网安备 33010602011771号