OpenTelemetry Integration for Symfony: Full Guide

Vladimir Mihailenco
April 28, 2026
7 min read

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:

SolutionSetupCostDistributed TracingVendor Lock-in
OpenTelemetryMediumFree + BackendExcellentNone
Symfony ProfilerEasyFreeNoneNone
BlackfireEasyPaidLimitedYes
New RelicEasyExpensiveExcellentYes

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

Step 1: Install OpenTelemetry Extension

The extension enables auto-instrumentation for Symfony applications. First, set up the development environment:

shell Linux (apt)
sudo apt-get install gcc make autoconf php-dev
shell macOS (homebrew)
brew install gcc make autoconf

Then install the extension using one of these methods:

shell pecl (recommended)
pecl install opentelemetry
shell picle
php pickle.phar install opentelemetry
shell php-extension-installer (Docker)
install-php-extensions opentelemetry

Add the extension to your php.ini file (run php --ini to find the file location):

ini
[opentelemetry]
extension=opentelemetry.so

Verify the installation works:

bash
php --ri opentelemetry
# Should display extension information and version

Step 2: Install Symfony Packages

Install the required packages for Symfony OpenTelemetry integration:

bash
# 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
<?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:

env
# 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:

bash
symfony server:start
# or
php -S localhost:8000 -t public/

Make some requests to generate traces:

bash
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
<?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
<?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):

yaml
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
<?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:

env
# Full sampling for debugging
OTEL_TRACES_SAMPLER_ARG=1.0

Production:

env
# 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:

dockerfile
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:

bash
# 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:

env
# 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:

bash
# 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:

bash
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