Перейти к содержанию

Добавление трассировки обработки запросов и счётчиков производительности на сервере

Начиная с TESSA 4.0 в платформе появилась поддержка трассировки запросов и счётчиков производительности, с помощью которых можно отслеживать текущее состояние системы и определять узкие места. Напишем пример контроллера, возвращающего карточку по её идентификатору и использующего API трассировки и счётчиков производительности.

Счётчики производительности

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

using System.Collections.Generic; using System.Diagnostics.Metrics; using System.Threading; using Tessa.Platform;

namespace Tessa.Extensions.Server.Counters { public sealed class AbMetrics { private readonly Meter meter;

private long currentCardGetRequests; private readonly Counter<long> totalCardGetRequestsCounter; private readonly ObservableGauge<long> currentCardGetRequestsCounter;

public AbMetrics() { this.meter = new("Ab", "1.0.0");

this.totalCardGetRequestsCounter = this.meter.CreateCounter<long>("ab-total-get-card-requests", description: "Total Get Card Requests"); this.currentCardGetRequestsCounter = this.meter.CreateObservableGauge<long>("ab-current-get-card-requests", () => Interlocked.Read(ref this.currentCardGetRequests), description: "Current Get Card Requests"); }

public void StartGetCardRequest() { Interlocked.Increment(ref this.currentCardGetRequests); }

public void EndGetCardRequest(bool requestSuccessful) { Interlocked.Decrement(ref this.currentCardGetRequests); // Добавляем в счётчики флаг успешности запроса this.totalCardGetRequestsCounter.Add(1, new KeyValuePair<string, object?>("RequestSuccessful", BooleanBoxes.Box(requestSuccessful))); } } }

Зарегистрируем класс со счётчиками производительности в контейнере Unity.

using Unity;

namespace Tessa.Extensions.Server.Counters { [Registrator] public sealed class Registrator : RegistratorBase { public override void RegisterUnity() { this.UnityContainer .RegisterSingleton<AbMetrics>(); } } }

Трассировка запросов

Напишем контроллер, позволяющий получить карточку по её идентификатору. Также воспользуемся API трассировки и классом со счётчиками производительности, написанным выше.

using System; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using NLog; using Tessa.Cards; using Tessa.Extensions.Server.Counters; using Tessa.Platform; using Tessa.Platform.Data; using Tessa.Platform.Validation; using Tessa.Web; using Unity; using ISession = Tessa.Platform.Runtime.ISession;

namespace Tessa.Extensions.Server.Web.Services { [Route("tracing"), AllowAnonymous, ApiController] [ProducesErrorResponseType(typeof(PlainValidationResult))] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] public sealed class AbTracingController : Controller { public AbTracingController(ICardRepository cardRepository, IDbScope dbScope, ISession session, [OptionalDependency(nameof(AbTracingController))] ActivitySource? activitySource = null, [OptionalDependency] AbMetrics? metrics = null) { this.cardRepository = NotNullOrThrow(cardRepository); this.dbScope = NotNullOrThrow(dbScope); this.session = NotNullOrThrow(session); this.activitySource = activitySource; this.metrics = metrics; }

private readonly ICardRepository cardRepository;

private readonly IDbScope dbScope;

private readonly ISession session;

private readonly ActivitySource? activitySource;

private readonly AbMetrics? metrics;

private static readonly ILogger logger = LogManager.GetCurrentClassLogger();

// запрос вида tracing/e594c5fc-b229-41d3-985c-bcbdfa224a0f [HttpGet("{id:guid}"), SessionMethod] [ProducesResponseType(StatusCodes.Status200OK)] public async Task<IActionResult> GetCard(Guid id, CancellationToken cancellationToken = default) { using var getCardActivity = this.activitySource?.StartActivity(nameof(this.GetCard)); if (getCardActivity is { IsAllDataRequested: true }) { // Добавляем различные тэги для отображения в трассировке getCardActivity .AddTag("CardID", id) .AddTag("UserID", this.session.User.ID) .AddTag("UserName", this.session.User.Name); }

this.metrics?.StartGetCardRequest(); var requestSuccessful = false;

try { var cardExists = await this.CardExistsAsync(id, cancellationToken); if (!cardExists) { return this.ErrorView("Карточка не существует", caption: await LocalizeNameAsync("UI_Common_Dialog_Error", cancellationToken)); }

var getCardResponse = await this.GetCardGetResponseAsync(id, cancellationToken); requestSuccessful = getCardResponse.ValidationResult.IsSuccessful(); return await this.TypedJsonAsync(getCardResponse, cancellationToken: cancellationToken); } catch (Exception ex) when (ex is not OperationCanceledException) { logger.LogException(ex, LogLevel.Warn);

// отображаем ошибку на странице в браузере return this.ErrorView(ex, "Ошибка доступа к карточке", caption: await LocalizeNameAsync("UI_Common_Dialog_Error", cancellationToken)); } finally { this.metrics?.EndGetCardRequest(requestSuccessful); } }

private async Task<bool> CardExistsAsync(Guid cardID, CancellationToken cancellationToken) { using var cardExistsActivity = this.activitySource?.StartActivity(nameof(this.CardExistsAsync)); var activityEnabled = cardExistsActivity is { IsAllDataRequested: true }; if (activityEnabled) { cardExistsActivity! .SetTag("CardID", cardID); }

await using var _ = this.dbScope.Create(); var db = this.dbScope.Db;

db .SetCommand( this.dbScope.BuilderFactory .Select().V(1) .From("Instances").NoLock() .Where().C("ID").Equals().P("CardID") .Build(), db.Parameter("CardID", cardID)) .LogCommand();

bool cardExists;

try { cardExists = await db.ExecuteAsync<bool>(cancellationToken); } catch (Exception ex) when (ex is not OperationCanceledException) { if (activityEnabled) { cardExistsActivity! .SetStatus(ActivityStatusCode.Error, ex.GetShortText()); } throw; }

if (activityEnabled) { cardExistsActivity! .SetTag("CardExists", BooleanBoxes.Box(cardExists)); }

return cardExists; }

private async Task<CardGetResponse> GetCardGetResponseAsync(Guid cardID, CancellationToken cancellationToken) { using var getCardGetResponseActivity = this.activitySource?.StartActivity(nameof(this.GetCardGetResponseAsync)); var activityEnabled = getCardGetResponseActivity is { IsAllDataRequested: true }; if (activityEnabled) { getCardGetResponseActivity! .SetTag("CardID", cardID); }

var cardGetRequest = new CardGetRequest { CardID = cardID, ServiceType = CardServiceType.Client };

CardGetResponse cardGetResponse;

try { cardGetResponse = await this.cardRepository.GetAsync(cardGetRequest, cancellationToken); } catch (Exception ex) when (ex is not OperationCanceledException) { if (activityEnabled) { getCardGetResponseActivity! .SetStatus(ActivityStatusCode.Error, ex.GetShortText()); } throw; }

if (activityEnabled) { getCardGetResponseActivity! .SetTag("IsSuccessful", BooleanBoxes.Box(cardGetResponse.ValidationResult.IsSuccessful())); }

return cardGetResponse; } } }

Необходимо зарегистрировать ActivitySource в UnityContainer для созданного контроллера. Так же добавим этот ActivitySource в отдельную группу для более простого взаимодействия.

using Tessa.Platform; using Tessa.Tracing;

namespace Tessa.Extensions.Server.Web.Services { [Registrator] public sealed class Registrator : RegistratorBase { public override void RegisterUnity() { this.UnityContainer .RegisterActivitySource<AbTracingController>(); }

public override void FinalizeRegistration() { if (this.UnityContainer.TryResolve<ITracingSourceNameRegistrator>() is { } tracingSourceNameRegistrator) { // Добавляем трассировку в группу tracingSourceNameRegistrator.Register(nameof(AbTracingController), "Controllers"); } } } }

Ниже приведён пример результата трассировки контроллера.

Back to top