Le blog

Observer votre application Symfony en toute simplicité avec OpenTelemetry (partie 2)

Publié le 13 janvier 2025

Dans cette série en trois articles, nous explorons comment observer une application Symfony à l'aide du framework OpenTelemetry. Dans la première partie, nous avons abordé les fondamentaux de l'observabilité et présenté les concepts d'OpenTelemetry. Dans ce deuxième volet, nous allons passer à la pratique et créer une application que nous observerons. Vous pouvez aussi découvrir d'ores et déjà notre dernière partie, consacrée à l'observation de notre application.

#

Mise en place

Pour rappel, et pour suivre plus facilement, le repo de démo se trouve à cette adresse !

#
Symfony et Docker

Partons donc sur un projet Symfony classique, créé via : 

symfony new symfony-otl

Le projet ayant pour but de couvrir un bon nombre de signaux, il nécessitera par conséquent une grande quantité d’applications diverses pour les traiter. C’est pourquoi nous nous sommes dirigés vers une architecture Docker + Compose, au moins localement (pour la production, envisagez peut-être Kubernetes selon vos besoins).

Note : pour faire simple et ne pas mélanger trop d’inconnues, nous utilisons une stack PHP-FPM + Caddy assez classique plutôt que sur un build ZTS comme FrankenPHP, mais il est tout à fait possible que cela fonctionne sans soucis également, à tester !

Vous pouvez retrouver le Dockerfile à la racine du projet, et il est en soi tout à fait classique. À noter cependant les lignes suivantes :

Il s’agit des extensions nécessaires et/ou recommandées par OpenTelemetry pour PHP. Les extensions grpc et protobuf permettent de transmettre de manière ultra performante les diverses données émises au(x) collecteurs que nous verrons plus tard.

L’extension OpenTelemetry est quant à elle indispensable. Elle vient notamment ajouter une fonction hook qui permet de s’attacher à une fonction en indiquant deux Closure pre et post qui seront appelées respectivement avant et après le déclenchement de la fonction choisie. Elles servent en général à démarrer une Trace en pre-hook, à la terminer puis à l’émettre en post-hook. Voici comment on pourrait l’attacher à la fonction doSomeWork de la classe MyService :

commande dans le cadre d'un projet OpenTelemetry" class="wp-image-10567#
Compose

Le fichier compose.yml à la racine du projet regroupe les différents services qui permettront de recevoir et traiter les données d’instrumentation fournies par notre application. On y trouve ainsi le container PHP correspondant à notre application. Vous pourrez noter la présence de certaines variables d’environnement, qui permettent la configuration interne du SDK OpenTelemetry interne à l’application :

Commande" class="wp-image-10571
  • OTEL_PHP_AUTOLOAD_ENABLED : permet l’autoloading du SDK
  • OTEL_SERVICE_NAME : nom du service associé, utile pour différencier les sources de données
  • OTEL_[ TRACES | METRICS | LOGS ]_EXPORTER : type d’exporter utilisé respectivement pour les Traces, Metrics et Logs. Nous suivons ici la spécification OTLP, mais les Traces peuvent être exportées en suivant d’autres specs, comme Jaeger ou Zipkin, et les Metrics peuvent être exportées directement au format Prometheus.
  • OTEL_EXPORTER_OTLP_PROTOCOL : le protocole de transfert, HTTP ou (ici) gRCP.
  • OTEL_EXPORTER_OTLP_ENDPOINT : l’adresse à laquelle envoyer les données, ici le service otel-collector.

À noter que ces variables de configuration sont prises en compte soit en tant que “vraies” variables d’environnement (les configurer dans un .env ne fonctionnera pas), ou directement dans le php.ini. Nous trouvons également :

  • Le container Caddy pour servir l’application.
  • Un container RabbitMQ pour nos messages Messenger.
  • Un container “worker” PHP qui va servir de consumer pour ces messages
  • Un container otel-collector qu’on peut considérer comme un passe-plat. Il est configuré pour recevoir tous les signaux de l’application et les renvoyer vers leurs backends respectifs. Sa configuration est disponible dans docker/opentelemetry/otel-collector/otel-collector-config.yml :
commande 4" class="wp-image-10574

Ici, nous n’avons pas mis en place de processor mais il est possible d’en indiquer dans chaque pipeline, pour modifier et/ou filtrer les données avant leur export.

  • Un container Tempo, comme backend de Traces.
  • Un container Loki, comme backend de Logs.
  • Un container Mimir et un container Prometheus, pour les Metrics.
  • Et finalement, un container Grafana pour la visualisation des données.

