ASP.NET Core中使用NLog和注解实现日志记录

发布于:2025-08-03 ⋅ 阅读:(8) ⋅ 点赞:(0)

本文介绍了在ASP.NET Core中使用NLog实现日志记录的方法。主要内容包括:1) 通过NuGet安装NLog并配置nlog.config文件,设置日志格式、存储路径和归档规则;2) 定义LogAttribute注解类,可配置日志级别、是否记录请求参数/响应结果/执行时间等选项;3) 实现LogActionFilter过滤器,在请求处理前后记录相关信息,支持根据注解配置过滤敏感字段。该方案通过AOP方式实现了灵活可配置的日志记录功能,便于系统监控和问题排查。

配置NLog

NuGet 安装 NLog,根目录新建 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"
      autoReload="true"
      watchInterval="30"
      watchTimeout="5000"
      internalLogLevel="Warn"
      internalLogFile="${basedir:dir=logs}/nlog-internal.log"
      throwExceptions="false"
      throwConfigExceptions="false">

	<variable name="logDirectory" value="${basedir:dir=logs}" />
	<variable name="archiveDirectory" value="${basedir:dir=archives}" />
	<variable name="defaultLayout" value="${longdate:format=yyyy-MM-dd HH\:mm\:ss.fff} | ${level:uppercase=true} | ${logger:shortName=true} | ${message} ${onexception:${newline}EXCEPTION: ${exception:format=ToString,Data:maxInnerExceptionLevel=5} ${newline}STACKTRACE: ${stacktrace:topFrames=10} ${newline}}" />
	<variable name="consoleLayout" value="[${date:format=yyyy-MM-dd HH\:mm\:ss}] [${level:uppercase=true}] ${logger:shortName=true} | ${message} ${onexception:EXCEPTION: ${exception:format=Message}}" />

	<targets async="true">
		<target name="log_file" xsi:type="File"
                fileName="${logDirectory}/${shortdate}/${level}-${shortdate}.log"
                layout="${defaultLayout}"
                createDirs="true"
                archiveFileName="${archiveDirectory}/${shortdate}/${level}-${shortdate}-{00000}.log"
                archiveAboveSize="10485760"
                archiveNumbering="Sequence"
                maxArchiveFiles="30"
                concurrentWrites="true"
                keepFileOpen="false"
                encoding="UTF-8"
                writeBom="false"
                enableFileDelete="true"
                bufferSize="8192"
                flushTimeout="5000">
		</target>

		<target name="colorConsole" xsi:type="ColoredConsole"
                layout="${consoleLayout}">
			<highlight-row condition="level == LogLevel.Trace" foregroundColor="DarkGray" />
			<highlight-row condition="level == LogLevel.Debug" foregroundColor="Gray" />
			<highlight-row condition="level == LogLevel.Info" foregroundColor="Cyan" />
			<highlight-row condition="level == LogLevel.Warn" foregroundColor="Yellow" backgroundColor="DarkGray" />
			<highlight-row condition="level == LogLevel.Error" foregroundColor="White" backgroundColor="Red" />
			<highlight-row condition="level == LogLevel.Fatal" foregroundColor="White" backgroundColor="DarkRed" />
		</target>
	</targets>

	<rules>
		<logger name="Microsoft.*" minlevel="Info" maxlevel="Info" final="true" />
		<logger name="Microsoft.*" minlevel="Warn" writeTo="log_file,colorConsole" final="true" />
		<logger name="*" minlevel="Trace" maxlevel="Debug" writeTo="log_file" />
		<logger name="*" minlevel="Info" writeTo="log_file" />
		<logger name="*" minlevel="Warn" writeTo="colorConsole" />
	</rules>
</nlog>

定义日志过滤器注解

using System;

/// <summary>
/// 日志记录注解
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true)]
public class LogAttribute : Attribute
{
    /// <summary>
    /// 是否记录请求参数
    /// </summary>
    public bool LogParameters { get; set; } = true;

    /// <summary>
    /// 是否记录响应结果
    /// </summary>
    public bool LogResult { get; set; } = true;

    /// <summary>
    /// 是否记录执行时间
    /// </summary>
    public bool LogExecutionTime { get; set; } = true;

    /// <summary>
    /// 排除的字段(用于敏感信息过滤)
    /// </summary>
    public string[] ExcludeFields { get; set; } = Array.Empty<string>();

    /// <summary>
    /// 日志级别
    /// </summary>
    public string LogLevel { get; set; } = "Info";
}

过滤器处理日志逻辑

using System.Reflection;
using System.Text;
using System.Text.Json;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using NLog;

/// <summary>
/// 日志过滤器
/// </summary>
public class LogActionFilter : IAsyncActionFilter
{
    private static readonly Logger Logger = LogManager.GetCurrentClassLogger();

    public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        // 获取当前请求的日志配置(方法级别优先于类级别)
        var logConfig = GetLogConfiguration(context);
        if (logConfig == null)
        {
            // 没有日志注解,直接执行后续操作
            await next();
            return;
        }

        // 记录请求信息
        var requestLog = new StringBuilder();
        var actionDescriptor = context.ActionDescriptor as ControllerActionDescriptor;

        requestLog.AppendLine($"请求开始: {context.HttpContext.Request.Method} {context.HttpContext.Request.Path}");
        requestLog.AppendLine($"控制器: {actionDescriptor?.ControllerName}, 方法: {actionDescriptor?.ActionName}");

