Как стать автором
Обновить

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

Время на прочтение6 мин
Количество просмотров14K

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

Теги:
Хабы:
Всего голосов 4: ↑4 и ↓0+4
Комментарии0

Публикации

Истории

Работа

Ближайшие события