Aller au contenu principal
Retour au blog

Messenger en 2026 : vers l'observabilité avec messenger:show et LoggableMiddleware

Flavien Métivier12 septembre 20269 min

Un incident classique en production : la file async gonfle, les jobs mettent trois fois plus de temps que d'habitude à se traiter, et personne ne sait pourquoi. On sait combien de workers tournent, on sait combien de messages ont échoué grâce à la failure queue, mais on ne sait ni ce qui attend réellement dans la file, ni quel handler consomme le plus de ressources, ni si un message est en train d'être redélivré pour la troisième fois. Symfony Messenger est excellent pour découpler l'émission d'un message de son traitement — mais côté observabilité, il est resté longtemps une boîte noire qu'on n'ouvre qu'en ajoutant soi-même du logging artisanal. Plusieurs chantiers ouverts sur la branche de développement de Symfony (donc à destination de la 8.2, pas encore mergés à l'heure où j'écris ces lignes) visent justement à combler ce trou. C'est l'occasion de faire le point sur ce qui arrive, sur ce qui est encore en discussion, et surtout sur ce qu'on peut déjà mettre en place aujourd'hui avec les composants stables.

Le point aveugle classique : une file Messenger, une boîte noire

Dès qu'un projet dépasse la démo et tourne réellement en production, les mêmes questions reviennent sur la table dès qu'un incident survient sur les workers Messenger :

  • Combien de messages sont réellement en attente sur le transport async, et de quel type sont-ils ?
  • Quel handler prend 200 ms en moyenne et lequel en prend 4 secondes, avec quel pic mémoire ?
  • Un message qui vient d'échouer a-t-il déjà été redélivré une fois par le broker, ou est-ce un vrai échec applicatif ?
  • Le retry strategy configuré correspond-il à ce qui se passe vraiment, ou les tentatives s'épuisent-elles plus vite que prévu ?

Aujourd'hui, répondre à ces questions impose de bricoler : écouter WorkerMessageReceivedEvent et WorkerMessageHandledEvent à la main, ajouter un Stopwatch dans un middleware custom, ou se fier aux capacités propres de RabbitMQ / SQS pour compter les messages en file — quand le transport le permet. C'est exactement le vide que plusieurs propositions récentes tentent de combler côté cœur du framework, en particulier autour de la commande messenger:show, du LoggableMiddleware, d'une option de configuration sur le RejectRedeliveredMessageMiddleware, et d'un nouveau transport asynchrone AMPHP SQL.

messenger:show : enfin inspecter ce qui attend en file

La proposition la plus attendue est une commande messenger:show, portée dans une pull request qui répond à une demande de longue date de la communauté. L'idée : lister ou afficher le détail des messages en attente sur un transport donné, à condition que ce transport implémente ListableReceiverInterface — ce qui est le cas de Doctrine, de certains transports in-memory, mais pas nécessairement de tous les brokers externes selon leur configuration. Quand le transport ne supporte pas la navigation, la commande est censée le signaler proprement plutôt que d'échouer silencieusement.

D'après la description de la pull request, la commande accepterait plusieurs options : filtrer par transport, filtrer par classe de message, limiter le nombre de résultats affichés, afficher des statistiques agrégées, et consulter le détail d'un message précis via son identifiant. Voici à quoi ressemblerait l'usage, tel que décrit dans la proposition actuelle :

# Lister les messages en attente sur le transport async (syntaxe proposée, PR en cours de revue)
php bin/console messenger:show --transport=async

# Filtrer par classe de message et limiter l'affichage
php bin/console messenger:show \
  --transport=async \
  --class-filter="App\\Message\\SendInvoiceMessage" \
  --max=20

# Statistiques agrégées sur le transport
php bin/console messenger:show --transport=async --stats

# Détail d'un message précis par son identifiant
php bin/console messenger:show --transport=async 3f2c9a1e-...

L'intérêt pour un Lead Dev est immédiat : c'est l'équivalent Messenger de rabbitmqctl list_queues, mais intégré au framework et cohérent avec le vocabulaire du transport configuré. Point d'honnêteté important : à la date de rédaction, cette pull request est encore ouverte, avec des allers-retours de revue (notamment sur la traduction de certains libellés). Le nom exact de la commande, la liste finale des options et le comportement précis peuvent encore évoluer avant un merge définitif — ne bâtissez pas une automatisation critique dessus tant qu'elle n'est pas dans une release taguée.

