.NET Serilog

概述

本文档旨在说明如何配置 Serilog,以实现在运行时动态路由日志,并提供企业生产环境中关键参数的详细配置说明。

  • 核心场景: 你拥有一个静态工具类(LogUtil),你希望在调用它时传递一个"标签"(如功能名称 featureName),Serilog 根据这个标签将该条日志写入到特定的文件中。

  • 核心机制:

    1. 代码 (C#): 使用 Serilog.Context.LogContextfeatureName 动态"附加"到日志条目上。
    2. 配置 (JSON): 使用 Serilog.Filters.Expressions 读取这个附加的属性,并配置 Logger 规则将日志路由到对应的 File Sink。
  • 文档内容: 本文将详细分解 File Sink(用于文件定制、大小限制、保留策略)和 Expressions Filter(用于路由)的配置参数。


1. 基础核心:ASP.NET Core 集成

  • 功能说明:

    让 Serilog 接管 .NET 默认的 ILogger,并使其能够从 appsettings.json 文件中读取所有配置。

  • 所需模块包 (NuGet):

    • Serilog.AspNetCore (此包会自动包含 Serilog 核心库和 Serilog.Settings.Configuration 配置库)
  • 配置实现 (C# - Program.cs):

    这是启动 Serilog 的标准"模板代码",用于引导和注册 Serilog。

    C# 复制代码
    using Serilog;
    
    // 1. 在构建 WebApplicationBuilder 之前,配置一个临时的静态 Logger
    //    这能确保在 DI 容器构建失败或应用启动早期阶段的日志能被捕获
    Log.Logger = new LoggerConfiguration()
        .ReadFrom.Configuration(builder.Configuration) // 尝试从配置中读取
        .Enrich.FromLogContext() // (关键) 确保 LogContext 在引导时也可用
        .CreateBootstrapLogger(); 
    
    try
    {
        var builder = WebApplication.CreateBuilder(args);
    
        // 2. (关键) 将 Serilog 注册到 .NET 主机
        //    这将替换掉所有 .NET 默认的 LoggerProvider
        builder.Host.UseSerilog((context, services, configuration) => configuration
            .ReadFrom.Configuration(context.Configuration) // 读取 appsettings.json
            .ReadFrom.Services(services)                   // 从 DI 读取
            .Enrich.FromLogContext()); // (关键) 确保 LogContext 能被 DI Logger 识别
    
        // ... (添加你的 Services) ...
    
        var app = builder.Build();
    
        // 3. (可选但推荐) 自动记录所有 HTTP 请求
        //    详细配置参见本文档第 6 节
        app.UseSerilogRequestLogging();
    
        // ... (配置你的中间件) ...
    
        Log.Information("应用程序启动中...");
        app.Run();
    }
    catch (Exception ex)
    {
        Log.Fatal(ex, "应用程序启动失败");
    }
    finally
    {
        // 4. 确保在应用退出时,将缓冲区中的日志刷入文件
        Log.CloseAndFlush();
    }

2. 核心功能:动态上下文 (LogContext)

  • 功能说明:

    在代码中创建一个静态工具类 LogUtil,它能接收一个 featureName 字符串,并将其作为属性附加到**当前线程(或异步上下文)**的后续日志中。

  • 所需模块包 (NuGet):

    • Serilog (已由 Serilog.AspNetCore 自动引入)
  • 配置实现 (C# - LogUtil.cs):

    C# 复制代码
    using Serilog;
    using Serilog.Context; // 1. 必须引入 LogContext 命名空间
    
    public static class LogUtil
    {
        // 你可以根据需要添加更多参数,如 LogEventLevel
        public static void Write(string featureName, string message, params object[] propertyValues)
        {
            // 2. (关键) PushProperty 会将 "FeatureName" 属性添加到
            //    当前 LogContext 中。
            //    using 块确保了该属性只在块执行期间有效,
            //    并在退出时自动移除,防止"污染"后续日志。
            using (LogContext.PushProperty("FeatureName", featureName))
            {
                // 3. 使用全局静态 Logger 写入日志 (使用结构化日志)
                //    这条日志现在会自动携带 { FeatureName: "..." } 属性
                Log.Information(message, propertyValues);
            }
        }
    }
  • 配置实现 (C# - Controller 调用示例):

    C# 复制代码
    [ApiController]
    public class OrdersController : ControllerBase
    {
        [HttpGet]
        public IActionResult GetOrder(int id)
        {
            // ... 业务逻辑 ...
    
            // 运行时动态指定 "Orders" 标签,并使用结构化日志
            LogUtil.Write("Orders", "已完成对订单 {OrderId} 的查询。", id);
    
            return Ok();
        }
    }

3. 核心功能:表达式过滤器 (Serilog.Filters.Expressions)

  • 功能说明: 提供了强大的 SQL-Like 语法来过滤日志,这是实现动态路由的核心。

  • 所需模块包 (NuGet): Serilog.Filters.Expressions

  • Args (参数) 详细解析:

    在 Filter 配置中,expression (表达式) 是关键。

    JSON 复制代码
    "Filter": [
      {
        "Name": "ByIncludingOnly", // 或 ByExcluding (排除)
        "Args": {
          // 示例:只包含 FeatureName = 'Orders' 并且级别为 Warning 以上的日志
          "expression": "FeatureName = 'Orders' and Level >= LogLevel.Warning"
        }
      }
    ]
  • 常用表达式函数和语法:

示例表达式 说明
FeatureName = 'Orders' (你的方案) 属性等于 'Orders' (注意:字符串用单引号)。
FeatureName is not null (你的方案) FeatureName 属性存在。
Level >= LogLevel.Warning 过滤级别。可以是 Verbose, Debug, Information, Warning, Error, Fatal
StartsWith(SourceContext, 'MyProject.Features') (方案 A) SourceContext 以...开头。
Contains(Message, 'timeout') 日志消息体 (Message) 包含 "timeout" 字符串。
... or ... / ... and ... 复杂的布尔逻辑。
StatusCode = 200 (用于 HTTP 请求日志) 过滤 HTTP 状态码。
Elapsed > 1000 (用于 HTTP 请求日志) 过滤耗时超过 1000ms 的请求。

4. 核心功能:文件日志 (Serilog.Sinks.File) - 详细参数

这是日志配置的核心,用于自定义文件名、大小限制和保留策略

  • 所需模块包 (NuGet): Serilog.Sinks.File

  • 配置实现 (appsettings.json):

    下面是一个"全功能"的文件日志配置示例,我们将在下面详细分解它的 Args

    JSON 复制代码
    {
      "Serilog": {
        "WriteTo": [
          {
            "Name": "File",
            "Args": {
              "path": "logs/app_logs/my-log-.log",
              "rollingInterval": "Day",
              "fileSizeLimitBytes": 104857600,
              "retainedFileCountLimit": 30,
              "shared": true,
              "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}",
              "buffered": true,
              "flushToDiskInterval": "00:00:05",
              "restrictedToMinimumLevel": "Debug"
            }
          }
        ]
      }
    }

Args (参数) 详细解析:

a. 自定义文件名 (path) 和滚动 (rollingInterval)

  • path (路径和文件名模板):

    • 示例: "logs/app_logs/my-log-.log"

    • 说明: path 决定了文件名和滚动方式。路径中最后一个破折号 (-) 是一个占位符

    • rollingInterval 会决定这个占位符被什么内容替换:

      • "rollingInterval": "Day" (默认) -> my-log-20251116.log
      • "rollingInterval": "Hour" -> my-log-2025111619.log (19点)
    • 无占位符: 如果路径是 "logs/my-log.log" (没有 -),它将永远写入这一个文件,直到达到大小限制。

b. 文件大小限制 (fileSizeLimitBytes)

  • fileSizeLimitBytes (文件大小限制 - 字节):

    • 示例: 104857600 (这是 100 MB)

    • 说明: 当一个日志文件(例如 my-log-20251116.log)达到这个大小时,Serilog 会自动创建一个新文件,并

      在文件名后加上 _001, _002...

    • 默认值: null (无限制)。在生产环境中,强烈建议设置此值!

c. 文件保留策略 (retainedFileCountLimit)

  • retainedFileCountLimit (保留文件数限制):

    • 示例: 30

    • 说明: Serilog 会自动清理旧的 日志文件,只保留最近的 N 个文件。

    • 重要: 这个"文件"是基于滚动策略的。

      • 如果 rollingInterval 是 "Day",30 就意味着保留最近 30 天的日志
      • 如果 rollingInterval 是 "Hour",30 就意味着保留最近 30 小时的日志
    • 默认值: null (永久保留)。在生产环境中,强烈建议设置此值!

d. 性能与并发 (shared, buffered, flushToDiskInterval)

  • shared (共享访问):

    • 示例: true
    • 说明: (解决你的"文件被占用"需求) 设置为 true 时,Serilog 会在每次写入后释放文件锁,允许其他进程(和你)读取该文件。
  • buffered (缓冲区写入):

    • 示例: true (默认值)
    • 说明: 为了性能,Serilog 会先将日志写入内存缓冲区,而不是每次都访问磁盘。
  • flushToDiskInterval (缓冲区刷盘间隔):

    • 示例: "00:00:05" (5 秒)
    • 说明:bufferedtrue 时,此设置决定缓冲区多久被强制写入磁盘。5 秒是一个合理的折中,可在应用崩溃时最多只丢失 5 秒的日志。

e. 日志格式 (outputTemplate)

  • outputTemplate (输出模板):

    • 示例: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}"

    • 说明: 控制日志在 .txt 文件中的外观

    • 常用标记:

      • {Timestamp:yyyy-MM-dd ...}: 时间戳(可自定义格式)。
      • {Level:u3}: 日志级别(u3 表示大写的3个字母,如 INF, WRN, ERR)。
      • {SourceContext}: 日志来源(通常是类名)。
      • {FeatureName}: 你用 LogContext 添加的自定义属性
      • {Message:lj}: 你记录的消息体(lj 表示换行和 JSON 字符串化)。
      • {NewLine}: 换行符。
      • {Exception}: 异常堆栈信息。

5. (可选) 远程日志 (Serilog.Sinks.Seq) - 生产环境

  • 功能说明: 将日志发送到 Seq 服务器,用于集中查看和分析。

  • 所需模块包 (NuGet): Serilog.Sinks.Seq

  • Args (参数) 详细解析:

    JSON 复制代码
    {
      "Name": "Seq",
      "Args": {
        "serverUrl": "http://my-seq-server:5341",
        "apiKey": "YOUR_API_KEY_IF_NEEDED",
        "restrictedToMinimumLevel": "Information",
        "bufferBaseFilename": "logs/seq_buffer/buffer"
      }
    }
    • serverUrl: (必需) 你的 Seq 服务器地址。

    • restrictedToMinimumLevel: (推荐)

      • 说明: 仅针对此 Sink 的级别过滤。
      • 示例: Information。这意味着 Debug 级别的日志不会被发送 到 Seq(节省网络流量和存储),但如果你的本地文件 Sink 设置了 Debug,它仍然会被写入本地文件。
    • bufferBaseFilename: (生产环境关键特性)

      • 说明: 持久化缓冲区 。如果你的应用无法连接到 Seq 服务器(网络闪断),Serilog 会将日志缓存到本地磁盘 (路径为 logs/seq_buffer/)。一旦连接恢复,它会自动将缓存的日志重新发送到 Seq,确保日志的高可用性

6. (可选) HTTP 请求日志 (UseSerilogRequestLogging) - C# 配置

  • 功能说明: 自动记录所有进入 ASP.NET Core 的 HTTP 请求。

  • 配置实现 (C# - Program.cs):

    app.UseSerilogRequestLogging() 方法接受一个配置委托 (delegate),允许你进行精细控制。

    C# 复制代码
    // 在 Program.cs 中 app.Build() 之后
    app.UseSerilogRequestLogging(options =>
    {
        // 自定义日志级别
        options.GetLevel = (httpContext, elapsed, ex) =>
        {
            // 如果是 404 (Not Found),降级为 Information,避免噪音
            if (httpContext.Response.StatusCode == 404)
            {
                return Serilog.Events.LogEventLevel.Information;
            }
            // 其他 4xx 错误 (如 401, 403) 仍为 Warning
            if (httpContext.Response.StatusCode >= 400 && httpContext.Response.StatusCode < 500)
            {
                return Serilog.Events.LogEventLevel.Warning;
            }
            // 5xx 错误或异常为 Error
            if (ex != null || httpContext.Response.StatusCode >= 500)
            {
                return Serilog.Events.LogEventLevel.Error;
            }
            // 默认情况 (如 2xx, 3xx) 为 Information
            return Serilog.Events.LogEventLevel.Information;
        };
    
        // 为所有请求日志添加自定义属性
        options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
        {
            // 从 HttpContext 中获取并添加 UserID
            var userId = httpContext.User.Identity?.Name;
            if (userId != null)
            {
                diagnosticContext.Set("UserId", userId);
            }
            // 添加一个相关 ID,用于追踪
            diagnosticContext.Set("CorrelationId", httpContext.TraceIdentifier);
        };
    });

7. 综合配置:完整的 appsettings.json 示例

这是一个将动态路由生产级参数相结合的完整示例:

JSON 复制代码
{
  "Serilog": {
    "Using": [
      "Serilog.Sinks.Console",
      "Serilog.Sinks.File",
      "Serilog.Filters.Expressions",
      "Serilog.Sinks.Seq"
    ],
    "MinimumLevel": { // 全局最小级别 (应用启动时)
      "Default": "Debug",
      "Override": {
        "Microsoft": "Warning", // 过滤掉 .NET Core 的冗余日志
        "Microsoft.Hosting.Lifetime": "Information",
        "System": "Warning"
      }
    },
    "Enrich": [
      "FromLogContext", // 必须,用于读取 FeatureName
      "WithMachineName",
      "WithThreadId"
    ],
    "WriteTo": [
      // --- (可选) DEV 环境输出到控制台 ---
      { 
        "Name": "Console", 
        "Args": { 
          "restrictedToMinimumLevel": "Information" 
        } 
      },

      // --- 路由规则 1: "Orders" 功能 ---
      {
        "Name": "Logger",
        "Args": {
          "configureLogger": {
            "Filter": [ { "Name": "ByIncludingOnly", "Args": { "expression": "FeatureName = 'Orders'" } } ],
            "WriteTo": [
              {
                "Name": "File",
                "Args": {
                  "path": "logs/features/orders-.log",
                  "rollingInterval": "Day",
                  "shared": true,
                  "fileSizeLimitBytes": 10485760, // 10MB
                  "retainedFileCountLimit": 7,     // 保留 7 天
                  "outputTemplate": "{Timestamp:HH:mm:ss} [{Level:u3}] {Message:lj}{NewLine}{Exception}"
                }
              }
            ]
          }
        }
      },

      // --- 路由规则 2: "Users" 功能 ---
      {
        "Name": "Logger",
        "Args": {
          "configureLogger": {
            "Filter": [ { "Name": "ByIncludingOnly", "Args": { "expression": "FeatureName = 'Users'" } } ],
            "WriteTo": [
              {
                "Name": "File",
                "Args": {
                  "path": "logs/features/users-.log",
                  "rollingInterval": "Day",
                  "shared": true,
                  "fileSizeLimitBytes": 10485760, // 10MB
                  "retainedFileCountLimit": 7      // 保留 7 天
                }
              }
            ]
          }
        }
      },

      // --- 路由规则 3: 兜底日志 (所有其他日志) ---
      {
        "Name": "Logger",
        "Args": {
          "configureLogger": {
            "Filter": [ { "Name": "ByExcluding", "Args": { "expression": "FeatureName is not null" } } ],
            "WriteTo": [
              {
                "Name": "File",
                "Args": {
                  "path": "logs/application-all-.log",
                  "rollingInterval": "Day",
                  "shared": true,
                  "fileSizeLimitBytes": 104857600, // 100MB (兜底日志可以大一些)
                  "retainedFileCountLimit": 14,      // 保留 14 天
                  "restrictedToMinimumLevel": "Information" // 兜底文件不记录 Debug
                }
              }
            ]
          }
        }
      },

      // --- 路由规则 4: PRD 环境发送到 Seq ---
      {
        "Name": "Seq",
        "Args": {
          "serverUrl": "http://my-seq-server:5341",
          "restrictedToMinimumLevel": "Information", // 不发送 Debug 日志
          "bufferBaseFilename": "logs/seq_buffer/buffer" // 启用高可用性缓存
        }
      }
    ]
  }
}

8. 避坑建议、最佳实践与官方文档

⚠️ 避坑建议 (Potholes)

  1. 忘记 Enrich.FromLogContext():

    这是最常见的错误。如果你在 appsettings.json 或 Program.cs 中忘记了 .Enrich.FromLogContext(),FeatureName 属性将永远为 null,导致过滤器 FeatureName = 'Orders' 匹配失败,所有日志都将进入"兜底文件"。

  2. LogContext 的异步/线程问题:

    LogContext.PushProperty 是基于 AsyncLocal 实现的。这意味着它在 await 之间是安全的。但你必须使用 using 块来确保属性被正确移除。否则,该属性可能会"泄露"到当前请求处理完毕后的其他日志中(例如,在线程池线程被重用时)。

  3. "魔法字符串" (Magic Strings) 依赖:

    你的代码 LogUtil.Write("Orders", ...) 和配置 "expression": "FeatureName = 'Orders'" 之间是硬编码的字符串依赖。如果有人在代码中改成了 "Order",路由就会失效。

✨ 最佳实践 (Best Practices)

  1. 用常量管理标签:

    为了解决"魔法字符串"问题,在项目中创建一个静态常量类。

    C# 复制代码
    public static class LogFeatures
    {
        public const string Orders = "Orders";
        public const string Users = "Users";
    }
    
    // 调用时:
    LogUtil.Write(LogFeatures.Orders, "...");
    
    // 配置中 ("appsettings.json" 无法使用常量,但至少代码侧是受控的)
    // "expression": "FeatureName = 'Orders'" 
  2. 混合使用 ILogger:

    不要用 LogUtil 替换所有的日志记录。ILogger 仍然是最佳实践,因为它能自动附加 SourceContext(类名)。

    • 建议: 在你的 ControllerService 内部优先使用 ILogger<T> (通过 DI 注入)来进行常规的 Debug 和 Info 级别的日志记录。这些日志会自动进入"兜底文件" (all-.txt)。
    • 仅在 你需要触发特定路由规则 时(例如 "Controller 执行完毕后"),才调用 LogUtil.Write(LogFeatures.Orders, ...)
  3. 使用结构化日志:

    始终使用结构化日志模板,而不是字符串拼接($)。

    C# 复制代码
    // 不好:
    Log.Information($"已完成对订单 {id} 的查询。");
    
    // 推荐:
    Log.Information("已完成对订单 {OrderId} 的查询。", id); 
    • 为什么? 推荐的方式会生成 { "OrderId": 123 },而 $ 字符串只会生成一个扁平的 message,你无法在 Seq 等工具中按 OrderId 进行搜索。

📚 官方文档推荐

相关推荐
程序猿小蒜3 小时前
基于springboot的汽车资讯网站开发与实现
java·前端·spring boot·后端·spring
q***98523 小时前
前端的dist包放到后端springboot项目下一起打包
前端·spring boot·后端
vx_bisheyuange3 小时前
基于SpringBoot的热门旅游推荐系统设计与实现
java·spring boot·后端·毕业设计
代码or搬砖3 小时前
SpringBoot整合SpringMVC
java·spring boot·后端
程序定小飞3 小时前
基于springboot的汽车资讯网站开发与实现
java·开发语言·spring boot·后端·spring
Moment3 小时前
LangChain 1.0 发布:agent 框架正式迈入生产级
前端·javascript·后端
回家路上绕了弯3 小时前
朋友圈更新怎么实时通知?从发布到接收的全链路解析
后端·微服务
小坏讲微服务3 小时前
整合Spring Cloud Alibaba与Gateway实现跨域的解决方案
java·开发语言·后端·spring cloud·云原生·gateway
q***13613 小时前
Spring Cloud Gateway 整合Spring Security
java·后端·spring