OpenTelemetry Integration for Symfony: Full Guide
OpenTelemetry integration with Symfony applications provides comprehensive observability through distributed tracing, metrics, and logging. This guide shows you how to implement OpenTelemetry in Symfony using auto-instrumentation and custom spans for production-ready monitoring.
Note on examples: This guide uses Uptrace as the observability backend. OpenTelemetry is vendor-neutral — replace the DSN and endpoint with any OTLP-compatible backend (Grafana Cloud, Datadog, Jaeger, etc.).
What is OpenTelemetry?
OpenTelemetry is an open-source observability framework that provides a standardized way to collect, process, and export telemetry data from applications and infrastructure. It combines metrics, logs, and distributed traces into a unified toolkit that helps developers understand how their systems are performing. For Symfony applications, it offers:
- Auto-instrumentation: Automatic tracing of Symfony framework components (HTTP kernel, HttpClient, Messenger)
- Distributed tracing: End-to-end request tracking across services
- Performance monitoring: Database queries, HTTP requests, and message queue processing
- Custom metrics: Business-specific measurements and KPIs
For general PHP instrumentation details, see the OpenTelemetry PHP guide.
Why Use OpenTelemetry with Symfony?
Compared to other Symfony monitoring solutions:
| Solution | Setup | Cost | Distributed Tracing | Vendor Lock-in |
|---|---|---|---|---|
| OpenTelemetry | Medium | Free + Backend | Excellent | None |
| Symfony Profiler | Easy | Free | None | None |
| Blackfire | Easy | Paid | Limited | Yes |
| New Relic | Easy | Expensive | Excellent | Yes |
OpenTelemetry provides industry-standard observability without vendor lock-in, making it ideal for distributed Symfony applications. Unlike the built-in Symfony Profiler (which only works in development), OpenTelemetry is designed for production monitoring across multiple services.
Requirements
Before implementing OpenTelemetry in Symfony applications:
- PHP 8.1+ (required for auto-instrumentation; PHP 8.3+ recommended)
- Symfony 5.4+ / 6.x / 7.x (full support across LTS and current releases)
- Composer 2.x
- OpenTelemetry PHP Extension
- Composer for dependency management
Quick Start: For fastest setup without code changes, see the PHP zero-code instrumentation guide.
Step 1: Install OpenTelemetry Extension
The extension enables auto-instrumentation for Symfony applications. First, set up the development environment:
sudo apt-get install gcc make autoconf php-dev
Then install the extension using one of these methods:
pecl install opentelemetry
Add the extension to your php.ini file (run php --ini to find the file location):
[opentelemetry]
extension=opentelemetry.so
Verify the installation works:
php --ri opentelemetry
# Should display extension information and version
Step 2: Install Symfony Packages
Install the required packages for Symfony OpenTelemetry integration:
# Core Symfony auto-instrumentation package (latest: v1.2.0, released 2026-03-24)
composer require open-telemetry/opentelemetry-auto-symfony
# OpenTelemetry SDK and exporter
composer require open-telemetry/sdk open-telemetry/exporter-otlp
# For Uptrace integration (optional)
composer require uptrace/uptrace
Step 3: Configure OpenTelemetry
Add OpenTelemetry initialization to public/index.php right after the autoloader:
<?php
use App\Kernel;
use Uptrace\Distro;
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
/*
|--------------------------------------------------------------------------
| Configure OpenTelemetry
|--------------------------------------------------------------------------
| This runs before the runtime closure so the SDK is ready
| when Symfony boots the kernel.
*/
$uptrace = Distro::builder()
->setDsn($_ENV['UPTRACE_DSN'] ?? 'your-project-dsn')
->setServiceName($_ENV['OTEL_SERVICE_NAME'] ?? 'symfony-app')
->setServiceVersion($_ENV['OTEL_SERVICE_VERSION'] ?? '1.0.0')
->buildAndRegisterGlobal();
return function (array $context) {
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
};
Set up environment variables in your .env file using the standard OpenTelemetry environment variables:
# Service identification
OTEL_SERVICE_NAME=symfony-ecommerce
OTEL_SERVICE_VERSION=1.0.0
# Uptrace configuration
UPTRACE_DSN=https://<secret>@api.uptrace.dev/project-id
# Enable auto-instrumentation
OTEL_PHP_AUTOLOAD_ENABLED=true
OTEL_TRACES_EXPORTER=otlp
OTEL_METRICS_EXPORTER=otlp
OTEL_LOGS_EXPORTER=otlp
# OTLP protocol settings
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
OTEL_EXPORTER_OTLP_ENDPOINT=https://api.uptrace.dev:443
OTEL_EXPORTER_OTLP_COMPRESSION=gzip
# Sampling (adjust based on traffic)
OTEL_TRACES_SAMPLER=parentbased_traceidratio
OTEL_TRACES_SAMPLER_ARG=1.0
# Context propagation
OTEL_PROPAGATORS=tracecontext,baggage
Step 4: Test the Integration
Start your Symfony application and OpenTelemetry will automatically instrument it:
symfony server:start
# or
php -S localhost:8000 -t public/
Make some requests to generate traces:
curl http://localhost:8000/
curl http://localhost:8000/api/products
You should see traces appearing in your Uptrace project dashboard.
Custom Instrumentation Examples
While auto-instrumentation covers most Symfony components (HTTP kernel, HttpClient, Messenger), you can add custom instrumentation for business logic. Doctrine requires the separate open-telemetry/opentelemetry-auto-pdo package (already listed in Step 2 troubleshooting).
Controller with Custom Spans
This example shows how to add business context to your Symfony controllers:
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Attribute\Route;
use OpenTelemetry\API\Globals;
use OpenTelemetry\API\Trace\StatusCode;
use App\Entity\Order;
use Doctrine\ORM\EntityManagerInterface;
class OrderController extends AbstractController
{
public function __construct(
private EntityManagerInterface $entityManager,
) {}
#[Route('/api/orders', methods: ['POST'])]
public function create(Request $request): JsonResponse
{
$tracer = Globals::tracerProvider()->getTracer('order-service');
$span = $tracer->spanBuilder('create-order')
->setAttribute('user.id', $this->getUser()?->getUserIdentifier())
->startSpan();
$scope = $span->activate();
try {
$data = json_decode($request->getContent(), true);
$order = new Order();
$order->setCustomerEmail($data['customer_email']);
$order->setTotal($this->calculateTotal($data['items']));
$order->setStatus('pending');
// Doctrine queries are automatically traced
$this->entityManager->persist($order);
$this->entityManager->flush();
$span->setAttribute('order.id', $order->getId());
$span->setAttribute('order.items.count', count($data['items']));
$span->setStatus(StatusCode::STATUS_OK);
return $this->json(['order_id' => $order->getId()], 201);
} catch (\Exception $e) {
$span->setStatus(StatusCode::STATUS_ERROR, $e->getMessage());
$span->recordException($e);
throw $e;
} finally {
$scope->detach();
$span->end();
}
}
private function calculateTotal(array $items): float
{
return array_sum(array_column($items, 'price'));
}
}
Custom Metrics with Event Subscriber
Track business metrics using a Symfony event subscriber:
<?php
namespace App\EventSubscriber;
use OpenTelemetry\API\Globals;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\KernelEvents;
class MetricsSubscriber implements EventSubscriberInterface
{
private $requestCounter;
private $responseDuration;
private $errorCounter;
public function __construct()
{
$meter = Globals::meterProvider()->getMeter('symfony-metrics');
$this->requestCounter = $meter->createCounter(
'http_requests_total',
'count',
'Total number of HTTP requests'
);
$this->responseDuration = $meter->createHistogram(
'http_response_duration',
'ms',
'HTTP response duration in milliseconds'
);
$this->errorCounter = $meter->createCounter(
'http_errors_total',
'count',
'Total number of HTTP errors'
);
}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::REQUEST => 'onRequest',
KernelEvents::RESPONSE => 'onResponse',
KernelEvents::EXCEPTION => 'onException',
];
}
public function onRequest(RequestEvent $event): void
{
if (!$event->isMainRequest()) {
return;
}
$event->getRequest()->attributes->set(
'_otel_start_time',
hrtime(true)
);
$this->requestCounter->add(1, [
'http.method' => $event->getRequest()->getMethod(),
'http.route' => $event->getRequest()->attributes->get('_route', 'unknown'),
]);
}
public function onResponse(ResponseEvent $event): void
{
if (!$event->isMainRequest()) {
return;
}
$startTime = $event->getRequest()->attributes->get('_otel_start_time');
if ($startTime) {
$duration = (hrtime(true) - $startTime) / 1_000_000; // Convert to ms
$this->responseDuration->record($duration, [
'http.method' => $event->getRequest()->getMethod(),
'http.status_code' => $event->getResponse()->getStatusCode(),
]);
}
}
public function onException(ExceptionEvent $event): void
{
$this->errorCounter->add(1, [
'http.method' => $event->getRequest()->getMethod(),
'exception.type' => get_class($event->getThrowable()),
]);
}
}
Register the subscriber in config/services.yaml (if not using autoconfigure):
services:
App\EventSubscriber\MetricsSubscriber:
tags: ['kernel.event_subscriber']
Messenger and Async Tracing
The opentelemetry-auto-symfony package automatically creates spans for Symfony Messenger dispatches and transports. However, when a message crosses a process boundary (e.g., dispatched by a web request, consumed by a worker), you must propagate the trace context so the worker span links to the original request trace:
<?php
namespace App\Message;
use OpenTelemetry\API\Globals;
use OpenTelemetry\API\Trace\StatusCode;
class ProcessOrderHandler
{
public function __invoke(ProcessOrder $message): void
{
$tracer = Globals::tracerProvider()->getTracer('order-worker');
$span = $tracer->spanBuilder('process-order')
->setAttribute('order.id', $message->getOrderId())
->startSpan();
$scope = $span->activate();
try {
// Process the order...
$span->setStatus(StatusCode::STATUS_OK);
} catch (\Exception $e) {
$span->setStatus(StatusCode::STATUS_ERROR, $e->getMessage());
$span->recordException($e);
throw $e;
} finally {
$scope->detach();
$span->end();
}
}
}
The auto-instrumentation package injects and extracts trace context via Messenger stamps automatically — spans from the worker appear as children of the original dispatch span in your trace waterfall.
Production Configuration
Environment-Specific Settings
Configure different sampling rates for different environments:
Development:
# Full sampling for debugging
OTEL_TRACES_SAMPLER_ARG=1.0
Production:
# Reduced sampling for performance
OTEL_TRACES_SAMPLER_ARG=0.1
OTEL_BSP_SCHEDULE_DELAY=5000
OTEL_BSP_MAX_EXPORT_BATCH_SIZE=512
Docker Configuration
For containerized Symfony applications:
FROM php:8.2-fpm
# Install system dependencies
RUN apt-get update && apt-get install -y \
git unzip libzip-dev \
&& docker-php-ext-install zip
# Install OpenTelemetry extension
RUN pecl install opentelemetry \
&& echo "extension=opentelemetry" > /usr/local/etc/php/conf.d/opentelemetry.ini
# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
COPY . /var/www/html
WORKDIR /var/www/html
RUN composer install --optimize-autoloader --no-dev
ENV OTEL_PHP_AUTOLOAD_ENABLED=true
ENV OTEL_SERVICE_NAME=symfony-app
EXPOSE 9000
CMD ["php-fpm"]
Common Issues
No Traces Appearing
Check if the OpenTelemetry extension is properly loaded:
# Verify extension is installed
php -m | grep opentelemetry
# Check if autoloading is enabled
echo $OTEL_PHP_AUTOLOAD_ENABLED
# Test with console exporter to see traces in terminal
OTEL_TRACES_EXPORTER=console php -S localhost:8000 -t public/
Performance Impact
If you notice performance degradation, optimize the configuration:
# Reduce sampling rate
OTEL_TRACES_SAMPLER_ARG=0.05
# Increase batch processing interval
OTEL_BSP_SCHEDULE_DELAY=10000
OTEL_BSP_MAX_EXPORT_BATCH_SIZE=1024
# Disable specific instrumentation if not needed
OTEL_PHP_DISABLED_INSTRUMENTATIONS=psr3,psr16
Auto-instrumentation Not Triggering
If composer require open-telemetry/opentelemetry-auto-symfony finishes but no Symfony spans appear, confirm the SDK autoload entrypoint is loaded before the Symfony Runtime returns the kernel closure (as shown in Step 3). Loading it inside a controller or service is too late — the hooks register at extension load time and only intercept calls after registration.
Missing Doctrine Traces
Ensure database instrumentation packages are installed and not disabled:
# Install Doctrine auto-instrumentation if not present
composer require open-telemetry/opentelemetry-auto-pdo
# Verify OTEL_PHP_DISABLED_INSTRUMENTATIONS does not contain 'pdo'
Symfony Cache Interference
If traces are missing after cache warmup, clear and rebuild:
php bin/console cache:clear
php bin/console cache:warmup
Conclusion
OpenTelemetry provides comprehensive observability for Symfony applications with minimal setup effort. The auto-instrumentation package handles most common scenarios automatically, while custom instrumentation allows you to add business-specific context where needed.
Key benefits:
- Zero-code auto-instrumentation for Symfony HTTP kernel, HttpClient, and Messenger
- Industry-standard telemetry compatible with any OpenTelemetry APM backend
- Production-ready performance with configurable sampling
- Extensible with custom metrics, spans, and event subscribers
For alternative PHP frameworks, explore Laravel for rapid development with auto-instrumentation or Slim for lightweight microservices.
Ready to start monitoring your Symfony application? Sign up for Uptrace and get comprehensive observability in minutes.
Resources
- OpenTelemetry PHP Documentation
- Symfony Auto-instrumentation Package
- FriendsOfOpenTelemetry Bundle — alternative Symfony bundle with Doctrine, HttpClient, and Messenger support via bundle configuration
- Uptrace Symfony Integration