在 ABP VNext 中集成 Serilog:打造可观测、结构化日志系统

发布于:2025-06-01 ⋅ 阅读:(24) ⋅ 点赞:(0)

🚀 在 ABP VNext 中集成 Serilog:打造可观测、结构化日志系统



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 流程图示例

启动项目
读取 appsettings.json 中 Serilog 配置
Program.cs 中 UseSerilog 初始化 Logger
创建 ABP 应用并调用 UseAbpSerilogEnrichers
HTTP 请求进入 → Enrichers 插入上下文
业务代码调用 ILogger 输出日志
Serilog 将日志写入 Console/File/Seq/ES/Loki
  • 通过 “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.jsonappsettings.Production.json 中覆盖 MinimumLevelWriteTo 节点。例如:

  • 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 代码流程图

Main 方法开始
创建 Bootstrap Logger
构建 WebHostBuilder
UseSerilog 读取配置并初始化 Logger
Services.AddApplication
Build 应用
UseAbpSerilogEnrichers 注入上下文
InitializeApplicationAsync 初始化 ABP 模块
RunAsync 启动 Kestrel 并监听请求
请求到达 → Enrichers 插入 TraceId 等
业务代码调用 ILogger 输出日志 → Serilog 写入 Sink
  • 该图展示了 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 模块已关闭!");
        }
    }
}
  • 说明
    1. AbpAspNetCoreSerilogModule 会将 ABP 框架内部的日志重定向到 Serilog。
    2. 如果您的业务需要后台任务或分布式事件,请在 DependsOn 一并引入对应模块。
    3. 如果项目启用多租户,一定要引入 AbpTenantManagementModule 等租户相关模块。
    4. 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);
        }
    }
}
  • 说明
    1. LogContext.PushProperty("OrderId", order.Id) 会在当前上下文中将 OrderId 写入所有后续日志。
    2. 使用 {@Order} 这种序列化写法,会把 Order 对象的所有字段写到 JSON 中,方便在 ES/Kibana/Loki 中查看结构化数据。
    3. 在控制台或日志平台中,该条日志会像:
      {
        "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
  • 说明
    1. -p 5341:80 将容器 80 端口映射到宿主机 5341 端口;访问地址为 http://localhost:5341
    2. -v seq_data:/data 挂载一个 Docker 卷,用于持久化 Seq 数据;容器重启后数据依然保留。
    3. 启动后,可在 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.0
    container_name: elasticsearch
    environment:
      - node.name=es-node-1
      - discovery.type=single-node
      - xpack.security.enabled=false
      - ES_JAVA_OPTS=-Xms1g -Xmx1g
    volumes:
      - es_data:/usr/share/elasticsearch/data
    ports:
      - 9200:9200

  kibana:
    image: docker.elastic.co/kibana/kibana:7.17.0
    container_name: kibana
    environment:
      - ELASTICSEARCH_HOSTS=http://elasticsearch:9200
    volumes:
      - kibana_data:/usr/share/kibana/data
    ports:
      - 5601:5601
    depends_on:
      - elasticsearch

运行:

docker-compose up -d
  • 说明
    1. Elasticsearch
      • 挂载 es_data 卷用以持久化数据;
      • 设置 JVM 堆大小为 1G(ES_JAVA_OPTS=-Xms1g -Xmx1g),生产环境可根据节点内存调整;
      • 关闭 X-Pack 安全认证方便本地测试;
    2. Kibana
      • 指定 ELASTICSEARCH_HOSTS 以连接 Elasticsearch;
      • 挂载 kibana_data 用于持久化 Kibana 配置;
      • 启动后访问 http://localhost:5601 即可登录;
8.2.1 在 Kibana 中创建 Index Pattern 🔍
  1. 打开 Kibana → 左侧菜单 “Management” → “Index Patterns” → “Create index pattern”。
  2. 在 “Index pattern name” 中输入 myapp-logs-*,点击 “Next step”。
  3. 选择时间字段(如 @timestamp)后点击 “Create index pattern”。
  4. 在 “Discover” 页面就能看到所有符合 myapp-logs-2025.05.XX 格式的索引,日志字段会以 JSON 形式展示,可以按字段进行过滤、排序、聚合。

提示:为了更直观地展示 Kibana 中的日志分组和聚合效果,可以创建一个简单的 Dashboard,比如“日志级别分布图”、“每小时错误请求数统计”等。这样在生产环境排查问题时,更加快捷。

8.3 对接 Grafana Loki(可选)🔗

如果您使用 Grafana Loki 作为日志收集平台,可参考以下步骤:

  1. 部署 Loki

    • 推荐使用 Loki 官方提供的 docker-compose.yml 或者 Helm Chart。
    • 简单示例(仅做测试用):
      version: '3.7'
      services:
        loki:
          image: grafana/loki:2.7.1
          container_name: loki
          command: -config.file=/etc/loki/local-config.yaml
          ports:
            - 3100:3100
      
        promtail:
          image: grafana/promtail:2.7.1
          container_name: promtail
          volumes:
            - /var/log:/var/log
            - ./promtail-config.yaml:/etc/promtail/config.yaml
          command: -config.file=/etc/promtail/config.yaml
      
    1. promtail-config.yaml 中,配置读取应用输出到标准输出(Console Sink)的日志,并推送到 Loki。
    2. 在 Grafana 中添加 Loki 数据源,创建 Dashboard 时选择 Loki 数据源即可查询 MyAbpApp 相关日志。
  2. 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 地址;
    • batchPostingLimitperiod:控制批量推送频率;
    • labels:为日志打上标签,便于在 Grafana 中按标签筛选。

注意:在容器化环境下,将 uri 指向 Loki Service(如 http://loki:3100/loki/api/v1/push),不要使用 localhost


9. 总结 📋

  • 🚀 性能

    • 支持 bufferedflushToDiskInterval 控制文件写入 IO,平衡延迟与吞吐;
    • Elasticsearch Sink 中可配置 batchPostingLimitperiod,避免过于频繁的小批量请求。
  • 📈 规模

    • 支持日志按天滚动(rollingInterval: Day)和限制单文件大小(fileSizeLimitBytes),通过 retainedFileCountLimit 最多保留 14 天历史,防止磁盘耗尽。
  • 🎨 可视化

    • 结构化 JSON 日志让 Seq/Kibana/Loki 能以字段形式展示,可按字段、日期、级别精准搜索和聚合统计,大幅提升故障排查效率。
  • 🔧 可配置

    • 通过 appsettings.Development.jsonappsettings.Production.json 差异化配置,可在不同环境灵活切换最小日志级别与输出通道。
    • 支持自定义 Enrichers 和 Sink,能够将任意业务上下文(如 OrderId、ProductId、TraceId 等)注入到日志中。
  • 🐳 容器化注意

    • 如果服务跑在 Docker 或 Kubernetes,推荐保留 Console Sink 输出,通过容器平台日志采集(如 Fluentd、Filebeat、堆栈驱动)统一收集。
    • 若仍需写入日志文件,请确保挂载 Volume 以避免容器磁盘耗尽。
    • 根据目标平台(Seq、Elasticsearch、Loki)是否开启安全认证,需在对应 Sink 的 Args 中添加凭证信息(用户名、密码或 API Key)。

📎 推荐阅读 📚



网站公告

今日签到

点亮在社区的每一天
去签到