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

推荐订阅源

有赞技术团队
有赞技术团队
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
P
Palo Alto Networks Blog
C
Cisco Blogs
The Hacker News
The Hacker News
T
Threatpost
S
Schneier on Security
K
Kaspersky official blog
Spread Privacy
Spread Privacy
博客园_首页
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
NISL@THU
NISL@THU
量子位
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Google DeepMind News
Google DeepMind News
Security Latest
Security Latest
博客园 - 司徒正美
云风的 BLOG
云风的 BLOG
博客园 - 叶小钗
H
Hackread – Cybersecurity News, Data Breaches, AI and More
N
News and Events Feed by Topic
爱范儿
爱范儿
P
Proofpoint News Feed
C
CERT Recently Published Vulnerability Notes
Project Zero
Project Zero
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Cisco Talos Blog
Cisco Talos Blog
GbyAI
GbyAI
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Apple Machine Learning Research
Apple Machine Learning Research
T
Tenable Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
V
Vulnerabilities – Threatpost
Forbes - Security
Forbes - Security
博客园 - 三生石上(FineUI控件)
C
Cyber Attacks, Cyber Crime and Cyber Security
N
News and Events Feed by Topic
V
V2EX
Webroot Blog
Webroot Blog
The Register - Security
The Register - Security
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
阮一峰的网络日志
阮一峰的网络日志
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Blog — PlanetScale
Blog — PlanetScale
M
MIT News - Artificial intelligence
Scott Helme
Scott Helme
Simon Willison's Weblog
Simon Willison's Weblog
L
LangChain Blog
W
WeLiveSecurity
Cloudbric
Cloudbric

博客园 - 我才是银古

第16章:常见问题、排错与最佳实践 第15章:扩展生态、MCAD 与外部集成 第12章:实战案例:机械结构与 3D 打印零件 第14章:构建、测试、调试与贡献流程 第13章:OpenSCAD 源码架构与核心执行流程 第11章:预览、渲染、网格精度与性能优化 第09章:列表推导、递归与算法建模 第08章:参数化零件库与复用设计 第10章:导入导出、命令行与自动化 第06章:CSG 布尔建模方法 第07章:二维图形、拉伸、旋转与投影 第05章:基础几何、坐标系与变换 第04章:参数、变量、函数、模块与作用域 OpenSCAD 教程目录 第03章:OpenSCAD 语言基础 第02章:安装、环境配置与开发工作流 第01章:OpenSCAD 项目全景与学习路线 第02章:源码获取、编译与开发环境配置 第01章:OCCT项目全景与学习路线 第18章:二次开发实战与综合案例 第18章:综合实战案例 第17章:数据交换与协同 第16章:源码架构与二次开发 第15章:插件与自定义工作台开发 第14章:Python脚本宏与自动化 第13章:FEM仿真分析 第12章:CAM数控加工 第11章:SurfaceMesh与逆向工程 第10章:Draft二维绘图与BIM建筑 第09章:工程图TechDraw 第07章:参数化表达式与Spreadsheet 第08章:装配设计Assembly 第06章:Part工作台与几何内核 第05章:PartDesign实体特征建模 第04章:草图Sketcher约束建模 第02章:安装版本与工作环境配置 第03章:界面工作台与基础操作 第01章:项目全景与学习路线 第十二章:插件开发、研究功能与最佳实践 第十章:定时任务与自动化(Cron) 第七章:技能、记忆与自学习闭环 第八章:MCP 集成与上下文文件 第六章:工具系统与终端后端 第五章:模型供应商与配置体系 Hermes Agent 教程目录 第十一章:语音、视觉、浏览器与子代理协作 第四章:CLI/TUI 与会话管理 第十二章:学习路线、实战方案与最佳实践 第十一章:源码结构、开发调试与插件开发 第十章:自动化、远程访问、日志与排障 第九章:Control UI、节点、Canvas 与语音能力 第七章:工具、技能、插件与能力扩展 第八章:安全模型、访问控制与沙箱实践 第六章:Agent 工作区、会话与多智能体路由 第五章:多通道消息接入与聊天平台配置 第四章:配置体系、模型接入与认证管理 第三章:Gateway 架构、协议与运行机制 第二章:安装、环境准备与快速上手 第一章:OpenClaw 项目概览与核心定位 oh-my-openagent 教程目录 09-命令模型回退与配置参考 10-实战案例最佳实践与故障排除 05-工作模式-Ultrawork-Prometheus-Atlas 08-Hooks与MCP系统 06-Category与Skill系统 07-核心工具链 04-智能体全景详解 03-安装与环境配置 02-整体架构与多模型编排机制 01-项目简介与核心理念 01-项目概览与学习路线 02-安装部署与工具适配 03-Skill机制与using-superpowers 05-TDD系统化调试与完成前验证 04-需求澄清方案设计与计划编写 07-并行智能体子智能体与Git-Worktree 第六章:代码审查、反馈处理与分支收尾 08-中国特色Skills与本土团队落地 09-MCP构建工作流执行与自定义Skill 第23章:FreeCAD-Python-API Clipper2 C# 源码解读教程 第19章:PolyTree 多边形树结构 第20章:实际应用与最佳实践 第18章:Minkowski 和与差 第17章:RectClip 矩形裁剪优化 第16章:ClipperOffset 偏移类详解 第15章:填充规则详解 第14章:布尔运算执行流程 第13章:ClipperD 浮点裁剪类 第11章:OutRec 与 OutPt 输出结构 第9章:Active 活动边结构 第10章:Vertex 顶点与 LocalMinima 局部极小值 第12章:Clipper64 裁剪类详解 第7章:高精度运算与128位整数 第8章:ClipperBase 基类详解 第5章:枚举类型与常量定义 第6章:InternalClipper 内部工具类 第2章:核心数据结构 - Point64、PointD 第3章:路径与多边形表示 - Path64、PathD、Paths64、PathsD 第4章:矩形边界 - Rect64、RectD
第九章:日志与审计
我才是银古 · 2026-06-21 · via 博客园 - 我才是银古

第九章:日志与审计

9.1 .NET 日志系统概述

日志是应用程序开发和运维中不可或缺的组成部分。它帮助开发者追踪程序运行状态、排查问题、分析性能瓶颈,以及满足合规审计要求。ASP.NET Core 提供了强大的内置日志框架(Microsoft.Extensions.Logging),Furion 在此基础上进行了增强和扩展。

9.1.1 日志级别

.NET 日志系统定义了以下日志级别(从低到高):

级别 枚举值 说明 使用场景
Trace 0 最详细的诊断信息 开发调试时使用
Debug 1 调试信息 开发环境使用
Information 2 一般信息 记录正常业务流程
Warning 3 警告信息 潜在的问题
Error 4 错误信息 功能失败但不影响系统运行
Critical 5 严重错误 系统级故障
None 6 不记录任何日志 禁用日志

9.1.2 基本日志使用

using Microsoft.Extensions.Logging;

public class OrderService : IDynamicApiController
{
    private readonly ILogger<OrderService> _logger;
    private readonly IRepository<Order> _orderRepo;

    public OrderService(ILogger<OrderService> logger, IRepository<Order> orderRepo)
    {
        _logger = logger;
        _orderRepo = orderRepo;
    }

    public async Task<Order> CreateOrder(CreateOrderInput input)
    {
        _logger.LogInformation("开始创建订单,用户ID:{UserId},金额:{Amount}",
            input.UserId, input.Amount);

        try
        {
            var order = new Order
            {
                OrderNo = GenerateOrderNo(),
                Amount = input.Amount,
                UserId = input.UserId,
                Status = OrderStatus.Pending
            };

            var result = await _orderRepo.InsertNowAsync(order);

            _logger.LogInformation("订单创建成功,订单号:{OrderNo}",
                result.Entity.OrderNo);

            return result.Entity;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "订单创建失败,用户ID:{UserId},错误:{Message}",
                input.UserId, ex.Message);
            throw;
        }
    }

    private string GenerateOrderNo()
    {
        return $"ORD{DateTime.Now:yyyyMMddHHmmss}{Random.Shared.Next(1000, 9999)}";
    }
}

9.2 Furion 日志增强

Furion 对 .NET 原生日志系统进行了增强,提供了更便捷的日志记录方式:

9.2.1 静态日志方法

using Furion;

public class ProductService : IDynamicApiController
{
    public async Task<Product> GetProduct(int id)
    {
        // 使用 Furion 静态方法记录日志
        "正在查询商品,ID:{0}".LogInformation(id);

        var product = await GetProductFromDb(id);

        if (product == null)
        {
            "商品不存在,ID:{0}".LogWarning(id);
            throw Oops.Oh("商品不存在");
        }

        return product;
    }
}

