🚀 在 ABP VNext 中集成 Serilog:打造可觀測、結構化日志系統
📚 目錄
- 🚀 在 ABP VNext 中集成 Serilog:打造可觀測、結構化日志系統
- 1. 為什么要使用結構化日志? 🤔
- 2. 核心集成步驟 🛠
- 2.1 流程圖示例
- 3. NuGet 包安裝 📦
- 4. appsettings.json 配置 📝
- 5. Program.cs 全局日志初始化 💻
- 5.1 代碼流程圖
- 6. ABP 模塊注冊 🏗
- 7. 上下文信息添加 🧩
- 7.1 UseAbpSerilogEnrichers() 自動插入
- 7.2 LogContext.PushProperty 自定義屬性
- 8. 對接平臺:Seq & ELK & Grafana Loki 🌐
- 8.1 部署 Seq(推薦開發階段)
- 8.2 部署 ELK(推薦生產環境)
- 8.2.1 在 Kibana 中創建 Index Pattern 🔍
- 8.3 對接 Grafana Loki(可選)🔗
- 9. 總結 📋
- 📎 推薦閱讀 📚
1. 為什么要使用結構化日志? 🤔
相比于簡單的文本日志,結構化日志有以下優勢:
- ? 傳統文本日志無法根據 TraceId、UserId 等字段方便地檢索
- ? 無法像 SQL 那樣對日志字段進行過濾與聚合
- ? 不易在可視化平臺(如 Kibana、Seq、Grafana Loki)上進行聯動分析
而 Serilog 以原生的 JSON 日志形式輸出,能夠輕松處理上下文、分析調用鏈,方便與 ELK / Seq / Loki 等平臺集成,做到精準定位故障點。
2. 核心集成步驟 🛠
步驟 | 內容 |
---|---|
1?? | 安裝 Volo.Abp.AspNetCore.Serilog 包 |
2?? | 配置 Serilog Sink(Console、File、Seq、Elasticsearch、Loki) |
3?? | 在 Program.cs 中初始化 Serilog |
4?? | 在 ABP 模塊中啟用 UseAbpSerilogEnrichers() 插入上下文 |
2.1 流程圖示例
- 通過 “Enrichers” 階段,可以自動將 TraceId、UserId、MachineName 等信息注入到每條日志。
3. NuGet 包安裝 📦
在項目根目錄下執行以下命令,添加所需依賴包。為了避免版本不一致,建議指定版本號:
dotnet add package Volo.Abp.AspNetCore.Serilog
dotnet add package Serilog.Sinks.Console
dotnet add package Serilog.Sinks.File
dotnet add package Serilog.Sinks.Seq
dotnet add package Serilog.Sinks.Elasticsearch # 如想支持 Grafana Loki:
dotnet add package Serilog.Sinks.Grafana.Loki
4. appsettings.json 配置 📝
在 appsettings.json
文件中,添加或修改 Serilog
節點,如下所示:
{"Serilog": {"Using": ["Serilog.Sinks.Console","Serilog.Sinks.File","Serilog.Sinks.Seq","Serilog.Sinks.Elasticsearch","Serilog.Sinks.Grafana.Loki"],"MinimumLevel": {"Default": "Information","Override": {"Microsoft": "Warning","Microsoft.EntityFrameworkCore": "Error","Volo.Abp": "Information"}},"WriteTo": [{"Name": "Console"},{"Name": "File","Args": {"path": "Logs/log-.log","rollingInterval": "Day","retainedFileCountLimit": 14,"fileSizeLimitBytes": 104857600,"buffered": true,"flushToDiskInterval": "00:00:05"}},{"Name": "Seq","Args": {"serverUrl": "http://localhost:5341"}},{"Name": "Elasticsearch","Args": {"nodeUris": "http://localhost:9200","indexFormat": "myapp-logs-{0:yyyy.MM.dd}","autoRegisterTemplate": true,"autoRegisterTemplateVersion": "ESv7","numberOfReplicas": 1,"numberOfShards": 5,"batchPostingLimit": 50,"period": "00:00:05","failureCallback": "e => Console.WriteLine(\"Unable to submit event to Elasticsearch: \" + e.Message)"}},{"Name": "GrafanaLoki","Args": {"uri": "http://localhost:3100/loki/api/v1/push","batchPostingLimit": 50,"period": "00:00:05","labels": "{\"Application\":\"MyAbpApp\",\"Environment\":\"${env:ASPNETCORE_ENVIRONMENT}\"}"}}],"Enrich": ["FromLogContext","WithMachineName","WithThreadId","WithEnvironmentName"]}
}
- Using:要加載的 Sink 包列表,包括 Console、File、Seq、Elasticsearch、Grafana Loki。
- MinimumLevel:全局最低日志級別及對各命名空間的 Override。
- WriteTo:各個輸出通道的配置:
- 📟
Console
:控制臺直接輸出,適合開發與容器模式下采集標準輸出。 - 📂
File
:寫入本地文件,rollingInterval: Day
按天滾動;retainedFileCountLimit: 14
最多保留 14 天日志;fileSizeLimitBytes: 100MB
,超出則滾動。 - 📊
Seq
:訪問地址為http://localhost:5341
的本地 Seq 服務。 - 🔍
Elasticsearch
:連接到http://localhost:9200
,索引名稱按天命名;批量發送 50 條 / 5 秒;失敗回調打印到控制臺。 - 📈
GrafanaLoki
:連接本地 Loki(端口 3100)并打上標簽,一旦在 Grafana 中按標簽篩選,方便定位。
- 📟
- Enrich:注入常見上下文字段(如 TraceId、MachineName、ThreadId、Environment)。
Tip:如需在開發/生產環境區分配置,可分別在
appsettings.Development.json
和appsettings.Production.json
中覆蓋MinimumLevel
與WriteTo
節點。例如:
- Development:將
MinimumLevel.Default
設置為Debug
,僅啟用 Console Sink;- Production:將
MinimumLevel.Default
設置為Information
,啟用 File、Seq、Elasticsearch、Loki Sink;并關閉 Console 輸出以減少 I/O 壓力。
5. Program.cs 全局日志初始化 💻
在 Program.cs
文件里,使用 Serilog 提供的“Bootstrap Logger”+“配置讀取”模版,示例如下:
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Serilog;
using Volo.Abp.Serilog;namespace MyAbpApp
{public class Program{public static async Task Main(string[] args){// 1. 創建 Bootstrap Logger(只輸出到 Console,用于捕獲最早期的日志)Log.Logger = new LoggerConfiguration().MinimumLevel.Override("Microsoft", Serilog.Events.LogEventLevel.Warning).WriteTo.Console().CreateBootstrapLogger();try{// 2. 構建 Host 并讀取 appsettings.json 中的 Serilog 配置var builder = WebApplication.CreateBuilder(args);builder.Host.UseSerilog((ctx, services, config) =>{config.ReadFrom.Configuration(ctx.Configuration) // 讀取 appsettings.json 的 Serilog 設置.ReadFrom.Services(services) // 讀取 DI 容器中注冊的 ILogEventEnricher.Enrich.FromLogContext() // 從 LogContext 拉取附加屬性.Enrich.WithProperty("Application", "MyAbpApp").Enrich.WithProperty("Environment", ctx.HostingEnvironment.EnvironmentName);});// 3. 添加 ABP 應用及所需模塊builder.Services.AddApplication<MyAbpAppModule>();// 4. 構建應用var app = builder.Build();// 5. 注入 Serilog Enrichers(TraceId、UserId、TenantId 等)app.UseAbpSerilogEnrichers();// 6. 初始化 ABP 模塊(包括 Routing、Authentication、Authorization 等)await app.InitializeApplicationAsync();// 7. 啟動 HTTP 服務并阻塞await app.RunAsync();}catch (Exception ex){// 8. 捕獲主機啟動時的異常并記錄 Fatal 日志Log.Fatal(ex, "Application start-up failed");Environment.ExitCode = 1;}finally{// 9. 應用退出時刷新并關閉日志Log.CloseAndFlush();}}}
}
5.1 代碼流程圖
- 該圖展示了
Program.cs
中從Main
開始,到最終應用啟動并接收請求,日志如何一步步初始化并插入上下文的執行路徑。
6. ABP 模塊注冊 🏗
在 MyAbpAppModule.cs
中聲明所需的依賴模塊,例如:
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Serilog;
using Volo.Abp;
using Volo.Abp.AspNetCore.Mvc;
using Volo.Abp.AspNetCore.Serilog;
using Volo.Abp.BackgroundWorkers;
using Volo.Abp.DistributedEventBus;
using Volo.Abp.Modularity;
using Volo.Abp.TenantManagement;namespace MyAbpApp
{[DependsOn(typeof(AbpAspNetCoreSerilogModule), // Serilog 集成模塊typeof(AbpAspNetCoreMvcModule), // MVC/Web API 基礎模塊typeof(AbpBackgroundWorkersModule), // 后臺任務模塊(示例)typeof(AbpDistributedEventBusModule), // 分布式事件總線模塊(示例)typeof(AbpTenantManagementModule) // 多租戶管理模塊(如需多租戶場景))]public class MyAbpAppModule : AbpModule{public override void OnApplicationInitialization(ApplicationInitializationContext context){var logger = context.ServiceProvider.GetRequiredService<ILogger<MyAbpAppModule>>();logger.LogInformation("🔥 ABP 模塊已啟動!");}public override void OnApplicationShutdown(ApplicationShutdownContext context){var logger = context.ServiceProvider.GetRequiredService<ILogger<MyAbpAppModule>>();logger.LogInformation("💤 ABP 模塊已關閉!");}}
}
- 說明:
AbpAspNetCoreSerilogModule
會將 ABP 框架內部的日志重定向到 Serilog。- 如果您的業務需要后臺任務或分布式事件,請在
DependsOn
一并引入對應模塊。 - 如果項目啟用多租戶,一定要引入
AbpTenantManagementModule
等租戶相關模塊。 - 在
OnApplicationInitialization
中寫一條“ABP 模塊已啟動”日志方便確認模塊加載成功;在OnApplicationShutdown
中寫一條“ABP 模塊已關閉”日志方便確認優雅退出。
7. 上下文信息添加 🧩
7.1 UseAbpSerilogEnrichers() 自動插入
在 Program.cs
中調用 app.UseAbpSerilogEnrichers();
后,Serilog 會自動注入以下常見上下文字段到每條日志中:
- 🔗 TraceId:分布式鏈路追蹤 ID(需配合 OpenTelemetry/Jaeger 等鏈路追蹤服務)
- 👤 UserId、UserName:當前登錄用戶信息(需在請求上下文中有認證信息)
- 🏷? TenantId:當前多租戶系統的租戶 ID(如啟用了多租戶模塊)
- 🖥? MachineName:主機名稱(適用于集群調試)
- 🧵 ThreadId:線程 ID(方便定位多線程日志)
- 🌐 Environment:部署環境(如 Development、Production)
注意:
UseAbpSerilogEnrichers()
必須放在UseRouting()
之后、UseAuthentication()
之前;如果您使用 ABP 腳手架模板,則無需手動調用中間件順序,InitializeApplicationAsync()
已自動處理。
7.2 LogContext.PushProperty 自定義屬性
在業務代碼里,如果需要在單次請求或某個操作中,對特定實體(如 Order)添加自定義屬性,可以使用 LogContext.PushProperty(...)
,示例如下:
using Serilog;
using Serilog.Context;
using Microsoft.Extensions.Logging;public class OrderService
{private readonly ILogger<OrderService> _logger;public OrderService(ILogger<OrderService> logger){_logger = logger;}public void ProcessOrder(Order order){// 在這個 using 塊內,所有日志都會帶上 OrderId 屬性using (LogContext.PushProperty("OrderId", order.Id)){_logger.LogInformation("? Processing order {@Order}", order);// … 其它業務邏輯_logger.LogWarning("?? Order {@Order} took too long to process", order);}}
}
- 說明:
LogContext.PushProperty("OrderId", order.Id)
會在當前上下文中將 OrderId 寫入所有后續日志。- 使用
{@Order}
這種序列化寫法,會把Order
對象的所有字段寫到 JSON 中,方便在 ES/Kibana/Loki 中查看結構化數據。 - 在控制臺或日志平臺中,該條日志會像:
{"Timestamp": "2025-05-31T20:00:00.0000000Z","Level": "Information","MessageTemplate": "? Processing order {@Order}","Properties": {"OrderId": 12345,"Order": { "Id": 12345, "Amount": 99.99, "CustomerId": 67890 },"TraceId": "abcdef1234567890","UserId": 42,"TenantId": 1,"MachineName": "server01","ThreadId": 12,"Environment": "Production"} }
8. 對接平臺:Seq & ELK & Grafana Loki 🌐
8.1 部署 Seq(推薦開發階段)
docker run -d \-p 5341:80 \-v seq_data:/data \datalust/seq
- 說明:
-p 5341:80
將容器 80 端口映射到宿主機 5341 端口;訪問地址為 http://localhost:5341-v seq_data:/data
掛載一個 Docker 卷,用于持久化 Seq 數據;容器重啟后數據依然保留。- 啟動后,可在 Seq Web 界面里創建 Dashboard,使用篩選條件(如
@l = "Error"
或@t >= "2025-05-01"
)進行日志定位。
8.2 部署 ELK(推薦生產環境)
創建一個 docker-compose.yml
文件,內容如下:
version: '3.7'volumes:es_data:kibana_data:services:elasticsearch:image: docker.elastic.co/elasticsearch/elasticsearch:7.17.0container_name: elasticsearchenvironment:- node.name=es-node-1- discovery.type=single-node- xpack.security.enabled=false- ES_JAVA_OPTS=-Xms1g -Xmx1gvolumes:- es_data:/usr/share/elasticsearch/dataports:- 9200:9200kibana:image: docker.elastic.co/kibana/kibana:7.17.0container_name: kibanaenvironment:- ELASTICSEARCH_HOSTS=http://elasticsearch:9200volumes:- kibana_data:/usr/share/kibana/dataports:- 5601:5601depends_on:- elasticsearch
運行:
docker-compose up -d
- 說明:
- Elasticsearch
- 掛載
es_data
卷用以持久化數據; - 設置 JVM 堆大小為 1G(
ES_JAVA_OPTS=-Xms1g -Xmx1g
),生產環境可根據節點內存調整; - 關閉 X-Pack 安全認證方便本地測試;
- 掛載
- Kibana
- 指定
ELASTICSEARCH_HOSTS
以連接 Elasticsearch; - 掛載
kibana_data
用于持久化 Kibana 配置; - 啟動后訪問 http://localhost:5601 即可登錄;
- 指定
- Elasticsearch
8.2.1 在 Kibana 中創建 Index Pattern 🔍
- 打開 Kibana → 左側菜單 “Management” → “Index Patterns” → “Create index pattern”。
- 在 “Index pattern name” 中輸入
myapp-logs-*
,點擊 “Next step”。 - 選擇時間字段(如
@timestamp
)后點擊 “Create index pattern”。 - 在 “Discover” 頁面就能看到所有符合
myapp-logs-2025.05.XX
格式的索引,日志字段會以 JSON 形式展示,可以按字段進行過濾、排序、聚合。
提示:為了更直觀地展示 Kibana 中的日志分組和聚合效果,可以創建一個簡單的 Dashboard,比如“日志級別分布圖”、“每小時錯誤請求數統計”等。這樣在生產環境排查問題時,更加快捷。
8.3 對接 Grafana Loki(可選)🔗
如果您使用 Grafana Loki 作為日志收集平臺,可參考以下步驟:
-
部署 Loki
- 推薦使用 Loki 官方提供的
docker-compose.yml
或者 Helm Chart。 - 簡單示例(僅做測試用):
version: '3.7' services:loki:image: grafana/loki:2.7.1container_name: lokicommand: -config.file=/etc/loki/local-config.yamlports:- 3100:3100promtail:image: grafana/promtail:2.7.1container_name: promtailvolumes:- /var/log:/var/log- ./promtail-config.yaml:/etc/promtail/config.yamlcommand: -config.file=/etc/promtail/config.yaml
- 在
promtail-config.yaml
中,配置讀取應用輸出到標準輸出(Console Sink)的日志,并推送到 Loki。 - 在 Grafana 中添加 Loki 數據源,創建 Dashboard 時選擇 Loki 數據源即可查詢
MyAbpApp
相關日志。
- 推薦使用 Loki 官方提供的
-
GrafanaLoki Sink 配置示例
已在第四節的appsettings.json
中給出完整配置。再次回顧重點字段:{"Name": "GrafanaLoki","Args": {"uri": "http://localhost:3100/loki/api/v1/push","batchPostingLimit": 50,"period": "00:00:05","labels": "{\"Application\":\"MyAbpApp\",\"Environment\":\"Production\"}"} }
- uri:Loki 的 Push API 地址;
- batchPostingLimit、period:控制批量推送頻率;
- labels:為日志打上標簽,便于在 Grafana 中按標簽篩選。
注意:在容器化環境下,將
uri
指向 Loki Service(如http://loki:3100/loki/api/v1/push
),不要使用localhost
。
9. 總結 📋
-
🚀 性能:
- 支持
buffered
與flushToDiskInterval
控制文件寫入 IO,平衡延遲與吞吐; - Elasticsearch Sink 中可配置
batchPostingLimit
和period
,避免過于頻繁的小批量請求。
- 支持
-
📈 規模:
- 支持日志按天滾動(
rollingInterval: Day
)和限制單文件大小(fileSizeLimitBytes
),通過retainedFileCountLimit
最多保留 14 天歷史,防止磁盤耗盡。
- 支持日志按天滾動(
-
🎨 可視化:
- 結構化 JSON 日志讓 Seq/Kibana/Loki 能以字段形式展示,可按字段、日期、級別精準搜索和聚合統計,大幅提升故障排查效率。
-
🔧 可配置:
- 通過
appsettings.Development.json
與appsettings.Production.json
差異化配置,可在不同環境靈活切換最小日志級別與輸出通道。 - 支持自定義 Enrichers 和 Sink,能夠將任意業務上下文(如 OrderId、ProductId、TraceId 等)注入到日志中。
- 通過
-
🐳 容器化注意:
- 如果服務跑在 Docker 或 Kubernetes,推薦保留 Console Sink 輸出,通過容器平臺日志采集(如 Fluentd、Filebeat、堆棧驅動)統一收集。
- 若仍需寫入日志文件,請確保掛載 Volume 以避免容器磁盤耗盡。
- 根據目標平臺(Seq、Elasticsearch、Loki)是否開啟安全認證,需在對應 Sink 的
Args
中添加憑證信息(用戶名、密碼或 API Key)。
📎 推薦閱讀 📚
- ABP Serilog 模塊文檔
- Serilog 官方文檔
- Seq 可視化平臺
- Grafana Loki 文檔