Symfony Bundle для экспорта статистик в формате Prometheus



    Работая над разными микро (и не очень) сервисами, написанными с помощью Symfony, я каждый раз сталкивался с необходимостью экспорта метрик для Prometheus. Поначалу мы просто копировали один и тот же код между проектами, но когда их стало больше трёх, я посчитал, что дальше так продолжаться не может (не буду вас тут утомлять объяснениями, почему это неудобно; думаю, каждый это и сам понимает).

    Было решено выделить всё это хозяйство в отдельный бандл: ArtprimaPrometheusMetricsBundle.

    Выбрав общие метрики для всех приложений, я принялся за написание бандла. В качестве библиотеки был выбран клиент Jimdo/prometheus_client_php, который позволяет использовать в качестве хранилища статистик: Redis (через ext-redis), APCu (через ext-apcu) и InMemory (в памяти PHP, лишь на время жизни процесса). Оговорюсь сразу, что оттестированным на практике у нас является только APCu (мне нужно было иметь per-instance статистики, тогда как в Redis будут разделяемые, если только не деплоить Redis к каждому инстансу). А InMemory вообще, имхо, может использоваться только для тестов.

    С Jimdo/prometheus_client_php сразу возникла проблема — релиза давно не было, и нужные фичи (APCu) в виде релиза недоступны. В связи с этим я сделал свой форк клиента, который отличается лишь только тем, что там есть нужный таг для формирования релиза.

    Вторая проблема, с которой я столкнулся — это рецепты для Symfony. В официальный репо можно даже не соваться, а в community очень долго апрувят пулл-реквесты (на данный момент бандлу почти месяц, а PR рецепта до сих пор находится на рассмотрении). В связи с чем при установке нужно провести необходимые настройки вручную. Благо их не так много.

    Собственно, вам нужно настроить основную конфигурацию:

    #config/packages/prometheus-metrics.yaml
    
    artprima_prometheus_metrics:
        # namespace вашего приложения в метриках prometheus
        namespace: myapp
    
        # хранилище для метрик
        type: in_memory # возможные значения: in_memory, apcu, redis
    
        # Игнорирование некоторых маршрутов, чтобы запросы к ним не попадали в метрики
        ignored_routes: [some_route_name, another_route_name]
    
        # Настройка для типа хранилища "redis" (в остальных случаях игнорируются)
        redis:
            host: 127.0.0.1
            port: 6379
            timeout: 0.1
            read_timeout: 10
            persistent_connections: false
            password: ~
    

    Затем нужно экспортировать маршрут:

    # делает доступным маршрут /metrics/prometheus
    app_metrics:
        resource: '@ArtprimaPrometheusMetricsBundle/Resources/config/routing.xml'
    

    Или же вы можете настроить маршрут вручную:

    app_metrics:
      path: /mypath/mymetrics
      controller: Artprima\PrometheusMetricsBundle\Controller\MetricsController::prometheus
    

    Готово. В результате ваши метрики будут доступны по адресу http[s]://your_host/metrics/prometheus (если вы не меняли путь). И после некоторого времени работы вы получите что-то вроде этого:

    пример метрик
    # HELP myapp_http_2xx_responses_total total 2xx response count
    # TYPE myapp_http_2xx_responses_total counter
    myapp_http_2xx_responses_total{action="GET-app_ep1"} 3911
    myapp_http_2xx_responses_total{action="GET-app_ep2"} 29974
    myapp_http_2xx_responses_total{action="GET-app_ep3"} 323
    myapp_http_2xx_responses_total{action="GET-app_ep4"} 18
    myapp_http_2xx_responses_total{action="GET-app_ep5"} 3627
    myapp_http_2xx_responses_total{action="GET-app_search"} 5962
    myapp_http_2xx_responses_total{action="GET-app_ep8"} 42967
    myapp_http_2xx_responses_total{action="GET-app_variants"} 641
    myapp_http_2xx_responses_total{action="all"} 87423
    # HELP myapp_http_4xx_responses_total total 4xx response count
    # TYPE myapp_http_4xx_responses_total counter
    myapp_http_4xx_responses_total{action="GET-app_ep2"} 71
    myapp_http_4xx_responses_total{action="GET-app_ep8"} 2584
    myapp_http_4xx_responses_total{action="all"} 2660
    # HELP myapp_http_requests_total total request count
    # TYPE myapp_http_requests_total counter
    myapp_http_requests_total{action="GET-app_ep1"} 3922
    myapp_http_requests_total{action="GET-app_ep2"} 30098
    myapp_http_requests_total{action="GET-app_ep3"} 468
    myapp_http_requests_total{action="GET-app_ep4"} 18
    myapp_http_requests_total{action="GET-app_ep5"} 3631
    myapp_http_requests_total{action="GET-app_search"} 5964
    myapp_http_requests_total{action="GET-app_ep8"} 45556
    myapp_http_requests_total{action="GET-app_variants"} 644
    myapp_http_requests_total{action="all"} 90301
    # HELP myapp_instance_name app instance name
    # TYPE myapp_instance_name gauge
    myapp_instance_name{instance="myapp-77c4d96744-cf9nz"} 1
    # HELP myapp_request_durations_histogram_seconds request durations in seconds
    # TYPE myapp_request_durations_histogram_seconds histogram
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep1",le="0.005"} 0
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep1",le="0.01"} 0
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep1",le="0.025"} 0
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep1",le="0.05"} 0
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep1",le="0.075"} 0
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep1",le="0.1"} 0
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep1",le="0.25"} 59
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep1",le="0.5"} 1915
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep1",le="0.75"} 3115
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep1",le="1"} 3553
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep1",le="2.5"} 3879
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep1",le="5"} 3911
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep1",le="7.5"} 3911
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep1",le="10"} 3911
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep1",le="+Inf"} 3911
    myapp_request_durations_histogram_seconds_count{action="GET-app_ep1"} 3911
    myapp_request_durations_histogram_seconds_sum{action="GET-app_ep1"} 2413.185
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep2",le="0.005"} 0
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep2",le="0.01"} 0
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep2",le="0.025"} 50
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep2",le="0.05"} 62
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep2",le="0.075"} 63
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep2",le="0.1"} 72
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep2",le="0.25"} 914
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep2",le="0.5"} 2761
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep2",le="0.75"} 14326
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep2",le="1"} 23571
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep2",le="2.5"} 29569
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep2",le="5"} 30045
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep2",le="7.5"} 30045
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep2",le="10"} 30045
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep2",le="+Inf"} 30045
    myapp_request_durations_histogram_seconds_count{action="GET-app_ep2"} 30045
    myapp_request_durations_histogram_seconds_sum{action="GET-app_ep2"} 26552.86
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep3",le="0.005"} 141
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep3",le="0.01"} 144
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep3",le="0.025"} 144
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep3",le="0.05"} 144
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep3",le="0.075"} 145
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep3",le="0.1"} 145
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep3",le="0.25"} 145
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep3",le="0.5"} 199
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep3",le="0.75"} 408
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep3",le="1"} 442
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep3",le="2.5"} 468
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep3",le="5"} 468
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep3",le="7.5"} 468
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep3",le="10"} 468
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep3",le="+Inf"} 468
    myapp_request_durations_histogram_seconds_count{action="GET-app_ep3"} 468
    myapp_request_durations_histogram_seconds_sum{action="GET-app_ep3"} 216.495
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep4",le="0.005"} 0
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep4",le="0.01"} 0
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep4",le="0.025"} 0
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep4",le="0.05"} 4
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep4",le="0.075"} 10
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep4",le="0.1"} 13
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep4",le="0.25"} 16
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep4",le="0.5"} 17
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep4",le="0.75"} 18
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep4",le="1"} 18
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep4",le="2.5"} 18
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep4",le="5"} 18
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep4",le="7.5"} 18
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep4",le="10"} 18
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep4",le="+Inf"} 18
    myapp_request_durations_histogram_seconds_count{action="GET-app_ep4"} 18
    myapp_request_durations_histogram_seconds_sum{action="GET-app_ep4"} 2.291
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep5",le="0.005"} 0
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep5",le="0.01"} 0
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep5",le="0.025"} 1
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep5",le="0.05"} 16
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep5",le="0.075"} 97
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep5",le="0.1"} 269
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep5",le="0.25"} 2308
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep5",le="0.5"} 3351
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep5",le="0.75"} 3566
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep5",le="1"} 3612
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep5",le="2.5"} 3627
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep5",le="5"} 3627
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep5",le="7.5"} 3627
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep5",le="10"} 3627
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep5",le="+Inf"} 3627
    myapp_request_durations_histogram_seconds_count{action="GET-app_ep5"} 3627
    myapp_request_durations_histogram_seconds_sum{action="GET-app_ep5"} 959.773
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_search",le="0.005"} 0
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_search",le="0.01"} 0
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_search",le="0.025"} 18
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_search",le="0.05"} 547
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_search",le="0.075"} 968
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_search",le="0.1"} 1387
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_search",le="0.25"} 3784
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_search",le="0.5"} 5377
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_search",le="0.75"} 5811
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_search",le="1"} 5913
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_search",le="2.5"} 5959
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_search",le="5"} 5960
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_search",le="7.5"} 5960
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_search",le="10"} 5960
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_search",le="+Inf"} 5962
    myapp_request_durations_histogram_seconds_count{action="GET-app_search"} 5962
    myapp_request_durations_histogram_seconds_sum{action="GET-app_search"} 1516.982
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep8",le="0.005"} 0
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep8",le="0.01"} 0
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep8",le="0.025"} 2724
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep8",le="0.05"} 8348
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep8",le="0.075"} 18828
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep8",le="0.1"} 28811
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep8",le="0.25"} 39709
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep8",le="0.5"} 43958
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep8",le="0.75"} 45161
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep8",le="1"} 45436
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep8",le="2.5"} 45541
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep8",le="5"} 45549
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep8",le="7.5"} 45549
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep8",le="10"} 45549
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_ep8",le="+Inf"} 45551
    myapp_request_durations_histogram_seconds_count{action="GET-app_ep8"} 45551
    myapp_request_durations_histogram_seconds_sum{action="GET-app_ep8"} 6125.712
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_variants",le="0.005"} 0
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_variants",le="0.01"} 0
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_variants",le="0.025"} 0
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_variants",le="0.05"} 252
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_variants",le="0.075"} 451
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_variants",le="0.1"} 511
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_variants",le="0.25"} 583
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_variants",le="0.5"} 631
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_variants",le="0.75"} 640
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_variants",le="1"} 641
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_variants",le="2.5"} 641
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_variants",le="5"} 641
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_variants",le="7.5"} 641
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_variants",le="10"} 641
    myapp_request_durations_histogram_seconds_bucket{action="GET-app_variants",le="+Inf"} 641
    myapp_request_durations_histogram_seconds_count{action="GET-app_variants"} 641
    myapp_request_durations_histogram_seconds_sum{action="GET-app_variants"} 62.744
    myapp_request_durations_histogram_seconds_bucket{action="all",le="0.005"} 145
    myapp_request_durations_histogram_seconds_bucket{action="all",le="0.01"} 148
    myapp_request_durations_histogram_seconds_bucket{action="all",le="0.025"} 2942
    myapp_request_durations_histogram_seconds_bucket{action="all",le="0.05"} 9378
    myapp_request_durations_histogram_seconds_bucket{action="all",le="0.075"} 20567
    myapp_request_durations_histogram_seconds_bucket{action="all",le="0.1"} 31213
    myapp_request_durations_histogram_seconds_bucket{action="all",le="0.25"} 47523
    myapp_request_durations_histogram_seconds_bucket{action="all",le="0.5"} 58214
    myapp_request_durations_histogram_seconds_bucket{action="all",le="0.75"} 73050
    myapp_request_durations_histogram_seconds_bucket{action="all",le="1"} 83191
    myapp_request_durations_histogram_seconds_bucket{action="all",le="2.5"} 89707
    myapp_request_durations_histogram_seconds_bucket{action="all",le="5"} 90224
    myapp_request_durations_histogram_seconds_bucket{action="all",le="7.5"} 90224
    myapp_request_durations_histogram_seconds_bucket{action="all",le="10"} 90224
    myapp_request_durations_histogram_seconds_bucket{action="all",le="+Inf"} 90228
    myapp_request_durations_histogram_seconds_count{action="all"} 90228
    myapp_request_durations_histogram_seconds_sum{action="all"} 37850.074


    Естественно, что базовых метрик скоро может не хватить, поэтому у вас всегда есть возможность написать свой генератор метрик (в дополнение к существующему) и экспортировать его вот так:

    # config/services.yaml
    
        App\Metrics\MyMetricsGenerator:
            tags:
                - { name: prometheus_metrics_bundle.metrics_generator }
    

    При этом ваш генератор должен имплементировать интерфейс Artprima\PrometheusMetricsBundle\Metrics\MetricsGeneratorInterface. Подсмотреть, как может выглядеть генератор можно в Artprima\PrometheusMetricsBundle\Metrics\AppMetrics. Главное, что вам тут нужно знать, это то, что метод collectRequest вызывается на событие kernel.request, а метод collectResponse — на событие kernel.terminate.

    Принимаю критику по существу. Как в виде советов, так и PR :-)

    P.S. Предупреждая замечания о том, что мы изобрели велосипед: да, я знаю, что есть альтернативы. И, скорее всего, они конечно же лучше. В частности одна из них — TweedeGolfPrometheusBundle. К сожалению, я уже не помню, почему мы не выбрали это решение, но чуть больше полугода назад мы его пробовали, и отказались от него. Так или иначе, я считаю, что иметь выбор всегда хорошо, поэтому наш бандл и был опубликован в OpenSource.
    Поделиться публикацией
    Комментарии 2
      0
      Сразу говорю, я профан в prometheus (только бегло глянул что и как), но чем не подошёл github.com/prometheus/pushgateway?
        0
        Из фака самого Prometheus:

        Why do you pull rather than push?

        Pulling over HTTP offers a number of advantages:

        • You can run your monitoring on your laptop when developing changes.
        • You can more easily tell if a target is down.
        • You can manually go to a target and inspect its health with a web browser.

        Overall, we believe that pulling is slightly better than pushing, but it should not be considered a major point when considering a monitoring system.


        Мы, например, используем эндпоинт с метриками для Prometheus, чтобы еще побочно мониторить живость и доступность сервиса с помощью Pingdom.

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое