andydefer/laravel-logger

A structured logging package for Laravel that writes logs in JSONL format (JSON Lines).
3,028
Install
composer require andydefer/laravel-logger
Latest Version:v3.9.49
PHP:>=8.1
Laravel:12 · 13
License:MIT
Last Updated:Oct 16, 2026
Links: GitHub  ·  Packagist
Maintainer: andydefer

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\DomainStructures\Utils\StrictDataObject;
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 StrictDataObject([
            'event' => 'user_login',
            'user_id' => 123,
            'ip' => '127.0.0.1',
            'success' => 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":{"event":"user_login","user_id":123,"ip":"127.0.0.1","success":true}}}

⚠️ Le payload utilise StrictDataObject qui préserve exactement les noms de clés (camelCase ou snake_case). Le timestamp est automatique.


Les 4 niveaux de log

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

Types de payload

StrictDataObject accepte n'importe quelle structure clé-valeur :

Type Exemple
int 'user_id' => 123
float 'amount' => 99.99
string 'ip' => '127.0.0.1'
bool 'success' => true
null 'optional' => null
array 'tags' => ['premium', 'vip']
AbstractRecord 'user' => $userRecord
TypedCollection 'items' => $collection

Travailler avec le payload

Lire des propriétés

$userId = $log->data->payload->user_id;      // Accès direct
$ip = $log->data->payload->ip;               // via propriété
$value = $log->data->payload->get('key');    // avec valeur par défaut
$hasKey = $log->data->payload->has('key');   // Vérifier existence

Convertir en tableau

$array = $log->data->payload->toArray();
// ['event' => 'user_login', 'user_id' => 123, ...]

Immuabilité - Créer une nouvelle version

$newPayload = $payload->with('status', 'completed');  // Ajoute/modifie
$merged = $payload->merge(['new_key' => 'value']);    // Fusionne
$reduced = $payload->without('temp_key');              // Supprime

Requêter les logs

Query par type d'événement

use AndyDefer\Logger\Records\LogQueryRecord;
use AndyDefer\Logger\ValueObjects\IsoZuluTime;

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

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

Query par niveau

use AndyDefer\Logger\Enums\LogLevel;

$query = new LogQueryRecord(
    from: new IsoZuluTime('2026-04-01T00:00:00Z'),
    to: new IsoZuluTime('2026-04-30T23:59:59Z'),
    level: LogLevel::ERROR,
);

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

Query combinée

$from = new IsoZuluTime(now()->subDay()->toIso8601ZuluString());

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

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

Parcourir les résultats

foreach ($results as $log) {
    echo $log->time->getValue() . "\n";
    echo $log->level->value . "\n";
    echo $log->data->type . "\n";
    echo $log->data->payload->user_id . "\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 StrictDataObject([
    'event' => 'user_login',
    'user_id' => $user->id,
    'ip' => request()->ip(),
    'success' => true,
]);

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

// Échec de connexion
$payload = new StrictDataObject([
    'event' => 'user_login_failed',
    'email' => request()->email,
    'ip' => request()->ip(),
    'reason' => 'invalid_password',
]);

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

Paiement

// Paiement réussi
$payload = new StrictDataObject([
    'event' => 'payment_success',
    'order_id' => $order->id,
    'stripe_id' => $stripeId,
    'amount' => $order->total,
]);

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

// Paiement échoué
$payload = new StrictDataObject([
    'event' => 'payment_failed',
    'order_id' => $order->id,
    'error' => $exception->getMessage(),
]);

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

Log avec un Record personnalisé

use AndyDefer\DomainStructures\Abstracts\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 StrictDataObject([
    'event' => 'user_created',
    'user' => $userRecord,
]);

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

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->user_id === 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->user_id === 123));

// ❌ MAUVAIS - Fragile
$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. Première propriété = type d'événement

// ✅
$payload = new StrictDataObject([
    'event' => 'user_login',
    'user_id' => $userId,
    'ip' => $ip,
]);

// ❌
$payload = new StrictDataObject([
    'user_id' => $userId,
    'event' => 'user_login',
]);

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->user_id === 123

// ❌ Tester du texte
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 StrictDataObject([
    'event' => 'user_login',
    'user_id' => $userId,
    'ip' => $ip,
    'success' => 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->user_id === $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
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 avec IsoZuluTime
Tests robustes On teste la structure ($log->payload->user_id), 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
Directive intégrée Nettoyage via CLI sans dépendre d'Artisan

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 StrictDataObject([
    'event' => 'user_login',
    'user_id' => $user->id,
    'ip' => $ip,
    'success' => true,
]);

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

Licence

MIT © Andy Defer

---

Related Packages

doctrine/dbal

Powerful PHP database abstraction layer (DBAL) with many features for database s...

644,549,754 9,698
laravel/framework

The Laravel Framework.

598,760,099 34,966
laravel/tinker

Powerful REPL for the Laravel framework.

507,193,856 7,441
laravel/serializable-closure

Laravel Serializable Closure provides an easy and secure way to serialize closur...

423,477,751 607
nunomaduro/collision

Cli error handling for console/command-line PHP applications.

399,241,428 4,660

Version History

Version Released PHP Laravel License
v3.9.49 >=8.1 ^12.0|^13.0|^14.0|^15.0 MIT
v3.9.48 >=8.1 ^12.0|^13.0|^14.0|^15.0 MIT
v3.9.47 >=8.1 ^12.0|^13.0|^14.0|^15.0 MIT
v3.9.46 >=8.1 ^12.0|^13.0|^14.0|^15.0 MIT
v3.9.45 >=8.1 ^12.0|^13.0|^14.0|^15.0 MIT