9.2.2 日志上下文

using Microsoft.Extensions.Logging;

/// <summary>
/// 带有上下文信息的日志记录
/// </summary>
public class ContextualLoggingService : IDynamicApiController
{
    private readonly ILogger<ContextualLoggingService> _logger;
    private readonly IHttpContextAccessor _httpContextAccessor;

    public ContextualLoggingService(
        ILogger<ContextualLoggingService> logger,
        IHttpContextAccessor httpContextAccessor)
    {
        _logger = logger;
        _httpContextAccessor = httpContextAccessor;
    }

    public async Task<string> DoWork()
    {
        var traceId = _httpContextAccessor.HttpContext?.TraceIdentifier;
        var userId = _httpContextAccessor.HttpContext?.User?.FindFirstValue("UserId");

        using (_logger.BeginScope(new Dictionary<string, object>
        {
            ["TraceId"] = traceId,
            ["UserId"] = userId,
            ["Module"] = "ContextualLogging"
        }))
        {
            _logger.LogInformation("开始处理业务逻辑");
            // 在此作用域内的所有日志都会自动附带 TraceId、UserId、Module 信息
            await ProcessStep1();
            await ProcessStep2();
            _logger.LogInformation("业务逻辑处理完成");
        }

        return "完成";
    }

    private Task ProcessStep1()
    {
        _logger.LogDebug("执行步骤1");
        return Task.CompletedTask;
    }

    private Task ProcessStep2()
    {
        _logger.LogDebug("执行步骤2");
        return Task.CompletedTask;
    }
}

9.3 内置日志提供程序

9.3.1 配置日志提供程序

// Program.cs
var builder = WebApplication.CreateBuilder(args);

// 配置日志
builder.Logging.ClearProviders();  // 清除默认提供程序

builder.Logging
    .AddConsole()       // 控制台日志
    .AddDebug()         // 调试输出日志
    .AddEventLog()      // Windows 事件日志(仅 Windows)
    .AddEventSourceLogger();  // EventSource 日志

9.3.2 appsettings.json 日志配置

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information",
      "Microsoft.EntityFrameworkCore": "Warning",
      "System": "Warning"
    },
    "Console": {
      "LogLevel": {
        "Default": "Information"
      },
      "FormatterName": "simple",
      "FormatterOptions": {
        "SingleLine": true,
        "TimestampFormat": "yyyy-MM-dd HH:mm:ss ",
        "IncludeScopes": true
      }
    }
  }
}

9.4 Serilog 集成

Serilog 是 .NET 生态中最流行的结构化日志框架,Furion 可以无缝集成 Serilog。

9.4.1 安装 Serilog

dotnet add package Serilog
dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.Console
dotnet add package Serilog.Sinks.File
dotnet add package Serilog.Sinks.Seq
dotnet add package Serilog.Enrichers.Environment
dotnet add package Serilog.Enrichers.Thread

9.4.2 配置 Serilog

// Program.cs
using Serilog;
using Serilog.Events;

// 配置 Serilog
Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Debug()
    .MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
    .MinimumLevel.Override("Microsoft.Hosting.Lifetime", LogEventLevel.Information)
    .MinimumLevel.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Warning)
    .Enrich.FromLogContext()
    .Enrich.WithEnvironmentName()
    .Enrich.WithMachineName()
    .Enrich.WithThreadId()
    // 控制台输出
    .WriteTo.Console(
        outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}")
    // 文件输出 - 按日期滚动
    .WriteTo.File(
        path: "logs/app-.log",
        rollingInterval: RollingInterval.Day,
        retainedFileCountLimit: 30,
        outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}",
        fileSizeLimitBytes: 100 * 1024 * 1024)  // 100MB
    // 错误日志单独输出
    .WriteTo.File(
        path: "logs/error-.log",
        rollingInterval: RollingInterval.Day,
        restrictedToMinimumLevel: LogEventLevel.Error,
        retainedFileCountLimit: 90)
    .CreateLogger();

