Изображение с: https://www.elastic.co/guide/en/apm/get-started/current/images/apm-architecture-cloud.png

APM расшифровывается как Application Performance Monitoring (мониторинг производительности приложений). Если на вашем пути встречается эта аббревиатура, то речь скорее всего идет о измерении производительность вашего приложения и ваших серверов. Как они справляются, сколько памяти они потребляют, где узкие места? И это далеко не все. С помощью APM можно настроить специальные уведомления, которые будут оповещать вас, например, о том, что потребление памяти достигло очень высоких показателей или удаленный вызов занимает слишком много времени. Триггеры для подобных уведомлений могут опираться на довольно широкий набор показателей и событий. Но давайте не будем забегать вперед.

Немного истории

Когда я еще работал в Pathao, мы использовали New Relic. New Relic очень удобен. Чтобы начать с ним работу в PHP вам просто нужно установить его пакет и агент. И на этом все. Вы сразу же увидите всю необходимую информацию на дашборде New Relic, как только сервер начнет обслуживать запросы. Но моя нынешняя компания (Digital Healthcare Solutions, ранее известная как Telenor Health), предложила Elastic APM. Поэтому мне пришлось разбираться, как он работает. До этого момент я уже как минимум 4 раза пробовал заставить себя освоить Elastic Search, но все безуспешно. Поэтому я искал доступные готовые пакеты. И я нашел кое-что. Это был очень хороший пакет, но под капотом он отправлял HTTP-запросы на сервер APM. Что на самом деле достаточно затратно. И он не поддерживал APM Server 7.x.

github.com

Вот почему мне пришлось создать свой собственный пакет практически с нуля. В этой статье я собираюсь продемонстрировать вам, как можно использовать мой пакет с абсолютно любым PHP-кодом. Пакет уже достаточно просто использовать с Laravel или Lumen. Но вам не обязательно использовать что-либо из этого. Прелесть заключается в том, что вы можете использовать его как захотите.

Что такое Elastic APM?

Обратили ли вы внимание на изображение вначале статьи? Давайте ненадолго вернемся к нему. На этом изображении отражена основная структура того, как это все работает. Вам нужно установить APM-агент для вашего конкретного языка (APM-агенты до сих пор поддерживают не все языки, так что имейте это ввиду). Этот агент будет собирать данные о выполнении вашего кода. Затем он будет отправлять их на APM-сервер. Далее серверы APM передают эти данные на сервер Elasticsearch, и вы сможете просмотреть эти данные в Kibana. Собственно, ничего сверхъестественного. Но я обнаружил, что APM Dashboard UI поставляется с XPack, а это означает, что вам придется немного раскошелиться.

Инсталляция

Elastic предоставляет APM-агента для PHP. Этот агент будет собирать данные с нашего сервера и передавать их на APM-сервер. Свое PHP-приложение я разместил в докер-контейнере. Докер-файл выглядит следующим образом:

FROM php:7.4-fpm

RUN apt-get update

RUN apt-get install -y libpq-dev libpng-dev curl nano unzip zip git jq supervisor

RUN docker-php-ext-install pdo_pgsql

RUN pecl install -o -f redis

RUN docker-php-ext-enable redis

RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

RUN apt-get -y update && apt-get -y install build-essential autoconf libcurl4-openssl-dev --no-install-recommends
RUN mkdir -p /home/apm-agent && \
    cd /home/apm-agent && \
    git clone https://github.com/elastic/apm-agent-php.git apm && \
    cd apm/src/ext && \
    /usr/local/bin/phpize && ./configure --enable-elastic_apm && \
    make clean && make && make install
COPY ./elastic_apm.ini /usr/local/etc/php/conf.d/elastic_apm.ini
RUN mkdir /app
WORKDIR /app


CMD ["php", "-S", "0.0.0.0:80", "index.php"]

Докер-файл для PHP-контейнера. Игнорируйте раздел CMD.

В приведенном выше фрагменте вам стоит обратить внимание на  команды в строке 16. Мы клонируем git-репозиторий APM, а затем настраиваем его и устанавливаем в наш контейнер.

В строке 22 мы копируем .ini файл. Вот этот .ini файл:

extension=elastic_apm.so
elastic_apm.bootstrap_php_part_file=/home/apm-agent/apm/src/bootstrap_php_part.php
elastic_apm.enabled=true
elastic_apm.server_url="http://docker.for.mac.localhost:8200"
; elastic_apm.secret_token=
elastic_apm.service_name="PHP APM Test Service"
; service_version= ${git rev-parse HEAD}
elastic_apm.log_level=DEBUG
; Available Log levels
; OFF | CRITICAL | ERROR | WARNING | NOTICE | INFO | DEBUG | TRACE

