← Tous les cours / Observabilité PHP & Symfony Cours
00 — VUE D'ENSEMBLE

Observabilité PHP — trois signaux, un fil conducteur

Logs Monolog, traces OTel et métriques — reliés par le trace_id pour une corrélation directe dans Grafana.

Observer une application PHP en production, c'est collecter trois types de signaux en continu : des logs pour comprendre ce qui s'est passé, des métriques pour mesurer le comportement global, et des traces pour suivre une requête de bout en bout.

SignalOutil PHPBackend LGTMQuestion répondue
LogsMonologLokiQu'est-ce qui s'est passé sur cette requête ?
MétriquesSDK OTel / prometheus/client-phpPrometheus / MimirComment se comporte le service globalement ?
TracesSDK OTel PHPTempoOù a été passé le temps dans cette requête ?

OpenTelemetry PHP unifie les traces et les métriques sous un seul SDK. Pour les logs, Monolog reste l'outil de référence — et on les relie aux traces via le trace_id.

Le fil conducteur : trace_id
Le trace_id est l'identifiant partagé entre les trois signaux. Chaque log émis pendant une requête instrumentée porte ce trace_id. Depuis Grafana, tu passes d'un log d'erreur à la trace complète en un clic.

Une requête Symfony instrumentée — les signaux générés automatiquement apparaissent dans l'ordre.

💡 Overhead minimal : l'auto-instrumentation OTel PHP ajoute typiquement 1–5 ms par requête. Le SDK est conçu pour la production. L'essentiel du coût est l'envoi réseau vers le Collector — asynchrone, avec retry et batching.
01 — LOGS STRUCTURÉS

Monolog — logs structurés en JSON

Du texte opaque vers du JSON indexable. La différence entre grep et LogQL.

Monolog est le logger standard de l'écosystème PHP. Symfony l'intègre nativement. Son architecture en handlers, processors et formatters permet de produire des logs structurés envoyés vers plusieurs destinations simultanément.

Architecture Monolog
Un Logger reçoit un enregistrement, le fait passer par une chaîne de processors (enrichissement), puis par une liste de handlers (destination). Chaque handler applique un formatter avant d'écrire. Le JsonFormatter produit une ligne JSON par entrée de log.

Configuration Symfony pour des logs JSON vers stdout (idéal pour les conteneurs) :

# config/packages/monolog.yaml
monolog:
  handlers:
    main:
      type:      stream
      path:      php://stdout
      level:     debug
      formatter: Monolog\Formatter\JsonFormatter
      channels:  ["!event"]

Les processors enrichissent automatiquement chaque enregistrement. Les plus utiles :

ProcessorCe qu'il ajoute
PsrLogMessageProcessorInterpolation des placeholders {key} dans le message
WebProcessorURL, méthode HTTP, IP du client
UidProcessorIdentifiant unique par requête (request_id)
Custom TraceIdProcessorInjection du trace_id OTel (section 05)
Envoi vers Loki
Deux approches : (1) écrire en JSON vers stdout et laisser Promtail/Alloy collecter les logs du conteneur ; (2) utiliser un handler Loki dédié (itk-open-technology/monolog-loki-handler ou équivalent). L'approche stdout est recommandée en production : elle ne bloque jamais la requête.

Compare le format texte classique au format JSON structuré — et vois quelle requête LogQL devient possible.

02 — SDK OTEL PHP

SDK OpenTelemetry PHP — installation et configuration

Trois packages Composer, cinq variables d'environnement, zéro ligne de code de démarrage.

Le SDK OpenTelemetry PHP est composé de plusieurs packages Composer : le cœur (open-telemetry/sdk), l'exporteur OTLP (open-telemetry/exporter-otlp), et des packages d'auto-instrumentation spécifiques à chaque bibliothèque ou framework.

Extension PHP opentelemetry
L'extension C (pecl install opentelemetry) est nécessaire pour l'auto-instrumentation via les hooks PHP (OpenTelemetry\API\Instrumentation\CachedInstrumentation). Sans elle, tu peux toujours utiliser l'API OTel manuellement, mais l'instrumentation automatique des frameworks ne fonctionnera pas.

La quasi-totalité de la configuration se fait via des variables d'environnement, sans modifier le code applicatif :

# .env.local ou docker-compose.yml
OTEL_PHP_AUTOLOAD_ENABLED=true
OTEL_SERVICE_NAME=mon-service
OTEL_SERVICE_VERSION=1.4.2
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318
OTEL_TRACES_EXPORTER=otlp
OTEL_METRICS_EXPORTER=otlp
OTEL_PROPAGATORS=tracecontext,baggage

