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.
| Signal | Outil PHP | Backend LGTM | Question répondue |
|---|---|---|---|
| Logs | Monolog | Loki | Qu'est-ce qui s'est passé sur cette requête ? |
| Métriques | SDK OTel / prometheus/client-php | Prometheus / Mimir | Comment se comporte le service globalement ? |
| Traces | SDK OTel PHP | Tempo | Où 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
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.
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.
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 :
| Processor | Ce qu'il ajoute |
|---|---|
PsrLogMessageProcessor | Interpolation des placeholders {key} dans le message |
WebProcessor | URL, méthode HTTP, IP du client |
UidProcessor | Identifiant unique par requête (request_id) |
Custom TraceIdProcessor | Injection du trace_id OTel (section 05) |
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.
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.
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.
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.
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é.
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 :
| Package | Ce qui est instrumenté | Spans créés |
|---|---|---|
auto-symfony | HTTP kernel, controllers, console | Requête HTTP, dispatch, console command |
auto-psr18 | Clients HTTP PSR-18 (Symfony HttpClient) | Requêtes HTTP sortantes avec propagation |
auto-psr3 | Logger PSR-3 (Monolog) | Span events sur chaque appel de log |
auto-guzzle | Guzzle HTTP client | Requê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.
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.
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.
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();
}
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.
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.
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.
(?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.
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.
OTel SDK + Monolog
batch · sample
traces · logs
corrélation
Checklist d'intégration
- Installer l'extension PHP
opentelemetry(PECL) - Ajouter les packages Composer (
open-telemetry/sdk,auto-symfony,auto-psr18) - Définir
OTEL_SERVICE_NAME,OTEL_EXPORTER_OTLP_ENDPOINT,OTEL_PHP_AUTOLOAD_ENABLED=true - Configurer Monolog avec
JsonFormattervers stdout - Ajouter
TraceIdProcessordans la chaîne Monolog - Ajouter les spans manuels sur les opérations métier importantes
- 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
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.