概述
在现代 .NET Core 应用程序中,一个强大且灵活的日志系统是保障系统稳定性和可维护性的基石。本文将深入探讨如何在 .NET Core 3.1 项目中,通过 NLog 实现企业级日志解决方案,重点解决三个核心问题:
- ✅ 物理隔离:将接口交互日志与系统运行日志分离存储,提高日志查询效率
- ✅ 格式精简化:自定义布局模板,实现清晰、结构化的日志输出
- ✅ 智能记录:基于自定义特性(Attribute)和中间件,精准捕获 API 请求/响应数据
环境准备与 NuGet 包安装
在开始之前,请确保您的开发环境满足以下要求:
- .NET Core 3.1 SDK
- Visual Studio 2019 或更高版本
- 项目类型为 ASP.NET Core Web 应用程序
必需的 NuGet 包
执行以下命令安装所需的 NuGet 包:
Install-Package NLog.Web.AspNetCore -Version 4.14.0
Install-Package NLog -Version 4.7.15
注意:如果您使用 Autofac 作为依赖注入容器,还需安装:
Install-Package Autofac.Extensions.DependencyInjection -Version 8.0.0
核心配置:NLog.config
在项目根目录创建 NLog.config 文件,并设置文件属性为 "始终复制到输出目录" (在 Visual Studio 中右键文件 → 属性 → 复制到输出目录)。
完整配置文件
<?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"
internalLogLevel="Error"
internalLogFile="c:\temp\internal-nlog.txt">
<!-- 扩展 NLog.Web.AspNetCore 功能 -->
<extensions>
<add assembly="NLog.Web.AspNetCore"/>
</extensions>
<!-- 全局布局变量定义 -->
<variable name="commonLayout" value="----- $ {newline}【 $ {level:upperCase=true}】 $ {newline}时间: $ {longdate} $ {newline}IP : $ {aspnet-request-ip:CheckForwardedForHeader=true} $ {newline}位置: $ {logger} -> $ {aspnet-mvc-action} $ {newline}URL : $ {aspnet-request-url:IncludeQueryString=true} $ {newline}消息: $ {message} $ {onexception: $ {newline}异常: $ {exception:format=tostring}} $ {newline}-----" />
<variable name="apiExchangeLayout" value="-----$ {newline}【接口交互】 $ {newline}时间: $ {longdate} $ {newline}IP : $ {aspnet-request-ip:CheckForwardedForHeader=true} $ {newline}位置: $ {logger} -> $ {aspnet-mvc-action} $ {newline}URL : $ {aspnet-request-url:IncludeQueryString=true} $ {newline}详情: $ {newline} $ {message} $ {onexception: $ {newline}异常: $ {exception:format=tostring}} $ {newline}-----" />
<!-- 日志目标配置 -->
<targets>
<!-- 系统运行日志 -->
<target xsi:type="File" name="own_file" fileName="log/ $ {shortdate}/system_ $ {shortdate}.log" encoding="utf-8" layout=" $ {commonLayout}" />
<!-- 错误日志 -->
<target xsi:type="File" name="error_file" fileName="log/ $ {shortdate}/error_ $ {shortdate}.log" encoding="utf-8" layout=" $ {commonLayout}" />
<!-- API接口交互日志 -->
<target xsi:type="File" name="api_exchange_file" fileName="log/ $ {shortdate}/api_exchange_ $ {shortdate}.log" encoding="utf-8" layout=" $ {apiExchangeLayout}" />
<!-- 黑洞目标,用于过滤不需要的日志 -->
<target xsi:type="Null" name="blackhole" />
</targets>
<!-- 日志路由规则 -->
<rules>
<!-- 优先处理API交互日志 -->
<logger name="ApiExchangeLogger" minlevel="Trace" writeTo="api_exchange_file" final="true" />
<!-- 过滤Microsoft框架日志 -->
<logger name="Microsoft.*" maxlevel="Info" writeTo="blackhole" final="true" />
<!-- 错误日志单独记录 -->
<logger name="*" minlevel="Error" writeTo="error_file" />
<!-- 其他日志记录到系统日志 -->
<logger name="*" minlevel="Trace" writeTo="own_file" />
</rules>
</nlog>
基础设施集成
Program.cs:NLog 全局初始化
在 .NET Core 3.1 中,推荐在 Program.cs 中进行 NLog 的全局初始化,这样可以确保捕获应用程序启动和关闭时的关键日志。
using NLog;
using NLog.Web;
public class Program
{
public static void Main(string[] args)
{
// 初始化 NLog
var logger = NLogBuilder.ConfigureNLog("nlog.config").GetCurrentClassLogger();
try
{
logger.Info("应用程序启动中...");
CreateHostBuilder(args).Build().Run();
}
catch (Exception ex)
{
// 捕获启动异常
logger.Error(ex, "应用程序启动失败");
throw;
}
finally
{
// 确保日志正确关闭
NLog.LogManager.Shutdown();
}
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup();
})
.ConfigureLogging(logging =>
{
logging.ClearProviders(); // 清除默认日志提供程序
logging.SetMinimumLevel(LogLevel.Trace); // 设置最低日志级别
})
.UseNLog(); // 注册 NLog
}
Startup.cs:请求管道配置
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// 必须注入 HttpContextAccessor 以支持 NLog 的 ASP.NET Core 布局渲染器
services.AddHttpContextAccessor();
// 添加控制器
services.AddControllers();
// 其他服务注册...
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// 处理反向代理(如 Nginx、IIS)的真实客户端 IP
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});
app.UseRouting();
// 自定义日志中间件(必须在 UseRouting 之后)
app.UseMiddleware();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
高级功能:API 交互日志追踪
自定义标记特性 [Log]
创建一个自定义特性,用于标记需要记录交互日志的控制器或方法。
using System;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class LogAttribute : Attribute
{
///
/// 是否记录响应体(默认仅记录非200状态的响应)
///
public bool LogResponseBody { get; set; } = false;
///
/// 构造函数
///
/// 是否强制记录所有响应体
public LogAttribute(bool logResponseBody = false)
{
LogResponseBody = logResponseBody;
}
}
核心中间件实现
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Routing;
public class HttpLoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger _apiLogger;
private const string newline = "rn";
public HttpLoggingMiddleware(RequestDelegate next, ILoggerFactory loggerFactory)
{
_next = next;
// 使用在 NLog.config 中定义的特定日志记录器
_apiLogger = loggerFactory.CreateLogger("ApiExchangeLogger");
}
public async Task Invoke(HttpContext context)
{
// 获取端点元数据
var endpoint = context.GetEndpoint();
var logAttr = endpoint?.Metadata.GetMetadata();
// 仅处理标记了 [Log] 特性的请求
if (logAttr == null)
{
await _next(context);
return;
}
// 1. 读取请求数据
var (queryString, requestBody) = await ReadRequestData(context);
// 2. 替换响应流以便捕获响应内容
var originalBody = context.Response.Body;
await using var responseBody = new MemoryStream();
context.Response.Body = responseBody;
try
{
await _next(context);
// 3. 读取响应数据(根据策略决定是否记录)
string responseText = await ReadResponseData(context, logAttr, responseBody);
// 4. 记录日志
LogApiExchange(context, queryString, requestBody, responseText, logAttr);
// 5. 恢复原始响应流
await WriteResponseToClient(context, responseBody, originalBody);
}
finally
{
context.Response.Body = originalBody;
}
}
private async Task ReadRequestData(HttpContext context)
{
var queryString = context.Request.QueryString.Value ?? string.Empty;
string requestBody = "[无请求体]";
// 跳过文件上传请求的读取
bool isUpload = context.Request.HasFormContentType &&
context.Request.ContentType?.Contains("multipart/form-data", StringComparison.OrdinalIgnoreCase);
if (!isUpload && context.Request.ContentLength > 0)
{
context.Request.EnableBuffering();
requestBody = await ReadStreamContent(context.Request.Body);
}
return (queryString, requestBody);
}
private async Task ReadResponseData(HttpContext context, LogAttribute logAttr, Stream responseBody)
{
if (context.Response.StatusCode == 200 && !logAttr.LogResponseBody)
{
return "[状态200,未记录响应体]";
}
return await ReadStreamContent(responseBody);
}
private void LogApiExchange(HttpContext context, string queryString, string requestBody, string responseText, LogAttribute logAttr)
{
var logMessage = new StringBuilder();
logMessage.AppendLine("[请求]: Query:{queryString} | Body:{requestBody}");
logMessage.AppendLine("[响应]: {responseText}");
logMessage.AppendLine("[状态]: {context.Response.StatusCode}");
// 根据状态码决定日志级别
if (context.Response.StatusCode >= 500)
{
_apiLogger.LogError(logMessage.ToString());
}
else if (context.Response.StatusCode >= 400)
{
_apiLogger.LogWarning(logMessage.ToString());
}
else
{
_apiLogger.LogInformation(logMessage.ToString());
}
}
private async Task WriteResponseToClient(HttpContext context, Stream responseBody, Stream originalBody)
{
responseBody.Seek(0, SeekOrigin.Begin);
await responseBody.CopyToAsync(originalBody);
}
private async Task ReadStreamContent(Stream stream)
{
if (stream == null || !stream.CanRead)
return "[不可读取的流]";
stream.Seek(0, SeekOrigin.Begin);
using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, bufferSize: 1024, leaveOpen: true);
var content = await reader.ReadToEndAsync();
stream.Seek(0, SeekOrigin.Begin);
return content.Length > 1000 ? content.Substring(0, 1000) + "..." : content;
}
}
示例
[HttpPost]
[Log] // 只有这个接口会产生交互日志
public IActionResult PushData([FromBody] MyData data)
{
return Ok("Success");
}
[HttpGet]
public IActionResult Heartbeat() // 这个接口再也不会出现在日志里
{
return Ok("Alive");
}
[Log(true)] // 即使状态码是 200,也会记录出参
public IActionResult GetImportantInfo() { ... }
[Route("api/[controller]")]
[Log] // 该控制器下的所有 Action 都会记录日志
public class OrderController : ControllerBase { ... }
最佳实践与注意事项
关键配置要点
- ✅ 单点集成原则:
严格遵循在Program.cs中通过.UseNLog()进行集成,避免在Startup.cs中重复调用AddNLog(),这可能导致日志重复或丢失。 - ✅ 流处理安全:
每次读取请求/响应流后,必须调用stream.Seek(0, SeekOrigin.Begin)重置流位置,否则客户端将收到空响应。中间件中使用try-finally确保流的正确恢复。 - ✅ 日志分流机制:
在NLog.config的规则配置中,关键属性final="true"用于阻止日志"冒泡"到后续规则,这是实现日志精准分流的核心机制。 - ✅ 生产环境优化:
- 将
internalLogLevel从 "Error" 调整为 "Warn" 以减少内部日志 - 添加文件归档策略:
<target xsi:type="File" ... archiveEvery="Day" maxArchiveFiles="30" /> - 考虑使用异步包装器提升性能:
<targets async="true">
性能考量
⚡ 避免大型请求体记录:中间件已自动跳过multipart/form-data请求的读取,防止文件上传导致的内存溢出
⚡ 响应体截断:超过1000字符的响应体会被截断,平衡日志详细度与性能
⚡ 异步日志写入:NLog 默认使用异步写入,但可通过<targets async="true">进一步优化
总结
- 通过本文介绍的 NLog 高级配置方案,您可以构建一个企业级的日志系统,实现:
- 精准的问题定位:通过接口交互日志快速定位 API 层问题
- 高效的日志管理:物理隔离不同类型的日志,提高查询效率
- 灵活的扩展性:基于特性标记的机制,可以轻松扩展到其他监控场景
💡 扩展建议:考虑将此方案与ELK Stack(Elasticsearch, Logstash, Kibana)或 Application Insights 集成,实现日志的集中化管理和可视化分析。

Comments NOTHING