andydefer/laravel-logger

A structured logging package for Laravel that writes logs in JSONL format (JSON Lines). Each log entry is a valid JSON object on its own line, making it easy to parse, stream, and analyze.

Maintainers

Package info

github.com/andydefer/laravel-logger

pkg:composer/andydefer/laravel-logger

Statistics

Installs: 5

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

1.0.0 2026-05-22 10:24 UTC

This package is auto-updated.

Last update: 2026-05-22 10:57:29 UTC


README

Un package de logging structuré pour Laravel qui écrit les logs au format JSONL (JSON Lines).

PHP Version Laravel Version License

Installation

composer require andydefer/laravel-logger

Le package s'enregistre automatiquement via Laravel.

Configuration

Variables d'environnement (optionnel)

LOGGER_PATH=/custom/log/path
LOGGER_RETENTION_DAYS=60

Publication du fichier de config (optionnel)

php artisan vendor:publish --tag=logger-config

Premier log

use AndyDefer\Logger\Collections\MixedPayloadCollection;
use AndyDefer\Logger\Records\LogDataRecord;
use AndyDefer\Logger\Contracts\LoggerInterface;

class UserController extends Controller
{
    public function __construct(
        private readonly LoggerInterface $logger,
    ) {}
    
    public function login()
    {
        $payload = new MixedPayloadCollection();
        $payload->add('user_login', 123, '127.0.0.1', true);

        $logData = new LogDataRecord(type: 'auth', payload: $payload);

        $this->logger->info($logData);
    }
}

Résultat dans le fichier de log :

{"time":"2026-04-05T10:26:00Z","level":"info","data":{"type":"auth","payload":["user_login",123,"127.0.0.1",true]}}

Les 4 niveaux de log

$logger->debug($logData);   // DEBUG
$logger->info($logData);    // INFO
$logger->warning($logData); // WARNING
$logger->error($logData);   // ERROR

Le timestamp est automatique.

Types acceptés dans un payload

Type Exemple
int $payload->add(123)
float $payload->add(99.99)
string $payload->add('hello')
bool $payload->add(true)
null $payload->add(null)
AbstractRecord $payload->add($userRecord)
TypedCollection $payload->add($tags)

La méthode add() accepte plusieurs paramètres : $payload->add('user_login', 123, '127.0.0.1', true)

Travailler avec le payload

Lire des éléments

$first = $payload->firstItem();      // Premier élément
$last = $payload->lastItem();        // Dernier élément
$array = $payload->toArray();        // Tout en tableau

Compter

$count = $payload->count();           // Nombre d'éléments
$isEmpty = $payload->isEmpty();       // Collection vide ?
$isNotEmpty = $payload->isNotEmpty(); // Collection non vide ?

Filtrer

// Éléments > 3
$filtered = $payload->filter(fn($item) => $item > 3);

// Uniquement les strings
$strings = $payload->ofType('string');

// Uniquement les entiers
$ints = $payload->ofType('int');

// Uniquement les scalaires
$scalars = $payload->scalars();

// Uniquement les Records
$records = $payload->records();

Transformer

// Doubler chaque élément
$doubles = $payload->map(fn($item) => $item * 2);

// Supprimer les doublons
$unique = $payload->unique();

// Mélanger
$shuffled = $payload->shuffle();

Calculs (pour collections numériques)

$total = $payload->sum();     // Somme
$moyenne = $payload->avg();   // Moyenne
$max = $payload->max();       // Maximum
$min = $payload->min();       // Minimum

Vérifications

// Un élément existe ?
if ($payload->contains(123)) { ... }

// Tous sont du même type ?
if ($payload->isHomogeneous()) { ... }

// Tous sont des entiers ?
$payload->assertAllOfType('int');

Rechercher des logs

Query par type d'événement

use AndyDefer\Logger\Records\LogQueryRecord;

$query = new LogQueryRecord(
    from: '2026-04-05T00:00:00Z',
    to: '2026-04-05T23:59:59Z',
    type: 'user_login',
);

$results = $logger->query($query);

Query par niveau