        // 记录请求参数(根据配置)
        if (logConfig.LogParameters && context.ActionArguments.Count > 0)
        {
            requestLog.AppendLine("请求参数:");
            foreach (var arg in context.ActionArguments)
            {
                // 过滤敏感字段
                var filteredValue = FilterSensitiveData(arg.Value, logConfig.ExcludeFields);
                requestLog.AppendLine($"{arg.Key}: {JsonSerializer.Serialize(filteredValue)}");
            }
        }

        // 根据配置的日志级别记录
        LogMessage(logConfig.LogLevel, requestLog.ToString());

        // 记录开始时间
        var startTime = DateTime.UtcNow;
        var resultContext = await next(); // 执行后续操作

        // 记录响应信息
        var responseLog = new StringBuilder();
        responseLog.AppendLine($"请求结束: {context.HttpContext.Request.Method} {context.HttpContext.Request.Path}");

        // 记录执行时间(根据配置)
        if (logConfig.LogExecutionTime)
        {
            var executionTime = DateTime.UtcNow - startTime;
            responseLog.AppendLine($"执行时间: {executionTime.TotalMilliseconds:F2}ms");
        }

        // 记录响应结果(根据配置)
        if (logConfig.LogResult)
        {
            if (resultContext.Result is ObjectResult objectResult)
            {
                var filteredResult = FilterSensitiveData(objectResult.Value, logConfig.ExcludeFields);
                responseLog.AppendLine($"响应结果: {JsonSerializer.Serialize(filteredResult)}");
            }
            else if (resultContext.Result is ContentResult contentResult)
            {
                responseLog.AppendLine($"响应内容: {contentResult.Content}");
            }
            else if (resultContext.Result is EmptyResult)
            {
                responseLog.AppendLine("响应结果: 空");
            }
        }

        // 记录异常
        if (resultContext.Exception != null)
        {
            Logger.Error(resultContext.Exception, $"请求执行异常: {resultContext.Exception.Message}");
        }
        else
        {
            LogMessage(logConfig.LogLevel, responseLog.ToString());
        }
    }

    /// <summary>
    /// 获取日志配置(方法 > 类)
    /// </summary>
    private LogAttribute GetLogConfiguration(ActionExecutingContext context)
    {
        var actionDescriptor = context.ActionDescriptor as ControllerActionDescriptor;
        if (actionDescriptor == null) return null;

        // 先检查方法是否有日志注解
        var methodAttr = actionDescriptor.MethodInfo.GetCustomAttribute<LogAttribute>();
        if (methodAttr != null)
            return methodAttr;

        // 再检查类是否有日志注解
        return actionDescriptor.ControllerTypeInfo.GetCustomAttribute<LogAttribute>();
    }

    /// <summary>
    /// 过滤敏感数据
    /// </summary>
    private object FilterSensitiveData(object data, string[] excludeFields)
    {
        if (data == null || excludeFields.Length == 0)
            return data;

        // 简单实现:将敏感字段替换为***
        // 实际项目中可根据需要扩展
        var json = JsonSerializer.Serialize(data);
        foreach (var field in excludeFields)
        {
            json = json.Replace($"\"{field}\":\"[^\"]*\"", $"\"{field}\":\"***\"");
        }
        return JsonSerializer.Deserialize<object>(json);
    }

    /// <summary>
    /// 根据日志级别记录日志
    /// </summary>
    private void LogMessage(string level, string message)
    {
        switch (level?.ToLower())
        {
            case "trace":
                Logger.Trace(message);
                break;
            case "debug":
                Logger.Debug(message);
                break;
            case "warn":
                Logger.Warn(message);
                break;
            case "error":
                Logger.Error(message);
                break;
            case "fatal":
                Logger.Fatal(message);
                break;
            default: // info
                Logger.Info(message);
                break;
        }
    }
}

注册过滤器并使用注解

builder.Services.AddControllers(options =>
{
    // 注册日志过滤器
    options.Filters.Add<LogActionFilter>();
});

using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/[controller]")]
// 类级别日志配置
[Log(LogExecutionTime = true, ExcludeFields = new[] { "Password", "Token" })]
public class UserController : ControllerBase
{
    // 方法级别日志配置(会覆盖类级别配置)
    [HttpPost("login")]
    [Log(LogParameters = true, LogResult = true, LogLevel = "Info")]
    public IActionResult Login([FromBody] LoginRequest request)
    {
        // 业务逻辑
        if (request.Username == "admin" && request.Password == "123456")
        {
            return Ok(new { Token = "fake-jwt-token", Expires = DateTime.Now.AddHours(1) });
        }
        return Unauthorized("用户名或密码错误");
    }

    [HttpGet("{id}")]
    [Log(LogParameters = true, LogResult = true, LogExecutionTime = false, LogLevel = "Debug")]
    public IActionResult GetUser(int id)
    {
        var user = new { Id = id, Name = "Test User", Email = "test@example.com" };
        return Ok(user);
    }

    // 不使用日志注解,不会记录日志
    [HttpPost("logout")]
    public IActionResult Logout()
    {
        return Ok();
    }
}

public class LoginRequest
{
    public string Username { get; set; }
    public string Password { get; set; }
}