timestamp = new DateTimeImmutable('2024-01-15 10:30:45', new DateTimeZone('Europe/Berlin')); $this->basicContext = LogContext::withData(['user_id' => 123, 'action' => 'login']); }); describe('LineFormatter', function () { it('formats basic log record', function () { $formatter = new LineFormatter(); $record = new LogRecord('Test message', $this->basicContext, LogLevel::INFO, $this->timestamp); $output = $formatter($record); expect($output)->toContain('2024-01-15 10:30:45'); expect($output)->toContain('INFO'); expect($output)->toContain('Test message'); expect($output)->toContain('user_id'); }); it('supports custom format template', function () { $formatter = new LineFormatter('{level}: {message}'); $record = new LogRecord('Custom format', $this->basicContext, LogLevel::WARNING, $this->timestamp); $output = $formatter($record); expect($output)->toBe('WARNING: Custom format'); }); it('includes channel in output', function () { $formatter = new LineFormatter(); $record = new LogRecord('Channel test', $this->basicContext, LogLevel::ERROR, $this->timestamp, 'security'); $output = $formatter($record); expect($output)->toContain('security.ERROR'); }); }); describe('JsonFormatter', function () { it('formats record as valid JSON', function () { $formatter = new JsonFormatter(); $record = new LogRecord('JSON test', $this->basicContext, LogLevel::DEBUG, $this->timestamp); $output = $formatter($record); $decoded = json_decode($output, true); expect($decoded)->toBeArray(); expect($decoded['message'])->toBe('JSON test'); expect($decoded['level'])->toBe('DEBUG'); expect($decoded['context']['user_id'])->toBe(123); }); it('formats with pretty print when enabled', function () { $formatter = new JsonFormatter(prettyPrint: true); $record = new LogRecord('Pretty JSON', $this->basicContext, LogLevel::INFO, $this->timestamp); $output = $formatter($record); expect($output)->toContain("\n"); expect($output)->toContain(" "); }); it('excludes extras when disabled', function () { $formatter = new JsonFormatter(includeExtras: false); $record = new LogRecord('No extras', $this->basicContext, LogLevel::INFO, $this->timestamp); $record->addExtra('test_extra', 'value'); $output = $formatter($record); $decoded = json_decode($output, true); expect(isset($decoded['extra']))->toBeFalse(); }); }); describe('DevelopmentFormatter', function () { it('formats with human-readable output', function () { $formatter = new DevelopmentFormatter(colorOutput: false); $record = new LogRecord('Dev message', $this->basicContext, LogLevel::ERROR, $this->timestamp); $output = $formatter($record); expect($output)->toContain('10:30:45'); expect($output)->toContain('ERROR'); expect($output)->toContain('Context:'); expect($output)->toContain('user_id: 123'); }); it('formats exception details', function () { $formatter = new DevelopmentFormatter(colorOutput: false); $exception = new Exception('Test exception'); $context = LogContext::withData(['exception' => $exception]); $record = new LogRecord('Exception occurred', $context, LogLevel::ERROR, $this->timestamp); // Simulate exception processor enrichment $record = $record->withExtras([ 'exception_class' => Exception::class, 'exception_file' => __FILE__, 'exception_line' => __LINE__, 'exception_hash' => 'abc12345', 'exception_severity' => 'medium', 'stack_trace_short' => 'File.php:123 → Class::method()', ]); $output = $formatter($record); expect($output)->toContain('Exception:'); expect($output)->toContain('Class: Exception'); expect($output)->toContain('Hash: abc12345'); expect($output)->toContain('Trace: File.php:123'); }); }); describe('StructuredFormatter', function () { it('formats as logfmt by default', function () { $formatter = new StructuredFormatter(); $record = new LogRecord('Structured test', $this->basicContext, LogLevel::INFO, $this->timestamp); $output = $formatter($record); expect($output)->toBeString(); expect($output)->toContain('level=INFO'); expect($output)->toContain('msg="Structured test"'); expect($output)->toContain('user_id=123'); }); it('formats as key-value when specified', function () { $formatter = new StructuredFormatter(format: 'kv'); $record = new LogRecord('KV test', $this->basicContext, LogLevel::WARNING, $this->timestamp); $output = $formatter($record); expect($output)->toContain('level: WARNING'); expect($output)->toContain('msg: KV test'); expect($output)->toContain(', '); }); it('returns array when format is array', function () { $formatter = new StructuredFormatter(format: 'array'); $record = new LogRecord('Array test', $this->basicContext, LogLevel::INFO, $this->timestamp); $output = $formatter($record); expect($output)->toBeArray(); expect($output['level'])->toBe('INFO'); expect($output['msg'])->toBe('Array test'); }); it('sanitizes keys properly', function () { $context = LogContext::withData(['_internal_key' => 'value', 'key-with-dash' => 'value2']); $formatter = new StructuredFormatter(format: 'array'); $record = new LogRecord('Sanitize test', $context, LogLevel::INFO, $this->timestamp); $output = $formatter($record); expect($output)->toHaveKey('internal_key'); // _ prefix removed expect($output)->toHaveKey('key_with_dash'); // dash converted to underscore }); }); describe('Formatter Integration', function () { it('all formatters handle same record consistently', function () { $context = LogContext::withData(['test' => 'value'])->addTags('integration'); $record = new LogRecord('Multi-formatter test', $context, LogLevel::INFO, $this->timestamp); $lineFormatter = new LineFormatter(); $jsonFormatter = new JsonFormatter(); $devFormatter = new DevelopmentFormatter(colorOutput: false); $structuredFormatter = new StructuredFormatter(); $lineOutput = $lineFormatter($record); $jsonOutput = $jsonFormatter($record); $devOutput = $devFormatter($record); $structuredOutput = $structuredFormatter($record); // All should contain the message expect($lineOutput)->toContain('Multi-formatter test'); expect($jsonOutput)->toContain('Multi-formatter test'); expect($devOutput)->toContain('Multi-formatter test'); expect($structuredOutput)->toContain('Multi-formatter test'); // JSON should be valid $jsonDecoded = json_decode($jsonOutput); expect($jsonDecoded !== null)->toBeTrue(); }); });