LoggableMiddleware : mesurer le coût réel de chaque message

Deuxième chantier, complémentaire du premier : un middleware optionnel baptisé LoggableMiddleware, dont la proposition vise à journaliser, pour chaque message traité, le temps d'exécution en millisecondes et le pic de mémoire consommé. L'objectif affiché est double : repérer plus vite les messages lents ou gourmands en mémoire, et améliorer le diagnostic directement en production, sans instrumentation manuelle.

Sur le principe, ce middleware s'insérerait dans la chaîne comme n'importe quel middleware Messenger, avec un alias court à activer dans la configuration — les échanges de revue évoquent un nom du type logging, mais ce point n'est pas encore stabilisé au moment où j'écris ces lignes, tout comme la question de savoir si la durée doit être stockée en entier (pour l'exploitation statistique) plutôt qu'en chaîne déjà formatée. Voici la forme la plus probable, sous réserve d'ajustement avant merge :

# config/packages/messenger.yaml — configuration probable (PR en cours de revue, syntaxe non stabilisée)
framework:
    messenger:
        buses:
            messenger.bus.default:
                middleware:
                    - 'logging' # nom d'alias proposé pour LoggableMiddleware

        transports:
            async:
                dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
                retry_strategy:
                    max_retries: 3
                    delay: 1000
                    multiplier: 2

En attendant que cette fonctionnalité soit mergée et stabilisée, rien n'empêche de reproduire le même résultat dès aujourd'hui avec un middleware custom, sur une version stable de Messenger. C'est même l'approche la plus sûre pour un système déjà en production : vous ne dépendez pas d'une API qui peut encore bouger, et vous gardez la main sur ce qui est journalisé (identifiant de corrélation, nom du handler, tags métier).

Besoin d'un expert Symfony ?

Réserver un appel →
<?php
// src/Messenger/Middleware/PerformanceLoggingMiddleware.php
declare(strict_types=1);

namespace App\Messenger\Middleware;

use Psr\Log\LoggerInterface;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Middleware\MiddlewareInterface;
use Symfony\Component\Messenger\Middleware\StackInterface;

final class PerformanceLoggingMiddleware implements MiddlewareInterface
{
    public function __construct(
        private readonly LoggerInterface $logger,
    ) {
    }

    public function handle(Envelope $envelope, StackInterface $stack): Envelope
    {
        $memoryBefore = memory_get_usage(true);
        $start = microtime(true);

        try {
            return $stack->next()->handle($envelope, $stack);
        } finally {
            $durationMs = (int) round((microtime(true) - $start) * 1000);
            $memoryPeakMb = round(
                (memory_get_peak_usage(true) - $memoryBefore) / 1024 / 1024,
                2
            );

            $this->logger->info('messenger.message_processed', [
                'message_class' => $envelope->getMessage()::class,
                'duration_ms' => $durationMs,
                'memory_peak_mb' => $memoryPeakMb,
            ]);
        }
    }
}

Ce middleware s'enregistre comme n'importe quel service taggé messenger.middleware et se branche dans la chaîne aux côtés de vos autres middlewares métier. En sortie, chaque ligne de log devient exploitable par votre stack d'observabilité — logs structurés vers Loki, métriques Prometheus via un exporter, ou export direct de traces si vous êtes déjà équipé en OpenTelemetry sur Symfony. C'est d'ailleurs l'un des angles morts que LoggableMiddleware ne couvrira pas nativement : la corrélation avec une trace distribuée reste à votre charge, middleware maison ou pas.

Diagnostiquer les redeliveries sans perdre de messages

Troisième chantier, plus discret mais tout aussi concret pour qui exploite RabbitMQ ou SQS en production : une pull request propose de rendre optionnel le comportement du RejectRedeliveredMessageMiddleware. Aujourd'hui, ce middleware rejette inconditionnellement un message redélivré par le transport avant même de tenter de le rejouer — un choix pensé pour éviter les boucles infinies de redelivery, mais qui peut aussi provoquer une perte de message si le transport de failure n'est lui-même pas disponible au moment du rejet.

La proposition introduit une option de configuration, exposée sous framework.messenger.reject_redelivered_message_middleware, activée par défaut pour préserver le comportement actuel — donc aucune rupture de compatibilité attendue si elle est mergée telle quelle. Ce qui change, c'est la possibilité de la désactiver explicitement pour les cas où perdre un message est pire que risquer une redelivery en boucle, typiquement quand le transport de failure lui-même n'est pas fiable à 100 % :

# config/packages/messenger.yaml — option proposée (PR en cours de revue, sous réserve d'ajustement)
framework:
    messenger:
        reject_redelivered_message_middleware: false # désactive le rejet auto des messages redélivrés
        transports:
            async:
                dsn: '%env(MESSENGER_TRANSPORT_DSN)%'

Sur le plan du diagnostic, c'est un signal précieux à croiser avec la future commande messenger:show et vos logs applicatifs : un message qui revient plusieurs fois avec le même identifiant, sur un handler donné, pointe presque toujours vers un problème d'idempotence côté handler ou vers un timeout d'acquittement trop court côté broker — pas vers un bug transitoire qu'on peut ignorer.

Un nouveau venu : le transport AMPHP SQL

Dernier chantier en cours, plus expérimental : une pull request propose d'ajouter un transport Messenger adossé à AMPHP SQL, permettant d'utiliser Messenger sans bloquer une boucle d'événements Amp — un cas d'usage qui concerne surtout les projets qui font déjà tourner du code asynchrone non bloquant en PHP (workers Amp, serveurs HTTP asynchrones) et qui veulent y intégrer Messenger sans réintroduire des appels bloquants au niveau du transport. La documentation associée à cette proposition est elle-même en cours de rédaction en parallèle du code. Je reste volontairement prudent sur les détails d'implémentation exacts — DSN, options de configuration, garanties de livraison — puisque rien n'est stabilisé ni mergé à ce stade : c'est un signal à surveiller plutôt qu'un outil à adopter dès maintenant.

Construire une stack observable dès aujourd'hui, sans attendre la 8.2

Ce qui ressort de ces quatre chantiers, c'est une direction claire donnée au composant Messenger : moins de boîte noire, plus d'introspection native. Mais aucune de ces fonctionnalités n'est mergée à l'heure où j'écris ces lignes, et leur forme finale peut encore bouger. La bonne nouvelle, c'est qu'on n'a pas besoin d'attendre pour construire une stack observable sur des workers Messenger déjà en production, avec les composants stables d'aujourd'hui.

  • Middleware de mesure — le PerformanceLoggingMiddleware ci-dessus, ou son équivalent, à brancher dès maintenant sur les buses sensibles.
  • Écoute des événements du worker — WorkerMessageReceivedEvent, WorkerMessageHandledEvent et WorkerMessageFailedEvent permettent d'exporter des métriques (compteurs, histogrammes de durée) vers Prometheus ou tout backend compatible dès aujourd'hui.
  • Introspection du transport — quand le broker le permet (management API RabbitMQ, CloudWatch pour SQS, requêtes SQL directes pour le transport Doctrine), rien n'empêche de scripter un équivalent maison de messenger:show en attendant sa stabilisation officielle.
  • Traçage distribué — propager un identifiant de corrélation dans les stamps de l'enveloppe dès l'émission du message, pour reconstituer le parcours complet d'une requête HTTP jusqu'au handler asynchrone qui la termine.

C'est aussi l'un des points qu'on aborde souvent en revue d'architecture avec des équipes qui découvrent en pleine incident de production que leur pipeline Messenger n'expose aucune métrique exploitable : le bon réflexe n'est pas d'attendre une version future du framework, c'est de définir dès maintenant les cas d'usage réels de vos handlers — synchrones, asynchrones, retryables ou non — comme on l'a détaillé en évoquant les attributs #[AsMessage] et leurs conditions whenNot, puis de brancher l'observabilité en fonction de ces cas d'usage plutôt que d'ajouter du logging générique partout.

Si votre système Messenger tourne déjà en production et que personne dans l'équipe ne peut répondre en trente secondes à la question « combien de messages sont en attente et lequel coûte le plus cher à traiter », c'est le signe qu'il manque une couche d'observabilité — pas forcément qu'il faut attendre la prochaine version majeure de Symfony pour la construire. C'est exactement le type de diagnostic qu'on mène avec les équipes qu'on accompagne : cartographier les transports et les handlers existants, identifier les angles morts réels avant de choisir les outils, puis instrumenter ce qui compte vraiment sans multiplier les dépendances. Si ce chantier vous parle sur votre propre stack Messenger, c'est une conversation qu'on peut avoir.

Cet article vous a plu ? Partagez-le !

Besoin d'un expert Symfony ?

20 ans d'expérience sur l'écosystème PHP/Symfony.