Skip to content

异常日志

本页介绍 Ape‑Volo‑Admin 的异常日志捕获与落库机制,涵盖开关行为、采集字段、统一响应格式、注册方式与最佳实践。

你将获得

  • 全局异常捕获与统一错误响应的工作原理
  • IsExceptionLogSaveDB 开关的作用与生效条件
  • 采集字段明细(包含异常上下文与客户端环境)
  • 持久化策略:直接写库(无 Redis 队列)与跳过条件
  • 与 Serilog 的协同及常见排查思路

异常过滤器

位置:Ape.Volo.Infrastructure/ActionFilter/ExceptionLogFilter.cs

csharp
/// <summary>
/// 异常日志过滤器
/// </summary>
public class ExceptionLogFilter : IAsyncExceptionFilter
{
    private readonly IExceptionLogService _exceptionLogService;
    private readonly ISettingService _settingService;
    private readonly IBrowserDetector _browserDetector;
    private readonly ILogger<ExceptionLogFilter> _logger;
    private readonly ISearcher _ipSearcher;

    public ExceptionLogFilter(IExceptionLogService exceptionLogService, ISearcher searcher,
        ISettingService settingService, IBrowserDetector browserDetector,
        ILogger<ExceptionLogFilter> logger)
    { /* 依赖注入略 */ }

    public async Task OnExceptionAsync(ExceptionContext context)
    {
        var exceptionType = context.Exception.GetType();
        var statusCode = StatusCodes.Status500InternalServerError;
        if (exceptionType == typeof(BadRequestException))
            statusCode = StatusCodes.Status400BadRequest;

        // 构造统一响应体
        var remoteIp = context.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "0.0.0.0";
        var ipAddress = _ipSearcher.Search(remoteIp);
        var throwMsg = context.Exception.Message;
        context.Result = new ContentResult
        {
            Content = new ActionResultVm
            {
                Status = statusCode,
                ActionError = new ActionError { Errors = new Dictionary<string, string>() },
                Message = throwMsg,
                Timestamp = DateTime.Now.ToUnixTimeStampMillisecond().ToString(),
                Path = context.HttpContext.Request.Path.Value?.ToLower()
            }.ToJson(),
            ContentType = "application/json; charset=utf-8",
            StatusCode = statusCode
        };

        if (App.GetOptions<MiddlewareOptions>().MiniProfiler.Enabled)
        {
            MiniProfiler.Current.CustomTiming("Errors:", throwMsg);
        }

        // 记录到日志管道(如 Serilog)
        _logger.LogError(ExceptionHelper.ErrorFormat(context.HttpContext, remoteIp, ipAddress, context.Exception,
            App.HttpUser.Account,
            _browserDetector.Browser?.OS, _browserDetector.Browser?.DeviceType, _browserDetector.Browser?.Name,
            _browserDetector.Browser?.Version));

        // 条件落库
        var saveDb = await _settingService.GetSettingValue<bool>("IsExceptionLogSaveDB");
        if (saveDb && exceptionType != typeof(DemoRequestException))
        {
            var log = CreateLog(context);
            if (log.IsNotNull())
            {
                await Task.Factory.StartNew(() => _exceptionLogService.CreateAsync(log))
                    .ConfigureAwait(false);
            }
        }
    }

    private ExceptionLog CreateLog(ExceptionContext context)
    {
        // 组装异常实体(路由/请求/客户端/异常详情等)——见下文“采集字段”
        // ...
        return new ExceptionLog();
    }
}

功能概述

  • 全局统一异常处理:在单一位置捕获未处理异常,避免各处分散 try/catch,简化代码并降低遗漏风险。
  • 统一错误响应与安全性:对外只返回规范化摘要,避免将堆栈或敏感信息暴露给客户端,利于安全合规。
  • 完整上下文采集:自动记录路由、请求参数、客户端环境、IP、异常栈等信息,为快速定位问题提供必要线索。
  • 结构化日志输出:通过日志管道(如 Serilog)输出结构化数据,便于搜索、聚合和与外部存储(ES/数据库)对接。
  • 可配置的持久化策略:通过开关与异常类型控制是否写库,既支持审计/持久化需求,又可避免大量无用数据写入。
  • 支持监控与告警:集中度量异常数量与类型,便于设置阈值告警、发现异常趋势或突发故障。
  • 提升排障效率:结合 TraceId、时间戳和完整异常链路,可快速还原请求路径和根因,缩短故障恢复时间。
  • 降低运行时影响:采用异步写库与采样/限频策略,兼顾异常记录与系统性能,减少因记录行为导致的请求阻塞或资源耗尽。

