11.【.NET 8 实战--孢子记账--从单体到微服务--转向微服务】--微服务基础工具与技术--Ocelot 网关--整合日志

时间:2025-03-17 06:55:31

网关作为微服务架构的入口,承载着各服务间的请求转发与安全校验,其日志信息尤为关键。通过整合网关日志,可以将分散在不同系统中的访问记录、错误提示和异常信息集中管理,为问题排查提供全景视角。在排查故障时,统一日志能帮助定位请求链路中的瓶颈与错误,缩短故障恢复时间;在性能监控中,通过统计响应时间、请求频次等指标,及时发现系统负载变化及性能瓶颈;在安全审计方面,集中日志记录能追踪异常行为、识别恶意攻击,并为后续调查提供证据。日志整合的目标是构建一个集中化、智能化的日志平台,实现数据共享、实时监控与预警机制,最终提升系统稳定性和安全性,保障业务连续运行。日志整合促协作高效,速解故障,极大改善用户体验显。

一、日志框架选择与技术方案

1.1 常用日志框架比较

Serilog、NLog 和 Log4net 是 .NET 平台上广泛应用的日志框架,各自都有独特的优势和适用场景。

Serilog 的特点在于其对结构化日志的原生支持,它采用消息模板记录日志,可以自动提取关键数据并实现日志聚合和过滤,这对分布式系统和微服务架构下的日志分析非常有利。Serilog 的配置方式主要采用链式 API,代码风格简洁直观,并且拥有丰富的插件,能够将日志输出到文件、数据库、Elasticsearch、Seq 等多种介质,使其非常适合需要精细日志数据分析和检索的现代化应用。

相比之下,NLog 以高性能和灵活性著称,支持异步日志记录,能在高并发环境下稳定工作。它主要通过 XML 配置文件实现详细的日志级别、格式和输出目标的设置,虽然这种配置方式上手可能稍显繁琐,但其灵活性和可扩展性使其非常适合企业级大规模日志管理,内置支持多种目标,如文件、数据库、邮件等,并且允许开发者自定义扩展。

Log4net 则是 log4j 在 .NET 环境下的成熟移植版,其历史悠久且在传统项目中应用广泛,具有很高的稳定性。虽然 Log4net 通过 XML 配置文件可以完成基本的日志设置,但在结构化日志的直接支持和扩展性方面相对较弱。

选择哪个框架需要结合具体项目需求来考量,如果项目对日志结构化要求较高,Serilog 是理想选择;若重点考虑高性能和灵活配置,NLog 更能满足要求;而对于历史项目或对传统日志功能需求较高的系统,Log4net 则能提供稳定可靠的日志记录能力。

1.2 Ocelot 与日志框架的集成方式

Ocelot 本身构建于 ASP.NET Core 之上,因此它利用了 ASP.NET Core 的日志抽象(Microsoft.Extensions.Logging),这使得与各类日志框架的集成变得非常方便。Ocelot 的集成方式主要体现在以下几个方面:

首先在 ASP.NET Core 应用的启动阶段,通过配置 logging providers,开发者可以设置全局日志记录规则。由于 Ocelot 内部组件也依赖于 ILogger 接口,因此一旦配置好日志提供程序,Ocelot 就会沿用这些设置,将其内部运行、请求转发、异常捕获等日志信息输出到指定的目标。

其次除了基础日志记录之外,我们还可以通过自定义中间件或委托处理程序来扩展日志功能。在 Ocelot 的请求处理管道中,可以插入自定义组件,用于捕捉请求的进入、响应输出及错误处理,并将这些信息记录下来。这样一来,不仅可以统一日志格式,还能在日志中加入诸如请求跟踪 ID、用户身份、调用链信息等有助于故障排查和性能监控的上下文数据。

借助各日志框架的扩展功能,我们可以为 Ocelot 的日志设置丰富的输出目标和格式,比如将日志存储到 Elasticsearch、Seq、Splunk 等集中式日志平台,以便实现实时监控和数据分析。这种集成方式不仅满足了基础的日志记录需求,还支持后续对日志进行高级处理和自动化预警。

