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().