开关与行为

  • IsExceptionLogSaveDB(系统设置项):是否保存异常日志到数据库。
    • ISettingService.GetSettingValue<bool>("IsExceptionLogSaveDB") 读取。
  • MiniProfiler:当启用时,记录自定义异常计时 Errors,用于性能与异常热点分析。
  • DemoRequestException:若抛出该异常类型,跳过持久化(仍会写入日志管道)。

行为总结:

  • 当 IsExceptionLogSaveDB=true 且异常类型 ≠DemoRequestException:直接异步写库(_exceptionLogService.CreateAsync)。
  • 当 IsExceptionLogSaveDB=false 或异常为 DemoRequestException:不落库,仅输出统一响应并写入日志管道。

采集范围与字段

异常日志实体(ExceptionLog)通常包含:

  • 基本信息:Id、CreateBy、CreateTime
  • 路由与请求:Area、Controller、Action、Method、RequestUrl、RequestParameters
  • 文案描述:[Description] 特性值(支持 App.L.R 多语言转换)
  • 客户端环境:RequestIp、IpAddress(ISearcher)、UserAgent、OperatingSystem、DeviceType、BrowserName、Version(IBrowserDetector
  • 异常详情:ExceptionMessage、ExceptionMessageFull(完整链路信息)、ExceptionStack、LogLevel(固定为 Error)

统一错误响应

过滤器会将未处理异常转换为统一结构的 JSON 响应(ActionResultVm):

json
{
  "status": 500,
  "actionError": { "errors": {} },
  "message": "错误信息摘要",
  "timestamp": "1730712345678",
  "path": "/api/demo"
}

状态码策略:

  • 缺省 500;若为 BadRequestException,则置为 400。
  • 可扩展映射自定义业务异常到 4xx/5xx 更精细的状态码(示例代码中已留注释)。

全局注册

将异常日志过滤器注册为全局过滤器:

csharp
services.AddControllers(options =>
{
    options.Filters.Add<ExceptionLogFilter>();
});

请确保依赖项已注册:IExceptionLogServiceISettingServiceIBrowserDetectorISearcherILogger<ExceptionLogFilter> 等。

与 Serilog 的协同

过滤器内部使用 _logger.LogError(...) 写入日志管道,推荐结合 Serilog 配置不同环境的输出(文件/控制台/数据库/Elasticsearch)。参见《Serilog 集成》。

最佳实践

  • 脱敏与最小化:异常消息中避免泄漏敏感信息(连接串、密钥、内部路径等)。
  • 统一映射:规范业务异常到合理的 HTTP 状态码与错误码,前后端口径一致。
  • 采样与限频:高频重复异常可做聚合/采样,避免刷库与告警风暴。
  • 追踪上下文:结合 TraceId/SpanId 贯穿请求链路,提升排障效率。
  • 监控与告警:对异常量突增、落库失败进行监控与告警。

常见问题(FAQ)

  • 为什么数据库里没有异常日志?

    • 检查 IsExceptionLogSaveDB 是否为 true。
    • 是否抛出了 DemoRequestException(该类型会跳过持久化)。
    • 过滤器是否已注册为全局;异常是否在更早的中间件被处理掉。
  • 会不会把堆栈返回给前端?

    • 不会。对外仅返回统一 JSON 摘要;完整堆栈仅写入日志与数据库,避免信息泄露。
  • 能否改为按类型路由到不同存储?

    • 可以。根据异常类型或来源分类,扩展 IExceptionLogService 实现或增加消息管道即可。

相关阅读:

版权所有 © 2021-2026 ApeVolo-Team