Оригинал: Track Every Request: Symfony Monitoring with OpenTelemetry and Grafana
Перевод для канала Мы ж программист
Современные веб-приложения требуют прозрачности. Вам необходимо знать, что происходит внутри вашего приложения Symfony, когда пользователи взаимодействуют с ним. В этом руководстве рассказывается, как подключить OpenTelemetry, Monolog и Grafana для отслеживания поведения вашего приложения.
Что вы создадите
Вы настроите систему мониторинга, которая будет собирать три типа данных:
- Трассировки: отслеживание запросов на протяжении всего пути по вашему приложению
- Логи: запись событий на каждом этапе
- Метрики: подсчет операций и измерение производительности
Все данные поступают в Grafana, где вы сможете их визуализировать и проанализировать.
Требования
Вам понадобятся:
- Symfony 7.x
- PHP 8.1 или выше
- Docker и Docker Compose
- Composer
Обзор архитектуры
Комплект работает так:
- Ваше приложение Symfony генерирует трассировки, журналы и метрики
- OpenTelemetry собирает и форматирует эти данные
- Коллектор OpenTelemetry принимает данные
- Коллектор отправляет журналы в Loki, а трассировки — в Tempo
- Grafana считывает данные из Loki и Tempo для отображения всей информации
Шаг 1: ставим зависимости
Начнем с добавления необходимых пакетов в ваш Symfony проект:
composer require open-telemetry/sdk
composer require open-telemetry/exporter-otlp
composer require open-telemetry/opentelemetry-auto-symfony
composer require monolog/monologЭти пакеты предоставят интеграцию с OpenTelemetry SDK.
Шаг 2: настраиваем сервисы Docker
Создайте файл docker-compose.yml в корне проекта:
version: '3.8'
services:
otel-collector:
image: otel/opentelemetry-collector-contrib:0.91.0
command: ["--config=/etc/otel-collector-config.yaml"]
volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
ports:
- "4317:4317"
- "4318:4318"
- "8888:8888"
networks:
- monitoring
tempo:
image: grafana/tempo:2.3.1
command: ["-config.file=/etc/tempo.yaml"]
volumes:
- ./tempo.yaml:/etc/tempo.yaml
- tempo-data:/tmp/tempo
ports:
- "3200:3200"
- "4317"
networks:
- monitoring
loki:
image: grafana/loki:2.9.3
ports:
- "3100:3100"
command: -config.file=/etc/loki/local-config.yaml
networks:
- monitoring
grafana:
image: grafana/grafana:10.2.3
ports:
- "3000:3000"
environment:
- GF_AUTH_ANONYMOUS_ENABLED=true
- GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
volumes:
- grafana-data:/var/lib/grafana
- ./grafana-datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml
networks:
- monitoring
networks:
monitoring:
driver: bridge
volumes:
tempo-data:
grafana-data:Шаг 3: настраиваем коллектор OpenTelemetry
Создайте otel-collector-config.yaml:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
batch:
timeout: 10s
send_batch_size: 1024
exporters:
otlp/tempo:
endpoint: tempo:4317
tls:
insecure: true
loki:
endpoint: http://loki:3100/loki/api/v1/push
format: json
labels:
attributes:
service.name: ""
level: ""
logging:
loglevel: debug
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [otlp/tempo, logging]
logs:
receivers: [otlp]
processors: [batch]
exporters: [loki, logging]Эта конфигурация заставит коллектор получать данные через протокол OTLP, отправлять трейсы в Tempo и логировать в Loki.
Шаг 4: настраиваем Tempo
Создайте tempo.yaml:
server:
http_listen_port: 3200
distributor:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
storage:
trace:
backend: local
local:
path: /tmp/tempo/blocks
compactor:
compaction:
block_retention: 48hШаг 5: настраиваем источники данных Grafana
Создайте grafana-datasources.yaml:
apiVersion: 1
datasources:
- name: Tempo
type: tempo
access: proxy
url: http://tempo:3200
uid: tempo
jsonData:
httpMethod: GET
serviceMap:
datasourceUid: tempo
- name: Loki
type: loki
access: proxy
url: http://loki:3100
uid: loki
jsonData:
maxLines: 1000Шаг 6: настраиваем OpenTelemetry в Symfony
Создайте файл конфигурации config/services.yaml или дополните существующий:
services:
OpenTelemetry\SDK\Trace\TracerProvider:
factory: ['App\OpenTelemetry\TracerProviderFactory', 'create']
OpenTelemetry\API\Trace\TracerInterface:
factory: ['@OpenTelemetry\SDK\Trace\TracerProvider', 'getTracer']
arguments:
- 'symfony-app'
- '1.0.0'
App\OpenTelemetry\TracerProviderFactory: ~Шаг 7: создаем фабрику TracerProvider
Создайте src/OpenTelemetry/TracerProviderFactory.php:
<?php
namespace App\OpenTelemetry;
use OpenTelemetry\API\Trace\TracerInterface;
use OpenTelemetry\SDK\Trace\TracerProvider;
use OpenTelemetry\SDK\Trace\SpanProcessor\SimpleSpanProcessor;
use OpenTelemetry\SDK\Resource\ResourceInfo;
use OpenTelemetry\SDK\Resource\ResourceInfoFactory;
use OpenTelemetry\SDK\Common\Attribute\Attributes;
use OpenTelemetry\Contrib\Otlp\SpanExporter;
use OpenTelemetry\Contrib\Otlp\OtlpHttpTransportFactory;
use OpenTelemetry\SDK\Common\Export\TransportInterface;
class TracerProviderFactory
{
public static function create(): TracerProvider
{
$transport = (new OtlpHttpTransportFactory())->create(
'http://localhost:4318/v1/traces',
'application/json'
);
$exporter = new SpanExporter($transport);
$resource = ResourceInfoFactory::emptyResource()->merge(
ResourceInfo::create(
Attributes::create([
'service.name' => 'symfony-app',
'service.version' => '1.0.0',
'deployment.environment' => $_ENV['APP_ENV'] ?? 'dev',
])
)
);
return new TracerProvider(
new SimpleSpanProcessor($exporter),
null,
$resource
);
}
}Эта фабрика создает трейсер, который шлет пакеты в ваш коллектор OpenTelemetry.
Шаг 8: настраиваем Monolog для OpenTelemetry
Дополним config/packages/monolog.yaml:
monolog:
handlers:
main:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug
formatter: monolog.formatter.json
otlp:
type: service
id: App\Monolog\OtlpHandler
level: info
when@prod:
monolog:
handlers:
main:
type: fingers_crossed
action_level: error
handler: nested
nested:
type: stream
path: php://stderr
formatter: monolog.formatter.json
otlp:
type: service
id: App\Monolog\OtlpHandler
level: warningШаг 9: создаем обработчик логов OTLP
Создайте src/Monolog/OtlpHandler.php:
<?php
namespace App\Monolog;
use Monolog\Handler\AbstractProcessingHandler;
use Monolog\LogRecord;
use OpenTelemetry\API\Logs\LoggerProviderInterface;
use OpenTelemetry\SDK\Logs\LoggerProvider;
use OpenTelemetry\SDK\Logs\LogRecordExporter;
use OpenTelemetry\SDK\Logs\Processor\SimpleLogRecordProcessor;
use OpenTelemetry\Contrib\Otlp\LogsExporter;
use OpenTelemetry\Contrib\Otlp\OtlpHttpTransportFactory;
use OpenTelemetry\SDK\Resource\ResourceInfo;
use OpenTelemetry\SDK\Resource\ResourceInfoFactory;
use OpenTelemetry\SDK\Common\Attribute\Attributes;
class OtlpHandler extends AbstractProcessingHandler
{
private LoggerProviderInterface $loggerProvider;
public function __construct()
{
parent::__construct();
$transport = (new OtlpHttpTransportFactory())->create(
'http://localhost:4318/v1/logs',
'application/json'
);
$exporter = new LogsExporter($transport);
$resource = ResourceInfoFactory::emptyResource()->merge(
ResourceInfo::create(
Attributes::create([
'service.name' => 'symfony-app',
])
)
);
$this->loggerProvider = new LoggerProvider(
new SimpleLogRecordProcessor($exporter),
$resource
);
}
protected function write(LogRecord $record): void
{
$logger = $this->loggerProvider->getLogger('symfony-app');
$logger->emit(
severityText: $record->level->getName(),
body: $record->message,
attributes: [
'level' => $record->level->getName(),
'channel' => $record->channel,
'context' => json_encode($record->context),
'extra' => json_encode($record->extra),
]
);
}
}Шаг 10: создаем простой контроллер
Создайте src/Controller/MonitoringDemoController.php:
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Annotation\Route;
use OpenTelemetry\API\Trace\TracerInterface;
use Psr\Log\LoggerInterface;
class MonitoringDemoController extends AbstractController
{
public function __construct(
private TracerInterface $tracer,
private LoggerInterface $logger
) {
}
#[Route('/api/demo', name: 'monitoring_demo', methods: ['GET'])]
public function demo(): JsonResponse
{
$span = $this->tracer->spanBuilder('demo-operation')->startSpan();
$scope = $span->activate();
try {
$this->logger->info('Demo endpoint called', [
'trace_id' => $span->getContext()->getTraceId(),
'span_id' => $span->getContext()->getSpanId(),
]);
$result = $this->processData();
$span->addEvent('Data processed successfully');
$span->setAttribute('result.count', count($result));
$this->logger->info('Processing completed', [
'item_count' => count($result),
]);
return new JsonResponse([
'status' => 'success',
'data' => $result,
'trace_id' => $span->getContext()->getTraceId(),
]);
} catch (\Exception $e) {
$span->recordException($e);
$span->setStatus(\OpenTelemetry\API\Trace\StatusCode::STATUS_ERROR);
$this->logger->error('Processing failed', [
'error' => $e->getMessage(),
'trace_id' => $span->getContext()->getTraceId(),
]);
throw $e;
} finally {
$span->end();
$scope->detach();
}
}
private function processData(): array
{
$span = $this->tracer->spanBuilder('process-data')->startSpan();
$scope = $span->activate();
try {
$this->logger->debug('Starting data processing');
sleep(1);
$data = [
'id' => 1,
'name' => 'Sample Item',
'timestamp' => time(),
];
$this->logger->debug('Data processing complete');
return $data;
} finally {
$span->end();
$scope->detach();
}
}
}Шаг 11: запускаем стек
Запустите все сервисы с помощью Docker Compose:
docker-compose up -dПодождем около 30 секунд, чтобы все сервисы запустились.
Шаг 12: протестируем комплект
Запустите сервер разработки Symfony:
symfony server:startОтправьте тестовый запрос:
curl http://localhost:8000/api/demoВы должны получить ответ в формате JSON с идентификатором трейса.
Шаг 13: смотрим данные в Grafana
Откройте Grafana на http://localhost:3000.
Просмотр трассировок
- Нажмите «Explore» в левом меню
- Выберите «Tempo» в качестве источника данных
- Нажмите «Search»
- Вы увидите список своих трассировок
- Нажмите на трассировку, чтобы увидеть полную информацию о диапазоне
Просмотр логов
- Нажмите «Explore» в левом меню
- Выберите «Loki» в качестве источника данных
- Используйте следующий запрос:
{service_name=«symfony-app»} - Вы увидите все журналы из вашего приложения
Сопоставление трассировок и журналов
Настоящая мощь заключается в сопоставлении трассировок и журналов. Каждая запись журнала содержит идентификатор трассировки, поэтому вы можете переходить из трассировки к соответствующим записям журнала и обратно.
Шаг 14: создаем дашборд
Создайте новый дашборд в Grafana:
- Нажмите «Dashboards» в левом меню
- Нажмите «New», затем «New Dashboard»
- Нажмите «Add visualization»
Добавьте панель для частоты запросов:
rate({service_name="symfony-app"} |= "Demo endpoint called" [5m])Добавьте панель для уровня ошибок:
rate({service_name="symfony-app"} |= "error" [5m])Дополнительно: пользовательские метрики
Вы можете добавлять пользовательские метрики для отслеживания данных, характерных для вашего бизнеса. Установите пакет метрик:
composer require open-telemetry/sdk-metricsСоздайте сервис метрик src/Metrics/MetricsService.php:
<?php
namespace App\Metrics;
use OpenTelemetry\SDK\Metrics\MeterProvider;
use OpenTelemetry\SDK\Metrics\MetricExporter\ConsoleMetricExporter;
use OpenTelemetry\SDK\Metrics\MetricReader\ExportingReader;
use OpenTelemetry\API\Metrics\MeterInterface;
class MetricsService
{
private MeterInterface $meter;
public function __construct()
{
$reader = new ExportingReader(new ConsoleMetricExporter());
$meterProvider = MeterProvider::builder()
->addReader($reader)
->build();
$this->meter = $meterProvider->getMeter('symfony-app');
}
public function recordRequestCount(string $endpoint): void
{
$counter = $this->meter->createCounter(
'http.requests',
'requests',
'Number of HTTP requests'
);
$counter->add(1, ['endpoint' => $endpoint]);
}
public function recordProcessingTime(float $duration, string $operation): void
{
$histogram = $this->meter->createHistogram(
'operation.duration',
'ms',
'Operation processing time'
);
$histogram->record($duration, ['operation' => $operation]);
}
}Обновите контроллер, чтобы использовать метрики:
public function demo(): JsonResponse
{
$start = microtime(true);
$span = $this->tracer->spanBuilder('demo-operation')->startSpan();
$scope = $span->activate();
try {
$this->metricsService->recordRequestCount('/api/demo');
$result = $this->processData();
$duration = (microtime(true) - $start) * 1000;
$this->metricsService->recordProcessingTime($duration, 'demo-operation');
return new JsonResponse([
'status' => 'success',
'data' => $result,
]);
} finally {
$span->end();
$scope->detach();
}
}Рекомендации для прода
При развертывании в производственной среде измените следующие настройки:
Обновите эндпоинты OpenTelemetry
Задайте переменные среды в файле .env:
OTEL_EXPORTER_OTLP_ENDPOINT=http://your-collector:4318
OTEL_SERVICE_NAME=symfony-app
OTEL_SERVICE_VERSION=1.0.0Обновите фабрику, чтобы использовать эти значения:
public static function create(): TracerProvider
{
$endpoint = $_ENV['OTEL_EXPORTER_OTLP_ENDPOINT'] ?? 'http://localhost:4318';
$transport = (new OtlpHttpTransportFactory())->create(
$endpoint . '/v1/traces',
'application/json'
);
// Остальной код...
}Настройка выборки
Для приложений с высокой нагрузкой используйте выборочную запись трассировок, чтобы снизить накладные расходы:
use OpenTelemetry\SDK\Trace\Sampler\ParentBased;
use OpenTelemetry\SDK\Trace\Sampler\TraceIdRatioBasedSampler;
$sampler = new ParentBased(new TraceIdRatioBasedSampler(0.1)); // Sample 10%
return TracerProvider::builder()
->addSpanProcessor(new SimpleSpanProcessor($exporter))
->setResource($resource)
->setSampler($sampler)
->build();Защитите эндпоинты
Добавьте аутентификацию к вашему коллектору OpenTelemetry:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
auth:
authenticator: basicauth
extensions:
basicauth:
htpasswd:
file: /etc/otel/.htpasswd
service:
extensions: [basicauth]
pipelines:
traces:
receivers: [otlp]
exporters: [otlp/tempo]Решение проблем
Трассировки не появляются в Tempo
Проверьте, что коллектор запущен:
docker-compose logs otel-collectorПроверьте, что коллектор может достучаться до Tempo:
docker-compose exec otel-collector wget -O- http://tempo:3200/readyЛоги не видно в Loki
Проверьте Loki напрямую:
curl http://localhost:3100/readyПроверьте логи Loki:
docker-compose logs lokiПриложение не может достучаться до коллектора
Если ваше приложение Symfony работает вне Docker, ему необходимо установить соединение с Collector на вашем хосте. Измените эндпоинт:
$transport = (new OtlpHttpTransportFactory())->create(
'http://host.docker.internal:4318/v1/traces',
'application/json'
);В Linux вам, возможно, придётся использовать свой реальный IP-адрес.
Что вы узнали
Теперь у вас есть полноценная система мониторинга для вашего приложения Symfony. Вы можете:
- Отслеживать запросы в приложении с помощью распределённого трассирования
- Собирать структурированные логи с контекстом
- Просматривать и запрашивать как трассировки, так и логи в Grafana
- Сопоставлять логи с трассировками с помощью идентификаторов трассировок
- Добавлять настраиваемые метрики для бизнес-данных
Система фиксирует все, что происходит в вашем приложении, и представляет эту информацию в понятном виде, позволяя вам принимать соответствующие меры. Вы можете находить узкие места в производительности, отлаживать ошибки и понимать поведение пользователей.
Начните с простых трассировок и логов, а затем добавляйте дополнительные инструменты по мере того, как вы узнаете, что важно для вашего приложения.