try
{
    Log.Information("应用程序正在启动...");

    var builder = WebApplication.CreateBuilder(args);

    // 使用 Serilog
    builder.Host.UseSerilog();

    // ... 其他配置

    var app = builder.Build();

    // Serilog 请求日志中间件
    app.UseSerilogRequestLogging(options =>
    {
        options.MessageTemplate = "HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.0000}ms";
        options.GetLevel = (httpContext, elapsed, ex) =>
        {
            if (ex != null) return LogEventLevel.Error;
            if (httpContext.Response.StatusCode > 499) return LogEventLevel.Error;
            if (elapsed > 5000) return LogEventLevel.Warning;
            return LogEventLevel.Information;
        };
        options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
        {
            diagnosticContext.Set("RequestHost", httpContext.Request.Host.Value);
            diagnosticContext.Set("UserAgent", httpContext.Request.Headers["User-Agent"].ToString());
            diagnosticContext.Set("ClientIP", httpContext.Connection.RemoteIpAddress?.ToString());
        };
    });

    app.Run();
}
catch (Exception ex)
{
    Log.Fatal(ex, "应用程序启动失败");
}
finally
{
    Log.CloseAndFlush();
}

9.4.3 使用 appsettings.json 配置 Serilog

{
  "Serilog": {
    "Using": ["Serilog.Sinks.Console", "Serilog.Sinks.File"],
    "MinimumLevel": {
      "Default": "Information",
      "Override": {
        "Microsoft": "Warning",
        "System": "Warning",
        "Microsoft.EntityFrameworkCore": "Warning"
      }
    },
    "WriteTo": [
      {
        "Name": "Console",
        "Args": {
          "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {SourceContext}: {Message:lj}{NewLine}{Exception}"
        }
      },
      {
        "Name": "File",
        "Args": {
          "path": "logs/app-.log",
          "rollingInterval": "Day",
          "retainedFileCountLimit": 30
        }
      }
    ],
    "Enrich": ["FromLogContext", "WithMachineName", "WithThreadId"]
  }
}
// 从配置文件读取 Serilog 配置
Log.Logger = new LoggerConfiguration()
    .ReadFrom.Configuration(builder.Configuration)
    .CreateLogger();

9.5 结构化日志

结构化日志是现代日志实践的核心,它将日志信息以结构化的方式存储,方便检索和分析。

9.5.1 结构化日志语法

public class StructuredLoggingDemo : IDynamicApiController
{
    private readonly ILogger<StructuredLoggingDemo> _logger;

    public StructuredLoggingDemo(ILogger<StructuredLoggingDemo> logger)
    {
        _logger = logger;
    }

    public void Demo()
    {
        // 使用命名占位符(推荐)
        _logger.LogInformation("用户 {UserName} 在 {LoginTime} 登录系统",
            "张三", DateTime.Now);

        // 使用对象属性(结构化存储)
        var order = new { OrderNo = "ORD001", Amount = 99.99, UserId = 1 };
        _logger.LogInformation("新订单创建 {@Order}", order);

        // 使用析构操作符 $(只记录 ToString 结果)
        _logger.LogInformation("当前时间 {$Time}", DateTime.Now);

        // 避免使用字符串插值(会丢失结构化信息)
        // 错误示例:_logger.LogInformation($"用户 {userName} 登录");
        // 正确示例:_logger.LogInformation("用户 {UserName} 登录", userName);
    }
}

9.5.2 日志模板最佳实践

/// <summary>
/// 日志模板常量(统一管理日志消息模板)
/// </summary>
public static class LogTemplates
{
    // 用户操作日志
    public const string UserLogin = "用户登录 | 用户名: {UserName} | IP: {ClientIP} | 结果: {Result}";
    public const string UserLogout = "用户登出 | 用户ID: {UserId}";
    public const string UserCreated = "用户创建 | 用户名: {UserName} | 创建者: {Creator}";

    // 订单操作日志
    public const string OrderCreated = "订单创建 | 订单号: {OrderNo} | 金额: {Amount} | 用户ID: {UserId}";
    public const string OrderStatusChanged = "订单状态变更 | 订单号: {OrderNo} | {OldStatus} -> {NewStatus}";

    // 系统日志
    public const string ApiRequest = "API请求 | {Method} {Path} | 状态码: {StatusCode} | 耗时: {Elapsed}ms";
    public const string DatabaseQuery = "数据库查询 | 表: {Table} | 耗时: {Elapsed}ms | 返回行数: {RowCount}";
}

// 使用示例
public class OrderWithLogging : IDynamicApiController
{
    private readonly ILogger<OrderWithLogging> _logger;

    public OrderWithLogging(ILogger<OrderWithLogging> logger)
    {
        _logger = logger;
    }

    public async Task CreateOrder(CreateOrderInput input)
    {
        _logger.LogInformation(LogTemplates.OrderCreated,
            "ORD001", input.Amount, input.UserId);
    }
}

9.6 日志级别与过滤

9.6.1 运行时动态调整日志级别

/// <summary>
/// 日志级别管理服务
/// </summary>
public class LogLevelService : IDynamicApiController
{
    private readonly IConfiguration _configuration;

    public LogLevelService(IConfiguration configuration)
    {
        _configuration = configuration;
    }

    /// <summary>
    /// 获取当前日志级别配置
    /// </summary>
    public Dictionary<string, string> GetLogLevels()
    {
        var logLevels = new Dictionary<string, string>();
        var section = _configuration.GetSection("Logging:LogLevel");
        foreach (var child in section.GetChildren())
        {
            logLevels[child.Key] = child.Value;
        }
        return logLevels;
    }
}

9.6.2 日志过滤规则

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "Microsoft.EntityFrameworkCore.Database.Command": "Information",
      "YourApp.Services": "Debug",
      "YourApp.Controllers": "Information"
    }
  }
}
命名空间 建议级别 说明
Default Information 默认日志级别
Microsoft.AspNetCore Warning ASP.NET Core 框架日志
Microsoft.EntityFrameworkCore Warning EF Core 日志
Microsoft.EntityFrameworkCore.Database.Command Information SQL 查询日志
YourApp.Services Debug 业务服务层日志
System.Net.Http.HttpClient Warning HTTP 客户端日志

9.7 请求日志(访问日志)

9.7.1 请求日志中间件

/// <summary>
/// 请求日志中间件
/// </summary>
public class RequestLoggingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<RequestLoggingMiddleware> _logger;

    public RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var stopwatch = System.Diagnostics.Stopwatch.StartNew();
        var requestId = context.TraceIdentifier;

        // 记录请求信息
        _logger.LogInformation(
            "请求开始 | RequestId: {RequestId} | {Method} {Path} | IP: {ClientIP} | UserAgent: {UserAgent}",
            requestId,
            context.Request.Method,
            context.Request.Path + context.Request.QueryString,
            context.Connection.RemoteIpAddress?.ToString(),
            context.Request.Headers["User-Agent"].ToString());

        // 记录请求体(仅 POST/PUT)
        if (context.Request.Method is "POST" or "PUT")
        {
            context.Request.EnableBuffering();
            var body = await ReadRequestBody(context.Request);
            if (!string.IsNullOrEmpty(body))
            {
                _logger.LogDebug("请求体 | RequestId: {RequestId} | Body: {Body}",
                    requestId, body);
            }
            context.Request.Body.Position = 0;
        }

        await _next(context);

        stopwatch.Stop();

        // 记录响应信息
        var logLevel = context.Response.StatusCode >= 500
            ? LogLevel.Error
            : context.Response.StatusCode >= 400
                ? LogLevel.Warning
                : LogLevel.Information;

        _logger.Log(logLevel,
            "请求结束 | RequestId: {RequestId} | 状态码: {StatusCode} | 耗时: {Elapsed}ms",
            requestId,
            context.Response.StatusCode,
            stopwatch.ElapsedMilliseconds);
    }

    private async Task<string> ReadRequestBody(HttpRequest request)
    {
        using var reader = new StreamReader(
            request.Body,
            encoding: System.Text.Encoding.UTF8,
            detectEncodingFromByteOrderMarks: false,
            leaveOpen: true);
        var body = await reader.ReadToEndAsync();
        return body.Length > 4096 ? body[..4096] + "...(truncated)" : body;
    }
}

// 注册中间件
app.UseMiddleware<RequestLoggingMiddleware>();

9.8 操作审计日志

9.8.1 审计日志模型

/// <summary>
/// 审计日志实体
/// </summary>
public class AuditLog : EntityBase
{
    /// <summary>
    /// 操作用户ID
    /// </summary>
    public int? UserId { get; set; }

    /// <summary>
    /// 用户名
    /// </summary>
    [MaxLength(50)]
    public string UserName { get; set; }

    /// <summary>
    /// 操作模块
    /// </summary>
    [MaxLength(100)]
    public string Module { get; set; }

