PHP Profi

Как связать Monolog и E.L.K. Перевод

E.L.K. — это отличный стек для хранения, управления и мониторинга логов. Monolog — это отличная PHP-библиотека для логирования. Давайте заставим их работать вместе.

Что такое Monolog

Монолог — это библиотека PHP для работы с логами. Взгляните на неё на github. Вы можете установить её через composer, простой командой composer require monolog/monolog.

В двух словах, Monolog предоставляет вам логер, в который вы посылаете ваши лог-записи. Этот логер имеет несколько обработчиков, которые рассылают эти записи туда, где они вам потребуются. Монолог имеет множество реализованных разных обработчиков, которые позволяют вам легко отправлять логи в разные места, например, в файлы, на электронную почту, в slack, в logstash и т. д..

Полный список обработчиков можно посмотреть в документации Monolog-а. Вы можете использовать Monolog как есть, но есть готовые интеграции Monolog-а в различные фреймворки, что очень упрощает жизнь.

Для Nette есть пакет Kdyby.
Для Yii2 есть пакет от Mero и официальный.
Symfony(2+) и Laravel(4+) используют Monolog из коробки.
Также из коробки его поддерживают: Silex, Lumen, FuelPHP, Equip Framework..
Доступны пакеты для:
CakePHP: cakephp-monolog;
Slim: Slim-Monolog.

Что такое стек ELK

Стек ELK (сейчас известный как Elastic-стек) состоит из Elasticsearch, Logstash и Kibana. Эти три технологии используются для мощного манипулирования логами.

Logstash используется для управления журналами. Он собирает, анализирует и сохраняет их. Имеет много реализованных входящих потоков("приёмников" – как/откуда принимать), фильтров и исходящих потоков (куда отправлять).

Elasticsearch — это мощная распределенная NoSQL база данных с REST API. В Elastic-стеке, Elasticsearch используется как постоянное хранилище для наших журналов.

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

Интеграция Monolog и ELK Stack

Как я уже упоминал, Monolog имеет много обработчиков, которые могут отправлять логи. Logstash имеет много различных "приёмников", так что существует не один способ, чтобы соединить их, а мы можем выбрать из нескольких вариантов.

Прямой вывод логов в Elasticseach

Самый простой вариант – обойти Logstash и отправлять логи напрямую в Elasticsearch с помощью ElasticSearchHandler. Это самый простой подход, очень прост в настройке, но имеет некоторые недостатки, когда ваша инфраструктура становится более сложной. Если ваше приложение работает на другом сервере, чем ваш Elasticsearch, вам ещё нужно повозиться с аутентификацией и безопасностью Elasticsearch.

И, очевидно, вы не можете использовать сам Logstash, но это не особо и проблема, поскольку одна из основных фич Logstash – это форматирование логов и предварительная обработка, а Monolog тоже всё это умеет, но в PHP, который является более удобным, чем конфиг Logstash-а.

Gelf

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

Monolog предоставляет GelfHandler. Самое большое возможное преимущество использования Gelf – это GelfMessageFormatter, который добавляет много полезной информации в ваши журналы.

Но к сожалению, вы не можете использовать это форматирование напрямую с другими обработчиками, т. к. он выводит объект Gelf\Message.

Вот как использовать его в Monolog в нативном PHP при тестировании на localhost:

$host = '127.0.0.1';
$port = 12201;
$handler = new \Monolog\Handler\GelfHandler(new \Gelf\Publisher(new \Gelf\Transport\UdpTransport($host, $port)));

В Symfony, вместо этого, вы можете указать его в config.yml:

monolog:
    logstash:
        type: gelf
        publisher:
            hostname: 127.0.0.1
            port: 12201
        formatter: monolog.formatter.gelf_message

Теперь мы должны настроить logstash, чтобы принять журналы GelfHandler. Мы просто в logstash.conf добавляем настройку gelf в секцию input:

input {
    gelf {
        port => 12201
        codec => "json"
    }
}

Когда вы экспериментируете с Logstash, удобно настроить его на стандартный вывод, чтобы мы сразу могли видеть все входящие логи:

output {
    stdout {
        codec => rubydebug
    }
}

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

Теперь мы можем проверить, получает ли Logstash логи извне в нужном нам виде:

echo '{"version": "1.1","host":"example.org","short_message":"A short message that helps you identify what is going on","full_message":"Backtrace here\n\nmore stuff","level":1,"_user_id":9001,"_some_info":"foo","_some_env_var":"bar"}' | gzip | nc -u -w 1 127.0.0.1 12201

Этот bash-скрипт отправляет сообщение в формате json по протоколу UDP на 127.0.0.1 / порт 12201. Если мы видим это сообщение в выводе logstash-а, он работает правильно.

Давайте теперь проверим отправляет ли наше PHP-приложение логи в Logstash, просто отправив некое сообщение в наш Monolog-логер:

$logger = new \Monolog\Logger('main', [$handler]);
$logger->log(\Monolog\Logger::INFO, 'short message');

Опять же, если мы видим short message в выводе Logstash-а, значит, всё ок.

Если вы используете symfony/console, вы можете создать команду для отправки сообщений в логи. Я использую эту команду для тестирования отправки и доставки сообщений:

declare(strict_types=1);

namespace AppBundle\Command;

use Psr\Log\LoggerInterface;
use Symfony\Bridge\Monolog\Logger;
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class GenerateLogsCommand extends ContainerAwareCommand {
    /** @var LoggerInterface */
    private $logger;

    public function __construct(LoggerInterface $logger, ?string $name = null) {
        parent::__construct($name);
        $this->logger = $logger;
    }

    /**
     * {@inheritdoc}
     */
    protected function configure(): void {
        $this
            ->setName('app:generate:logs')
            ->setDescription('Just generates some logs to see whether monolog works')
            ->addArgument('level', InputArgument::REQUIRED, 'Level')
            ->addArgument('repeat', InputArgument::OPTIONAL, 'number of repeats', 1);
    }

    /**
     * {@inheritdoc}
     */
    protected function execute(InputInterface $input, OutputInterface $output): void {
        $level = $input->getArgument('level');
        $levels = [
            'debug' => Logger::DEBUG,
            'info' => Logger::INFO,
            'warning' => Logger::WARNING,
            'error' => Logger::ERROR,
        ];
        $repeat = $input->getArgument('repeat');
        for ($i = 0; $i < $repeat; $i++) {
            $this->logger->log($levels[$level], 'This is generated log.');
        }

        $output->writeln("Just wrote $repeat log messages.");
    }
}

Например, php bin/console app:generate:logs info 10 отправляет 10 информационных сообщений.

Когда я сам устанавливал ELK, я провел много часов, пытаясь понять, почему мое приложение не отправляет логи через Gelf. Я просто установил localhost в качестве хоста для Logstash вместо 127.0.0.1, так что мой совет – использовать IP-адрес вместо доменного имени, когда это возможно, потому что это сэкономит вам много часов дебага.

Некоторые заметки для отладки: мы можем просто отправлять сообщение в Logstash по udp из bash-скрипта, т. о. мы можем легко видеть было ли сообщение получено.

Но тестирование отправляет ли наше приложение сообщение по UDP во внешний мир немного сложнее.

На Linux, вы можете использовать tcpdump для этого:

tcpdump -i lo udp port 12201 -vv -X

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

Для Windows, вы можете скачать RawCap здесь и затем использовать его выполнив:

RawCap.exe 127.0.0.1 localhost_capture.pcap

Использование RabbitMQ

RabbitMQ является наиболее широко известной реализацией протокола AMQP.

Основная задача, которую выполняет, – он забирает сообщения на одной стороне и отдаёт их на другой стороне. RabbitMQ очень мощный инструмент, но мы будем использовать только его основные черты.

Логи – по сути, просто сообщения, так что мы можем использовать RabbitMQ в качестве посредника между Monolog-ом и Logstash-ем.

Такой вариант имеет преимущество, потому что RabbitMQ может находиться на той же машине, что и веб-приложение. Это означает, что Monolog отправляет логи только локально и наше приложение не имеет сетевых задержек. А уже потом RabbitMQ отправляет логи в Logstash.

В RabbitMQ есть две важных части — обмен и очередь. Обмен — это как точка входа для сообщений. Очередь — это (на удивление) очередь, она держит наши логи до тех пор, пока их не обработает какой-нибудь получатель (Logstash). Мы можем привязать одну или несколько очередей к одному обмену.

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

Для простоты мы можем использовать RabbitMQ в Docker-е, взяв официальный образ rabbitmq:3-management. Все *-management образы содержат веб-интерфейс управления RabbitMQ, где вы можете экспериментировать с конфигурацией RabbitMQ. Но основной способ настроить "кроликов" – это файл конфигурации, как ни странно. При использовании docker-compose, мы можем запустить RabbitMQ следующим образом:

version: "3"
services:
    rabbitmq:
        image: rabbitmq:3-management
        ports:
            - '5672:5672'
            - '15672:15672'
        volumes:
            - ./rabbitmq-data:/var/lib/rabbitmq/mnesia
            - ./rabbitmq.config /etc/rabbitmq/rabbitmq.config
            - ./rabbit.json /etc/rabbitmq/rabbit.json

Порт 5672 используется для самого RabbitMQ, а 15672 - для веб-интерфейса.

/var/lib/rabbitmq/mnesia – где хранятся данные.

/etc/rabbitmq/rabbitmq.config – config.

/etc/rabbitmq/rabbit.json – тут задаём настройки наших обменов и очередей.

/etc/rabbitmq/rabbitmq.config должен выглядеть так:

[
  {
    rabbit,
      [
        { loopback_users, [] }
      ]
  },
  {
    rabbitmq_management,
      [
        { load_definitions, "/etc/rabbitmq/rabbit.json" }
      ]
  }
]

В rabbitmq_management мы указываем где хранится наш файл с настройками очередей.

/etc/rabbitmq/rabbitmq.config должен выглядеть так:

{
    "rabbit_version": "3.6.12",
    "users": [
        {
            "name": "guest",
            "password_hash": "iG25ELqd4wB2c3pmqBwdI4nH9czcb8JKRZSEVSuyuhOienVF",
            "hashing_algorithm": "rabbit_password_hashing_sha256",
            "tags": "administrator"
        }
    ],
    "vhosts": [
        {
            "name": "/"
        }
    ],
    "permissions": [
        {
            "user": "guest",
            "vhost": "/",
            "configure": ".*",
            "write": ".*",
            "read": ".*"
        }
    ],
    "parameters": [],
    "global_parameters": [
        {
            "name": "cluster_name",
            "value": "rabbit@5a81d356219a"
        }
    ],
    "policies": [],
    "queues": [
        {
            "name": "logs",
            "vhost": "/",
            "durable": true,
            "auto_delete": false,
            "arguments": {}
        }
    ],
    "exchanges": [
        {
            "name": "logs",
            "vhost": "/",
            "type": "fanout",
            "durable": true,
            "auto_delete": false,
            "internal": false,
            "arguments": {}
        }
    ],
    "bindings": [
        {
            "source": "logs",
            "vhost": "/",
            "destination": "logs",
            "destination_type": "queue",
            "routing_key": "",
            "arguments": {}
        }
    ]
}

Теперь у нас есть настроенный RabbitMQ, который готов для подключения к Monolog-у и Logstash-у.

Нам нужно изменить наш logstash.conf и настроить входной поток:

input {
    rabbitmq {
        host => [my host ip]
        port => 5672
        queue => "logs"
        durable => true
        passive => true
        exchange => "logs"
        user => "guest"
        password => "guest"
    }
}

guest:guest - это пользователь по умолчанию в RabbitMQ, обязательно смените его в продакшене.

Для Monolog-а, настройка немного дольше.

  1. Мы устанавливаем либу amqp такой командой composer install php-amqplib/php-amqplib.
  2. Этот шаг отличается при использовании в Symfony и нативном PHP:

    • Для Symfony: мы регистрируем необходимые сервисы в services.yml:

      PhpAmqpLib\Channel\AMQPSocketConnection:
          arguments:
              $connection: '@PhpAmqpLib\Connection\AMQPSocketConnection'
      PhpAmqpLib\Connection\AMQPConnection:
          arguments:
              $host: localhost
              $port: 5672
              $user: guest
              $password: guest

      Затем, мы добавляем AmqpHandler в config.yml:

      monolog:
          handlers:
              main:
                  type: amqp
                  exchange: 'PhpAmqpLib\Channel\AMQPChannel'
                  exchange_name: 'logs'
                  formatter: 'AppBundle\Monolog\MyFormatter'
                  level: debug
    • В нативном PHP:
      $host = 'localhost';
      $port = 5672;
      $user = 'guest';
      $password = 'guest';
      $connection = new \PhpAmqpLib\Connection\AMQPSocketConnection($host, $port, $user, $password);
      $channel = new \PhpAmqpLib\Channel\AMQPChannel($connection);
      $handler = new \Monolog\Handler\AmqpHandler($channel, 'logs');

Вот теперь мы закончили, и можем радостно отправлять логи с помощью Monolog-а в ELK.

Примечание

Существует несколько возможностей подключения к AMQP. Когда касается скорости, этот benchmark показывает, что AMQPSocketConnection гораздо быстрее, чем AMQPStreamConnection, вот почему я использовал его в этой статье.

Итого

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

2017-11-01 оригинал

Последние посты

Комментарии

авторизуйтесь или зарегистрируйтесь, чтобы оставить комментарий