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

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

配置NLog

NuGet 安装 NLog,根目录新建 nlog.config

xml 复制代码
<?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>

定义日志过滤器注解

csharp 复制代码
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";
}

过滤器处理日志逻辑

csharp 复制代码
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;
        }
    }
}

注册过滤器并使用注解

csharp 复制代码
builder.Services.AddControllers(options =>
{
    // 注册日志过滤器
    options.Filters.Add<LogActionFilter>();
});
csharp 复制代码
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; }
}
相关推荐
孟君的编程札记2 分钟前
别只知道 Redis,真正用好缓存你得懂这些
java·后端
用户960102251624 分钟前
kubesphere的告别,从可用环境提取Kubesphere镜像
后端
种子q_q6 分钟前
组合索引、覆盖索引、聚集索引、非聚集索引的区别
后端·面试
码事漫谈8 分钟前
WaitForSingleObject 函数参数影响及信号处理分析
后端
ffutop8 分钟前
gRPC mTLS 问题调试指南
后端
讨厌吃蛋黄酥10 分钟前
利用Mock实现前后端联调的解决方案
前端·javascript·后端
JavaArchJourney10 分钟前
Spring Cloud 微服务架构
后端
苦学编程的谢1 小时前
SpringBoot统一功能处理
java·spring boot·后端
程序员爱钓鱼1 小时前
Go语言实战案例:使用channel实现生产者消费者模型
后端·go·trae
程序员爱钓鱼1 小时前
Go语言实战案例:使用select监听多个channel
后端·go·trae