Logging événementiel en PHP/Symfony : pourquoi votre SaaS ne peut pas s'en passer
Cet article s'inspire d'une publication technique de Dmitry Isaenko sur dev.to, dans laquelle il décrit l'architecture de journalisation qu'il a mise en place pour son SaaS Laravel. Nous l'adaptons ici dans une perspective PHP/Symfony.
Le problème que tout le monde reporte à plus tard
« On ajoutera les logs plus tard. » Cette phrase, chaque développeur l'a prononcée au moins une fois. Et dans la majorité des cas, « plus tard » arrive le jour où quelque chose casse en production et où personne ne peut expliquer ce qui s'est passé.
Dans les applications SaaS, le logging est souvent traité comme une tâche secondaire : quelques appels éparpillés dans les contrôleurs, des messages de format inconsistant, et des fichiers texte impossibles à interroger efficacement. Le résultat est prévisible — un système de journalisation qui donne l'illusion de traçabilité sans en offrir la substance.
Pourtant, pour un SaaS en production, un système de logging robuste n'est pas un luxe. C'est une fondation opérationnelle qui permet de :
- Diagnostiquer rapidement les incidents sans fouiller le code
- Auditer les actions utilisateurs pour répondre aux exigences de conformité
- Identifier les comportements anormaux avant qu'ils ne deviennent des incidents critiques
- Instaurer la confiance avec vos clients grâce à une transparence sur les événements du système
L'approche manuelle : pourquoi elle échoue à l'échelle
L'approche classique consiste à placer manuellement des appels de log dans chaque contrôleur ou service :
// Éparpillé dans des dizaines de contrôleurs
$this->logger->info('Utilisateur connecté', ['user_id' => $user->getId()]);
$this->logger->info('Produit créé', ['product_id' => $product->getId()]);
$this->logger->warning('Connexion échouée', ['email' => $email]);
Cette approche présente plusieurs défauts structurels :
- Incohérence : chaque développeur formate les données différemment
- Oublis fréquents : il suffit d'une PR sans log pour perdre une traçabilité critique
- Contexte insuffisant : pas d'information sur l'appareil, la localisation, ou l'état de la requête
- Difficulté d'interrogation : rechercher des patterns dans des fichiers de logs plats est laborieux
Dès lors que l'application grossit et que l'équipe s'agrandit, ce modèle devient ingérable.
L'architecture événementielle : zéro appel manuel
L'alternative robuste repose sur une architecture event-driven. L'idée centrale : les logs ne sont plus écrits manuellement, ils sont générés automatiquement en réponse à des événements métier.
En Symfony, cela se traduit concrètement par l'utilisation combinée du EventDispatcher, des Doctrine Lifecycle Callbacks, et du système de Messenger pour le traitement asynchrone.
1. Capturer les événements sans modifier le code métier
Plutôt que d'insérer des appels de log dans chaque service, on définit des Event Subscribers qui écoutent les événements système et métier :
// src/EventSubscriber/ActivityLogSubscriber.php
class ActivityLogSubscriber implements EventSubscriberInterface
{
public function __construct(
private readonly ActivityLoggerService $logger,
private readonly RequestStack $requestStack
) {}
public static function getSubscribedEvents(): array
{
return [
SecurityEvents::LOGIN_SUCCESS => 'onLoginSuccess',
SecurityEvents::LOGIN_FAILURE => 'onLoginFailure',
];
}
public function onLoginSuccess(LoginSuccessEvent $event): void
{
$this->logger->log(
actor: $event->getUser(),
action: 'auth.login_success',
request: $this->requestStack->getCurrentRequest()
);
}
}
Pour les entités Doctrine, les Entity Listeners permettent de capturer automatiquement les créations, modifications et suppressions sans toucher aux services métier.
2. Enrichir les logs avec du contexte
Un bon log ne se contente pas d'enregistrer « qu'est-ce qui s'est passé » — il documente aussi dans quel contexte. Cela implique de collecter systématiquement :
- L'adresse IP et le User-Agent de la requête
- Les informations de géolocalisation (traitées de façon asynchrone pour ne pas impacter les performances)
- Une empreinte de l'appareil (device fingerprinting) pour détecter les connexions depuis de nouveaux équipements
- L'identité de l'utilisateur et son rôle au moment de l'action
En Symfony, un RequestContextService centralisé peut extraire et normaliser ces données à partir de l'objet Request :
class RequestContextService
{
public function extract(Request $request): array
{
return [
'ip' => $request->getClientIp(),
'user_agent' => $request->headers->get('User-Agent'),
'device_fingerprint' => $this->buildFingerprint($request),
'session_id' => $request->getSession()->getId(),
];
}
}
3. Traitement asynchrone avec Symfony Messenger
La géolocalisation et certains enrichissements de logs peuvent être coûteux en temps. Les exécuter de manière synchrone dans le cycle de vie d'une requête HTTP est une mauvaise pratique. Symfony Messenger permet de déléguer ce travail à un worker en arrière-plan :
// Dispatch du message dans le subscriber
$this->bus->dispatch(new EnrichActivityLogMessage($logId));
// Handler exécuté de façon asynchrone
class EnrichActivityLogHandler implements MessageHandlerInterface
{
public function __invoke(EnrichActivityLogMessage $message): void
{
$log = $this->repository->find($message->getLogId());
$geoData = $this->geoService->resolve($log->getIpAddress());
$log->setGeoData($geoData);
$this->entityManager->flush();
}
}
Ce pattern garantit que la journalisation n'ajoute aucune latence perceptible pour l'utilisateur final.
Rendre les logs actionnables
Collecte et stockage ne suffisent pas. Les logs doivent être exploitables par les équipes techniques et les administrateurs. Cela suppose :
- Une interface d'administration permettant de filtrer par utilisateur, action, période ou niveau de criticité
- Des alertes automatiques pour les événements sensibles (connexion depuis un pays inhabituel, nombre d'échecs d'authentification anormal)
- Un système de notification multi-canal (email, Slack, webhook) pour les événements critiques
En Symfony, les notifications peuvent s'appuyer sur le composant Notifier combiné à des règles métier définies dans un service dédié :
class ActivityAlertService
{
public function evaluate(ActivityLog $log): void
{
if ($this->isSuspiciousLogin($log)) {
$this->notifier->send(
new Notification('Connexion suspecte détectée', ['email', 'slack']),
$log->getActor()
);
}
}
}
Conclusion : le logging comme investissement produit
Mettre en place un système de logging événementiel demande un investissement initial plus important qu'une série d'appels manuels. Mais le retour sur investissement est rapide et mesurable : moins de temps passé à diagnostiquer des incidents, meilleure conformité aux exigences d'audit, et une confiance accrue de la part de vos clients.
L'architecture décrite ici — EventSubscriber, RequestContext centralisé, enrichissement asynchrone via Messenger, alertes automatiques — est directement applicable dans un projet Symfony. Elle transforme le logging d'une corvée en un véritable outil de pilotage.
Chez MulerTech, nous intégrons systématiquement ces pratiques dans les projets SaaS que nous développons, car un système observable est un système que l'on peut maintenir et faire évoluer sereinement.