Une API de logging fluide pour PHP
Des logs qui expriment votre intention.
Choisissez un niveau, ajoutez du contexte structuré et décidez quand écrire. Phlogger fournit une API de logging fluide pour PHP 8.3+ et le logger PSR-3 que vous utilisez déjà.
use Phlogger\FluentLogger;
use Phlogger\Level;
$logger = new FluentLogger($psrBackend, minimumLevel: Level::Info);
$logger->atWarning()
->withCause($exception)
->atMostEvery(30)
->log('Payment failed for order {id}', ['id' => $orderId]);
Installation
Phlogger nécessite PHP 8.3 ou une version ultérieure. Sa seule dépendance d'exécution est psr/log. Fournissez un backend PSR-3 : le logger de votre application, un logger de test ou Psr\Log\NullLogger.
Installez Phlogger depuis Packagist avec Composer :
composer require roukmoute/phlogger
Phlogger fournit l'API de logging. Le backend fourni assure le stockage, la mise en forme et le transport.
Premiers pas
Chaque méthode at*() démarre un enregistrement fluide. Terminez par log() pour l'émettre. Les huit niveaux PSR-3 sont disponibles : debug, info, notice, warning, error, critical, alert et emergency.
$logger->atInfo()
->with('user_id', $userId)
->log('User {user_id} signed in');
$logger->at(Level::Notice)
->withContext(['job' => 'daily-report', 'rows' => 120])
->log('Report generated');
$logger->atError()
->withCause($exception)
->log('Report generation failed');
log() transmet le modèle du message et son contexte au backend. L'interpolation des paramètres appartient au backend. Utilisez logf() pour demander explicitement une mise en forme avec sprintf().
$logger->atInfo()->logf('Processed %d rows in %.2f seconds', $rows, $seconds);
Contexte et exceptions
Utilisez with() pour une valeur et withContext() pour un groupe. Le contexte du logger, celui de l'enregistrement fluide puis celui de l'appel terminal sont fusionnés dans cet ordre : la valeur la plus spécifique l'emporte.
$requestLogger = $logger->withContext([
'request_id' => $requestId,
'component' => 'http',
]);
$requestLogger->atInfo()
->with('component', 'checkout')
->log('Checkout started', ['component' => 'payment']);
// The backend receives component=payment and request_id=$requestId.
Le logger et les builders fluides sont immuables. Ajouter du contexte retourne une nouvelle valeur : le logger d'origine conserve ses valeurs par défaut. Les clones contextuels partagent le même état de limitation.
withCause($throwable) place la cause sous la clé PSR-3 exception. Les exceptions et les erreurs PHP sont acceptées. withCause(null) attribue null à cette clé et efface ainsi une cause précédente. Le contexte de l'appel terminal log() peut remplacer cette clé.
Les tableaux, objets, valeurs nulles et closures ordinaires restent des données structurées. Phlogger ne convertit pas les valeurs arbitraires en chaînes et n'exécute pas les closures ordinaires du contexte.
Évaluation différée
Encapsulez une valeur de contexte ou un argument de mise en forme coûteux dans LazyValue::of(). Le message peut également être une closure. Ces calculs sont exécutés seulement après l'admission de l'enregistrement par les filtres, conditions et limitations de Phlogger.
use Phlogger\LazyValue;
$logger->atDebug()
->with('summary', LazyValue::of(fn () => calculateSummary()))
->log('Summary: {summary}');
$logger->atDebug()
->log(fn () => buildDiagnosticMessage());
$logger->atInfo()
->every(100)
->logf('Queue size: %d', LazyValue::of(fn () => countQueue()));
Une même valeur différée est évaluée une fois par enregistrement, puis son résultat est transmis au backend. Seules les valeurs au premier niveau du contexte sont résolues : Phlogger ne parcourt pas les tableaux ou objets imbriqués. Une closure ordinaire du contexte est conservée telle quelle.
Le backend peut encore filtrer l'enregistrement. PSR-3 ne permet pas de consulter les niveaux actifs d'un backend. Configurez minimumLevel ou filter dans Phlogger pour éviter les calculs différés aux niveaux désactivés par votre application.
Évitez les effets de bord applicatifs dans les callbacks différés. Les exceptions levées par les callbacks et par le backend remontent à l'appelant.
Filtres et conditions
Le niveau minimum par défaut est Level::Debug. Augmentez-le pour exclure les enregistrements plus détaillés. Une closure filter optionnelle reçoit le Level sélectionné et retourne un booléen ; elle restreint davantage le logging.
$logger = new FluentLogger(
$psrBackend,
minimumLevel: Level::Info,
filter: fn (Level $level): bool => $level !== Level::Notice,
);
$logger->atWarning()
->when(fn () => $queue->isOverloaded())
->log('Queue is overloaded');
$logger->atInfo()
->when($shouldAudit)
->log('Audit event recorded');
Plusieurs conditions when() doivent toutes autoriser l'enregistrement. isEnabled() permet de vérifier le niveau avant un calcul séparé : cette méthode ne consomme aucun quota et ne prédit pas le résultat des conditions ou limitations.
if ($logger->atDebug()->isEnabled()) {
// Prefer LazyValue for work belonging to a single record.
}
Limitation des logs
Contrôlez les logs répétitifs directement au site d'appel. Les limites sont vérifiées avant l'évaluation différée du message et du contexte.
| Méthode | Comportement |
|---|---|
every(3) | Admet le premier appel, puis les appels 4, 7, 10, etc. every(1) n'a aucun effet. |
first(5) | Admet seulement les cinq premiers enregistrements éligibles pour ce site et cette politique. first(0) supprime tous les enregistrements. |
atMostEvery(30) | Admet le premier appel, puis impose au moins 30 secondes depuis le dernier appel admis. Zéro désactive cette limite temporelle. |
$logger->atWarning()
->atMostEvery(30)
->log('Remote service is unavailable');
$logger->atDebug()
->first(5)
->log('Initial sample', ['sample' => $sample]);
Si une chaîne combine plusieurs limites, toutes doivent autoriser l'appel. Les compteurs d'intervalle et la fenêtre temporelle sont réinitialisés ensemble après admission. Un enregistrement rejeté par un filtre de niveau ou une condition ne consomme aucun quota.
every() exige un nombre strictement positif ; first() accepte zéro ou un nombre positif. atMostEvery() exige une durée finie et non négative en secondes. Répéter une méthode de limitation remplace la valeur précédente de cette méthode.
Sites d'appel et clés explicites
Par défaut, l'état dépend du fichier et de la ligne de l'appel terminal, du niveau et de la politique de limitation, au sein de la famille du logger. Des lignes différentes sont indépendantes. Deux appels sur la même ligne avec le même niveau et la même politique partagent leur état.
Utilisez withRateLimitKey() pour choisir une clé stable, notamment dans une fonction qui encapsule le logging. Une clé partage son état seulement si le niveau et la politique sont également identiques.
$logger->atWarning()
->withRateLimitKey('payment-provider-unavailable')
->atMostEvery(30)
->log('Payment provider unavailable');
Durée de vie et mémoire
L'état est local à la famille du logger et au processus PHP courant. Les clones contextuels le partagent ; des loggers construits séparément et des workers distincts ne le partagent pas. Les limites ne coordonnent pas plusieurs workers PHP-FPM ou instances d'application.
Le stockage contient jusqu'à 1 024 entrées par défaut. Le paramètre de constructeur rateLimitCapacity permet de modifier cette capacité. Quand il est plein, l'entrée utilisée le moins récemment est supprimée. Une clé supprimée recommence avec un quota neuf. Préférez des clés stables à des identifiants issus d'un ensemble illimité.
Les limites temporelles utilisent une horloge monotone. Une closure clock optionnelle dans le constructeur, retournant des secondes, permet des tests déterministes. resetRateLimits() vide le stockage partagé.
L'admission consomme le quota. Un enregistrement admis par Phlogger consomme sa limite même si un callback différé lève une exception, si le backend échoue ou s'il refuse l'écriture. PSR-3 ne permet pas à Phlogger de confirmer la livraison.
Intégration PSR-3
FluentLogger implémente Psr\Log\LoggerInterface. Fournissez-le aux bibliothèques qui attendent un logger PSR-3 ou utilisez ses méthodes classiques à côté des appels fluides.
use Psr\Log\LoggerInterface;
function reportFailure(LoggerInterface $logger, Throwable $error): void
{
$logger->error('Operation failed', ['exception' => $error]);
}
reportFailure($logger, $exception);
$logger->info('User {id} signed in', ['id' => $userId]);
$logger->log('notice', 'Job completed', ['job' => $jobId]);
Les méthodes PSR-3 directes utilisent les mêmes filtres de niveau et le contexte par défaut. Le log() terminal d'une chaîne fluide reçoit un message ; le log() PSR-3 du logger reçoit un niveau puis un message. Les niveaux inconnus provoquent une Psr\Log\InvalidArgumentException.
Pour préserver la compatibilité PSR-3, les méthodes directes du logger conservent toutes les valeurs de contexte telles qu'elles sont fournies, y compris les objets LazyValue. La résolution des valeurs différées est une fonctionnalité des enregistrements fluides.
Intégration avec Monolog
Phlogger est une façade fluide pour tout backend PSR-3. Monolog est une option : il fournit les handlers, processors et formatters, tandis que Phlogger gère le contexte fluide, les conditions et les limitations. Votre application choisit d'installer Monolog ; la seule dépendance d'exécution de Phlogger reste psr/log.
composer require "monolog/monolog:^3"
Cet exemple écrit des enregistrements JSON sur la sortie standard. PsrLogMessageProcessor interpole les paramètres PSR-3, tandis que JsonFormatter met en forme l'enregistrement et son contexte structuré.
use Monolog\Formatter\JsonFormatter;
use Monolog\Handler\StreamHandler;
use Monolog\Logger as MonologLogger;
use Monolog\Processor\PsrLogMessageProcessor;
use Phlogger\FluentLogger;
use Phlogger\LazyValue;
use Phlogger\Level;
$handler = new StreamHandler('php://stdout', Monolog\Level::Info);
$handler->setFormatter(new JsonFormatter());
$backend = new MonologLogger('app');
$backend->pushHandler($handler);
$backend->pushProcessor(new PsrLogMessageProcessor());
$logger = new FluentLogger(
$backend,
filter: static fn (Level $level): bool => $backend->isHandling($level->value),
);
$requestLogger = $logger->withContext(['request_id' => 'request-123']);
$requestLogger->atInfo()
->log('Order {order_id} accepted', ['order_id' => 42]);
$error = new RuntimeException('Payment provider unavailable');
$requestLogger->atError()
->withCause($error)
->log('Payment failed for order {order_id}', ['order_id' => 42]);
L'enregistrement info contient le message Order 42 accepted et conserve les identifiants de requête et de commande dans son contexte. L'enregistrement error porte la cause sous exception, que le formatter de Monolog peut normaliser.
Éviter les calculs différés aux niveaux rejetés
Le filtre vérifie si le pipeline configuré dans Monolog accepte actuellement le niveau choisi. Avec le handler de niveau info ci-dessus, un enregistrement debug est rejeté avant que Phlogger évalue ses valeurs différées :
$debugEvaluations = 0;
$requestLogger->atDebug()
->with('details', LazyValue::of(static function () use (&$debugEvaluations): string {
++$debugEvaluations;
return 'Diagnostic summary';
}))
->log('Diagnostics: {details}');
// $debugEvaluations remains 0.
Vérifiez le pipeline complet. isHandling() estime si un handler accepte un niveau ; cette méthode ne garantit pas la livraison. Les handlers de buffering ou de routage peuvent prendre des décisions supplémentaires ensuite. Utilisez la vérification du logger configuré plutôt que de recopier le seuil du handler de fichier final : un handler extérieur peut volontairement recueillir les niveaux plus détaillés. Le minimumLevel de Phlogger peut restreindre davantage ce pipeline.
Retrouver la ligne d'appel dans l'application
Configurez le processor d'introspection de Monolog pour ignorer les appels internes de Phlogger et les méthodes PSR héritées comme info(). Attachez-le au logger pour qu'il s'exécute avant les handlers de buffering ; un processor placé derrière FingersCrossedHandler voit la ligne de vidage du buffer plutôt que l'appel d'origine.
$backend->pushProcessor(new Monolog\Processor\IntrospectionProcessor(
skipClassesPartials: ['Phlogger\\', 'Psr\\Log\\'],
));
L'exemple d'introspection conserve le fichier et la ligne de chaque appel dans l'application, même lorsque les enregistrements sont mis en attente.
Exécuter l'exemple complet
Le dépôt contient un exemple Monolog exécutable. Dans un clone de développement, composer install installe Monolog comme dépendance de développement pour les exemples et tests d'intégration :
composer install
php examples/monolog.php
Consultez le guide d'utilisation de Monolog pour configurer les handlers et processors.
Symfony
Service fluide supplémentaire
Déclarez Phlogger comme service supplémentaire dans config/services.yaml, puis injectez FluentLogger dans les services de votre application :
services:
Phlogger\FluentLogger:
arguments:
$backend: '@logger'
Le logger PSR existant reste disponible. Pour un canal Monolog déclaré dans votre application, utilisez plutôt @monolog.logger.payments comme backend. MonologBundle continue de configurer les handlers, processors et règles de routage.
PSR-3 ne permet pas de découvrir le seuil du backend. Configurez $minimumLevel selon votre application ou utilisez une factory avec le filtre de disponibilité Monolog pour éviter les calculs différés aux niveaux rejetés par le backend.
L'exemple Symfony exécutable utilise le vrai conteneur d'injection. Le deuxième job voit son avertissement supprimé ; le troisième peut en émettre un après une remise à zéro explicite. Le guide d'intégration en français contient la configuration applicative et les recettes de canaux.
Faire de Phlogger le @logger par défaut
Utilisez cette autre définition dans config/services.yaml pour que @logger, LoggerInterface injecté automatiquement et FluentLogger désignent la même instance Phlogger :
services:
Phlogger\FluentLogger:
decorates: logger
decoration_inner_name: phlogger.backend
arguments:
$backend: '@phlogger.backend'
Psr\Log\LoggerInterface: '@logger'
Remplacez la définition du service supplémentaire ci-dessus par celle-ci. Symfony conserve le logger d'origine derrière phlogger.backend. Utiliser @logger comme backend du décorateur créerait une dépendance circulaire.
Les consommateurs typés LoggerInterface gardent leurs méthodes PSR comme info(). Injectez FluentLogger pour utiliser atInfo() et l'API fluide. Monolog continue de gérer ses handlers, processors et son cycle de vie ; les canaux nommés injectés explicitement conservent leurs propres loggers.
Le minimum debug par défaut transmet tous les niveaux PSR. Configurez délibérément $minimumLevel si vous voulez filtrer tous les consommateurs du logger par défaut. Les consommateurs qui ont besoin des méthodes du backend peuvent injecter @phlogger.backend.
L'exemple du logger par défaut compile cette configuration, injecte les deux types et transmet les logs PSR et fluides à Monolog. Exécutez php examples/symfony-default-logger.php depuis un clone de développement.
Laravel
Ajoutez une liaison scoped dans AppServiceProvider::register(), puis injectez FluentLogger dans vos contrôleurs, services ou jobs :
use Illuminate\Contracts\Foundation\Application;
use Phlogger\FluentLogger;
use Psr\Log\LoggerInterface;
$this->app->scoped(
FluentLogger::class,
static fn (Application $app): FluentLogger => new FluentLogger(
$app->make(LoggerInterface::class),
),
);
Cette configuration conserve le gestionnaire de logs Laravel, son contexte partagé et ses événements. Pour un canal configuré, utilisez $app->make('log')->channel('payments') comme backend ; pour une stack, utilisez $app->make('log')->stack(['single', 'slack']). Continuez à écrire via le logger Laravel plutôt que son backend Monolog extrait.
La factory laisse le minimum de Phlogger à debug. Configurez minimumLevel si Phlogger doit rejeter les calculs différés plus tôt. Gardez la liaison fluide distincte de LoggerInterface pour préserver les logs existants et éviter une résolution circulaire.
L'exemple Laravel exécutable utilise de vrais composants et renouvelle explicitement les scopes entre deux jobs simulés. Il ne démarre pas une application Laravel complète ni un serveur Octane.
Choisir la durée de vie des quotas dans un worker
Les clones contextuels partagent l'état du limiteur de leur parent. Un logger partagé dans un worker conserve ses quotas entre les jobs ; une nouvelle instance ou resetRateLimits() crée des quotas frais. Une remise à zéro par job laisse ainsi passer le premier avertissement de chaque job, même avec une limite d'une minute.
Symfony
Les services sont partagés par défaut. Pour effacer les quotas lorsque Symfony réinitialise ses services, notamment entre les messages avec la configuration habituelle de Messenger, ajoutez ce tag à la variante du service supplémentaire :
services:
Phlogger\FluentLogger:
arguments:
$backend: '@logger'
tags:
- { name: kernel.reset, method: resetRateLimits }
Pour le décorateur du logger par défaut, ajoutez le même tag à sa définition existante en conservant $backend: '@phlogger.backend'. Omettez le tag pour des quotas conservés dans le worker. Désactiver la réinitialisation des services du framework supprime aussi cette remise à zéro. Dans une boucle séquentielle personnalisée, appelez vous-même la méthode entre les jobs.
Laravel
Une liaison scoped() est renouvelée à chaque requête ou job de queue, y compris avec Octane et les workers de queue. Une liaison singleton() conserve les quotas dans le worker. Résolvez à nouveau les consommateurs scoped au prochain scope ; un singleton qui capture un logger scoped conserve l'ancienne instance et son état.
Quotas et backend ont des cycles distincts. Réinitialiser Phlogger ne vide pas les buffers Monolog ni le contexte partagé Laravel. Conservez le cycle de vie habituel du backend géré par le framework. Ne réinitialisez pas une famille de loggers partagée pendant qu'un autre traitement concurrent l'utilise.
Les tests utilisent les vrais composants Symfony et Illuminate pour vérifier l'injection, les canaux, le contexte et les événements Laravel, ainsi que les quotas partagés ou renouvelés. Ils ne démarrent pas des kernels ou workers complets. Les composants des frameworks restent des dépendances de développement ; psr/log reste la seule dépendance de production de Phlogger.
Benchmarks
Mesurez le coût de l'API depuis un clone de développement, sans bibliothèque de benchmark :
composer install
composer benchmark -- --iterations=100000 --samples=5
php benchmarks/run.php --iterations=100000 --samples=5 --json
Le script compare les appels PSR directs ou filtrés, les instructions fluides acceptées ou rejetées, les valeurs différées, les entrées réutilisées et les quotas. Il utilise NullLogger, un échauffement et plusieurs mesures, sans écriture de fichier ni accès réseau. La sortie JSON inclut les mesures brutes et la configuration du runtime.
Les temps correspondent à chaque tentative de log. Le pic de mémoire temporaire ne compte pas les allocations ; les filtres manuels offrent moins de fonctionnalités que les quotas partagés par clé. Ces observations ne prédisent pas le débit de l'application et n'imposent aucun seuil de temps dans la CI. Consultez le guide du benchmark en français pour connaître les limites de comparaison.
Référence de l'API
| Point d'entrée | Rôle |
|---|---|
at(Level|string), huit méthodes at*() | Démarrer un enregistrement fluide immuable à un niveau PSR-3. |
with(string, mixed) | Ajouter une valeur de contexte. |
withContext(array) | Fusionner le contexte d'un enregistrement ou créer un logger contextuel. |
withCause(?Throwable) | Définir la valeur de contexte exception, y compris null. |
when(bool|Closure) | Exiger une condition avant d'admettre un enregistrement. |
every(int), first(int), atMostEvery(float) | Limiter les appels par fréquence, nombre ou durée en secondes. |
withRateLimitKey(string) | Choisir la clé de l'état de limitation à la place du site d'appel. |
isEnabled() | Vérifier le niveau sans consommer de quota. |
log(string|Stringable|Closure, array) | Émettre un modèle de message et du contexte structuré. Une closure de message retourne une chaîne ou un Stringable. |
logf(string, mixed ...) | Émettre un message mis en forme avec sprintf(). Les arguments doivent se résoudre en valeurs scalaires, nulles ou Stringable. |
LazyValue::of(Closure) | Différer une valeur de contexte au premier niveau ou un argument de mise en forme. |
resetRateLimits() | Vider l'état de limitation partagé par la famille du logger. |
Retrouvez les sources et exemples sur GitHub. L'API s'intègre à la spécification PSR-3.