Avec OTEL_PHP_AUTOLOAD_ENABLED=true, le SDK s'initialise automatiquement via le mécanisme de hooks PHP. Il n'y a aucune ligne de code à ajouter dans public/index.php.

🔑 OTEL_PROPAGATORS : définis toujours tracecontext,baggage. tracecontext = W3C Trace Context (header traceparent) pour la propagation entre services. baggage = W3C Baggage pour les métadonnées propagées. Sans ça, les traces restent isolées à un seul service.

Parmi ces variables, OTEL_TRACES_SAMPLER=traceidratio mérite qu'on s'y arrête : en production, tracer 100 % des requêtes coûte cher en stockage. L'échantillonnage n'en garde qu'une fraction. Le simulateur ci-dessous le rend tangible — règle le taux et regarde, sur un flux continu, quelles requêtes sont tracées ou jetées. À 10 %, tu n'en stockes qu'une sur dix, assez pour observer sans exploser la facture.

03 — AUTO-INSTRUMENTATION

Auto-instrumentation — les spans gratuits

Requêtes HTTP, controllers, clients HTTP sortants — instrumentés automatiquement par des hooks PHP.

L'auto-instrumentation installe des hooks PHP sur les fonctions des bibliothèques cibles. Tu n'écris rien — les spans apparaissent dès que le code instrumenté est exécuté.

Comment ça fonctionne
Le package d'auto-instrumentation enregistre un hook sur une fonction précise (ex: Symfony\Component\HttpKernel\HttpKernel::handle). À l'appel, le hook ouvre un span avant l'exécution et le ferme après. L'extension PHP est le mécanisme qui rend ces hooks possibles sans modifier le code source de la bibliothèque.

Chaque package d'auto-instrumentation cible une bibliothèque :

PackageCe qui est instrumentéSpans créés
auto-symfonyHTTP kernel, controllers, consoleRequête HTTP, dispatch, console command
auto-psr18Clients HTTP PSR-18 (Symfony HttpClient)Requêtes HTTP sortantes avec propagation
auto-psr3Logger PSR-3 (Monolog)Span events sur chaque appel de log
auto-guzzleGuzzle HTTP clientRequêtes HTTP sortantes

Les attributs ajoutés par l'auto-instrumentation Symfony respectent les Semantic Conventions OTel : http.request.method, http.route, http.response.status_code, server.address. Ces noms standardisés rendent les dashboards Grafana interopérables entre projets.

⚠️ Doctrine DBAL : il n'existe pas de package officiel OpenTelemetry pour Doctrine à ce jour. Tu peux instrumenter les requêtes SQL manuellement, ou utiliser un package communautaire. Vérifie packagist.org/packages/open-telemetry/ pour l'état actuel de l'écosystème.

Trace d'une requête Symfony avec uniquement l'auto-instrumentation. Clique sur un span pour voir sa source.

04 — INSTRUMENTATION MANUELLE

Instrumentation manuelle — le contexte métier

spanBuilder() + setAttribute() + addEvent() + recordException() — le pattern complet en PHP.

L'auto-instrumentation donne les spans d'infrastructure. Pour ajouter du contexte métier — le montant d'une commande, l'ID utilisateur, l'étape d'un workflow — tu utilises l'API OTel directement dans ton code.

Pattern try / finally
Un span doit toujours être fermé, même en cas d'exception. Le pattern canonique PHP est : $span->activate() dans le bloc principal, $span->end() dans le finally. Sans ça, le span reste ouvert en mémoire jusqu'au timeout du SDK.
use OpenTelemetry\API\Globals;
use OpenTelemetry\API\Trace\StatusCode;

$tracer = Globals::tracerProvider()->getTracer('mon-service');

$span = $tracer->spanBuilder('processOrder')
    ->setAttribute('order.id', $orderId)
    ->setAttribute('order.amount', $amount)
    ->startSpan();

$scope = $span->activate();
try {
    // ... logique métier ...
    $span->addEvent('stock.reserved', ['quantity' => $qty]);
    $span->setStatus(StatusCode::STATUS_OK);
} catch (\Throwable $e) {
    $span->recordException($e);
    $span->setStatus(StatusCode::STATUS_ERROR, $e->getMessage());
    throw $e;
} finally {
    $scope->detach();
    $span->end();
}
Span events
Un événement est un timestamp nommé à l'intérieur d'un span ($span->addEvent('stock.reserved')). Utile pour marquer des étapes importantes : "paiement initié", "stock réservé", "email envoyé". Visible dans le waterfall Grafana comme un marqueur vertical.

