Observability в PHP: трейсинг, метрики и логи — полный гайд

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Observability в PHP: как перестать гадать и начать видеть всё насквозь

Когда ваше PHP-приложение начинает тормозить, выдавать 500-е ошибки или просто вести себя странно, первая реакция — залезть в логи. Но логи (даже идеально структурированные) — это лишь часть пазла. Современная разработка требует гораздо большего: нужно знать как долго выполнялся каждый запрос, какие внешние сервисы тормозят систему, и сколько памяти жрёт конкретный эндпоинт.

Всё это — про observability (наблюдаемость). В этой статье мы разберём три столпа observability в PHP: трейсинг, метрики и логи. Вы узнаете, как настроить реальный мониторинг с примерами кода, и получите готовые рецепты, которые можно внедрить уже сегодня.

Что такое observability и зачем это PHP-разработчику?

Observability (O11y) — это способность понимать внутреннее состояние системы по её внешним выходным данным. В мире PHP это означает, что вы можете ответить на вопросы:

  • Почему конкретный API-запрос занял 5 секунд?
  • Какой SQL-запрос выполнялся дольше всего за последний час?
  • Сколько запросов в секунду обрабатывает ваш сервер прямо сейчас?
  • Где именно «падает» память?

Без observability вы работаете вслепую. С ней — видите каждый чих вашего приложения. И да, это применимо не только к микросервисам на Go или Java. PHP-монолит тоже можно и нужно «наблюдать».

Трейсинг в PHP: распределённая трассировка с OpenTelemetry

Трейсинг (трассировка) — это история одного запроса. Вы видите, как запрос проходит через ваше приложение, какие функции вызываются, какие внешние API дёргаются, и сколько времени тратится на каждый шаг. В PHP для этого используется стандарт OpenTelemetry.

Установка OpenTelemetry для PHP

Сначала установим необходимые пакеты через Composer:

composer require open-telemetry/opentelemetry
composer require open-telemetry/transport-grpc
composer require open-telemetry/exporter-otlp

Теперь настроим базовый трейсер. Создадим файл tracing.php:

// tracing.php
use OpenTelemetry\API\Globals;
use OpenTelemetry\SDK\Trace\TracerProvider;
use OpenTelemetry\SDK\Trace\SpanProcessor\BatchSpanProcessor;
use OpenTelemetry\SDK\Trace\Exporter\ConsoleSpanExporter;

require 'vendor/autoload.php';

// Создаём экспортёр (пока в консоль)
$exporter = new ConsoleSpanExporter();
$processor = new BatchSpanProcessor($exporter);
$tracerProvider = new TracerProvider($processor);

// Регистрируем глобальный трейсер
Globals::setTracerProvider($tracerProvider);

// Получаем трейсер с именем вашего приложения
$tracer = Globals::tracerProvider()->getTracer('my-php-app');

Создание спанов (spans) в реальном коде

Допустим, у вас есть контроллер, который обрабатывает заказ. Обернём ключевые операции в спаны:

// OrderController.php
class OrderController {
    public function createOrder(array $data) {
        $tracer = Globals::tracerProvider()->getTracer('order-service');
        
        // Главный спан — весь запрос
        $span = $tracer->spanBuilder('POST /orders')->startSpan();
        $span->setAttribute('order.id', $data['id'] ?? 'unknown');
        
        try {
            // Спан для проверки пользователя
            $userSpan = $tracer->spanBuilder('validate.user')->startSpan();
            $user = $this->validateUser($data['user_id']);
            $userSpan->end();
            
            // Спан для сохранения в БД
            $dbSpan = $tracer->spanBuilder('save.order')->startSpan();
            $order = $this->saveOrder($data);
            $dbSpan->end();
            
            // Спан для отправки email
            $emailSpan = $tracer->spanBuilder('send.email')->startSpan();
            $this->sendConfirmationEmail($order);
            $emailSpan->end();
            
            $span->end();
            return $order;
        } catch (\Exception $e) {
            $span->recordException($e);
            $span->setStatus(\OpenTelemetry\API\Trace\StatusCode::STATUS_ERROR);
            $span->end();
            throw $e;
        }
    }
}

Важно: в продакшене вы будете отправлять трейсы не в консоль, а в Jaeger, Zipkin или Grafana Tempo. Для этого используйте OTLP-экспортёр:

use OpenTelemetry\Contrib\Otlp\OtlpHttpExporter;
use OpenTelemetry\SDK\Trace\Exporter\ExporterFactory;

$exporter = new OtlpHttpExporter('http://jaeger:4318/v1/traces');

Метрики в PHP: считаем всё, что движется

Метрики — это агрегированные числовые данные: количество запросов, время ответа, использование памяти, количество ошибок. В PHP стандартом де-факто является связка Prometheus + php-exporter или OpenTelemetry Metrics.

Установка Prometheus-клиента для PHP

composer require promphp/prometheus_client_php

Создадим метрики для вашего приложения:

// metrics.php
use Prometheus\CollectorRegistry;
use Prometheus\Storage\InMemory;
use Prometheus\RenderTextFormat;

$registry = new CollectorRegistry(new InMemory());

// Счётчик запросов
$counter = $registry->registerCounter(
    'app_requests_total',
    'Total number of requests',
    ['method', 'endpoint']
);

// Гистограмма времени ответа
$histogram = $registry->registerHistogram(
    'app_response_time_seconds',
    'Response time in seconds',
    ['method', 'endpoint'],
    [0.1, 0.5, 1.0, 2.0, 5.0] // бакеты
);

// Gauge — текущее количество активных пользователей
$gauge = $registry->registerGauge(
    'app_active_users',
    'Current active users'
);

Теперь обновляем метрики в вашем middleware:

// Middleware.php
class MetricsMiddleware {
    public function handle($request, $next) {
        $start = microtime(true);
        $method = $request->getMethod();
        $endpoint = $request->getUri()->getPath();
        
        // Увеличиваем счётчик
        $counter->incBy(1, [$method, $endpoint]);
        
        // Выполняем запрос
        $response = $next($request);
        
        // Записываем время
        $histogram->observe(microtime(true) - $start, [$method, $endpoint]);
        
        return $response;
    }
}

Чтобы Prometheus мог «забрать» метрики, нужно отдавать их в формате plain text по эндпоинту /metrics:

// metrics_endpoint.php
$renderer = new RenderTextFormat();
$result = $renderer->render($registry->getMetricFamilySamples());
header('Content-Type: ' . RenderTextFormat::MIME_TYPE);
echo $result;

Логирование в PHP: структура и контекст

Логи — это классика, но современное логирование — это не просто error_log().

Блоги

Рекомендаций по книгам