Мной давно не публиковались статьи и вот опять… Данная статья получилась не очень большой, но, надеюсь, полезной. Когда-то мы решили использовать для сбора метрик 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 TestClientMetricsService (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 в качестве инструмента для сбора метрик.