Bascule pour voir l'effet de l'instrumentation manuelle sur la trace — les spans métier en violet s'ajoutent aux spans auto.

05 — CORRÉLATION LOGS↔TRACES

Corrélation — du log d'erreur à la trace en un clic

TraceIdProcessor injecte le trace_id dans Monolog. Grafana crée le lien Loki → Tempo automatiquement.

Le trace_id est le lien entre les logs Monolog et les traces Tempo. Si chaque log contient le trace_id de la requête en cours, tu peux passer d'une ligne de log d'erreur à la trace complète en un clic dans Grafana.

TraceIdProcessor
Un Monolog Processor est une fonction appelée pour chaque enregistrement de log. Il peut lire le contexte OTel courant (le span actif) et injecter son trace_id et span_id dans les champs extra du log. Monolog les inclut ensuite dans le JSON final.
use Monolog\LogRecord;
use OpenTelemetry\API\Trace\Span;

final class TraceIdProcessor
{
    public function __invoke(LogRecord $record): LogRecord
    {
        $span    = Span::getCurrent();
        $context = $span->getContext();

        if (!$context->isValid()) {
            return $record;
        }

        return $record->with(extra: array_merge(
            $record->extra,
            [
                'trace_id' => $context->getTraceId(),
                'span_id'  => $context->getSpanId(),
            ]
        ));
    }
}

Enregistre ce processor dans config/packages/monolog.yaml sous la clé processors. Chaque log émis pendant une requête instrumentée portera désormais trace_id et span_id dans son JSON.

🔑 Champ dérivé Grafana : dans la configuration Loki (datasource Grafana), ajoute un champ dérivé avec la regex (?P<traceId>[0-9a-f]{32}) appliqué au champ extra.trace_id. Grafana crée alors un lien cliquable "Open in Tempo" sur chaque valeur de trace_id.

Clique sur un trace_id dans les logs pour ouvrir la trace correspondante dans Tempo.

06 — SYNTHÈSE

Synthèse — stack LGTM complet en PHP

Checklist en 7 étapes, considérations de performance, pièges courants.

L'observabilité complète d'une application PHP/Symfony repose sur trois intégrations qui se renforcent mutuellement : Monolog en JSON pour les logs, le SDK OTel pour les traces, et un TraceIdProcessor pour les corréler.

Symfony App
OTel SDK + Monolog
OTLP →
OTel Collector
batch · sample
Tempo + Loki
traces · logs
Grafana
corrélation

Checklist d'intégration

🔑 Les sept étapes :
  1. Installer l'extension PHP opentelemetry (PECL)
  2. Ajouter les packages Composer (open-telemetry/sdk, auto-symfony, auto-psr18)
  3. Définir OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_PHP_AUTOLOAD_ENABLED=true
  4. Configurer Monolog avec JsonFormatter vers stdout
  5. Ajouter TraceIdProcessor dans la chaîne Monolog
  6. Ajouter les spans manuels sur les opérations métier importantes
  7. Configurer les champs dérivés Loki → Tempo dans Grafana

Considérations de performance

Le SDK OTel PHP utilise un exporter synchrone par défaut : chaque span est envoyé au Collector à la fin de la requête, ce qui peut ajouter quelques millisecondes. En production, configure l'exporter batch (BatchSpanProcessor) qui accumule les spans en mémoire et les envoie périodiquement en arrière-plan.

Pour réduire le volume, configure le sampler :

# 10% des traces — adapte selon ton volume
OTEL_TRACES_SAMPLER=traceidratio
OTEL_TRACES_SAMPLER_ARG=0.1

# Mieux : head sampling côté app + tail sampling dans le Collector
# Le Collector garde 100% des traces en erreur, 5% des OK
Pièges courants
Extension manquante : sans extension=opentelemetry.so, l'auto-instrumentation ne s'active pas silencieusement — tu obtiens zéro span auto sans message d'erreur.

Propagation dans les tâches async : Symfony Messenger, les jobs de queue et les processus séparés ne propagent pas automatiquement le contexte de trace. Tu dois extraire le contexte avant d'enqueuer le message et l'injecter manuellement dans le handler.

OTEL_PHP_AUTOLOAD_ENABLED en prod : certains hébergeurs ou configurations PHP désactivent les fonctions de hook. Si les spans n'apparaissent pas, vérifie phpinfo() → section opentelemetry.

Avec ces sept étapes en place, chaque requête de ta Symfony est observable de bout en bout : tu vois les spans dans Tempo, les logs dans Loki, et tu passes de l'un à l'autre via le trace_id dans Grafana — le mode Explore te donne la réponse en quelques secondes lors d'un incident.