Elastic APM в приложении

от автора

Мной давно не публиковались статьи и вот опять… Данная статья получилась не очень большой, но, надеюсь, полезной. Когда-то мы решили использовать для сбора метрик Prometheus, но… Спустя время, мы решили перейти на Elastic APM, т. к. весь стек для Elastic у нас уже был и мы решили поддерживать метрики в рамках этого стека.

Итак, Elastic APM — инструмент для работы с метриками, для этого в приложении используется Elastic APM Agent — агенты для сбора метрик для различных языков. Мы использовали Elastic APM .NET Agent. Пакет поддерживается .NET Framework, начиная с версии 4.6.2.

Агенты отправляют метрики на сервер (Elastic APM Server). Все необходимые настройки прописываются в web.config с определёнными ключами, которые ожидает Elastic APM Agent.
Нам необходимо настроить, как минимум, url для Elastic APM Server. Для того, чтобы разбить, при отображении метрики в ЛК, по приложениям и средам, нам нужны следующие параметры:

  • ElasticApm:ServiceName — имя сервиса, должно удовлетворять следующим правилам: ^[a-zA-Z0-9 _-]+$ .
  • ElasticApm:Environment — имя среды позволяет фильтровать данные на глобальном уровне в приложении. Поддерживается только в Kibana, начиная с версии 7.2.

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

  • ElasticApm:MetricsInterval — позволяет задать время, через какое метрики будут отправлены на сервер, по умолчанию — 5 с. Если время будет установлено в 0, метрики не будут отправляться на сервер. Все измерения для этого параметра ведутся в секундах.

Можно также настроить уровень логгирования: ElasticApm:LogLevel.

Пример заполнения web.config:

<?xml version="1.0" encoding="utf-8"?> <!-- ... --> <configuration>     <!-- ... -->     <appSettings>         <!-- ... -->         <add key="ElasticApm:ServerUrls" value="https://my-apm-server:8200" />         <add key="ElasticApm:MetricsInterval" value="10" />         <add key="ElasticApm:Environment" value="Stage" />         <add key="ElasticApm:ServiceName" value="Web.Api" />         <!-- ... -->     </appSettings>     <!-- ... --> </configuration>

Единицей работы пакета Elastic APM Agent являются транзакции — объекты типа ITransaction. Данные транзакции собираются внутри объекта Reporter и отправляются на сервер раз в заданное время. Настройки времени отправки приведены выше.

Старт транзакции начинается с вызова метода StartTransaction, завершение вызовом метода End(). Помимо методда StartTransaction также можно использовать метод CaptureTransaction, но для него не вызывается метод End(), всё необходимое передается в качестве параметров в метод. Если неободимо зафиксировать исключения, то для этого есть метод CaptureException.

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

Итак, что же получилось? Для начала добавляем модель, которая будет содержать необходимые для нас настройки объекта ITransaction:

public class MeasurementData {     public static string ApmServerUrl => ConfigurationManager.AppSettings["ElasticApm:ServerUrls"] ?? "http://localhost:8200";      public static string MetricsInterval => ConfigurationManager.AppSettings["ElasticApm:MetricsInterval"] ?? "10";      public static string ApmEnvironment => ConfigurationManager.AppSettings["ElasticApm:Environment"] ?? "local";      public static string ServiceName => ConfigurationManager.AppSettings["ElasticApm:ServiceName"] ?? "Api";      public ITransaction MetricsObject { get; set; }      // Создаём объект транзакции     public static ITransaction Create(string metricsName)     {         Environment.SetEnvironmentVariable(ConfigConsts.EnvVarNames.ServerUrls, ApmServerUrl);         Environment.SetEnvironmentVariable(ConfigConsts.EnvVarNames.MetricsInterval, MetricsInterval);         Environment.SetEnvironmentVariable(ConfigConsts.EnvVarNames.Environment, ApmEnvironment);         Environment.SetEnvironmentVariable(ConfigConsts.EnvVarNames.ServiceName, ServiceName);         return Agent.Tracer.StartTransaction(metricsName, ApiConstants.TypeRequest);     } }

Дальше, создаём builder, в котором будем создавать объект транзакции, записывать в транзакцию метаданные, создавать транзакции для сохранения исключения, завершать работу с транзакцией:

public class MetricsBuilder {     private readonly MeasurementData _measurementData = new MeasurementData();     private string _metricName;      public void BuildMetrics(string metricName)     {         // если нам не передали название метрики, обобщаем его         _metricName = string.IsNullOrEmpty(metricName) ? "api_payment_request_duration" : metricName;         CheckAndCreateMetricsObjects();     }      // Добавляем метаданные для нашей метрики     public void AddMetricsLabels(string key, string value)     {         CheckAndCreateMetricsObjects();          if (!_measurementData.MetricsObject.Labels.ContainsKey(key))         {             _measurementData.MetricsObject.Labels.Add(key, value);             return;         }          _measurementData.MetricsObject.Labels[key] = value;     }      // Обрабатываем полученное исключение     public void CaptureMetricException(Exception exception)     {         CheckAndCreateMetricsObjects();         _measurementData.MetricsObject.CaptureException(exception);     }      public void Dispose()     {         CheckAndCreateMetricsObjects();         _measurementData.MetricsObject.End();     }      // Проверяем, была ли уже создан объект метрики,     // если нет - создаем     private void CheckAndCreateMetricsObjects()     {         if (_measurementData.MetricsObject == null)         {             _measurementData.MetricsObject = MeasurementData.Create(_metricName);              Logger.Info($"{nameof(MetricsBuilder)}: CurrentTransaction: {JsonConvert.SerializeObject(Agent.Tracer.CurrentTransaction)} " +                         $"ServerUrls: {JsonConvert.SerializeObject(Agent.Config.ServerUrls)} " +                         $"MetricsIntervalInMilliseconds: {JsonConvert.SerializeObject(Agent.Config.MetricsIntervalInMilliseconds)}");         }     } }  public interface IMetricsService : IDisposable {     void AddMetricsLabels(string key, string value);      void CaptureMetricException(string message, Exception exception); }  public class MetricsService : IMetricsService {     private readonly MetricsBuilder _builder;      public MetricsService(string metricName)     {         _builder = new MetricsBuilder();         Build(metricName);     }      public void AddMetricsLabels(string key, string value)     {         try         {             _builder.AddMetricsLabels(key, value);         }         catch (Exception exception)         {             CaptureMetricException("Can't write metrics labels", exception);         }     }      public void CaptureMetricException(string message, Exception exception)     {         Logger.Error(message, exception);          try         {             _builder.CaptureMetricException(exception);         }         catch (Exception exec)         {             Logger.Error("Can't write capture exception of metrics", exec);         }     }      public void Dispose()     {         try         {             _builder.Dispose();         }         catch (Exception exception)         {             CaptureMetricException($"Can't to do correct dispose of object: {typeof(MetricsService)}", exception);         }     }      private void Build(string metricName)     {         try         {             _builder.BuildMetrics(metricName);         }         catch (Exception exception)         {             CaptureMetricException("Can't create metrics object", exception);         }     } }

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

public class TestClientMetricsService : ITestObject {     private readonly ITestObject _testObject;      public GatewayClientMetricsService(ITestObject testObject)     {         _testObject = testObject;     }      public ProcessingResult TestMethod()     {         return WriteMetrics("Test_Method", () => _testObject.Test());     }      private ProcessingResult WriteMetrics(string methodName, Func<Result> testMethod)     {         using (var metricsService = new MetricsService(methodName))         {             try             {                 metricsService.AddMetricsLabels("assemblyName", Assembly.GetCallingAssembly().FullName);                  var requestResult = testMethod();                  metricsService.AddMetricsLabels("success", requestResult?.Success.ToString() ?? "false");                 metricsService.AddMetricsLabels("resultCode", requestResult?.GetResultCode().ToString());                  return requestResult;             }             catch (Exception exception)             {                 metricsService.CaptureMetricException("Can't write metrics", exception);                 throw;             }         }     } }

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

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


Комментарии

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

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