elastic_apm.ini file

В файле elastic_apm.ini строка 2 указывает путь к нашему файлу клонированного git-репозитория – src/bootstrap_php_part.php. Строка 4 указывает URL APM-сервера. Если вы не используете докер, вы можете сделать git clone репозитория, а затем просто инсталлировать его. Дальше мы интегрируем с php расширение. Это все, что касается установки APM-агента.

Прежде чем мы углубимся в сам пакет, вы можете получить некоторые сведения об основных понятиях из документации по агенту.

По сути все сводится всего к двум:

  • Транзакция: Когда ваше приложение работает, оно генерирует транзакции. Каждый запрос считается транзакцией. Каждая транзакция имеет имя и тип.

  • Спан: Когда вы выполняете какой-ниубдь код, информация, с которой вы имеете дело, может быть отправлена ​​на сервер в виде спана. Спан (span) — это запись операции, выполняемая одним фрагментом кода. Запрос к базе данных может быть спаном, точно так же как и информация HTTP-запроса.

Сам пакет и работа с ним

github.com

Итак, сначала давайте разберемся, как интегрировать пакет с PHP. Затем мы рассмотрим, как он интегрируется с Laravel/Lumen. Сам агент требует PHP версии ≥ 7.2, вот почему минимальное требование для пакета – PHP 7.2.

Инсталляция

composer require anik/elastic-apm-php

Интеграция с PHP

  • Класс Anik\ElasticApm\Agent является публичной точкой доступа для всех взаимодействий. Класс Agent не может быть инстанцирован – он представляет собой синглтон. То есть, всякий раз, когда вам нужно как-либо с ним взаимодействовать, вы будете вызывать Agent::instance(). Вы получите один и тот же объект, откуда бы вы его не вызывали, на протяжении всего жизненного цикла запроса.

  • Чтобы задать транзакции имя и тип вам нужно будет инстанцировать объект Anik\ElasticApm\Transaction с параметрами name и type. После успешного инстанцирования объекта вам нужно будет передать его в класс Agent посредством его метода setTransaction.

Agent::instance()->setTransaction(new Transaction('name', 'type'));
  • Если вы хотите отправить данные этой транзакции на сервер, вам придется использовать спан. Чтобы можно было создать новый спан, должен быть реализован интерфейс Anik\ElasticApm\Contracts\SpanContract, в котором можно найти такие методы, как getSpanData(), getName(), getType(), getSubType(). Но если вы используете трейт Anik\ElasticApm\Spans\SpanEmptyFieldsTrait, тогда вы можете опустить методы getAction() и getLabels(). Если же вы хотите отправлять данные на свой APM-сервер, то вам не помешало бы реализовать эти методы. Я не буду здесь останавливаться на возвращаемых значениях методов, вы можете найти эту информацию в документации по агенту приведенной выше.

  • Когда реализация класса Span будет готова, вы сможете добавлять спан следующим образом:

