OpenTelemetry Integration for Symfony: Full Guide

Vladimir Mihailenco
November 15, 2024
6 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.

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, Doctrine, Twig)
  • 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)
  • Symfony 5.4+ / 6.x / 7.x (full support across LTS and current releases)
  • 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
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:4318

# 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, Doctrine ORM, Twig templates), you can add custom instrumentation for business logic.

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']

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

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, Doctrine, and Twig
  • 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