46.【.NET8 实战--孢子记账--从单体到微服务--转向微服务】--扩展功能--集成网关--网关集成日志

本篇文章,我们一起在网关中集成日志功能,我们要在网关中记录下游微服务出现的异常信息、请求信息以及响应信息。在微服务架构中,网关作为系统的入口,承担着非常重要的职责。通过在网关层面集成日志功能,我们可以更好地监控和追踪整个系统的运行状况,特别是在处理下游服务异常时能够及时发现和定位问题。我们将使用结构化日志来记录请求的详细信息,包括请求路径、请求方法、响应状态码等关键数据,同时也会捕获并记录下游服务可能出现的异常信息。这样不仅有助于系统的运维和监控,也能为问题排查提供有力支持。通过这种方式,我们可以在整个微服务架构中建立起完整的日志追踪体系,提高系统的可观察性。

一、记录下游微服务异常日志

这一小节,我们编写记录下游服务异常响应的中间件代码,每当下游服务触发响应异常时,我们都将记录异常信息。在微服务架构中,下游服务的异常处理和日志记录是非常重要的环节,它能帮助我们及时发现和解决系统中的问题。通过在网关层面统一捕获和记录这些异常,我们可以更好地监控系统的运行状况,快速定位和解决问题。当下游服务出现异常时,比如服务不可用、请求超时或者内部错误等情况,我们的中间件会自动捕获这些异常,并记录下详细的异常信息,包括异常发生的时间、请求的URL和方法、异常的具体类型和详细信息,以及相关的请求追踪ID等关键数据。这些信息会以结构化日志的形式被保存下来,方便后续的查询和分析。同时,我们也会确保这些异常信息能够正确地传递给上游调用者,这样不仅保证了系统的可观察性,也为系统的运维和问题诊断提供了重要的支持。在接下来的内容中,我们将通过具体的代码实现来展示如何构建这个异常日志中间件,让整个系统的异常处理变得更加可控和透明。

在网关服务的Middleware文件夹下,创建记录下游服务错误响应与异常的 Ocelot 委托处理器DownstreamLoggingHandler,代码如下:

using System.Text.Json;
using SP.Common.Logger;namespace SP.Gateway.Middleware;/// <summary>
/// 记录下游服务错误响应与异常的 Ocelot 委托处理器
/// </summary>
public class DownstreamLoggingHandler : DelegatingHandler
{private static readonly HashSet<string> SensitiveHeaders = new(StringComparer.OrdinalIgnoreCase){"Authorization", "Cookie", "Set-Cookie", "X-Api-Key", "X-Auth-Token"};private readonly ILogger<DownstreamLoggingHandler> _logger;private readonly ILoggerService _loggerService;private readonly IHttpContextAccessor _httpContextAccessor;public DownstreamLoggingHandler(ILogger<DownstreamLoggingHandler> logger,ILoggerService loggerService,IHttpContextAccessor httpContextAccessor){_logger = logger;_loggerService = loggerService;_httpContextAccessor = httpContextAccessor;}protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken){try{var response = await base.SendAsync(request, cancellationToken);if (!response.IsSuccessStatusCode){var httpContext = _httpContextAccessor.HttpContext;var upstream = httpContext?.Request;var log = new{Upstream = upstream == null ? null : new{Method = upstream.Method,Path = upstream.Path.ToString(),QueryString = upstream.QueryString.ToString(),Headers = upstream.Headers.Where(h => !SensitiveHeaders.Contains(h.Key)).ToDictionary(h => h.Key, h => string.Join(",", h.Value))},Downstream = new{Method = request.Method.Method,Url = request.RequestUri?.ToString(),StatusCode = (int)response.StatusCode,Reason = response.ReasonPhrase,ResponseHeaders = response.Headers.Where(h => !SensitiveHeaders.Contains(h.Key)).ToDictionary(h => h.Key, h => string.Join(",", h.Value)),ContentHeaders = response.Content?.Headers?.Where(h => !SensitiveHeaders.Contains(h.Key)).ToDictionary(h => h.Key, h => string.Join(",", h.Value))},User = httpContext == null ? null : new{UserId = httpContext.User?.FindFirst("UserId")?.Value,UserName = httpContext.User?.FindFirst("UserName")?.Value,Email = httpContext.User?.FindFirst("Email")?.Value,IsAuthenticated = httpContext.User?.Identity?.IsAuthenticated},Timestamp = DateTime.UtcNow};var json = JsonSerializer.Serialize(log, new JsonSerializerOptions{PropertyNamingPolicy = JsonNamingPolicy.CamelCase,WriteIndented = false});_loggerService.LogError("下游服务返回错误: {Details}", json);}return response;}catch (Exception ex){_loggerService.LogError(ex, "调用下游服务发生异常: {Method} {Url}", request.Method.Method, request.RequestUri?.ToString());throw;}}
}

在上面这段代码中,我们实现了一个用于记录下游服务错误响应与异常的 Ocelot 委托处理器。这个处理器继承自 DelegatingHandler,主要用于拦截和处理 HTTP 请求。首先,我们定义了一个敏感头部信息的集合 SensitiveHeaders,包含了一些需要特别保护的 HTTP 头部字段,如授权令牌、Cookie 等。这些敏感信息在记录日志时会被过滤掉,以保护用户隐私和系统安全。

处理器通过依赖注入接收了三个重要的服务:ILogger 用于常规日志记录,ILoggerService 用于结构化日志记录,以及 IHttpContextAccessor 用于访问当前的 HTTP 上下文。在 SendAsync 方法中,我们重写了请求处理逻辑。该方法首先尝试发送请求到下游服务,然后检查响应状态。如果响应不是成功状态码,就会收集详细的日志信息,包括:上游请求信息(请求方法、路径、查询字符串和过滤后的请求头)、下游响应信息(请求方法、URL、状态码、响应原因、响应头部)、用户信息(用户ID、用户名、邮箱、认证状态)以及时间戳。所有这些信息被组织成一个匿名对象,然后通过 JsonSerializer 序列化成 JSON 格式。序列化时使用了驼峰命名规则,并禁用了格式化缩进以减少日志体积。最后,通过 ILoggerService 将这些信息记录为错误日志。如果在调用下游服务过程中发生异常,catch 块会捕获异常并记录错误信息,包括请求方法和URL,然后重新抛出异常以确保错误能够正确传播。这样的设计既保证了异常信息的完整记录,又不会影响系统的正常错误处理流程。

Tip:SensitiveHeaders中定义的头部信息大部分在我们当前项目中使用不到的,但是为什么还要加上去呢?这是因为虽然我们的项目用不到,但是我们引入的第三发包或者使用的第三方应用与我们的项目交互的时候有可能用到,因此在这里就需要提前做好屏蔽。

在编写完DownstreamLoggingHandler委托处理器后,我们需要在Program.cs文件中注册这个委托处理器,并且将全局异常处理也添加到网关中。代码如下:

// more code ...
// Ocelot + Nacos 服务发现,并添加下游响应日志处理器
builder.Services.AddOcelot(builder.Configuration).AddNacosDiscovery().AddDelegatingHandler<DownstreamLoggingHandler>(true);// more code ...// 全局异常处理(包含请求缓冲),优先放在最前面
app.UseFullExceptionHandling();// more code ...

全局异常处理前面的文章我们已经讲解过了,在这里就不再多讲了,不清楚的翻看前面的文章即可。到目前位置,我们的网关服务就已经集成了全局异常处理和记录下游服务错误响应与异常的功能。

二、记录请求/响应日志

在微服务架构中,网关作为所有外部请求的入口点,每天要处理大量的请求和响应。通过记录这些请求和响应的详细信息,我们可以实现多个重要目标:首先,当系统出现异常或性能问题时,完整的请求/响应日志可以帮助开发团队快速重现问题场景,定位故障源头;其次,通过分析这些日志,我们可以了解系统的使用模式,识别出频繁访问的接口和潜在的性能瓶颈,从而有针对性地进行系统优化;再次,这些日志记录也可以用于安全审计,帮助我们发现潜在的安全威胁和异常访问模式。此外,在系统升级或重构时,这些历史数据能为架构决策提供重要参考,帮助我们更好地理解系统的实际使用情况和优化方向。因此,在网关中实现全面的请求/响应日志记录不仅是一个技术需求,更是确保系统可维护性、安全性和可扩展性的关键措施。

我们首先在网关的Middleware文件夹下创建网关入口请求/响应日志中间件RequestResponseLoggingMiddleware,它只记录文本/JSON/XML 等可读内容,并且限制长度以及过滤敏感头信息。代码如下:

using System.Text;
using System.Text.Json;
using SP.Common.Logger;namespace SP.Gateway.Middleware;/// <summary>
/// 网关入口请求/响应日志中间件(仅记录文本/JSON/XML 等可读内容,限制长度并过滤敏感头)
/// </summary>
public class RequestResponseLoggingMiddleware
{private readonly RequestDelegate _next;private readonly ILoggerService _logger;private const int MaxBodyChars = 4096; // 限制日志体长度,避免过大private static readonly HashSet<string> SensitiveHeaders = new(StringComparer.OrdinalIgnoreCase){"Authorization", "Cookie", "Set-Cookie", "X-Api-Key", "X-Auth-Token"};public RequestResponseLoggingMiddleware(RequestDelegate next, ILoggerService logger){_next = next;_logger = logger;}public async Task Invoke(HttpContext context){// 记录请求var requestInfo = await ReadRequestAsync(context.Request);// 捕获响应var originalBodyStream = context.Response.Body;await using var responseBody = new MemoryStream();context.Response.Body = responseBody;try{await _next(context);}finally{var responseInfo = await ReadResponseAsync(context.Response);// 写回响应流context.Response.Body.Seek(0, SeekOrigin.Begin);await context.Response.Body.CopyToAsync(originalBodyStream);context.Response.Body = originalBodyStream;var log = new{Type = "Upstream",Request = requestInfo,Response = responseInfo,User = new{UserId = context.User?.FindFirst("UserId")?.Value,UserName = context.User?.FindFirst("UserName")?.Value,Email = context.User?.FindFirst("Email")?.Value,IsAuthenticated = context.User?.Identity?.IsAuthenticated},Timestamp = DateTime.UtcNow};var json = JsonSerializer.Serialize(log, new JsonSerializerOptions{PropertyNamingPolicy = JsonNamingPolicy.CamelCase});// 成功响应记录为信息级别,非成功为警告级别if (context.Response.StatusCode >= 200 && context.Response.StatusCode < 400)_logger.LogInformation("上游请求响应: {Details}", json);else_logger.LogWarning("上游请求响应(非成功): {Details}", json);}}private static async Task<object> ReadRequestAsync(HttpRequest request){request.EnableBuffering();string? body = null;if (IsTextBased(request.ContentType)){request.Body.Seek(0, SeekOrigin.Begin);using var reader = new StreamReader(request.Body, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, leaveOpen: true);var text = await reader.ReadToEndAsync();request.Body.Seek(0, SeekOrigin.Begin);body = Truncate(text, MaxBodyChars);}var headers = request.Headers.Where(h => !SensitiveHeaders.Contains(h.Key)).ToDictionary(h => h.Key, h => string.Join(",", h.Value));return new{Method = request.Method,Path = request.Path.ToString(),QueryString = request.QueryString.ToString(),Headers = headers,Body = body};}private static async Task<object> ReadResponseAsync(HttpResponse response){string? body = null;if (IsTextBased(response.ContentType)){response.Body.Seek(0, SeekOrigin.Begin);using var reader = new StreamReader(response.Body, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, leaveOpen: true);var text = await reader.ReadToEndAsync();response.Body.Seek(0, SeekOrigin.Begin);body = Truncate(text, MaxBodyChars);}var headers = response.Headers.Where(h => !SensitiveHeaders.Contains(h.Key)).ToDictionary(h => h.Key, h => string.Join(",", h.Value));return new{StatusCode = response.StatusCode,Headers = headers,Body = body};}private static bool IsTextBased(string? contentType){if (string.IsNullOrWhiteSpace(contentType)) return false;contentType = contentType.ToLowerInvariant();return contentType.StartsWith("text/")|| contentType.Contains("json")|| contentType.Contains("xml")|| contentType.Contains("javascript")|| contentType.Contains("html")|| contentType.Contains("plain")|| contentType.Contains("csv");}private static string Truncate(string input, int max){if (string.IsNullOrEmpty(input)) return input ?? string.Empty;return input.Length <= max ? input : input.Substring(0, max);}
}

