🚀 ABP vNext + Sentry + ELK Stack:打造高可用异常跟踪与日志可视化平台 🎉
技术选型
🛠️ 工具 | 功能 | 适用场景 |
---|---|---|
ABP vNext | 模块化应用框架 | 多租户、多模块 |
Serilog | .NET 结构化日志库 | 支持多种 Sink |
Sentry | 异常与性能链路监控 | 异常聚合、Trace 分析 |
Elasticsearch | 日志索引引擎 | 大规模写入与检索 |
Kibana | 日志可视化面板 | 仪表盘和图表展示 |
HealthChecks UI | 可视化健康检查 | 服务可用性与探针监控 |
系统架构图
依赖安装与多环境配置 🧰
dotnet add package Sentry.AspNetCore
dotnet add package Serilog.Sinks.Elasticsearch
dotnet add package Serilog.Enrichers.Environment
dotnet add package Serilog.Enrichers.Thread
dotnet add package Serilog.Enrichers.CorrelationId
dotnet add package Volo.Abp.Serilog
dotnet add package AspNetCore.HealthChecks.UI
// Program.cs - 配置读取顺序
var builder = WebApplication.CreateBuilder(args);
builder.Configuration
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
安全日志配置 🔐
appsettings.Development.json
{
"Sentry": {
"Dsn": "${SENTRY_DSN}",
"TracesSampleRate": 1.0,
"Debug": true
},
"Serilog": {
"MinimumLevel": { "Default": "Debug" },
"WriteTo": [ { "Name": "Console" } ]
}
}
appsettings.Production.json
{
"Sentry": {
"Dsn": "${SENTRY_DSN}",
"TracesSampleRate": 0.2,
"SendDefaultPii": true,
"AttachStacktrace": true,
"Debug": false,
"DiagnosticsLevel": "Error"
},
"Serilog": {
"Using": [ "Serilog.Sinks.Elasticsearch" ],
"MinimumLevel": {
"Default": "Information",
"Override": { "Microsoft": "Warning" }
},
"WriteTo": [
{
"Name": "Elasticsearch",
"Args": {
"NodeUris": "http://elasticsearch:9200",
"AutoRegisterTemplate": true,
"AutoRegisterTemplateVersion": "ESv7",
"IndexFormat": "abp-logs-{0:yyyy.MM.dd}"
}
}
],
"Enrich": [ "FromLogContext", "WithMachineName", "WithThreadId" ]
}
}
秘钥注入:
.NET 默认支持用环境变量SENTRY__DSN
(双下划线表示冒号)覆盖Sentry:Dsn
。
export SENTRY__DSN=https://xxxx@sentry.io/project
Elasticsearch 索引模板 📑
curl -X PUT "localhost:9200/_template/abp-logs-template" -H "Content-Type: application/json" -d '
{
"index_patterns": ["abp-logs-*"],
"settings": { "number_of_shards": 3 },
"mappings": {
"properties": {
"TenantId": { "type": "keyword" },
"Module": { "type": "keyword" },
"Timestamp": { "type": "date" },
"Level": { "type": "keyword" },
"Message": { "type": "text" }
}
}
}'
程序启动与 DI 注册 ⚙️
var builder = WebApplication.CreateBuilder(args);
// 1. CorrelationId 中间件
builder.Services.AddCorrelationId();
// 2. Sentry SDK
builder.Services.AddSentry(o =>
{
o.Dsn = builder.Configuration["Sentry:Dsn"];
o.TracesSampleRate = 0.2;
o.AttachStacktrace = true;
o.Debug = false;
});
// 3. Serilog 注册
builder.Host.UseSerilog((ctx, lc) =>
{
lc.ReadFrom.Configuration(ctx.Configuration)
.Enrich.WithCorrelationId()
.Enrich.WithMachineName()
.Enrich.WithEnvironmentUserName()
.Enrich.WithProcessId()
.Enrich.With<TenantLogEnricher>();
});
// 4. 全局异常订阅
builder.Services.AddSingleton<IExceptionSubscriber, GlobalExceptionSubscriber>();
// 5. HealthChecks + UI
builder.Services
.AddHealthChecks()
.AddSqlServer(builder.Configuration.GetConnectionString("Default"), name: "SQL")
.AddRedis(builder.Configuration["Redis:Configuration"], name: "Redis")
.AddHealthChecksUI()
.AddSqlServerStorage(builder.Configuration.GetConnectionString("HealthChecksUI:Storage"));
var app = builder.Build();
// 6. 中间件顺序
app.UseCorrelationId();
app.UseSerilogRequestLogging();
app.UseSentryTracing();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapHealthChecks("/health", new HealthCheckOptions
{
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
endpoints.MapHealthChecksUI(options => { options.UIPath = "/health-ui"; });
endpoints.MapControllers();
});
app.Run();
日志增强与异常捕获 🛡️
自定义 TenantLogEnricher
public class TenantLogEnricher : ILogEventEnricher
{
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory factory)
{
var tenantId = CurrentTenant.Id?.ToString() ?? "host";
var moduleName = Assembly.GetEntryAssembly()?.GetName().Name ?? "unknown";
logEvent.AddPropertyIfAbsent(factory.CreateProperty("TenantId", tenantId));
logEvent.AddPropertyIfAbsent(factory.CreateProperty("Module", moduleName));
}
}
全局异常订阅器
public class GlobalExceptionSubscriber : IExceptionSubscriber
{
private readonly ILogger<GlobalExceptionSubscriber> _logger;
public GlobalExceptionSubscriber(ILogger<GlobalExceptionSubscriber> logger)
=> _logger = logger;
public Task HandleAsync(ExceptionNotificationContext context)
{
// 业务异常也记录,级别 Warning
_logger.LogWarning(context.Exception, "业务异常:{Message}", context.Exception.Message);
// 全部异常上报到 Sentry
SentrySdk.CaptureException(context.Exception);
return Task.CompletedTask;
}
}
APM 事务监控示例 🔍
using var tx = SentrySdk.StartTransaction("OrderProcess", "order.process");
try
{
// … 业务逻辑 …
tx.Finish(SpanStatus.Ok);
}
catch (Exception)
{
tx.Finish(SpanStatus.InternalError);
throw;
}
HealthChecks 与 UI 🩺
// healthchecks-settings.json
{
"HealthChecksUI": {
"HealthChecks": [
{
"Name": "ABP Core",
"Uri": "http://localhost:5000/health"
}
],
"EvaluationTimeOnSeconds": 30,
"MinimumSecondsBetweenFailureNotifications": 60,
"Storage": {
"ConnectionString": "Server=...;Database=HealthChecks;User Id=...;"
}
}
}
已在 Program.cs 中通过 .AddSqlServerStorage(...)
完成持久化配置。
日志生命周期管理 (ILM) 🔄
# 创建 ILM 策略
PUT _ilm/policy/abp-logs-policy
{
"policy": {
"phases": {
"hot": { "actions": { "rollover": { "max_age": "7d", "max_size": "50gb" } } },
"warm": { "actions": { "forcemerge": { "max_num_segments": 1 } } },
"delete": { "actions": { "delete": { "min_age": "30d" } } }
}
}
}
# 创建 Alias 并激活 Rollover
PUT /abp-logs-write
{
"aliases": { "abp-logs": {} }
}
在 appsettings.Production.json
中,将 IndexFormat
修改为:
"IndexFormat": "abp-logs-write-{0:yyyy.MM.dd}"
容器化部署示例 🐳
version: '3.8'
services:
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:7.17.10
ports: ["9200:9200"]
environment:
- discovery.type=single-node
kibana:
image: docker.elastic.co/kibana/kibana:7.17.10
ports: ["5601:5601"]
depends_on: ["elasticsearch"]
logstash: # 可选:集中化管道
image: docker.elastic.co/logstash/logstash:7.17.10
ports: ["5044:5044"]
volumes:
- ./logstash/pipeline/:/usr/share/logstash/pipeline/
depends_on: ["elasticsearch"]
app:
image: yourorg/abp-sentry-elk-demo:latest
ports: ["5000:80"]
environment:
- ASPNETCORE_ENVIRONMENT=Production
- SENTRY__DSN=${SENTRY__DSN}
depends_on: ["elasticsearch"]
Kubernetes 部署示例 ☸️
apiVersion: v1
kind: Secret
metadata:
name: sentry-secret
stringData:
DSN: https://xxxx@sentry.io/project
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: abp-elk-app
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: abp-elk
template:
metadata:
labels:
app: abp-elk
spec:
containers:
- name: app
image: yourorg/abp-sentry-elk-demo:latest
env:
- name: ASPNETCORE_ENVIRONMENT
value: Production
- name: SENTRY__DSN
valueFrom:
secretKeyRef:
name: sentry-secret
key: DSN
ports:
- containerPort: 80