1.3 集中式日志解决方案介绍

随着微服务架构和分布式系统的普及,日志数据来源日益分散且数量庞大,单个服务的日志已难以满足故障排查、性能监控和安全审计的需求。通过集中式日志系统,所有日志数据能够实时汇聚到一个*平台,这不仅便于对历史数据进行归档和检索,更可以通过统一的查询和分析工具对全局日志进行聚合分析,从而迅速识别系统异常、瓶颈和安全隐患。

目前市面上较为成熟的集中式日志解决方案主要有 ELK Stack、EFK Stack 以及 Splunk 和 Graylog 等。以 ELK Stack 为例,Logstash 负责日志的收集、解析和传输,Elasticsearch 提供高效的日志数据存储与检索能力,Kibana 则通过直观的图表和仪表盘展现实时数据变化,为运维人员提供全面的监控视图。Splunk 则以其强大的数据索引和分析能力著称,适用于大规模企业级日志管理,而 Graylog 在用户体验和扩展性方面也有独到之处。

集中式日志解决方案不仅提升了系统监控和故障排查的效率,还增强了安全审计的能力,为企业在面对复杂多变的应用环境时提供了可靠的数据支撑和决策依据。通过集中管理,开发和运维团队能够更加迅速地响应系统异常,优化性能配置,最终确保系统的稳定运行和业务的持续发展。

二、日志整合实现步骤(以Serilog为例)

2.1 环境搭建与前置配置

首先在网关服务中引入Serilog包,在命令行中输入如下命令:

dotnet add package Serilog
dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.Console

Serilog包就不用多说了,现在来讲讲Serilog.AspNetCoreSerilog.Sinks.Console 包。

Serilog.AspNetCore提供了与 ASP.NET Core 框架的集成,能够在 ASP.NET Core 应用程序中更方便地使用 Serilog 进行日志记录,并且包括一些特定于 ASP.NET Core 的功能,例如请求和响应日志记录等。Serilog.Sinks.Console 可以将日志消息输出到控制台,通常用在开发和调试过程中。通过配置 Serilog,可以将日志消息格式化并输出到控制台。

2.2 整合

在网关服务的Program类中添加对Serilog的配置,代码如下:

using Ocelot.DependencyInjection;
using Ocelot.Middleware;
using Serilog;

namespace Gateway
{
    public class Program
    {
        public static void Main(string[] args)
        {
            // more code

            Log.Logger = new LoggerConfiguration()
                .MinimumLevel.Debug()
                .WriteTo.Console()
                .WriteTo.File("logs/ocelot-.txt", rollingInterval: RollingInterval.Day)
                .CreateLogger();
            builder.Host.UseSerilog();

            // more code

        }
    }
}

在代码中,首先创建了一个 Serilog 日志配置对象,通过调用 LoggerConfiguration() 构造器初始化日志配置,并使用 MinimumLevel.Debug() 将日志记录的最低级别设置为 Debug,也就是说 Debug 级别及以上的所有日志信息都会被捕捉到。接着,通过 WriteTo.Console() 指定将日志输出到控制台,并使用 WriteTo.File("logs/ocelot-.txt", rollingInterval: RollingInterval.Day) 将日志记录到文件中,其中日志文件会按照每天进行滚动生成新的日志文件。最后,调用 CreateLogger() 方法根据之前的所有配置创建出具体的 Logger 实例,并将该实例赋值给 Serilog 的全局静态属性 Log.Logger。随后调用 builder.Host.UseSerilog() 将 Serilog 集成到应用程序的构建过程中,从而让整个应用在运行时都使用 Serilog 进行日志记录,而不是使用默认的日志系统。

2.3 捕捉请求、响应及异常日志的实现方法

在上一小结,我们将Serilog添加到了网关服务中,但是这只是替换了Asp.Net Core 自带的日志组件,对请求、响应以及异常的日志记录没有做任何处理,下面我们就对这三个部分进行处理,要求输出的日志格式是:日志级别+日期时间+内容,其中包含请求参数、响应的数据、异常信息等。代码如下:

using Serilog;

namespace Gateway;

/// <summary>
/// 日志中间件
/// </summary>
public class RequestResponseLoggingMiddleware
{
    private readonly RequestDelegate _next;
    
    /// <summary>
    /// 构造函数
    /// </summary>
    /// <param name="next"></param>
    public RequestResponseLoggingMiddleware(RequestDelegate next)
    {
        _next = next;
    }
    
    /// <summary>
    /// 执行
    /// </summary>
    /// <param name="context"></param>
    public async Task Invoke(HttpContext context)
    {
        // 记录请求
        context.Request.EnableBuffering();
        using var requestBodyStream = new MemoryStream();
        await context.Request.Body.CopyToAsync(requestBodyStream);
        requestBodyStream.Seek(0, SeekOrigin.Begin);
        var requestBodyText = new StreamReader(requestBodyStream).ReadToEnd();
        Log.Information($"收到请求: Method=`{context.Request.Method}`, Path=`{context.Request.Path},` Body=`{requestBodyText}`");
        requestBodyStream.Seek(0, SeekOrigin.Begin);
        context.Request.Body = requestBodyStream;

        // 替换响应流
        var originalResponseBodyStream = context.Response.Body;
        using var responseBodyStream = new MemoryStream();
        context.Response.Body = responseBodyStream;

        try
        {
            await _next(context);

            // 记录响应
            context.Response.Body.Seek(0, SeekOrigin.Begin);
            var responseBodyText = new StreamReader(context.Response.Body).ReadToEnd();
            Log.Information($"输出响应: StatusCode=`{context.Response.StatusCode}`, Body=`{responseBodyText}`");
            // 在写回原始响应流之前,移除 Content-Length 头(如果存在)
            if (context.Response.Headers.ContainsKey("Content-Length"))
            {
                context.Response.Headers.Remove("Content-Length");
            }

            // 重置响应流的位置并写回原始流
            context.Response.Body.Seek(0, SeekOrigin.Begin);
            await responseBodyStream.CopyToAsync(originalResponseBodyStream);
        }
        catch (Exception ex)
        {
            // 记录异常
            Log.Error(ex, "执行请求时发生了未处理的异常。");
            throw;
        }
        finally
        {
            context.Response.Body = originalResponseBodyStream;
        }
    }
}

Invoke 方法中,首先调用 context.Request.EnableBuffering() 以允许对 HTTP 请求体进行多次读取,接着创建一个 MemoryStream 实例并将请求体复制到该流中,这样可以在不影响后续中间件读取请求体的前提下获取请求的内容。复制完成后,通过重置流的指针到起始位置并使用 StreamReader 将请求体内容读取为字符串,随后利用 Log.Information 方法记录下请求方法、路径及请求体;接着,将流的指针再次重置并将 MemoryStream 赋值回 context.Request.Body 保证后续中间件能够正常访问请求数据。

对响应进行处理时,首先将原始的 context.Response.Body 保存到 originalResponseBodyStream 中,然后将 context.Response.Body 替换为一个新的 MemoryStream,以便捕获下游中间件写入响应流中的数据。当调用 await _next(context) 执行下一个中间件后,通过重置响应流指针读取响应内容,同样将响应状态码和响应体日志记录下来,同时在写回原始响应流之前检查并移除 Content-Length 响应头,以防止由于数据实际写入字节数与 Content-Length 不一致而引发错误;最后,通过将 MemoryStream 中捕获的响应复制回原始响应流完成响应写入。在 try-catch 块中,如果下游中间件抛出异常,则使用 Log.Error 记录异常信息,并在 finally 块中将 context.Response.Body 恢复成原始响应流,保证中的流状态不会影响其他处理流程。

三、总结

网关作为微服务架构的入口,负责请求转发和安全校验,其日志记录对于故障排查、性能监控和安全审计至关重要。统一日志可以全面呈现请求链路信息,缩短故障恢复时间,同时通过统计响应时间和请求频次及时发现性能瓶颈。