在上面这段代码中,我们实现了一个用于记录网关入口请求和响应日志的中间件。这个中间件专门设计用来记录可读的文本内容,如JSON、XML等格式的数据,并对日志内容的长度进行了限制,同时也会过滤掉敏感的头部信息。中间件的核心是Invoke方法,它首先记录请求信息,然后使用内存流来捕获响应内容。这种设计允许我们在不影响原始响应的情况下读取响应内容。在处理过程中,中间件会保存原始的响应流,创建一个新的内存流来暂存响应,这样就能在发送响应之前读取和记录响应内容。

为了确保安全性和性能,中间件实现了多个重要的限制。首先,通过MaxBodyChars常量限制了日志体的最大长度为4096个字符,防止日志内容过大占用过多存储空间。其次,使用SensitiveHeaders集合定义了需要过滤的敏感头部信息,确保不会记录下包含敏感信息的请求头。

在日志记录方面,中间件会收集完整的请求和响应信息,包括请求方法、路径、查询字符串、请求头以及请求体,同样也会记录响应状态码、响应头和响应体。这些信息会被组织成结构化的日志对象,并添加用户信息和时间戳。根据响应的状态码,中间件会选择不同的日志级别:成功的请求(状态码200-399)记录为信息级别,而其他状态码则记录为警告级别。

中间件还包含了一些辅助方法,如IsTextBased方法用于判断内容类型是否为文本格式,Truncate方法用于截断过长的字符串。这些方法帮助确保日志记录的高效性和实用性。通过ReadRequestAsyncReadResponseAsync方法,中间件能够正确处理请求和响应的内容,确保在记录日志时不会影响到实际的请求处理流程。

在编写完记录请求/响应的中间件后,我们就需要在网关服务中引入这个中间件。在Program.cs文件中,我们可以使用AddMiddleware方法将中间件添加到应用程序的请求处理管道中。代码如下:

// more code ...
// 上游请求/响应日志(放在认证之后,便于记录用户信息)
app.UseMiddleware<RequestResponseLoggingMiddleware>();
// more code ...

到此为止,我们完成了网关集成日志的全部功能,包括记录请求和响应的详细信息、下游服务的异常响应,以及根据状态码选择不同的日志级别。

三、总结

本片文章主要介绍了如何在网关服务中集成日志功能,包括记录请求和响应的详细信息、下游服务的异常响应,以及根据状态码选择不同的日志级别。通过引入自定义的中间件,我们能够方便地记录请求和响应的日志,同时也能够及时发现和处理异常情况。这对于排查问题、监控系统运行状态以及优化性能都具有重要意义。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/pingmian/94164.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/94164.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

使用 FastAPI 的 WebSockets 和 Elasticsearch 来构建实时应用

作者&#xff1a;来自 Elastic Jeffrey Rengifo 学习如何使用 FastAPI WebSockets 和 Elasticsearch 构建实时应用程序。 更多阅读&#xff1a;使用 FastAPI 构建 Elasticsearch API 想要获得 Elastic 认证吗&#xff1f;看看下一次 Elasticsearch Engineer 培训什么时候开始&…

华为云ModelArts+Dify AI:双剑合璧使能AI应用敏捷开发

引言:AI应用开发的敏捷化转型需求 随着大语言模型(LLM)技术的迅猛发展,企业与开发者对AI应用开发的敏捷化转型需求日益凸显,亟需将大模型能力快速转化为实际业务价值。传统AI开发模式中,复杂的模型工程化、流程编排和部署维护工作往往需要专业技术团队支撑,典型痛点包括…

网络实践——Socket编程UDP

文章目录Socket编程UDPUDP接口的使用铺垫socketrecvform && sendtobind字节序转化使用(Tips)实践部分version_1echo_serverversion_2dict_serverversion_3chat_serverSocket编程UDP 在了解了相关的网络基础知识后&#xff0c;我们不会像学系统知识一样&#xff0c;先学…

GD32 波形发生器,三角波,正弦波,方波。AD9833+MCP410生成和MCU自身的DAC生成。波形,频率,振幅可以通过按键和OLED调整输出。

DIY一个简易的信号发生器驱动板&#xff0c;主要是三角波和正弦波&#xff0c;方波。主板有两个通道能输出波形&#xff0c;CH0由AD9833MCP410AD8051放大电路组成&#xff0c;理论可以生成0.1-12.5MHZ的频率信号&#xff0c;单电源振幅范围是1-9V。CH1由MCU外设DAC生成的信号&a…

VS2022的MFC中关联使用控制台并用printf输出调试信息

前言 MFC一般在调试的时候&#xff0c;可以在IDE中方便的看到调试的信息。但是&#xff0c;有时候运行的时候也要看调试的信息怎么办&#xff1f;最好如同在Console&#xff08;控制台&#xff09;程序中输出一般的方便&#xff0c;可以么&#xff1f;可以的。 一、设置 1.1、加…

ZKmall模块商城的推荐数据体系:从多维度采集到高效存储的实践

在电商领域&#xff0c;个性化推荐已成为提升用户体验与转化效率的核心手段。ZKmall 模块商城基于用户行为、商品属性与交易数据&#xff0c;构建了一套完整的推荐算法体系&#xff0c;而数据采集的全面性与存储的高效性是该体系的基础。本文将聚焦推荐算法的 “数据输入端”&a…

Qt + windows+exe+msvc打包教程

目录 1. Qt + windows+exe+msvc打包教程1 1.1. Enigma Virtual Box下载⏬1 1.2. Enigma Virtual Box安装2 1.3. Qt 打包成独立exe教程6 1.3.1. Qt项目创建6 1.3.2. Qt项目编译13 1.3.3. Qt 项目打包 windeployqt命令14 1.3.4. Qt 项目打包 Enigma Virtual Box工具18 Q…

大语言模型应用开发——利用OpenAI函数与LangChain结合从文本构建知识图谱搭建RAG应用全流程

概述 从文本等非结构化数据中提取结构化信息并非新鲜事物&#xff0c;但大语言模型&#xff08;LLMs&#xff09;为该领域带来了重大变革。以往需要机器学习专家团队策划数据集并训练自定义模型&#xff0c;如今只需访问LLM即可实现&#xff0c;显著降低了技术门槛&#xff0c…

Vue3+Spring Boot技术栈,前端提交混合表单数据(普通字段+文件字段),上传文件,后端插入数据,将文件保存到数据库

一、技术栈1、前端 Vue3 Element Plus TypeSprict2、后端 Spring Boot 3.2.12 Mybatis Plus3、模型特点3.1、表格展示列表数据3.2、行点击&#xff0c;弹出对话框3.3、前端使用 FormData 提交混合表单数据&#xff0c;包含普通字段和文件字段3.4、文件对应数据库结构类型为 …

【Qt开发】Qt的背景介绍(四)

目录 1 -> Qt Hello World 程序 1.1 -> 使用“按钮”实现 1.1.1 -> 纯代码方式实现 1.1.2 -> 可视化操作实现 1.2 -> 使用“标签”实现 1.2.1 -> 纯代码方式实现 1.2.2 -> 可视化操作实现 2 -> 项目文件解析 2.1 -> .pro文件解析 2.2 -&g…

Linux驱动开发笔记(六)——pinctrl GPIO