    /// <summary>
    /// 操作类型
    /// </summary>
    [MaxLength(50)]
    public string Action { get; set; }

    /// <summary>
    /// 请求方法
    /// </summary>
    [MaxLength(10)]
    public string HttpMethod { get; set; }

    /// <summary>
    /// 请求路径
    /// </summary>
    [MaxLength(500)]
    public string RequestUrl { get; set; }

    /// <summary>
    /// 请求参数
    /// </summary>
    public string RequestParams { get; set; }

    /// <summary>
    /// 响应结果
    /// </summary>
    public string ResponseResult { get; set; }

    /// <summary>
    /// 客户端IP
    /// </summary>
    [MaxLength(50)]
    public string ClientIP { get; set; }

    /// <summary>
    /// 浏览器信息
    /// </summary>
    [MaxLength(500)]
    public string UserAgent { get; set; }

    /// <summary>
    /// 执行耗时(毫秒)
    /// </summary>
    public long Elapsed { get; set; }

    /// <summary>
    /// 操作状态(成功/失败)
    /// </summary>
    public bool IsSuccess { get; set; }

    /// <summary>
    /// 异常信息
    /// </summary>
    public string ExceptionMessage { get; set; }
}

9.8.2 审计日志特性与过滤器

/// <summary>
/// 审计日志特性
/// </summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public class AuditLogAttribute : Attribute
{
    /// <summary>
    /// 模块名称
    /// </summary>
    public string Module { get; set; }

    /// <summary>
    /// 操作描述
    /// </summary>
    public string Action { get; set; }

    public AuditLogAttribute(string module, string action)
    {
        Module = module;
        Action = action;
    }
}

/// <summary>
/// 审计日志过滤器
/// </summary>
public class AuditLogFilter : IAsyncActionFilter
{
    private readonly IRepository<AuditLog> _auditLogRepo;
    private readonly ILogger<AuditLogFilter> _logger;

    public AuditLogFilter(IRepository<AuditLog> auditLogRepo, ILogger<AuditLogFilter> logger)
    {
        _auditLogRepo = auditLogRepo;
        _logger = logger;
    }

    public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        // 检查是否标记了审计日志特性
        var auditAttr = context.ActionDescriptor.EndpointMetadata
            .OfType<AuditLogAttribute>().FirstOrDefault();

        if (auditAttr == null)
        {
            await next();
            return;
        }

        var stopwatch = System.Diagnostics.Stopwatch.StartNew();

        var auditLog = new AuditLog
        {
            Module = auditAttr.Module,
            Action = auditAttr.Action,
            HttpMethod = context.HttpContext.Request.Method,
            RequestUrl = context.HttpContext.Request.Path + context.HttpContext.Request.QueryString,
            RequestParams = System.Text.Json.JsonSerializer.Serialize(context.ActionArguments),
            ClientIP = context.HttpContext.Connection.RemoteIpAddress?.ToString(),
            UserAgent = context.HttpContext.Request.Headers["User-Agent"].ToString(),
            UserId = GetCurrentUserId(context.HttpContext),
            UserName = GetCurrentUserName(context.HttpContext)
        };

        var executedContext = await next();
        stopwatch.Stop();

        auditLog.Elapsed = stopwatch.ElapsedMilliseconds;
        auditLog.IsSuccess = executedContext.Exception == null;

        if (executedContext.Exception != null)
        {
            auditLog.ExceptionMessage = executedContext.Exception.Message;
        }

        // 异步保存审计日志(不影响主流程)
        try
        {
            await _auditLogRepo.InsertNowAsync(auditLog);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "审计日志保存失败");
        }
    }

    private int? GetCurrentUserId(HttpContext context)
    {
        var userIdStr = context.User?.FindFirstValue("UserId");
        return int.TryParse(userIdStr, out var userId) ? userId : null;
    }

    private string GetCurrentUserName(HttpContext context)
    {
        return context.User?.FindFirstValue("UserName");
    }
}

// 注册审计日志过滤器
builder.Services.AddScoped<AuditLogFilter>();

// 在控制器中使用
public class UserManageService : IDynamicApiController
{
    private readonly IRepository<User> _userRepo;

    public UserManageService(IRepository<User> userRepo)
    {
        _userRepo = userRepo;
    }

    [AuditLog("用户管理", "创建用户")]
    public async Task<User> CreateUser(CreateUserInput input)
    {
        var user = new User { UserName = input.UserName, Email = input.Email };
        var result = await _userRepo.InsertNowAsync(user);
        return result.Entity;
    }

