异常日志
本页介绍 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>();
});请确保依赖项已注册:IExceptionLogService、ISettingService、IBrowserDetector、ISearcher、ILogger<ExceptionLogFilter> 等。
与 Serilog 的协同
过滤器内部使用 _logger.LogError(...) 写入日志管道,推荐结合 Serilog 配置不同环境的输出(文件/控制台/数据库/Elasticsearch)。参见《Serilog 集成》。
最佳实践
- 脱敏与最小化:异常消息中避免泄漏敏感信息(连接串、密钥、内部路径等)。
- 统一映射:规范业务异常到合理的 HTTP 状态码与错误码,前后端口径一致。
- 采样与限频:高频重复异常可做聚合/采样,避免刷库与告警风暴。
- 追踪上下文:结合 TraceId/SpanId 贯穿请求链路,提升排障效率。
- 监控与告警:对异常量突增、落库失败进行监控与告警。
常见问题(FAQ)
为什么数据库里没有异常日志?
- 检查 IsExceptionLogSaveDB 是否为 true。
- 是否抛出了
DemoRequestException(该类型会跳过持久化)。 - 过滤器是否已注册为全局;异常是否在更早的中间件被处理掉。
会不会把堆栈返回给前端?
- 不会。对外仅返回统一 JSON 摘要;完整堆栈仅写入日志与数据库,避免信息泄露。
能否改为按类型路由到不同存储?
- 可以。根据异常类型或来源分类,扩展
IExceptionLogService实现或增加消息管道即可。
- 可以。根据异常类型或来源分类,扩展
相关阅读:
- 《操作日志》
- 《Serilog 集成》

