Как работать с идентификатором запроса в приложениях .NET

Как работать с идентификатором запроса в приложениях .NET

26 октября 2023 г.

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

Идентификатор запроса

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

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

Идентификатор запроса служит следующим целям:

* Быстрый поиск журналов, связанных с конкретным запросом пользователя. Особенно полезно при обнаружении ошибки * Для отслеживания услуг. Полезно знать, какая из системных служб медленно обрабатывает запросы

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

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

Приступим к реализации. Для начала нам нужен класс, в котором мы будем хранить наш идентификатор запроса.

public class SessionData
{
   private string _requestId = Guid.NewGuid().ToString();

   public string RequestId
   {
       get => _requestId;

       set
       {
           if (!string.IsNullOrEmpty(value))
               _requestId = value;
       }
   }
}

n Здесь по умолчанию заполняем RequestId; это необходимо, если у нас приложение без API, но с фоновыми заданиями. То есть мы являемся инициаторами запроса. Мы также гарантируем, что RequestId никогда не будет пустым.

Далее нам нужно заполнить этот класс и убедиться, что этот экземпляр используется на протяжении всего времени обработки запроса. И здесь нам на помощь придет DI. Мы просто добавим его как Score в нашу коллекцию сервисов, а затем будем получать новый класс при каждом запросе к нашему сервису.

services.AddScoped<SessionData>();

Теперь нам нужно прочитать идентификатор запроса из заголовка. Для этого мы будем использовать промежуточное программное обеспечение.

public class RequestIdMiddleware
{
   private readonly RequestDelegate _next;

   public RequestIdMiddleware(RequestDelegate next)
   {
       _next = next;
   }

   public async Task InvokeAsync(HttpContext context, SessionData sessionData)
   {
       sessionData.RequestId = context.Request.Headers["X-Request-Id"];
       await _next(context);
   }
}

Отлично. Теперь давайте подключим наше промежуточное ПО:

app.UseMiddleware<RequestIdMiddleware>();

n Чтобы проверить работу, я напишу простой контроллер, который будет возвращать наш идентификатор запроса и вызывать метод службы, который будет печатать идентификатор запроса.

[ApiController]
[Route("request_id")]
public class RequestIdController: Controller
{
   private readonly SessionData _sessionData;
   private readonly RequestIdService _requestIdService;

   public RequestIdController(
       SessionData sessionData,
       RequestIdService requestIdService)
   {
       _sessionData = sessionData;
       _requestIdService = requestIdService;
   }

   [HttpGet]
   public ActionResult<string> Get()
   {
       _requestIdService.PrintRequestId();
       return _sessionData.RequestId;
   }
}

А вот и наш сервис. Не забудьте добавить его в IServiceCollection:

public class RequestIdService
{
   private readonly SessionData _sessionData;
   public RequestIdService(SessionData sessionData)
   {
       _sessionData = sessionData;
   }
   public void PrintRequestId()
   {
       Console.WriteLine($"RequestIdService request_id: {_sessionData.RequestId}");
   }
}

При вызове службы с заголовком X-Request-Id = SomeId. Нам возвращается наш RequestId SomeId + в консоли выводится строка RequestIdService request_id: SomeId.

Супер. Теперь давайте добавим его в заголовок при вызове другой службы. Для отправки сообщений я создал вспомогательный класс HttpSendHelper.

public class HttpSendHelper
{
   private readonly HttpClient _httpClient;
   private readonly SessionData _sessionData;

   public HttpSendHelper(
       HttpClient httpClient,
       SessionData sessionData)
   {
       _httpClient = httpClient;
       _sessionData = sessionData;
   }

   public async Task<string> GetRequestAsync(string url, CancellationToken cancellationToken)
   {
       using var request = new HttpRequestMessage(HttpMethod.Get, url);
       request.Headers.Add("X-Request-Id", _sessionData.RequestId);

       var response = await _httpClient.SendAsync(request, cancellationToken);
       string stringResult = await response.Content.ReadAsStringAsync(cancellationToken);

       return stringResult;
   }
}

n Затем я написал сервис, который вызывает метод нашего ранее написанного контроллера. Не забудем добавить его в IServiceCollection.

public class ExternalApiService
{
   private readonly HttpSendHelper _httpSendHelper;

   public ExternalApiService(HttpSendHelper httpSendHelper)
   {
       _httpSendHelper = httpSendHelper;
   }

   public Task<string> GetRequestIdAsync(CancellationToken cancellationToken)
   {
       return _httpSendHelper.GetRequestAsync("http://localhost:5000/request_id", cancellationToken);
   }
}

Давайте добавим в наш контроллер еще один метод:

[HttpGet("external")]
public Task<string> GetExternal(CancellationToken cancellationToken)
{
   return _externalApiService.GetRequestIdAsync(cancellationToken);
}

Теперь нам нужно запустить наше приложение дважды. И вызвать новый метод.


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE