using System;
using System.IO;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Serilog;
using Serilog.Core;
using Serilog.Events;
using Serilog.Formatting.Compact;
using Serilog.Sinks.SystemConsole.Themes;

namespace Dpz.Core.Infrastructure.Configuration;

public static class LogConfiguration
{
    private static bool LogFilter(LogEvent logEvent)
    {
        if (logEvent.Properties.TryGetValue("RequestPath", out var requestPath))
        {
            var pathPrefix = new[] { "/runtask", "/account", "/notification", "/app/notification" };
            if (requestPath is ScalarValue value)
            {
                var result = pathPrefix.Any(x =>
                    value
                        .Value?.ToString()
                        ?.StartsWith(x, StringComparison.CurrentCultureIgnoreCase)
                    ?? false
                );
                return result;
            }
        }

        return false;
    }

    public static void ConfigurationLog(
        this IHostBuilder host,
        LogSeq? logSeq,
        Func<LogEvent, bool>? filter = null
    )
    {
        if (
            logSeq == null
            || string.IsNullOrEmpty(logSeq.ServerUri)
            ||
#if !DEBUG
            string.IsNullOrEmpty(logSeq.ApiKey)
            ||
#endif
            string.IsNullOrEmpty(logSeq.Program)
        )
        {
            throw new ArgumentNullException(nameof(logSeq));
        }
        filter ??= LogFilter;

        var levelSwitch = new LoggingLevelSwitch();
        host.UseSerilog(
            (context, services, cfg) =>
                cfg
                    .ReadFrom.Configuration(context.Configuration)
                    .ReadFrom.Services(services)
                    //.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
                    .MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning)
                    .MinimumLevel.Override("System.Net.Http.HttpClient", LogEventLevel.Warning)
                    .MinimumLevel.Override("System.Net.Sockets", LogEventLevel.Warning)
                    .MinimumLevel.Override(
                        "ZiggyCreatures.Caching.Fusion.FusionCache",
                        LogEventLevel.Warning
                    )
                    .MinimumLevel.Override("ZiggyCreatures.Caching.Fusion", LogEventLevel.Warning)
                    .MinimumLevel.Override(
                        "Dpz.Core.Web.Jobs.Services.Implements",
                        LogEventLevel.Verbose
                    )
                    .MinimumLevel.Information()
                    .Enrich.WithProperty("Version", ApplicationTools.ApplicationVersion.Value)
                    .Enrich.WithProperty("Server", Environment.MachineName)
                    .Enrich.WithProperty("Program", logSeq.Program)
                    .Enrich.FromLogContext()
                    .WriteTo.Logger(lc =>
                    {
                        lc.WriteTo.File(
                            new CompactJsonFormatter(),
                            Path.Combine("logs", $"{DateTime.Now:yyyyMM}", ".json"),
                            rollingInterval: RollingInterval.Day,
                            fileSizeLimitBytes: 10240
                        );
                    })
                    .WriteTo.Logger(lc =>
                    {
                        lc.Filter.ByExcluding(filter)
                            .WriteTo.Async(x =>
                                x.Console(
                                    theme: AnsiConsoleTheme.Literate,
                                    applyThemeToRedirectedOutput: true
                                )
                            );
                    })
                    .WriteTo.Logger(lc =>
                    {
                        var loggerConfiguration = lc.Filter.ByExcluding(filter);
                        if (string.IsNullOrEmpty(logSeq.ApiKey))
                        {
                            loggerConfiguration.WriteTo.Seq(
                                logSeq.ServerUri,
                                controlLevelSwitch: levelSwitch
                            );
                        }
                        else
                        {
                            loggerConfiguration.WriteTo.Seq(
                                logSeq.ServerUri,
                                apiKey: logSeq.ApiKey,
                                controlLevelSwitch: levelSwitch
                            );
                        }
                    })
        );

#if DEBUG
        // 日志追踪需要更多评估,暂时补上生产环境
        host.ConfigureServices(
            (_, services) => services.AddHostedService<TracingLifetimeService>()
        );
#endif
    }
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