    [AuditLog("用户管理", "删除用户")]
    public async Task DeleteUser(int id)
    {
        await _userRepo.DeleteAsync(id);
        await _userRepo.SaveNowAsync();
    }

    [AuditLog("用户管理", "修改密码")]
    public async Task ChangePassword(int userId, ChangePasswordInput input)
    {
        // 修改密码逻辑...
    }
}

9.9 异常日志

9.9.1 异常日志记录策略

/// <summary>
/// 异常日志服务
/// </summary>
public class ExceptionLogService
{
    private readonly ILogger<ExceptionLogService> _logger;
    private readonly IRepository<ExceptionLog> _exceptionLogRepo;

    public ExceptionLogService(
        ILogger<ExceptionLogService> logger,
        IRepository<ExceptionLog> exceptionLogRepo)
    {
        _logger = logger;
        _exceptionLogRepo = exceptionLogRepo;
    }

    /// <summary>
    /// 记录异常日志到数据库
    /// </summary>
    public async Task LogException(Exception exception, HttpContext httpContext)
    {
        var exceptionLog = new ExceptionLog
        {
            ExceptionType = exception.GetType().FullName,
            Message = exception.Message,
            StackTrace = exception.StackTrace,
            InnerException = exception.InnerException?.Message,
            Source = exception.Source,
            RequestUrl = httpContext.Request.Path + httpContext.Request.QueryString,
            HttpMethod = httpContext.Request.Method,
            ClientIP = httpContext.Connection.RemoteIpAddress?.ToString(),
            UserId = GetCurrentUserId(httpContext),
            OccurredTime = DateTime.Now
        };

        try
        {
            await _exceptionLogRepo.InsertNowAsync(exceptionLog);
        }
        catch (Exception ex)
        {
            // 数据库写入失败时,至少写入文件日志
            _logger.LogError(ex, "异常日志写入数据库失败,原始异常:{Message}", exception.Message);
        }
    }

    private int? GetCurrentUserId(HttpContext context)
    {
        var userIdStr = context.User?.FindFirstValue("UserId");
        return int.TryParse(userIdStr, out var userId) ? userId : null;
    }
}

/// <summary>
/// 异常日志实体
/// </summary>
public class ExceptionLog : Entity
{
    [MaxLength(500)]
    public string ExceptionType { get; set; }

    public string Message { get; set; }

    public string StackTrace { get; set; }

    public string InnerException { get; set; }

    [MaxLength(200)]
    public string Source { get; set; }

    [MaxLength(500)]
    public string RequestUrl { get; set; }

    [MaxLength(10)]
    public string HttpMethod { get; set; }

    [MaxLength(50)]
    public string ClientIP { get; set; }

    public int? UserId { get; set; }

    public DateTime OccurredTime { get; set; }
}

9.10 日志持久化

9.10.1 文件日志配置

// 使用 Serilog 文件日志
Log.Logger = new LoggerConfiguration()
    .WriteTo.File(
        path: "logs/app-.log",
        rollingInterval: RollingInterval.Day,      // 按天滚动
        retainedFileCountLimit: 30,                 // 保留30天
        fileSizeLimitBytes: 100 * 1024 * 1024,     // 单文件100MB上限
        rollOnFileSizeLimit: true,                  // 超过大小自动滚动
        encoding: System.Text.Encoding.UTF8,
        outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}")
    .CreateLogger();

9.10.2 数据库日志

// 安装 Serilog 数据库 Sink
// dotnet add package Serilog.Sinks.MSSqlServer

using Serilog.Sinks.MSSqlServer;

Log.Logger = new LoggerConfiguration()
    .WriteTo.MSSqlServer(
        connectionString: "Server=localhost;Database=FurionLogs;...",
        sinkOptions: new MSSqlServerSinkOptions
        {
            TableName = "Logs",
            AutoCreateSqlTable = true,
            BatchPostingLimit = 50,
            BatchPeriod = TimeSpan.FromSeconds(5)
        },
        columnOptions: GetColumnOptions())
    .CreateLogger();

ColumnOptions GetColumnOptions()
{
    var columnOptions = new ColumnOptions();
    columnOptions.Store.Remove(StandardColumn.Properties);
    columnOptions.Store.Add(StandardColumn.LogEvent);
    columnOptions.TimeStamp.ConvertToUtc = false;
    return columnOptions;
}

