Логирование в микросервисной среде .Net на практике

от автора

Логирование является очень важным инструментом разработчика, но при создании распределённых систем оно становится камнем, который нужно заложить прямо в фундамент вашего приложения, иначе сложность разработки микросервисов очень быстро даст о себе знать.

В .Net Core 3 добавилась отличная возможность передачи контекста корреляции в HTTP-заголовках, поэтому если ваши приложения используют прямые HTTP-вызовы для межсервисного взаимодействия, то вы можете воспользоваться этой коробочной функцональностью. Однако, если архитектура вашего бекенда подразумевает взаимодействие через брокера сообщений (RabbitMQ, Kafka и т.п.), то вам по-прежнему необходимо озаботиться темой передачи корелляционного контекста через эти сообщения самостоятельно.

В этой статье мы возьмём простое веб-апи приложение и организуем логирование, которое будет

  • сохранять сквозную корелляцию между логами независимых сервисов так, чтобы можно было легко посмотреть все активности, которые были вызваны конкретным запросом с клиента

  • иметь единую точку входа с удобным анализом, чтобы инструментом логирования смогла пользоваться даже Поддержка, к которой летят вопросы вроде «у меня тут в приложении выскочила ошибка с таким-то айдишником запроса»

Во-первых, нам необходимо определиться с поставщиком логирования в нашем приложении. Главное требование к современному логированию это структурность, т.е. мы должны работать не с плоскими текстовыми сообщениями, а с объектами. Благодаря таким логам мы можем легко строить представления наших сообщений в разных разрезах и проводить аналитику.

Для нашего приложения мы воспользуемся пакетом Serilog (Серилог), который имеет отличную поддержку структурного логирования и богатую систему дополнений. Я опущу базовые этапы его настройки (вы можете найти большое количество статей на эту тему) и сделаю допущение о том, что

  • Серилог уже сконфигурирован и является логером по-умолчанию у вашего поставщика внедрения зависимостей

  • в его конфигурации включено обогащение сообщений свойствами контекста (Enrich.FromLogContext)

Следующим шагом необходимо выбрать в какую систему централизованного сбора логов посылать сообщения из Serilog. Пожалуй, самый распространённый на сегодня вариант из открытого ПО это стек ELK (Elasticsearch, Logstash и Kibana), его и возьмём. Для этого воспользуемся предложением от Logz.IO — после регистрации на бесплатном тарифе в наших руках оказывается вся мощь поискового движка Lucene.

Нам остаётся добавить в наш проект пакет Serilog.Sinks.Logzio

Install-Package Serilog.Sinks.Logzio 

И добавить соответствующий энричер в конфигурацию нашего логера, скормив ему токен доступа

LoggerConfiguration loggerConfig = new LoggerConfiguration(); loggerConfig.WriteTo.Logzio(secrets.LogzioToken, 10, TimeSpan.FromSeconds(10), null, LogEventLevel.Debug); 

Запустив приложение мы сможем наблюдать наши сообщения не только в консоли, но и в Кибане.

Интерфейсы

В приложении сервисного типа можно выделить два главных интерфейса его взаимодействия с внешним миром, обозначим их как вертикальный и горизонтальный. Вертикальный интерфейс — это веб-апи, через который прилетают вызовы от клиентского приложения. Горизонтальный — это брокер сообщений, который используется для обмена данными с другими внутренними сервисами.

Рассмотрим этапы внедрения корелляционности на каждом из этих интерфейсов.

Корелляция в HTTP-запросах

Чтобы получать как можно больше информации нам необходимо генерировать идентификатор корелляции как можно ближе к началу активности, т.е. на шлюзе или прямо на клиенте (мобильном или веб). Поскольку мы сегодня имеем дело с бекендным приложением, то просто обозначим на нём требование обязательного заголовка «X-Correlation-ID» во всех запросах к веб-апи.

Добавляем пакет CorrelationID, функция которого заключается в заборе значения из необходимого нам заголовка

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         });     } } 

Теперь с его помощью сделаем простой action-фильтр:

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);         }     } } 

В нашем приложении мы используем стандартный ILogger из пакета Microsoft.Extensions.Logging.Abstractions, поэтому значение будем добавлять с помощью нехитрого расширения к нему.

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>();     } } 

Теперь все активности, которые порождены запросами к нашему веб-апи, содержат корелляционный идентификатор по которому их можно легко связать.

Корелляция в сообщениях брокера

Следующим шагом нам необходимо наладить передачу и приём корелляционного идентификатора через брокер сообщений. В нашем примере мы будем использовать RabbitMQ, а в качестве клиента возьмём фреймворк MassTransit (МассТранзит). Опять же, опустим первоначальную настройку работы с МассТранзита и перейдём сразу к настройке логирования.

Для начала мы можем включить логи самого МассТранзита, для этого добавим в наше приложение пакет MassTransit.SerilogIntegration

Install-Package MassTransit.SerilogIntegration 

Теперь после добавления логера в настройки MassTransit мы сможем видеть логи фреймворка.

services     .AddSingleton(provider =>         {             return Bus.Factory.CreateUsingRabbitMq(cfg =>             {                 cfg.UseSerilog();             });         }); 

Пусть наше приложение в качестве реакции на POST-запрос отправляет событие SomethingDoneMessage со значением «done». Контракт такого сообщения можно описать так:

namespace MbMessages {     public interface ISomethingDoneMessageV1     {         string Value { get; }     } } 

Сообщения МассТранзита по сути являются конвертом, в который вложены сообщения брокера. Выглядит конверт примерно так:

{   "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; } } 

Если мы посмотрим на обновлённое сообщение, то увидим что идентификатор корелляции стал не только частью нашего сообщения, но и частью конверта — этот идентификатор теперь будет также использоваться во всех логах МассТранзита, а значит нам будет гораздо проще разбираться с проблемами на уровне брокера сообщений.

{   "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"   } } 

Нам осталось настроить логирование этих служебных свойств сообщения, для этого добавим в проект пакет Serilog.Enrichers.MassTransitMessage. Пакет добавляет фильтр в конвейер обработки сообщений MassTransit, который складывает контекст сообщения в потокобезопасный стек. Серилог читает контекст из стека и добавляет в наши объекты логов эти дополнительные свойства.

Install-Package Serilog.Enrichers.MassTransitMessage 

В МассТранзите вставляем фильтр

services     .AddSingleton(provider =>         {             return Bus.Factory.CreateUsingRabbitMq(cfg =>             {                 cfg.UseSerilog();                 cfg.UseSerilogMessagePropertiesEnricher();             });         }); 

А в конфигурации Серилога добавляем энричер

Log.Logger = new LoggerConfiguration()     .Enrich.FromMassTransitMessage()     .CreateLogger(); 

Поскольку приложение, которое получает сообщение из очереди RabbitMQ, имеет доступ ко всем свойствам конверта MassTransit, мы можем использовать полученный идентификатор корелляционности внутри приложения-потребителя, а также передавать его дальше по всей цепочке вызовов.

В результате наши логи стали содержать CorrelationId не только в пределах одного сервиса, но и при взаимодействии с другими приложениями.

Итак, полученная система логирования в .Net приложениях позволяет нам без особых проблем скореллировать логи из абсолютно разных микросервисов — даже тех, что работают через брокера сообщений. А с помощью Elasticsearch мы можем быстро и удобно проводить анализ логов, построив в Кибане необходимые нам дашборды (пример приведён на картинке к посту).

Разумеется, в таком виде логирование не покроет сложные варианты взаимодействия ваших сервисов и различных внешних систем, но наведение подобного порядка в самом начале развития проекта — это одна из тех вещей, за которые вы сами себе не раз скажете спасибо.

Поразбираться в исходном коде получившейся системы вы можете в проекте: github.com/a-postx/YA.ServiceTemplate

ссылка на оригинал статьи https://habr.com/ru/post/489188/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *