惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

IT之家
IT之家
Recent Announcements
Recent Announcements
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
The GitHub Blog
The GitHub Blog
MyScale Blog
MyScale Blog
爱范儿
爱范儿
GbyAI
GbyAI
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
美团技术团队
Y
Y Combinator Blog
博客园 - 叶小钗
Apple Machine Learning Research
Apple Machine Learning Research
Martin Fowler
Martin Fowler
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
罗磊的独立博客
M
MIT News - Artificial intelligence
博客园 - Franky
V
Visual Studio Blog
I
InfoQ
V
V2EX
Hugging Face - Blog
Hugging Face - Blog
腾讯CDC
博客园 - 司徒正美
L
LangChain Blog

博客园 - 郎中令

锦衣夜行,AI乐园 Redis实战:用缓存为数据库减负(二) Redis初体验: 搭建与简单应用(一) .Net Core的SwaggerUI接口分类+固定路由 真实性编码:配置NLog日志+Seq分布式 对接Java所谓的DES加解密 人大金仓数据库转换 人大金仓踩坑指南 RSA加解密笔记 HTTPS请求笔记- SSL安全通道验证问题 .NetCore打包部署(DLL) 字典映射处理 SM4算法快速预览与Framework4.5版本对接 简易首页防暴力-字典计时器 vite运行打包前端-部署Linux 记一次线上数据库异常的协助排查 降本增笑:记一次首页并发白屏优化过程 solr 基础介绍以及踩坑日记 动态展示缩放背景图
防御性编码:手搓NLog日志+Seq分布式
郎中令 · 2025-08-20 · via 博客园 - 郎中令

     关于日志,历时这么多年,我发现很多地方还是没有被普及,仍然采用传统的手搓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 包 

image

 然后移除默认配置文件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 一键操作都很简单)

image

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

image

2.  参数配置

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

image

 按照上面的风格,不想走配置路线,当然也是可以直接通过代码来控制,如下图,直接编写一个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;
    }
}

在日志帮助类第一次启动时调用即可

image

3. 运行项目,走个写入日志的方法

 NLogHelper.WriteInfo($"记录请求的日志,{obj.sign}", ParamConst.PathKiaser);

4. 线上查看,登录设置的seq地址 http://localhost:5341/ , 账号和密码取自安装时所设置的值,也可以根据需要考虑是否创建个人的 ApiKey

image

到这里,基本就结束了,可以简单的使用seq上直接对日志进行筛选查看了,而不是傻傻的逐个一台一台服务器上去查看实际记录的情况 ,后面排查线上问题就会方便很多

是不是感觉平时用的日志系统貌似也没有这么多,你的感觉是对的,下一篇 真实性编码:配置路线异步Nlog+seq, 无缝衔接,走配置路线,移除大量的业务代码