123, 'action' => 'login']; $logger->info('User logged in', $context); expect($handler->records)->toHaveCount(1); $record = $handler->records[0]; expect($record->getMessage())->toBe('User logged in'); expect($record->getContext())->toBe($context); expect($record->getLevel())->toBe(LogLevel::INFO); }); it('can log with LogContext', function () { $handler = new TestLogHandler(); $logger = new DefaultLogger( minLevel: LogLevel::DEBUG, handlers: [$handler] ); $logContext = LogContext::withData(['user_id' => 123, 'action' => 'login']) ->addTags('authentication', 'user-action') ->addMetadata('source', 'test'); $logger->info('User logged in', $logContext); expect($handler->records)->toHaveCount(1); $record = $handler->records[0]; expect($record->getMessage())->toBe('User logged in'); expect($record->getLevel())->toBe(LogLevel::INFO); // Context should be converted to array $context = $record->getContext(); expect($context)->toHaveKey('user_id', 123); expect($context)->toHaveKey('action', 'login'); expect($context)->toHaveKey('_tags', ['authentication', 'user-action']); expect($context)->toHaveKey('source', 'test'); // Structured data should be in extras expect($record->getExtra('structured_tags'))->toBe(['authentication', 'user-action']); }); it('converts LogContext with trace to array context', function () { $handler = new TestLogHandler(); $logger = new DefaultLogger( minLevel: LogLevel::DEBUG, handlers: [$handler] ); $trace = TraceContext::start(new SecureRandomGenerator()); $span = $trace->startSpan('test-span'); $logContext = LogContext::withData(['key' => 'value']) ->withTrace($trace); $logger->info('Test message', $logContext); $record = $handler->records[0]; $context = $record->getContext(); expect($context)->toHaveKey('_trace_id', $trace->getTraceId()); expect($context)->toHaveKey('_span_id', $span->spanId); // Trace should also be in extras $traceExtra = $record->getExtra('trace_context'); expect($traceExtra)->toHaveKey('trace_id', $trace->getTraceId()); expect($traceExtra)->toHaveKey('active_span'); }); it('converts LogContext with user to array context', function () { $handler = new TestLogHandler(); $logger = new DefaultLogger( minLevel: LogLevel::DEBUG, handlers: [$handler] ); $userContext = UserContext::authenticated('123', 'john'); $logContext = LogContext::withData(['key' => 'value']) ->withUser($userContext); $logger->info('Test message', $logContext); $record = $handler->records[0]; $context = $record->getContext(); expect($context)->toHaveKey('_user_id', '123'); // User should also be in extras $userExtra = $record->getExtra('user_context'); expect($userExtra)->toBeArray(); expect($userExtra)->toHaveKey('user_id', '123'); }); it('converts LogContext with request to array context', function () { $handler = new TestLogHandler(); $logger = new DefaultLogger( minLevel: LogLevel::DEBUG, handlers: [$handler] ); $requestContext = RequestContext::empty(); $logContext = LogContext::withData(['key' => 'value']) ->withRequest($requestContext); $logger->info('Test message', $logContext); $record = $handler->records[0]; // Request should be in extras $requestExtra = $record->getExtra('request_context'); expect($requestExtra)->toBeArray(); }); it('logs at different levels with LogContext', function () { $handler = new TestLogHandler(); $logger = new DefaultLogger( minLevel: LogLevel::DEBUG, handlers: [$handler] ); $context = LogContext::withData(['test' => 'value']); $logger->debug('Debug message', $context); $logger->info('Info message', $context); $logger->notice('Notice message', $context); $logger->warning('Warning message', $context); $logger->error('Error message', $context); $logger->critical('Critical message', $context); $logger->alert('Alert message', $context); $logger->emergency('Emergency message', $context); expect($handler->records)->toHaveCount(8); $levels = array_map(fn ($record) => $record->getLevel(), $handler->records); expect($levels)->toBe([ LogLevel::DEBUG, LogLevel::INFO, LogLevel::NOTICE, LogLevel::WARNING, LogLevel::ERROR, LogLevel::CRITICAL, LogLevel::ALERT, LogLevel::EMERGENCY, ]); }); it('respects minimum log level', function () { $handler = new TestLogHandler(); $logger = new DefaultLogger( minLevel: LogLevel::WARNING, handlers: [$handler] ); $context = LogContext::withData(['test' => 'value']); $logger->debug('Debug message', $context); $logger->info('Info message', $context); $logger->warning('Warning message', $context); $logger->error('Error message', $context); expect($handler->records)->toHaveCount(2); expect($handler->records[0]->getLevel())->toBe(LogLevel::WARNING); expect($handler->records[1]->getLevel())->toBe(LogLevel::ERROR); }); it('calls processors on enriched record', function () { $processor = new TestLogProcessor(); $processorManager = new ProcessorManager($processor); $handler = new TestLogHandler(); $logger = new DefaultLogger( minLevel: LogLevel::DEBUG, handlers: [$handler], processorManager: $processorManager ); $context = LogContext::withData(['test' => 'value']); $logger->info('Test message', $context); expect($processor->processedRecords)->toHaveCount(1); expect($handler->records)->toHaveCount(1); // Processor should have seen the enriched record $processedRecord = $processor->processedRecords[0]; expect($processedRecord->getExtra('structured_tags'))->toBeNull(); // No tags in this test }); it('handles empty LogContext', function () { $handler = new TestLogHandler(); $logger = new DefaultLogger( minLevel: LogLevel::DEBUG, handlers: [$handler] ); $logger->info('Test message', LogContext::empty()); expect($handler->records)->toHaveCount(1); $record = $handler->records[0]; expect($record->getContext())->toBe([]); expect($record->getExtras())->toBe([]); }); it('can get configuration', function () { $handler = new TestLogHandler(); $processor = new TestLogProcessor(); $processorManager = new ProcessorManager($processor); $logger = new DefaultLogger( minLevel: LogLevel::INFO, handlers: [$handler], processorManager: $processorManager ); $config = $logger->getConfiguration(); expect($config)->toHaveKey('minLevel', 200); expect($config)->toHaveKey('handlers'); expect($config)->toHaveKey('processors'); expect($config['handlers'])->toBe([TestLogHandler::class]); }); }); // Test helpers class TestLogHandler implements LogHandler { public array $records = []; public function isHandling(LogRecord $record): bool { return true; } public function handle(LogRecord $record): void { $this->records[] = $record; } } class TestLogProcessor implements \App\Framework\Logging\LogProcessor { public array $processedRecords = []; public function processRecord(LogRecord $record): LogRecord { $this->processedRecords[] = $record; return $record->addExtra('processed_by', 'TestLogProcessor'); } public function getPriority(): int { return 100; } public function getName(): string { return 'test-processor'; } }