这段代码是一个用于配置 Serilog 日志系统的静态扩展类,属于 Dpz.Core.Infrastructure.Configuration 命名空间。它集中定义了日志过滤规则、输出目标(文件、控制台、Seq)以及日志级别覆盖,并对 Seq 配置和调试环境做了特殊处理。

下面按代码结构逐部分解释:

1. LogFilter 方法 —— 日志过滤规则

private static bool LogFilter(LogEvent logEvent)
  • 作用是判断某条日志是否“应被排除”。
  • 它从日志事件的 Properties 中尝试读取 RequestPath 属性。
  • 如果存在该属性,且它是一个 ScalarValue,则取其字符串值,检查是否以数组 { "/runtask", "/account", "/notification", "/app/notification" } 中的任意前缀开头(忽略大小写)。
  • 只要前缀匹配,就返回 true
  • 在后面的配置中,这个方法会作为 Filter.ByExcluding(filter) 的默认参数,即返回 true 的日志会被过滤掉(不写入控制台和 Seq)。因此这些路径的请求日志不会被输出到外部,可能是为了避免敏感或高频日志干扰。

2. ConfigurationLog 扩展方法 —— 核心配置入口

public static void ConfigurationLog(this IHostBuilder host, LogSeq? logSeq, Func<LogEvent, bool>? filter = null)
  • IHostBuilder 扩展配置 Serilog 的方法。
  • 参数 logSeq 封装了 Seq 服务器的相关配置(如 ServerUriApiKeyProgram)。
  • 参数 filter 是自定义日志过滤委托,默认为 null,若不传则使用上面的 LogFilter

2.1 配置校验

if (logSeq == null || string.IsNullOrEmpty(logSeq.ServerUri) || ...)
{
    throw new ArgumentNullException(nameof(logSeq));
}
  • 强制要求 logSeq 不能为空,且必须包含 ServerUriProgram
  • 在非 DEBUG 编译环境下(#if !DEBUG),还要求 ApiKey 非空,否则抛出异常。这保证了生产环境 Seq 通信的安全性。

2.2 设置默认过滤器

filter ??= LogFilter;
  • 如果没有显式传入 filter,则使用 LogFilter 作为默认排除条件。

2.3 启用级别开关

var levelSwitch = new LoggingLevelSwitch();
  • 创建一个动态日志级别开关,用于在运行时通过 Seq 的动态级别控制来调整日志级别。

2.4 配置 Serilog

host.UseSerilog((context, services, cfg) => ...)
  • 使用 UseSerilog 扩展,定义一个完整的 Serilog 配置。
2.4.1 从配置和 DI 中读取
  • ReadFrom.Configuration:读取 appsettings.json 等配置文件中的 Serilog 设置。
  • ReadFrom.Services:从依赖注入容器中获取需要的服务(例如自定义 IDestructuringPolicy)。
2.4.2 覆盖多个命名空间的日志等级
.MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning)
.MinimumLevel.Override("System.Net.Http.HttpClient", LogEventLevel.Warning)
.MinimumLevel.Override("System.Net.Sockets", LogEventLevel.Warning)
...
.MinimumLevel.Information()
  • 将常用框架或第三方库的日志级别提升为 Warning,减少噪音。
  • 特别地将某个业务命名空间 Dpz.Core.Web.Jobs.Services.Implements 设为 Verbose,可能是为了调试该模块的详细日志。
  • 最后通过 .MinimumLevel.Information() 将全局默认级别设置为 Information
2.4.3 丰富日志上下文
.Enrich.WithProperty("Version", ...)
.Enrich.WithProperty("Server", Environment.MachineName)
.Enrich.WithProperty("Program", logSeq.Program)
.Enrich.FromLogContext()
  • 为每条日志添加固定的属性:Version(应用程序版本)、Server(机器名)、Program(程序标识),并从 LogContext 中引入活动上下文属性。