use AndyDefer\Logger\Enums\LogLevel;

$query = new LogQueryRecord(
    level: LogLevel::ERROR,
);

$errors = $logger->query($query);

Query combinée

$query = new LogQueryRecord(
    from: now()->subDay()->toIso8601ZuluString(),
    type: 'payment_failed',
    level: LogLevel::ERROR,
);

$failedPayments = $logger->query($query);

Parcourir les résultats

foreach ($results as $log) {
    echo $log->time . "\n";
    echo $log->level->value . "\n";
    echo $log->data->type . "\n";
    
    foreach ($log->data->payload as $item) {
        echo $item . "\n";
    }
}

Streaming (tous les logs d'un jour)

// Jour spécifique
$logs = $logger->stream('2026-04-05');

// Aujourd'hui
$logs = $logger->stream();

foreach ($logs as $log) {
    // Traitement...
}

Buffer d'écriture (performance)

Le buffer regroupe les logs en mémoire avant de les écrire sur le disque.

Activer le buffer

$logger->enableBuffer(100);  // 100 logs avant écriture automatique

Utilisation

$logger->enableBuffer(50);

// Ces logs restent en mémoire
for ($i = 0; $i < 50; $i++) {
    $logger->info($logData);
}

// Déclenche l'écriture automatique
$logger->info($logData);

// Ou vider manuellement
$logger->flush();

Désactiver

$logger->disableBuffer();  // Vide automatiquement le buffer

Callback à chaque flush

$logger->enableBuffer(100);
$logger->onFlush(function ($count) {
    \Log::info("{$count} logs écrits");
});

Exemples concrets

Authentification

// Connexion réussie
$payload = new MixedPayloadCollection();
$payload->add('user_login', $user->id, request()->ip(), true);

$logger->info(new LogDataRecord(type: 'auth', payload: $payload));

// Échec de connexion
$payload = new MixedPayloadCollection();
$payload->add('user_login_failed', request()->email, request()->ip(), 'invalid_password');

$logger->warning(new LogDataRecord(type: 'auth', payload: $payload));

Paiement

// Paiement réussi
$payload = new MixedPayloadCollection();
$payload->add('payment_success', $order->id, $stripeId, $order->total);

$logger->info(new LogDataRecord(type: 'payment', payload: $payload));

// Paiement échoué
$payload = new MixedPayloadCollection();
$payload->add('payment_failed', $order->id, $exception->getMessage());

$logger->error(new LogDataRecord(type: 'payment', payload: $payload));

Log avec un Record personnalisé

use AndyDefer\Records\AbstractRecord;

final class UserRecord extends AbstractRecord
{
    public function __construct(
        public readonly int $id,
        public readonly string $email,
        public readonly string $role,
    ) {}
}

$userRecord = new UserRecord(id: 1, email: 'john@example.com', role: 'admin');

$payload = new MixedPayloadCollection();
$payload->add('user_created', $userRecord);

$logger->info(new LogDataRecord(type: 'user', payload: $payload));

Log d'API externe

$payload = new MixedPayloadCollection();
$payload->add('api_call', 'stripe', '/v1/customers', 'POST', json_encode($data));

$logger->info(new LogDataRecord(type: 'api', payload: $payload));

Commandes Artisan

Nettoyer les vieux logs

# Nettoyer les logs de plus de 30 jours (valeur par défaut)
php artisan logger:clean

# Nettoyer les logs de plus de 60 jours
php artisan logger:clean --days=60

# Simulation (ne supprime rien)
php artisan logger:clean --dry-run

# Mode verbeux (affiche les fichiers à supprimer)
php artisan logger:clean --verbose

Tests unitaires

Mock du Logger

use AndyDefer\Logger\Contracts\LoggerInterface;
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;

#[AllowMockObjectsWithoutExpectations]
class UserServiceTest extends TestCase
{
    public function test_login_logs_success(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        
        $logger->expects($this->once())
            ->method('info')
            ->with($this->callback(function ($logData) {
                return $logData->type === 'auth'
                    && $logData->payload->contains('user_login')
                    && $logData->payload->contains(123);
            }));
        
        $service = new UserService($logger);
        $service->login(123);
    }
}

Tester la structure, pas le texte

// ✅ BON - Test robuste
$logger->expects($this->once())
    ->method('info')
    ->with($this->callback(fn($log) => $log->payload->contains(123)));

// ❌ MAUVAIS - Fragile (Laravel natif)
$logger->expects($this->once())
    ->method('info')
    ->with('User 123 logged in');

LogLevel - méthodes utilitaires

use AndyDefer\Logger\Enums\LogLevel;

$level = LogLevel::INFO;

$level->getLabel();   // 'Info'
$level->isDebug();    // false
$level->isInfo();     // true
$level->isWarning();  // false
$level->isError();    // false

// Toutes les valeurs
LogLevel::values();   // ['debug', 'info', 'warning', 'error']

// Depuis une valeur
LogLevel::fromValue('info'); // LogLevel::INFO

Bonnes pratiques

1. Premier élément = type d'événement

// ✅
$payload->add('user_login', $userId, $ip, $success);

// ❌
$payload->add($userId, 'user_login', $ip);

2. snake_case pour les types

// ✅
'type' => 'user_login'
'type' => 'payment_failed'

// ❌
'type' => 'userLogin'

3. Injection uniquement, pas de facade

// ✅ Injection explicite
class MyService
{
    public function __construct(
        private readonly LoggerInterface $logger,
    ) {}
}

// ❌ Éviter les facades
\Log::info(...);

4. Tester la structure

// ✅ Tester la présence des données
$log->payload->contains(123)

// ❌ Tester du texte (Laravel natif)
str_contains($log, 'User 123')

Règle d'or

ZÉRO appel statique. TOUTES les dépendances injectées. Le timestamp est automatique. Les tests vérifient la STRUCTURE, pas le TEXTE.

// ✅ Le log parfait
$payload = new MixedPayloadCollection();
$payload->add('user_login', $userId, $ip, true);

$logger->info(new LogDataRecord(type: 'auth', payload: $payload));
// ✅ Le test parfait
$logger->expects($this->once())
    ->method('info')
    ->with($this->callback(fn($log) => 
        $log->type === 'auth' 
        && $log->payload->contains($userId)
    ));

Pourquoi ce package ?

Les faiblesses du système de log natif de Laravel

Problème Explication Conséquence
Format non structuré Les logs sont du texte libre Impossible de parser ou filtrer efficacement
Types non préservés Log::info('message', ['user' => $user])"Array" Perte d'information, données inexploitables
Pas de requêtage On ne peut chercher que par texte Impossible de filtrer par type d'événement ou par niveau
Tests fragiles assertStringContainsString('User 123', $log) Un simple changement de texte casse les tests
Pas de séparation sémantique Message et contexte mélangés Impossible d'extraire proprement les données
Format non standard Format propriétaire Laravel Difficile à intégrer avec des outils externes (ELK, Loki, Datadog)

Les avantages de ce package

Avantage Explication
Format JSONL standard Chaque ligne est un JSON valide, compatible avec tous les outils
Types préservés Les entiers, booléens, objets restent typés
Requêtage puissant Filtrage par type, niveau, plage de dates
Tests robustes On teste la structure, pas le texte
Séparation claire type = événement, payload = données
Performance Buffer d'écriture, organisation par heure
Maintenance automatique Nettoyage des vieux logs configurable

Exemple comparatif

// ❌ Laravel natif - Perte d'information
Log::info("Utilisateur {$user->id} connecté", ['ip' => $ip]);
// Sortie: [2024-01-15 14:30:00] local.INFO: Utilisateur 123 connecté {"ip":"127.0.0.1"}

// ✅ Ce package - Structure complète
$payload = new MixedPayloadCollection();
$payload->add('user_login', $user->id, $ip, true);

$logger->info(new LogDataRecord(type: 'auth', payload: $payload));
// Sortie: {"time":"2024-01-15T14:30:00Z","level":"info","data":{"type":"auth","payload":["user_login",123,"127.0.0.1",true]}}

Licence

MIT © Andy Defer