9.10.3 Elasticsearch 日志

// 安装 Serilog Elasticsearch Sink
// dotnet add package Serilog.Sinks.Elasticsearch

using Serilog.Sinks.Elasticsearch;

Log.Logger = new LoggerConfiguration()
    .WriteTo.Elasticsearch(new ElasticsearchSinkOptions(new Uri("http://localhost:9200"))
    {
        IndexFormat = "furion-logs-{0:yyyy.MM.dd}",
        AutoRegisterTemplate = true,
        AutoRegisterTemplateVersion = AutoRegisterTemplateVersion.ESv7,
        NumberOfShards = 2,
        NumberOfReplicas = 1,
        BatchPostingLimit = 50,
        Period = TimeSpan.FromSeconds(2)
    })
    .CreateLogger();

9.11 日志最佳实践

9.11.1 日志规范

规范项 说明
使用结构化日志 使用命名占位符而非字符串插值
选择合适级别 根据信息重要程度选择日志级别
避免敏感信息 不要在日志中记录密码、密钥等敏感数据
控制日志量 生产环境避免使用 Debug/Trace 级别
包含上下文 日志应包含足够的上下文信息(RequestId、UserId等)
统一格式 使用统一的日志消息模板
异常记录 异常日志应包含完整的堆栈信息
定期清理 设置日志文件保留策略,避免磁盘空间占满

9.11.2 日志使用规范示例

/// <summary>
/// 日志使用规范示例
/// </summary>
public class LoggingBestPractices : IDynamicApiController
{
    private readonly ILogger<LoggingBestPractices> _logger;

    public LoggingBestPractices(ILogger<LoggingBestPractices> logger)
    {
        _logger = logger;
    }

    public async Task DemoLogging()
    {
        // ✅ 正确:使用结构化参数
        _logger.LogInformation("用户 {UserId} 执行了 {Action} 操作", 123, "CreateOrder");

        // ❌ 错误:使用字符串插值(丢失结构化信息)
        // _logger.LogInformation($"用户 {userId} 执行了 {action} 操作");

        // ✅ 正确:异常日志包含异常对象
        try
        {
            // 某些操作...
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "订单处理失败,订单号:{OrderNo}", "ORD001");
        }

        // ❌ 错误:不要记录敏感信息
        // _logger.LogInformation("用户登录,密码:{Password}", password);

        // ✅ 正确:脱敏处理
        _logger.LogInformation("用户登录,用户名:{UserName}", "张三");

        // ✅ 正确:使用 LoggerMessage.Define 提升性能(高频日志)
        LogUserAction(_logger, 123, "查询订单");
    }

    // 高性能日志(编译时生成,避免运行时装箱和字符串分配)
    private static readonly Action<ILogger, int, string, Exception> LogUserAction =
        LoggerMessage.Define<int, string>(
            LogLevel.Information,
            new EventId(1001, "UserAction"),
            "用户 {UserId} 执行操作:{Action}");
}

9.11.3 生产环境日志架构

推荐的生产环境日志架构如下:

应用日志 → Serilog → 多个 Sink
                        ├── Console(开发环境调试)
                        ├── File(本地文件备份)
                        ├── Elasticsearch(集中化存储和检索)
                        └── Seq / Grafana Loki(日志分析平台)

配置示例:

Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Information()
    .Enrich.FromLogContext()
    .Enrich.WithProperty("Application", "FurionApp")
    .Enrich.WithProperty("Environment", builder.Environment.EnvironmentName)
    // 开发环境:控制台
    .WriteTo.Conditional(
        _ => builder.Environment.IsDevelopment(),
        config => config.Console())
    // 所有环境:文件
    .WriteTo.File("logs/app-.log",
        rollingInterval: RollingInterval.Day,
        retainedFileCountLimit: 30)
    // 生产环境:Elasticsearch
    .WriteTo.Conditional(
        _ => builder.Environment.IsProduction(),
        config => config.Elasticsearch(new ElasticsearchSinkOptions(
            new Uri("http://elasticsearch:9200"))
        {
            IndexFormat = "app-logs-{0:yyyy.MM.dd}"
        }))
    .CreateLogger();

上一章: 第08章-安全授权与身份认证 | 返回目录 | 下一章: 第10章-缓存管理