Agent::instance()->addSpan(" class="formula inline">implementedSpanObject);
  • Наконец, когда вы закончите добавлять спаны, то, прежде чем возвращать результат, вам нужно будет отправить все эти транзакции и спаны в APM-агент. Для этого используйте

Agent::instance()->capture();

Приведенный выше метод обрабатывает все транзакции и спаны, а затем передает их агенту, после чего агент берет на себя заботу об отправке их на сервер.

Примечание: Если вы хотите делать все самостоятельно, вы можете использовать Agent::getElasticApmTransaction(), чтобы получить текущую транзакцию агента, или Agent::newApmTransaction($name, $type), чтобы создать новую транзакцию. Обязательно вызывайте метод end(), если вы создали новый объект Transaction. Или, если вы хотите поместить спаны, которые вы добавляете, в новую транзакцию, вы можете использовать метод Agent::captureOnNew(), чтобы отправить их с новой транзакцией. Вам не нужно вызывать end, когда вы используете captureOnNew. Если вам вдруг понадобиться получить свежий инстанс Agent, вы можете сначала вызвать Agent::reset(), а потом Agent::instance(), но Agent::reinstance() будет делать то же самое. Наконец, имейте в виду, что если вы вызываете любой из методов capture*(), то с ним должен быть предоставлен объект Transaction. Без объекта Transaction вы получите исключение Anik\ElasticApm\Exceptions\RequirementMissingException.

На этом мы закончили с интеграцией с PHP.

Интеграция с Laravel/Lumen

Для Laravel:

  • Пакет уже поддерживает функцию обнаружения пакетов. Но все же добавьте Anik\ElasticApm\Providers\ElasticApmServiceProvider::class в массив providers вашего config/app.php.

  • Добавьте Anik\ElasticApm\Facades\Agent::class в массив facade вашего config/app.php.

  • Запустите php artisan vendor:publish, чтобы опубликовать файл конфигурации.

Для Lumen:

  • Вам не нужно возиться с Facade, чтобы использовать этот пакет.

  • Скопируйте elastic-apm.php из каталога src/config пакета в каталог config вашего lumen-проекта.

// в ваш файл bootstrap/app.php.
use Anik\ElasticApm\Providers\ElasticApmServiceProvider;
$app->register(ElasticApmServiceProvider::class);
$app->configure('elastic-apm');

Вы также можете смело изменять файл конфигурации в соответствии с вашими требованиями.

Отслеживание ошибок приложений

Если вы хотите отправить данные об ошибке на ваш APM-сервер, то

  • Для Laravel, в bootstrap/app.php

// ЭТОТ РАЗДЕЛ СЛЕДУЕТ ЗАКОММЕНТИРОВАТЬ
/**
 *  $app->singleton(
 *      Illuminate\Contracts\Debug\ExceptionHandler::class,
 *      App\Exceptions\Handler::class
 *  );
 */
// ИСПОЛЬЗУЙТЕ ЭТОТ РАЗДЕЛ
use Illuminate\Contracts\Debug\ExceptionHandler;
use Anik\ElasticApm\Exceptions\Handler;
use App\Exceptions\Handler as AppExceptionHandler;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use GuzzleHttp\Exception\ConnectException;
$app->singleton(ExceptionHandler::class, function ($app) {
    return new Handler(new AppExceptionHandler($app), [
        // NotFoundHttpException::class, //(1)
        // ConnectException::class, //(2)
    ]);
});

Anik\ElasticApm\Exceptions\Handler принимает массив классов исключений в качестве второго параметра (который не будет отправляться на APM-сервер). По умолчанию ошибки NotFoundHttpException не отправляются на APM-сервер. Строчки, помеченные (1) и (2) были закомментированы, чтобы указать вам на это.

Если ваше приложение сталкивается с ошибкой, которая была успешно перехвачена обработчиком исключений (Exception Handler), и при этом уже настроены транзакции, то APM-сервер гарантированно получит стек-трейс ошибки. Поскольку PHP-агент не предоставляет API для отправки стека-трейса, ваш трейс может быть обрезан обработчиком исключений.

В ответе были возвращены код 500 (отмечено) и исключение со стек-трейсом.
В ответе были возвращены код 500 (отмечено) и исключение со стек-трейсом.

Отслеживание запросов и ответов вашего приложения

Для отслеживания количества запросов, которые обслуживает ваше приложение, кодов состояния и времени, затраченного на их обработку, у нас есть специальное встроенное middleware.

  • Для Laravel, в вашем классе app/Http/Kernel.php:

<?php
use Anik\ElasticApm\Middleware\RecordForegroundTransaction;
class Kernel extends HttpKernel {
    protected $middleware = [
        // ...        
        RecordForegroundTransaction::class,
        // ..
    ];
}
  • Для Lumen, в вашем файле bootstrap/app.php:

use Anik\ElasticApm\Middleware\RecordForegroundTransaction;
$app->middleware([
    // ...
    RecordForegroundTransaction::class,
    // ...
]);

Имя транзакции для обрабатываемых запросов подчиняется следующей логике:

  • Если route handler использует параметр uses, т.е.; HomeController@index (метод контроллера).

  • Если обработчик маршрута использует  параметр as, т.е.;['as' => 'home.index'] (именованный маршрут).

  • Если это не вариант выше, то тогда – HTTP_VERB ROUTE_PATH, т.е.; GET /user/api.

  • Если ничего не подходит, 404, затем используется index.php или предоставленное пользователем имя из конфигурации.

Транзакция запроса (Шаг 1)
Транзакция запроса (Шаг 1)
Называем маршрут как транзакцию (шаг 2)
Называем маршрут как транзакцию (шаг 2)
Маршрут с глаголом (шаг 3)
Маршрут с глаголом (шаг 3)
Маршруты не найдены (Шаг 4)
Маршруты не найдены (Шаг 4)
Спан с обработанным запросом
Спан с обработанным запросом

Отслеживание удаленных HTTP-вызовов 

Если вы используете Guzzle, вы можете использовать встроенное middleware для Guzzle.

use GuzzleHttp\HandlerStack;
use GuzzleHttp\Client;
use Anik\ElasticApm\Middleware\RecordHttpTransaction;

$stack = HandlerStack::create();
$stack->push(new RecordHttpTransaction(), 'whatever-you-wish');
$client = new Client([
    'base_uri' => 'https://httpbin.org',
    'timeout'  => 10.0,
    'handler'  => $stack,
]);
$client->request('GET', '/');
Отслеживание удаленного HTTP-вызова
Отслеживание удаленного HTTP-вызова

Отслеживание работника очереди

Чтобы отслеживать задачи (jobs), вам необходимо использовать встроенное (Job) middleware всякий раз, когда вы диспатчите новую задачу. Вы можете использовать любое из приведенных ниже:

  • Из класса с методом middleware:

use Anik\ElasticApm\Middleware\RecordBackgroundTransaction;
use Illuminate\Contracts\Queue\ShouldQueue;
class TestSimpleJob implements ShouldQueue 
{
    public function middleware () {
        return [ new RecordBackgroundTransaction()];
    }
    
    public function handle () {
        app('log')->info('job is handled');
    }
}
  • В противном случае, при диспатче задачи:

use Anik\ElasticApm\Middleware\RecordBackgroundTransaction as JM;
use App\Jobs\ExampleJob;
dispatch((new ExampleJob())->through([new JM()]);
Отслеживание обработки задач
Отслеживание обработки задач

Примечание: Если вы используете  php artisan queue:work, то это значит, что задача выполняется достаточно долго. Именно поэтому будет отправлена только одна транзакция. Если не создается процесс, то вы не получите ни транзакции, ни спана. С другой стороны, если вы используете queue:listen, т.е.: php artisan queue:listen – будет использован новый процесс для каждой задачи, поэтому вы получите новую транзакцию и спаны для этой транзакции для каждой задачи.

Отслеживание выполнения запроса

Выполнение запроса обрабатывается автоматически и передается на APM-сервер.

Выполнение запроса
Выполнение запроса

Вот и все. Надеюсь, вам понравилось. Не забудьте поставить этому проекту звезду)

Отслеживание выполнения Redis-запросов 

Выполнение Redis-запроса не обрабатывается автоматически. Если вы используете Redis в качестве драйвера кэширования (Cache Driver), вам нужно явно указать, что вы хотите включить Redis Query Logging, добавив ELASTIC_APM_SEND_REDIS=true в ваш .env файл.

Выполнение Redis-запросов
Выполнение Redis-запросов

А также в целях саморазвития, вот вам docker-compose.yml файл для ES, Kibana и APM (Не используйте в продакшене!)

version: "2"

services:
    php:
        build:
            dockerfile: php.dockerfile
            context: .
        volumes:
            - .:/app
        ports:
            - 8008:80
        links:
            - apm
    
    elasticsearch:
        image: bitnami/elasticsearch:7.8.0
        volumes:
            - ~/.backup/elasticsearch/elastic-apm:/bitnami/elasticsearch/data
            - ./bitnami_es_config.yml:/opt/bitnami/elasticsearch/config/elasticsearch.yml
        ports:
            - 60200:9200
        environment:
            - BITNAMI_DEBUG=true
    
    kibana:
        image: bitnami/kibana:7.8.0
        ports:
            - 5601:5601
        volumes:
            - ~/.backup/kibana/elastic-apm:/bitnami
        links:
            - elasticsearch
        environment:
            - KIBANA_ELASTICSEARCH_URL=elasticsearch
    
    apm:
        image: docker.elastic.co/apm/apm-server-oss:7.8.0
        ports:
            - 8200:8200
        user: apm-server
        links:
            - elasticsearch
            - kibana
        command: --strict.perms=false
        environment:
            - apm-server.host=0.0.0.0
            - apm-server.kibana.enabled=true
            - apm-server.kibana.host="http://kibana:5601"
            - output.elasticsearch.hosts=["elasticsearch:9200"]
            - output.elasticsearch.max_retries=1
    
    apm2:
        image: docker.elastic.co/apm/apm-server-oss:6.8.9
        ports:
            - 8201:8200
        user: apm-server
        links:
            - elasticsearch
            - kibana
        command: --strict.perms=false
        environment:
            - apm-server.host=0.0.0.0
            - apm-server.kibana.enabled=true
            - apm-server.kibana.host="http://kibana:5601"
            - output.elasticsearch.hosts=["elasticsearch:9200"]
            - output.elasticsearch.max_retries=1

docker-compose.yml


Материал подготовлен в преддверии старта онлайн-курса "PHP Developer. Professional".

Комментарии (2)


  1. MARDEN
    21.04.2023 13:59
    +1

    обратить внимание на команды в строке 16.

    В строке 22 мы копируем .ini файл.

    Предлагаете читателям самостоятельно нумеровать листинги?)


  1. BasilioCat
    21.04.2023 13:59

    Было бы логично рассмотреть интеграцию в код OpenTelemetry а не привязываться к одному вендору. А OpenTelemetry коллектор может отсылать трейсы и в ElasticAPM