Добавление трассировки обработки запросов и счётчиков производительности на сервере¶
Начиная с 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");
}
}
}
}
Ниже приведён пример результата трассировки контроллера.