andydefer/laravel-logger 问题修复 & 功能扩展

解决BUG、新增功能、兼容多环境部署,快速响应你的开发需求

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

andydefer/laravel-logger

最新稳定版本:v3.3.0

Composer 安装命令:

composer require andydefer/laravel-logger

包简介

A structured logging package for Laravel that writes logs in JSONL format (JSON Lines).

README 文档

README

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");
});

Commandes avec la directive

Le package intègre une directive pour nettoyer les vieux logs.

Nettoyer les vieux logs

# Nettoyer les logs de plus de 30 jours (valeur par défaut)
./vendor/bin/directive logger-clean

# Nettoyer les logs de plus de 60 jours
./vendor/bin/directive logger-clean --days=60

# Simulation (ne supprime rien)
./vendor/bin/directive logger-clean --dry-run

# Mode verbeux (affiche les fichiers à supprimer)
./vendor/bin/directive logger-clean --verbose

# Avec alias
./vendor/bin/directive clean-logs
./vendor/bin/directive log-clean

# Toutes les options combinées
./vendor/bin/directive logger-clean --days=90 --dry-run --verbose

Exemple de sortie

$ ./vendor/bin/directive logger-clean --dry-run --verbose

Current statistics:
  Files: 45
  Size: 12.5 MB
  Lines: 15230
  Range: 2024-01-01 to 2024-01-31
  Path: storage/logs/structured

Files to delete:
  - 2024-01-01/00-01 (1024 bytes)
  - 2024-01-01/01-02 (2048 bytes)
  - 2024-01-02/00-01 (512 bytes)

⚠️ Dry run mode - no files will be deleted
Would delete files older than 2024-01-01
Would delete 15 file(s)

Lister toutes les directives disponibles

./vendor/bin/directive --list

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

---

统计信息

  • 总下载量: 54
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 1
  • 依赖项目数: 1
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-05-22

承接程序开发

PHP开发

VUE

Vue开发

前端开发

小程序开发

公众号开发

系统定制

数据库设计

云部署

网站建设

安全加固