开发板&#xff1a;imx6ull mini 虚拟机&#xff1a;VMware17 ubuntu&#xff1a;ubuntu20.04 视频&#xff1a;第8.1讲 pinctrl和gpio子系统试验-pincrl子系统详解_哔哩哔哩_bilibili 文档&#xff1a;《【正点原子】I.MX6U嵌入式Linux驱动开发指南.pdf》四十五章 这一章…

SpringBoot 快速上手:从环境搭建到 HelloWorld 实战

在 Java 开发领域&#xff0c;Spring 框架占据着举足轻重的地位&#xff0c;但它复杂的配置曾让不少开发者望而却步。SpringBoot 的出现&#xff0c;如同为 Spring 框架装上了 “加速器”&#xff0c;以 “约定大于配置” 的理念简化了开发流程。本文将从环境准备、Maven 配置入…

图、最小生成树与最短路径

目录 并查集 并查集实现 图 概念 图的存储结构 邻接矩阵 邻接表 无向图 有向图 图的遍历 广度优先遍历 深度优先遍历 最小生成树 Kruskal算法&#xff08;克鲁斯卡尔算法&#xff09; Prim算法&#xff08;普利姆算法&#xff09; 最短路径 单源最短路径--Dij…

互联网电商新生态:开源AI智能名片、链动2+1模式与S2B2C商城小程序的融合赋能

摘要&#xff1a;本文聚焦互联网电商领域&#xff0c;探讨在当下直播电商蓬勃发展的背景下&#xff0c;开源AI智能名片、链动21模式以及S2B2C商城小程序如何相互融合&#xff0c;为创业者、企业和淘宝主播IP等电商参与者带来新的发展机遇。通过分析各要素的特点与优势&#xff…

企业车辆|基于SprinBoot+vue的企业车辆管理系统(源码+数据库+文档)

企业车辆管理系统 基于SprinBootvue的企业车辆管理系统 一、前言 二、系统设计 三、系统功能设计 系统功能实现 后台模块实现 管理员模块实现 驾驶员模块实现 四、数据库设计 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八、源码获取&#xff1a; 博…

自学嵌入式第二十五天:数据结构-队列、树

一、队列队列是只允许一段进行插入&#xff0c;另一端进行删除操作的线性表&#xff1b;允许插入的一端叫队尾&#xff0c;允许删除的一端叫对头&#xff1b;先进先出&#xff1b;用于解决速度不匹配&#xff08;例如一快一慢&#xff09;&#xff0c;做缓冲用&#xff1b;二、…

MySQL索引原理与优化全解析

1、MySQL索引是什么&#xff1f; 在关系数据库中&#xff0c;索引是一种单独的、物理的对数据库表中一列或多列的值进行排序的一种存储结构&#xff0c;它是某个表中一列或若干列值的集合和相应的指向表中物理标志这些值的数据页的逻辑指针清单。索引的作用相当于图书的目录&a…

模型对话状态管理方法详解

模型对话状态管理方法详解 目录 简介手动管理对话状态构建对话历史追加响应内容 API 支持的自动化对话状态管理使用 previous_response_id 链接话轮 Token 及上下文窗口管理上下文窗口定义与限制Token 计数与工具 安全与合规注意事项结语1. 简介 在多轮对话场景中&#xff0c;合…

GPT-5 上线风波深度复盘:从口碑两极到策略调整,OpenAI 的变与不变

摘要&#xff1a; 近日&#xff0c;备受瞩目的 GPT-5 正式上线&#xff0c;却意外地在社区引发了两极化争议。面对技术故障与用户质疑&#xff0c;OpenAI 迅速推出一系列补救措施。本文将深度复盘此次发布风波&#xff0c;解析其背后的技术挑战与应对策略&#xff0c;并探讨这一…

【Android】使用FragmentManager动态添加片段

三三要成为安卓糕手 上一篇文章&#xff0c;我们是在xml中静态添加fragment&#xff0c;但是一些修改或者其他事情是做不了的&#xff1b; 本章我们达成在java代码中灵活添加、删除、替换fragment操作 一&#xff1a;核心代码展示 简单做一个这种页面public class FragmentActi…