NLog配置File目标时requestBegin的${scopeproperty:domainName}取值为空
问题原因
requestBegin目标获取不到值的核心逻辑:NLog的File目标默认会缓存首次计算得到的文件路径,如果第一次往这个目标写日志的时候${scopeproperty:domainName}是空,后续即使有了正确值,也不会重新计算路径,就会一直往空domain的路径写,看起来就是拿不到值。而errorsLogFile记录的Error日志都出现在请求处理的中后期,第一次写的时候domainName已经赋值完成,路径计算正确,所以运行正常。- 首次写入时domainName为空通常是两个场景导致的:
- 应用启动时,
Microsoft.AspNetCore.Hosting.Diagnostics会输出Info级别的宿主启动日志,其中部分日志包含starting关键字,会命中你的过滤规则,但这类日志没有任何请求上下文,domainName自然为空,会提前创建错误路径的日志文件。 - 你给domainName赋值的Scope(一般是在自定义中间件里通过
logger.BeginScope写入)注册位置太靠后,Kestrel收到请求后第一时间输出的"Request starting"日志,是在中间件管道外层触发的,此时还没进入你创建的Scope,拿不到对应值。
- 应用启动时,
解决方法
按顺序操作即可解决问题:
- 第一步:关闭File目标的文件路径缓存,强制每次写日志时重新计算文件名
给两个File目标都加上keepFileOpen="false"配置,避免首次写入空路径后被固定:<target xsi:type="File" name="errorsLogFile" fileName="c:\inor_logs\${scopeproperty:domainName}\error-${shortdate}.log" layout="" keepFileOpen="false"/> <target xsi:type="File" name="requestBegin" fileName="c:\inor_logs\${scopeproperty:domainName}\request-begin-${shortdate}.log" layout="" keepFileOpen="false"/> - 第二步:优化日志过滤规则,排除无请求上下文的启动日志
在现有过滤条件中增加请求方法非空的判断,把启动阶段没有请求信息的日志过滤掉,避免空值日志提前触发文件创建:<rules> <!--All logs, including from Microsoft--> <logger name="*" minlevel="Error" writeTo="errorsLogFile" /> <logger name="Microsoft.AspNetCore.Hosting.Diagnostics" minlevel="Info" writeTo="requestBegin"> <filters defaultAction="Ignore"> <when condition="contains('${message}','starting') and '${aspnet-request-method}' != '' and (contains('${uppercase:${aspnet-request-method}}','POST') or contains('${uppercase:${aspnet-request-method}}','PUT') or contains('${uppercase:${aspnet-request-method}}','DELETE'))" action="Log" /> </filters> </logger> </rules> - 第三步:调整Scope的注册位置
如果你是通过自定义中间件+BeginScope的方式写入domainName,必须把这个中间件放在请求管道注册的最顶端,也就是WebApplication实例创建后,第一个调用Use注册的中间件,要早于UseRouting、UseAuthentication、UseEndpoints等所有内置中间件,保证请求进入管道后第一时间就进入domainName的作用域。 - 第四步(彻底规避Scope时机问题的方案):替换ScopeProperty为自定义布局渲染器
如果调整中间件顺序后还是拿不到值(因为Kestrel的请求启动日志确实在中间件管道外层,中间件内创建的Scope无法覆盖到这个日志点),就不要用Scope传domainName了,直接写一个自定义布局渲染器从HttpContext实时取值,完全不依赖Scope生命周期:
首先添加自定义渲染器的代码:
然后在Program.cs的启动最开头注册这个渲染器:using System.Text; using Microsoft.AspNetCore.Http; using NLog; using NLog.LayoutRenderers; using NLog.Web.LayoutRenderers; [LayoutRenderer("domain-name")] public class DomainNameLayoutRenderer : AspNetLayoutRendererBase { protected override void DoAppend(StringBuilder builder, LogEventInfo logEvent) { HttpContext context = HttpContextAccessor.HttpContext; if (context == null) { return; } // 这里替换成你自己解析domainName的业务逻辑,和之前写入Scope的逻辑保持一致即可 string domainValue = context.Request.Host.Host; builder.Append(domainValue); } }
最后把NLog配置里所有用到using NLog; // 注册自定义布局渲染器 LayoutRenderer.Register<DomainNameLayoutRenderer>("domain-name"); var builder = WebApplication.CreateBuilder(args); // 后续原有服务注册、中间件注册逻辑保持不变${scopeproperty:domainName}的地方替换成${domain-name}即可,这个方式只要有请求上下文就能拿到正确的domain值,完全不会受Scope注册时机影响。
内容的提问来源于stack exchange,提问作者Akram Egbariya
相关产品推荐
相关产品推荐

