
日誌記錄對於開發人員來說是一個非常重要的工具,但是在建立分散式系統時,它就成為需要奠定應用程式基礎的基石,否則開發微服務的複雜性很快就會顯現出來。
.Net Core 3 增加了一個很棒的 ,因此如果您的應用程式使用直接 HTTP 呼叫進行服務間通信,您可以利用這個開箱即用的功能。但是,如果您的後端架構涉及透過訊息代理(RabbitMQ,Kafka等)進行交互,那麼您仍然需要擔心自己透過這些訊息傳遞關聯上下文。
在本文中,我們將採用一個簡單的 Web API 應用程式並組織日誌記錄,以便
維護獨立服務日誌之間的端對端關聯,以便您可以輕鬆查看由客戶端的特定請求引起的所有活動
有一個方便分析的單一入口點,這樣即使是收到諸如“我在應用程式中遇到了某個請求 ID 的錯誤”之類的問題的支援人員也可以使用日誌記錄工具
首先,我們需要決定應用程式的日誌提供者。現代日誌記錄的主要要求是結構,即我們不應該處理平面文字訊息,而應該處理物件。有了這樣的日誌,我們可以輕鬆地在不同部分建立訊息的表示並進行分析。
對於我們的應用程序,我們將使用 Serilog 包,它對結構化日誌記錄和豐富的附加系統有出色的支援。我將跳過設定的基本步驟(你可以找到很多關於這個主題的文章),並假設
Serilog 已配置好且是依賴注入提供者的預設記錄器。
其配置包括使用上下文屬性來豐富訊息(Enrich.FromLogContext)
下一步是選擇從 Serilog 傳送訊息到哪個集中式日誌收集系統。也許當今最常看到的開源選項是 ELK 堆疊(Elasticsearch、Logstash 和 Kibana),所以讓我們採用它。為此,我們將使用 — 註冊免費方案後,Lucene 搜尋引擎的全部功能就掌握在我們手中。
我們剩下要做的就是在我們的專案中添加一個包
Install-Package Serilog.Sinks.Logzio
並將相應的豐富器新增至我們的記錄器配置中,並為其提供存取權杖
LoggerConfiguration loggerConfig = new LoggerConfiguration();
loggerConfig.WriteTo.Logzio(secrets.LogzioToken, 10, TimeSpan.FromSeconds(10), null, LogEventLevel.Debug);
透過運行該應用程序,我們不僅能夠在控制台中看到我們的訊息,而且還可以在 Kibana 中看到我們的消息。

接口

在服務型應用中,我們可以區分出它與外界互動的兩個主要介面;我們將它們指定為垂直和水平。垂直介面是一個 Web API,客戶端應用程式的呼叫透過該介面到達。 Horizontal 是一個訊息代理,用於與其他內部服務交換資料。
讓我們考慮一下在每個介面上實現關聯的階段。
HTTP 請求中的相關性
為了獲取盡可能多的信息,我們需要盡可能在活動開始時生成關聯標識符,即在網關上或直接在客戶端(移動或網路)上。由於我們今天要處理後端應用程序,因此我們將簡單地在所有對 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 本身的日誌,為此,我們將向我們的應用程式添加一個包
Install-Package MassTransit.SerilogIntegration
現在,將記錄器新增至 MassTransit 設定後,我們將能夠看到框架日誌。
services
.AddSingleton(provider =>
{
return Bus.Factory.CreateUsingRabbitMq(cfg =>
{
cfg.UseSerilog();
});
});
假設我們的應用程式發送一個 SomethingDoneMessage 事件,其值為“done”,作為對 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。
讓我們將 CorrelatedBy 介面加入到訊息契約中:
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 中建立所需的儀表板來快速方便地分析日誌(文章圖片中顯示了一個範例)。
當然,登入此表單不會涵蓋您的服務與各種外部系統之間的複雜交互,但在專案開發之初建立這樣的秩序是您會不止一次感謝自己的事情之一。
您可以在專案中了解最終系統的源代碼:
來源: www.habr.com