Les configurations respectives de ces derniers services sont disponibles dans docker/opentelemetry/{nom du service}. Elles sont ici minimales et nous n’iront pas en détail dans leur explication, libre à chacun·e d’en tirer le meilleur parti selon ses besoins ! 

Pour lancer toute cette stack, assurez vous d’avoir Docker et Docker Compose installés, puis lancez les commandes suivantes :

docker compose build --pull --no-cache

Puis, un fois le build terminé : 

docker compose up -d

Note : si l’image PHP custom prend un certain temps à se construire, c’est tout à fait normal, l’extension gRPC est particulièrement longue à build.

#
Dépendances Composer

Maintenant que notre ensemble de services est bien défini, jetons un œil aux dépendances PHP que nous avons pu ajouter :

Commande" class="wp-image-10575
  • open-telemetry/exporter-otlp, en combinaison avec open-telemetry/transport-grpc nous permettra d’exporter les données de télémétrie récoltées vers le endpoint configuré, ici le collector.
  • open-telemetry/sdk nous fournit tout un ensemble de classes qui permettront l’instrumentation automatique ou manuelle de notre code.
  • open-telemetry/opentelemetry-logger-monolog vient ajouter un Handler qui nous permettra d’exporter les logs de notre bibliothèque habituelle, Monolog, vers le collector.

Et enfin, une bibliothèque indispensable pour automatiser la plus grande partie de l’observation de notre application Symfony : open-telemetry/opentelemetry-auto-symfony. Celle-ci va en effet nous permettre, grâce à l’usage de la fonction hook évoquée plus haut, d’émettre des Traces pour une grande partie des événements liés à Symfony, et ce, sans avoir à écrire une seule ligne de code, n’est-ce pas magique ?

Jetons ensemble un œil à comment tout ceci peut fonctionner, en prenant pour exemple la classe OpenTelemetry\Contrib\Instrumentation\Symfony\SymfonyInstrumentation :

Commande" class="wp-image-10578

Dans le pre hook : 

" class="wp-image-10579

Et enfin dans le post hook :

" class="wp-image-10581

On retrouve donc bien la façon de fonctionner que nous avions expliquée plus haut ! Chaque requête HTTP reçue par le Kernel crée ainsi une Span contenant des informations sur la requête et la réponse, sans que nous ayons eu à écrire une ligne de code !

Et la magie ne s’arrête pas là, cette bibliothèque propose en effet de l’auto-instrumentation également pour l’envoi de requête, en créant des hooks sur HttpClientInterface::request, ainsi que pour l’envoi de messages Messenger, avec de hooks liés à MessageBusInterface::dispatch et SenderInterface::send. Des Traces sont donc créées et envoyées automatiquement pour une partie conséquente de notre application !

Note : l’auto-instrumentation de Doctrine/Dbal est actuellement en cours de développement, suivez la pull request ici !

#
Ajouts manuels

Néanmoins, on peut encore chercher à aller plus loin. Les requêtes HTTP, c’est bien beau, mais ce n’est pas le seul point d’entrée potentiel de notre application. Comment peut-on ainsi observer sans trop d’efforts les commandes Console ?

Nous avons créé un EventListener attaché aux évènements Symfony liés à la console et qui s’occupera de ceci pour nous ! Il se trouve dans src/EventListener/OpenTelemetry/ConsoleListener et voici ce qu’on y trouve :

" class="wp-image-10582" class="wp-image-10583" class="wp-image-10584

Et finalement :

" class="wp-image-10585

Ainsi, nos commandes Console sont également “auto” instrumentées !

Une dernière petite étape consistera pour nous à encore plus faciliter l’intégration avec Monolog. Dans la documentation OpenTelemetry, on retrouve l’exemple suivant : 

" class="wp-image-10586

De notre côté cependant, nous ne souhaitons pas forcément devoir réinstancier un Logger à chaque fois que nous en avons besoin, ni lui ajouter tous nos Handler déjà configurés en plus de celui nécessaire à l’export OTL. Nous avons donc créé le décorateur suivant :

" class="wp-image-10587

Il nous suffit dès lors au besoin d’injecter LoggerInterface dans nos classes pour avoir accès à ce nouveau Logger Monolog qui exportera ses données vers le collector. Rendez-vous dans notre troisième partie pour voir la suite de ce cas d'usage !

Hugo Nicolas

Hugo Nicolas

Backend developer

Mots-clésOpenTelemetry, Symfony

Le blog

Pour aller plus loin