toBeInstanceOf(DefaultLogger::class); expect($logger->getConfiguration()['minLevel'])->toBe(LogLevel::DEBUG->value); }); it('can create a logger with custom minimum level', function () { $logger = new DefaultLogger(minLevel: LogLevel::WARNING); expect($logger->getConfiguration()['minLevel'])->toBe(LogLevel::WARNING->value); }); it('respects minimum log level and filters lower priority messages', function () { $mockHandler = new class () implements LogHandler { public array $handledRecords = []; public function isHandling(LogRecord $record): bool { return true; } public function handle(LogRecord $record): void { $this->handledRecords[] = $record; } }; $logger = new DefaultLogger( minLevel: LogLevel::WARNING, handlers: [$mockHandler] ); // These should be ignored (lower than WARNING) $logger->debug('Debug message'); $logger->info('Info message'); $logger->notice('Notice message'); // These should be handled (WARNING or higher) $logger->warning('Warning message'); $logger->error('Error message'); $logger->critical('Critical message'); expect($mockHandler->handledRecords)->toHaveCount(3); expect($mockHandler->handledRecords[0]->getMessage())->toBe('Warning message'); expect($mockHandler->handledRecords[1]->getMessage())->toBe('Error message'); expect($mockHandler->handledRecords[2]->getMessage())->toBe('Critical message'); }); it('can log messages with all severity levels', function () { $recordedLevels = []; $mockHandler = new class ($recordedLevels) implements LogHandler { public function __construct(private array &$recordedLevels) { } public function isHandling(LogRecord $record): bool { return true; } public function handle(LogRecord $record): void { $this->recordedLevels[] = $record->getLevel()->value; } }; $logger = new DefaultLogger(handlers: [$mockHandler]); $logger->debug('Debug'); $logger->info('Info'); $logger->notice('Notice'); $logger->warning('Warning'); $logger->error('Error'); $logger->critical('Critical'); $logger->alert('Alert'); $logger->emergency('Emergency'); expect($recordedLevels)->toBe([ LogLevel::DEBUG->value, LogLevel::INFO->value, LogLevel::NOTICE->value, LogLevel::WARNING->value, LogLevel::ERROR->value, LogLevel::CRITICAL->value, LogLevel::ALERT->value, LogLevel::EMERGENCY->value, ]); }); it('can log with LogContext', function () { $capturedContext = null; $mockHandler = new class ($capturedContext) implements LogHandler { public function __construct(private ?array &$capturedContext) { } public function isHandling(LogRecord $record): bool { return true; } public function handle(LogRecord $record): void { $this->capturedContext = $record->getContext(); } }; $logger = new DefaultLogger(handlers: [$mockHandler]); $context = LogContext::withData(['user_id' => 123, 'action' => 'login']); $logger->info('User action', $context); expect($capturedContext)->toMatchArray(['user_id' => 123, 'action' => 'login']); }); it('can log with null context', function () { $capturedContext = null; $mockHandler = new class ($capturedContext) implements LogHandler { public function __construct(private ?array &$capturedContext) { } public function isHandling(LogRecord $record): bool { return true; } public function handle(LogRecord $record): void { $this->capturedContext = $record->getContext(); } }; $logger = new DefaultLogger(handlers: [$mockHandler]); // Log without context $logger->info('Simple message'); expect($capturedContext)->toBe([]); }); it('can be created without ProcessorManager', function () { $processedRecord = null; $mockHandler = new class ($processedRecord) implements LogHandler { public function __construct(private ?LogRecord &$processedRecord) { } public function isHandling(LogRecord $record): bool { return true; } public function handle(LogRecord $record): void { $this->processedRecord = $record; } }; $logger = new DefaultLogger( handlers: [$mockHandler] ); $logger->info('Test message'); expect($processedRecord)->toBeInstanceOf(LogRecord::class); expect($processedRecord->getMessage())->toBe('Test message'); }); it('logs with LogContext including tags', function () { $capturedContext = null; $mockHandler = new class ($capturedContext) implements LogHandler { public function __construct(private ?array &$capturedContext) { } public function isHandling(LogRecord $record): bool { return true; } public function handle(LogRecord $record): void { $this->capturedContext = $record->getContext(); } }; $logger = new DefaultLogger( handlers: [$mockHandler] ); $context = LogContext::withData(['request_id' => 'req-123']) ->addTags('api', 'v2'); $logger->info('API request', $context); expect($capturedContext)->toMatchArray([ 'request_id' => 'req-123', '_tags' => ['api', 'v2'], ]); }); it('provides channel loggers for different log channels', function () { $logger = new DefaultLogger(); // Test that channel() method returns Logger & HasChannel instances $securityLogger = $logger->channel(LogChannel::SECURITY); expect($securityLogger->channel)->toBe(LogChannel::SECURITY); $cacheLogger = $logger->channel(LogChannel::CACHE); expect($cacheLogger->channel)->toBe(LogChannel::CACHE); $databaseLogger = $logger->channel(LogChannel::DATABASE); expect($databaseLogger->channel)->toBe(LogChannel::DATABASE); $frameworkLogger = $logger->channel(LogChannel::FRAMEWORK); expect($frameworkLogger->channel)->toBe(LogChannel::FRAMEWORK); $errorLogger = $logger->channel(LogChannel::ERROR); expect($errorLogger->channel)->toBe(LogChannel::ERROR); }); it('can get channel loggers using string names', function () { $logger = new DefaultLogger(); // Test that channel() also accepts string channel names $securityLogger = $logger->channel('security'); expect($securityLogger->channel)->toBe(LogChannel::SECURITY); // Test that same channel returns same instance (singleton per channel) $securityLogger2 = $logger->channel(LogChannel::SECURITY); expect($securityLogger2)->toBe($securityLogger); }); it('can log to specific channels using channel loggers', function () { $capturedChannel = null; $capturedMessage = null; $mockHandler = new class ($capturedChannel, $capturedMessage) implements LogHandler { public function __construct( private ?string &$capturedChannel, private ?string &$capturedMessage ) { } public function isHandling(LogRecord $record): bool { return true; } public function handle(LogRecord $record): void { $this->capturedChannel = $record->getChannel(); $this->capturedMessage = $record->getMessage(); } }; $logger = new DefaultLogger(handlers: [$mockHandler]); // Log to security channel with LogContext $context = LogContext::withData(['ip' => '192.168.1.1'])->addTags('security'); $logger->channel(LogChannel::SECURITY)->warning('Unauthorized access attempt', $context); expect($capturedChannel)->toBe(LogChannel::SECURITY->value); expect($capturedMessage)->toBe('Unauthorized access attempt'); // Log to database channel without context $logger->channel(LogChannel::DATABASE)->error('Connection failed'); expect($capturedChannel)->toBe(LogChannel::DATABASE->value); expect($capturedMessage)->toBe('Connection failed'); }); it('calls multiple handlers when configured', function () { $handler1Called = false; $handler2Called = false; $handler3Called = false; $handler1 = new class ($handler1Called) implements LogHandler { public function __construct(private bool &$called) { } public function isHandling(LogRecord $record): bool { return true; } public function handle(LogRecord $record): void { $this->called = true; } }; $handler2 = new class ($handler2Called) implements LogHandler { public function __construct(private bool &$called) { } public function isHandling(LogRecord $record): bool { return $record->getLevel()->value >= LogLevel::WARNING->value; } public function handle(LogRecord $record): void { $this->called = true; } }; $handler3 = new class ($handler3Called) implements LogHandler { public function __construct(private bool &$called) { } public function isHandling(LogRecord $record): bool { return false; // Never handles } public function handle(LogRecord $record): void { $this->called = true; } }; $logger = new DefaultLogger(handlers: [$handler1, $handler2, $handler3]); $logger->error('Test error'); expect($handler1Called)->toBeTrue(); expect($handler2Called)->toBeTrue(); expect($handler3Called)->toBeFalse(); }); it('creates log records with correct timestamp', function () { $capturedRecord = null; $mockHandler = new class ($capturedRecord) implements LogHandler { public function __construct(private ?LogRecord &$capturedRecord) { } public function isHandling(LogRecord $record): bool { return true; } public function handle(LogRecord $record): void { $this->capturedRecord = $record; } }; $logger = new DefaultLogger(handlers: [$mockHandler]); $beforeLog = new \DateTimeImmutable('now', new \DateTimeZone('Europe/Berlin')); $logger->info('Test timestamp'); $afterLog = new \DateTimeImmutable('now', new \DateTimeZone('Europe/Berlin')); expect($capturedRecord)->toBeInstanceOf(LogRecord::class); expect($capturedRecord->getTimestamp())->toBeInstanceOf(\DateTimeImmutable::class); $timestamp = $capturedRecord->getTimestamp(); expect($timestamp >= $beforeLog)->toBeTrue(); expect($timestamp <= $afterLog)->toBeTrue(); expect($timestamp->getTimezone()->getName())->toBe('Europe/Berlin'); }); it('returns correct configuration information', function () { $handler1 = new class () implements LogHandler { public function isHandling(LogRecord $record): bool { return true; } public function handle(LogRecord $record): void { } }; $handler2 = new class () implements LogHandler { public function isHandling(LogRecord $record): bool { return true; } public function handle(LogRecord $record): void { } }; $logger = new DefaultLogger( minLevel: LogLevel::INFO, handlers: [$handler1, $handler2] ); $config = $logger->getConfiguration(); expect($config['minLevel'])->toBe(LogLevel::INFO->value); expect($config['handlers'])->toHaveCount(2); expect($config['processors'])->toBeArray(); });