
日志记录对于开发人员来说是一个非常重要的工具,但是在构建分布式系统时,它成为需要直接奠定应用程序基础的一块石头,否则开发微服务的复杂性很快就会显现出来。
.Net Core 3 添加了一个很棒的 ,因此,如果您的应用程序使用直接 HTTP 调用进行服务间通信,那么您可以利用此盒装功能。 但是,如果您的后端架构涉及通过消息代理(RabbitMQ、Kafka 等)进行交互,那么您仍然需要自己处理通过这些消息传递相关上下文的主题。
在本文中,我们将采用一个简单的 Web api 应用程序并组织日志记录,这将
保持独立服务日志之间的端到端关联,以便您可以轻松查看由客户端特定请求引起的所有活动
具有方便分析的单一入口点,因此即使支持人员也可以使用日志记录工具,应用程序中会弹出“我在这样那样的请求 ID 上遇到错误”之类的问题
首先,我们需要决定应用程序中的日志记录提供程序。 现代测井的主要要求是结构,即我们不应该处理平面文本消息,而应该处理对象。 借助此类日志,我们可以轻松地在不同部分构建消息视图并进行分析。
对于我们的应用程序,我们将使用 Serilog 包(Serilog),它对结构日志记录具有出色的支持和丰富的附加系统。 我将省略设置它的基本步骤(您可以找到大量有关此主题的文章)并假设:
serilog 已配置并且是依赖注入提供程序的默认记录器
在其配置中启用了使用上下文属性丰富消息(Enrich.FromLogContext)
下一步是选择将 Serilog 的消息发送到哪个集中式日志系统。 也许当今最常见的开源选项是 ELK 堆栈(Elasticsearch、Logstash 和 Kibana),我们就采用它吧。 为此,我们使用来自 - 注册免费计划后,Lucene 搜索引擎的全部功能就在我们手中。
我们仍然需要向我们的项目添加一个包
Install-Package Serilog.Sinks.Logzio
并通过向记录器提供访问令牌来将适当的 Enricher 添加到我们的记录器配置中
LoggerConfiguration loggerConfig = new LoggerConfiguration();
loggerConfig.WriteTo.Logzio(secrets.LogzioToken, 10, TimeSpan.FromSeconds(10), null, LogEventLevel.Debug);
通过运行该应用程序,我们不仅可以在控制台中观察消息,还可以在 Kibane 中观察消息。

接口

在服务类型的应用程序中,它与外界交互的两个主要界面可以区分;我们将它们表示为垂直和水平。 垂直接口是一个 Web API,来自客户端应用程序的调用通过它到达。 Horizontal 是一个消息代理,用于与其他内部服务交换数据。
让我们考虑一下每个接口上关联的实现阶段。
HTTP 请求中的相关性
为了获得尽可能多的信息,我们需要在尽可能接近活动开始时生成相关 ID,即在网关上或直接在客户端(移动或网络)上。 由于我们今天处理的是后端应用程序,因此我们将简单地在其上指示对 Web api 的所有请求中强制“X-Correlation-ID”标头的要求。
添加包 ,其作用是从header中取出我们需要的值
Install-Package CorrelationID
将其添加到请求处理管道中
public class Startup
{
public void Configure(IApplicationBuilder application)
{
application
.UseCorrelationId(new CorrelationIdOptions
{
Header = "X-Correlation-ID",
IncludeInResponse = false,
UpdateTraceIdentifier = false,
UseGuidForCorrelationId = false
});
}
}
现在让我们用它制作一个简单的动作过滤器:
public sealed class ApiRequestFilter : ActionFilterAttribute
{
public ApiRequestFilter(IApiRequestTracker apiRequestTracker, ICorrelationContextAccessor correlationContextAccessor)
{
_correlationContextAccessor = correlationContextAccessor ?? throw new ArgumentNullException(nameof(correlationContextAccessor));
}
private readonly ICorrelationContextAccessor _correlationContextAccessor;
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
if (!Guid.TryParse(_correlationContextAccessor.CorrelationContext.CorrelationId, out Guid correlationId))
{
context.Result = new BadRequestResult();
return;
}
await next.Invoke();
}
public override async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
{
await next.Invoke();
}
}
并将其添加到控制器中
[Route("[controller]")]
[ApiController]
[ServiceFilter(typeof(ApiRequestFilter))]
public class CarsController : ControllerBase
{
}
因此,对于没有带有相应标识符的标头的所有请求,控制器都会显示 400 Bad request。
当我们开始从客户端接收标识符后,我们必须将其添加到日志记录上下文中,我们将为此创建一个框架层:
public class CorrelationIdContextLogger
{
public CorrelationIdContextLogger(RequestDelegate next)
{
_next = next ?? throw new ArgumentNullException(nameof(next));
}
readonly RequestDelegate _next;
public async Task InvokeAsync(HttpContext httpContext, ILogger<CorrelationIdContextLogger> logger, ICorrelationContextAccessor correlationContextAccessor)
{
if (Guid.TryParse(correlationContextAccessor.CorrelationContext.CorrelationId, out Guid correlationId))
{
using (logger.BeginScopeWith(("CorrelationId", correlationId)))
{
await _next(context);
}
}
else
{
await _next(context);
}
}
}
在我们的应用程序中,我们使用 Microsoft.Extensions.Logging.Abstractions 包中的标准 ILogger,因此我们将使用简单的扩展为其添加一个值。
public static IDisposable BeginScopeWith(this ILogger logger, params (string key, object value)[] keys)
{
return logger.BeginScope(keys.ToDictionary(x => x.key, x => x.value));
}
我们向请求处理管道添加一层并获得所需的结果。
public class Startup
{
public void Configure(IApplicationBuilder application)
{
application.UseMiddleware<CorrelationIdContextLogger>();
}
}
现在,由对我们的 Web API 的请求生成的所有活动都包含一个相关标识符,通过该标识符可以轻松链接它们。