2.4.4 定义多个输出目标

整个配置通过三个独立的 WriteTo.Logger 子记录器将日志路由到不同目标,互不干扰。

a. 文件日志

.WriteTo.Logger(lc =>
{
    lc.WriteTo.File(
        new CompactJsonFormatter(),
        Path.Combine("logs", $"{DateTime.Now:yyyyMM}", ".json"),
        rollingInterval: RollingInterval.Day,
        fileSizeLimitBytes: 10240
    );
})
  • 将日志以压缩 JSON 格式写入 logs/yyyyMM/.json 路径下。
  • 按天滚动(RollingInterval.Day)并限制单文件大小为 10 KB。
  • 注意该路径写法有些可疑:Path.Combine("logs", "202504", ".json") 会产生 logs/202504/.json,文件名是 .json(隐藏文件),也许实际应生成类似 logs/202504/yyyyMMdd.json 或指定具体文件名。这是一个潜在的 bug 或故意的写法,但按此代码执行会生成以 .json 为文件名的文件,并且 DateTime.Now:yyyyMM 只精确到月份,导致所有日志都放在同一月目录下的 .json 文件中,再靠按天滚动区分旧文件。

b. 控制台日志

.WriteTo.Logger(lc =>
{
    lc.Filter.ByExcluding(filter)
       .WriteTo.Async(x =>
           x.Console(
               theme: AnsiConsoleTheme.Literate,
               applyThemeToRedirectedOutput: true
           )
       );
})
  • 使用默认的 LogFilter 排除与请求路径匹配的日志。
  • 通过 Async 包装异步写入控制台(避免阻塞主线程)。
  • 启用主题,并且即便输出重定向也应用 ANSI 主题色。

c. Seq 日志服务器

.WriteTo.Logger(lc =>
{
    var loggerConfiguration = lc.Filter.ByExcluding(filter);
    if (string.IsNullOrEmpty(logSeq.ApiKey))
    {
        loggerConfiguration.WriteTo.Seq(logSeq.ServerUri, controlLevelSwitch: levelSwitch);
    }
    else
    {
        loggerConfiguration.WriteTo.Seq(logSeq.ServerUri, apiKey: logSeq.ApiKey, controlLevelSwitch: levelSwitch);
    }
})
  • 同样使用 ByExcluding(filter) 排除某些日志。
  • 如果 ApiKey 为空,则不带认证连接 Seq;否则携带 ApiKey 认证。
  • 同时传入 controlLevelSwitch,使得 Seq 服务器可以远程控制客户端的最低日志级别(动态调整)。

2.5 DEBUG 环境特殊处理

#if DEBUG
    host.ConfigureServices((_, services) => services.AddHostedService<TracingLifetimeService>());
#endif
  • 仅在 Debug 编译下,注册一个后台主机服务 TracingLifetimeService(根据注释推测,可能是用于跟踪进程生命周期或调试日志追踪)。
  • 注释说明“日志追踪需要更多评估,暂时补上生产环境”,暗示这是临时措施,未来可能在生产环境也开启。

总结

这段代码是一个集中式的 Serilog 配置中心,具备以下特性:

  • 条件性配置:根据编译模式(DEBUG/RELEASE)要求不同的 Seq 配置。
  • 灵活过滤:默认会排除特定请求路径的日志,避免不必要的信息淹没日志系统。
  • 多端输出:同时输出到本地文件、控制台和远程 Seq 服务器,各端可独立过滤。
  • 级别调控:通过覆盖不同命名空间的级别,抑制框架日志噪音,保留业务日志。
  • 动态级别控制:借助 LoggingLevelSwitch 使 Seq 能远程调整客户端日志等级。
  • 调试辅助:在 Debug 构建下额外运行生命周期跟踪服务。

整体设计旨在为 ASP.NET Core 应用程序提供既安全又便于调试的日志基础设施。

评论加载中...