代理消息中的关联
下一步是通过消息代理设置相关标识符的传输和接收。 在我们的示例中,我们将使用 RabbitMQ,作为客户端,我们将采用 MassTransit 框架 (MassTransit)。 再次,让我们跳过使用 MassTransit 的初始设置,直接设置日志记录。
首先,我们可以启用 MassTransit 本身的日志,为此我们将向我们的应用程序添加一个包
Install-Package MassTransit.SerilogIntegration
现在,将记录器添加到 MassTransit 设置后,我们将能够看到框架日志。
services
.AddSingleton(provider =>
{
return Bus.Factory.CreateUsingRabbitMq(cfg =>
{
cfg.UseSerilog();
});
});
让我们的应用程序发送带有“done”值的 SomethingDoneMessage 事件作为对 POST 请求的响应。 这种消息的契约可以描述如下:
namespace MbMessages
{
public interface ISomethingDoneMessageV1
{
string Value { get; }
}
}
MassTransit 消息本质上是一个包含代理消息的信封。 信封看起来像这样:
{
"messageId": "59020000-5dba-0015-10b8-08d77ec28593",
"requestId": "59020000-5dba-0015-5674-08d77ec28592",
"conversationId": "59020000-5dba-0015-bca8-08d77ec28594",
"destinationAddress": "rabbitmq://bear.rmq.cloudamqp.com/aelzlsta/ya.servicetemplate.receiveendpoint",
"headers": {},
"messageType": [
"urn:message:MbMessages:ISomethingDoneMessageV1"
],
"message": {
"value": "done"
}
}
该消息显示了框架本身工作所需的服务字段,但我们可以将自己的附加属性添加到此信封中。 此外,MassTransit 具有用于处理一些可选字段的内置工具,其中我们最感兴趣的是 CorrelationId。
将 CorlatedBy 接口添加到消息契约中:
namespace MbMessages
{
public interface ISomethingDoneMessageV1 : CorrelatedBy<Guid>
{
string Value { get; }
}
}
让我们实现它并在创建消息时为 CorrelationId 属性赋值:
internal class SomethingDoneMessageV1 : ISomethingDoneMessageV1
{
internal SomethingDoneMessageV1(Guid correlationId, string value)
{
CorrelationId = correlationId;
Value = value;
}
public Guid CorrelationId { get; private set; }
public string Value { get; private set; }
}
如果我们查看更新后的消息,我们将看到相关标识符不仅成为消息的一部分,而且也成为信封的一部分 - 该标识符现在也将在所有 MassTransit 日志中使用,这意味着它将变得更加容易供我们处理消息代理级别的问题。
{
"messageId": "59020000-5dba-0015-10b8-08d77ec28593",
"requestId": "59020000-5dba-0015-5674-08d77ec28592",
"conversationId": "59020000-5dba-0015-bca8-08d77ec28594",
"correlationId": "c7ff562a-b639-415b-9add-c9e524a727cc",
"destinationAddress": "rabbitmq://bear.rmq.cloudamqp.com/aelzlsta/ya.servicetemplate.receiveendpoint",
"headers": {},
"messageType": [
"urn:message:MbMessages:ISomethingDoneMessageV1"
],
"message": {
"correlationId": "c7ff562a-b639-415b-9add-c9e524a727cc",
"value": "Hello"
}
}
我们仍然需要配置消息的这些服务属性的日志记录,为此我们将向项目添加一个包 。 该包向 MassTransit 消息处理管道添加了一个过滤器,将消息上下文推送到线程安全堆栈上。 Serilog 从堆栈中读取上下文并将这些附加属性添加到我们的日志对象中。
Install-Package Serilog.Enrichers.MassTransitMessage
在 MassTransit 中插入过滤器
services
.AddSingleton(provider =>
{
return Bus.Factory.CreateUsingRabbitMq(cfg =>
{
cfg.UseSerilog();
cfg.UseSerilogMessagePropertiesEnricher();
});
});
并在Serilog配置中,添加一个Enricher
Log.Logger = new LoggerConfiguration()
.Enrich.FromMassTransitMessage()
.CreateLogger();
由于从 RabbitMQ 队列接收消息的应用程序可以访问 MassTransit 信封的所有属性,因此我们可以在消费应用程序内使用接收到的相关标识符,并沿着调用链进一步传递它。
因此,我们的日志不仅在同一服务内开始包含 CorrelationId,而且在与其他应用程序交互时也包含 CorrelationId。

因此,.Net 应用程序中生成的日志系统使我们能够毫无问题地关联来自完全不同的微服务的日志 - 即使是那些通过消息代理工作的日志系统。 在Elasticsearch的帮助下,我们可以通过在Kibana中构建我们需要的仪表板来快速方便地分析日志(帖子中的图片显示了一个示例)。
当然,以这种形式登录不会涵盖您的服务和各种外部系统交互的复杂选项,但在项目开发之初建立这样的顺序是您会不止一次感谢自己的事情之一。
您可以了解项目中生成的系统的源代码:
来源: habr.com
