- Add comprehensive health check system with multiple endpoints - Add Prometheus metrics endpoint - Add production logging configurations (5 strategies) - Add complete deployment documentation suite: * QUICKSTART.md - 30-minute deployment guide * DEPLOYMENT_CHECKLIST.md - Printable verification checklist * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference * production-logging.md - Logging configuration guide * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation * README.md - Navigation hub * DEPLOYMENT_SUMMARY.md - Executive summary - Add deployment scripts and automation - Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment - Update README with production-ready features All production infrastructure is now complete and ready for deployment.
521 lines
17 KiB
PHP
521 lines
17 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\Logging\Formatter\DevelopmentFormatter;
|
|
use App\Framework\Logging\LogLevel;
|
|
use App\Framework\Logging\LogRecord;
|
|
use App\Framework\Logging\ValueObjects\LogContext;
|
|
|
|
describe('DevelopmentFormatter', function () {
|
|
beforeEach(function () {
|
|
$this->timestamp = new DateTimeImmutable('2024-01-15 10:30:45.123456', new DateTimeZone('Europe/Berlin'));
|
|
});
|
|
|
|
describe('constructor', function () {
|
|
it('accepts default configuration', function () {
|
|
$formatter = new DevelopmentFormatter();
|
|
|
|
expect($formatter instanceof DevelopmentFormatter)->toBeTrue();
|
|
});
|
|
|
|
it('accepts color output option', function () {
|
|
$formatter = new DevelopmentFormatter(colorOutput: false);
|
|
|
|
expect($formatter instanceof DevelopmentFormatter)->toBeTrue();
|
|
});
|
|
|
|
it('accepts include stack trace option', function () {
|
|
$formatter = new DevelopmentFormatter(includeStackTrace: false);
|
|
|
|
expect($formatter instanceof DevelopmentFormatter)->toBeTrue();
|
|
});
|
|
|
|
it('accepts both options', function () {
|
|
$formatter = new DevelopmentFormatter(
|
|
includeStackTrace: true,
|
|
colorOutput: true
|
|
);
|
|
|
|
expect($formatter instanceof DevelopmentFormatter)->toBeTrue();
|
|
});
|
|
});
|
|
|
|
describe('basic formatting', function () {
|
|
it('returns formatted string', function () {
|
|
$formatter = new DevelopmentFormatter(colorOutput: false);
|
|
|
|
$record = new LogRecord(
|
|
message: 'Test message',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::INFO,
|
|
timestamp: $this->timestamp
|
|
);
|
|
|
|
$result = $formatter($record);
|
|
|
|
expect($result)->toBeString();
|
|
});
|
|
|
|
it('includes timestamp with microseconds', function () {
|
|
$formatter = new DevelopmentFormatter(colorOutput: false);
|
|
|
|
$record = new LogRecord(
|
|
message: 'Test message',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::INFO,
|
|
timestamp: $this->timestamp
|
|
);
|
|
|
|
$result = $formatter($record);
|
|
|
|
expect(str_contains($result, '10:30:45.'))->toBeTrue();
|
|
});
|
|
|
|
it('includes log level', function () {
|
|
$formatter = new DevelopmentFormatter(colorOutput: false);
|
|
|
|
$record = new LogRecord(
|
|
message: 'Test message',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::WARNING,
|
|
timestamp: $this->timestamp
|
|
);
|
|
|
|
$result = $formatter($record);
|
|
|
|
expect(str_contains($result, 'WARNING'))->toBeTrue();
|
|
});
|
|
|
|
it('includes message', function () {
|
|
$formatter = new DevelopmentFormatter(colorOutput: false);
|
|
|
|
$record = new LogRecord(
|
|
message: 'Important test message',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::INFO,
|
|
timestamp: $this->timestamp
|
|
);
|
|
|
|
$result = $formatter($record);
|
|
|
|
expect(str_contains($result, 'Important test message'))->toBeTrue();
|
|
});
|
|
|
|
it('includes channel when provided', function () {
|
|
$formatter = new DevelopmentFormatter(colorOutput: false);
|
|
|
|
$record = new LogRecord(
|
|
message: 'Test message',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::INFO,
|
|
timestamp: $this->timestamp,
|
|
channel: 'security'
|
|
);
|
|
|
|
$result = $formatter($record);
|
|
|
|
expect(str_contains($result, 'security'))->toBeTrue();
|
|
});
|
|
});
|
|
|
|
describe('color output', function () {
|
|
it('includes ANSI color codes when enabled', function () {
|
|
$formatter = new DevelopmentFormatter(colorOutput: true);
|
|
|
|
$record = new LogRecord(
|
|
message: 'Test message',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::INFO,
|
|
timestamp: $this->timestamp
|
|
);
|
|
|
|
$result = $formatter($record);
|
|
|
|
// Should contain ANSI escape sequences
|
|
expect(str_contains($result, "\033["))->toBeTrue();
|
|
});
|
|
|
|
it('excludes ANSI color codes when disabled', function () {
|
|
$formatter = new DevelopmentFormatter(colorOutput: false);
|
|
|
|
$record = new LogRecord(
|
|
message: 'Test message',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::INFO,
|
|
timestamp: $this->timestamp
|
|
);
|
|
|
|
$result = $formatter($record);
|
|
|
|
// Should not contain ANSI escape sequences
|
|
expect(str_contains($result, "\033["))->toBeFalse();
|
|
});
|
|
|
|
it('uses different colors for different log levels', function () {
|
|
$formatter = new DevelopmentFormatter(colorOutput: true);
|
|
|
|
$infoRecord = new LogRecord(
|
|
message: 'Info',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::INFO,
|
|
timestamp: $this->timestamp
|
|
);
|
|
|
|
$errorRecord = new LogRecord(
|
|
message: 'Error',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::ERROR,
|
|
timestamp: $this->timestamp
|
|
);
|
|
|
|
$infoResult = $formatter($infoRecord);
|
|
$errorResult = $formatter($errorRecord);
|
|
|
|
// Different log levels should have different color codes
|
|
expect($infoResult !== $errorResult)->toBeTrue();
|
|
});
|
|
});
|
|
|
|
describe('context formatting', function () {
|
|
it('formats context data with indentation', function () {
|
|
$formatter = new DevelopmentFormatter(colorOutput: false);
|
|
|
|
$record = new LogRecord(
|
|
message: 'Test message',
|
|
context: LogContext::withData([
|
|
'user_id' => 123,
|
|
'action' => 'login'
|
|
]),
|
|
level: LogLevel::INFO,
|
|
timestamp: $this->timestamp
|
|
);
|
|
|
|
$result = $formatter($record);
|
|
|
|
expect(str_contains($result, 'user_id'))->toBeTrue();
|
|
expect(str_contains($result, '123'))->toBeTrue();
|
|
expect(str_contains($result, 'action'))->toBeTrue();
|
|
expect(str_contains($result, 'login'))->toBeTrue();
|
|
});
|
|
|
|
it('handles nested context arrays', function () {
|
|
$formatter = new DevelopmentFormatter(colorOutput: false);
|
|
|
|
$record = new LogRecord(
|
|
message: 'Test message',
|
|
context: LogContext::withData([
|
|
'user' => [
|
|
'id' => 123,
|
|
'name' => 'John Doe'
|
|
]
|
|
]),
|
|
level: LogLevel::INFO,
|
|
timestamp: $this->timestamp
|
|
);
|
|
|
|
$result = $formatter($record);
|
|
|
|
// Arrays are formatted as Array[n], names won't appear
|
|
expect(str_contains($result, 'user'))->toBeTrue();
|
|
expect(str_contains($result, 'Array'))->toBeTrue();
|
|
});
|
|
|
|
it('handles empty context gracefully', function () {
|
|
$formatter = new DevelopmentFormatter(colorOutput: false);
|
|
|
|
$record = new LogRecord(
|
|
message: 'Test message',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::INFO,
|
|
timestamp: $this->timestamp
|
|
);
|
|
|
|
$result = $formatter($record);
|
|
|
|
expect($result)->toBeString();
|
|
expect(str_contains($result, 'Test message'))->toBeTrue();
|
|
});
|
|
});
|
|
|
|
describe('extras formatting', function () {
|
|
it('formats extra data when present', function () {
|
|
$formatter = new DevelopmentFormatter(colorOutput: false);
|
|
|
|
$record = (new LogRecord(
|
|
message: 'Test message',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::INFO,
|
|
timestamp: $this->timestamp
|
|
))->addExtra('request_id', 'req-123')
|
|
->addExtra('session_id', 'sess-456');
|
|
|
|
$result = $formatter($record);
|
|
|
|
expect(str_contains($result, 'request_id'))->toBeTrue();
|
|
expect(str_contains($result, 'req-123'))->toBeTrue();
|
|
expect(str_contains($result, 'session_id'))->toBeTrue();
|
|
expect(str_contains($result, 'sess-456'))->toBeTrue();
|
|
});
|
|
|
|
it('handles empty extras gracefully', function () {
|
|
$formatter = new DevelopmentFormatter(colorOutput: false);
|
|
|
|
$record = new LogRecord(
|
|
message: 'Test message',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::INFO,
|
|
timestamp: $this->timestamp
|
|
);
|
|
|
|
$result = $formatter($record);
|
|
|
|
expect($result)->toBeString();
|
|
});
|
|
});
|
|
|
|
describe('exception formatting', function () {
|
|
it('formats exception from extras', function () {
|
|
$formatter = new DevelopmentFormatter(includeStackTrace: false, colorOutput: false);
|
|
|
|
$record = (new LogRecord(
|
|
message: 'Exception occurred',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::ERROR,
|
|
timestamp: $this->timestamp
|
|
))->addExtra('exception_class', 'RuntimeException')
|
|
->addExtra('exception_message', 'Test exception message')
|
|
->addExtra('exception_file', '/path/to/file.php')
|
|
->addExtra('exception_line', 42);
|
|
|
|
$result = $formatter($record);
|
|
|
|
expect(str_contains($result, 'RuntimeException'))->toBeTrue();
|
|
expect(str_contains($result, '/path/to/file.php'))->toBeTrue();
|
|
});
|
|
|
|
it('includes stack trace when enabled', function () {
|
|
$formatter = new DevelopmentFormatter(includeStackTrace: true, colorOutput: false);
|
|
|
|
$record = (new LogRecord(
|
|
message: 'Exception occurred',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::ERROR,
|
|
timestamp: $this->timestamp
|
|
))->addExtra('exception_class', 'RuntimeException')
|
|
->addExtra('exception_file', '/path/to/file.php')
|
|
->addExtra('exception_line', 42)
|
|
->addExtra('stack_trace_short', '#0 /path/to/caller.php(10)');
|
|
|
|
$result = $formatter($record);
|
|
|
|
expect(str_contains($result, 'RuntimeException'))->toBeTrue();
|
|
expect(str_contains($result, '#0'))->toBeTrue();
|
|
});
|
|
|
|
it('excludes stack trace when disabled', function () {
|
|
$formatter = new DevelopmentFormatter(includeStackTrace: false, colorOutput: false);
|
|
|
|
$record = (new LogRecord(
|
|
message: 'Exception occurred',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::ERROR,
|
|
timestamp: $this->timestamp
|
|
))->addExtra('exception_class', 'RuntimeException')
|
|
->addExtra('exception_file', '/path/to/file.php')
|
|
->addExtra('exception_line', 42)
|
|
->addExtra('stack_trace_short', '#0 /path/to/caller.php(10)');
|
|
|
|
$result = $formatter($record);
|
|
|
|
expect(str_contains($result, 'RuntimeException'))->toBeTrue();
|
|
// Stack trace should not be included when disabled
|
|
$hasStackTrace = str_contains($result, 'Trace:');
|
|
expect($hasStackTrace)->toBeFalse();
|
|
});
|
|
});
|
|
|
|
describe('log levels', function () {
|
|
it('formats DEBUG level', function () {
|
|
$formatter = new DevelopmentFormatter(colorOutput: false);
|
|
|
|
$record = new LogRecord(
|
|
message: 'Debug message',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::DEBUG,
|
|
timestamp: $this->timestamp
|
|
);
|
|
|
|
$result = $formatter($record);
|
|
|
|
expect(str_contains($result, 'DEBUG'))->toBeTrue();
|
|
});
|
|
|
|
it('formats INFO level', function () {
|
|
$formatter = new DevelopmentFormatter(colorOutput: false);
|
|
|
|
$record = new LogRecord(
|
|
message: 'Info message',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::INFO,
|
|
timestamp: $this->timestamp
|
|
);
|
|
|
|
$result = $formatter($record);
|
|
|
|
expect(str_contains($result, 'INFO'))->toBeTrue();
|
|
});
|
|
|
|
it('formats WARNING level', function () {
|
|
$formatter = new DevelopmentFormatter(colorOutput: false);
|
|
|
|
$record = new LogRecord(
|
|
message: 'Warning message',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::WARNING,
|
|
timestamp: $this->timestamp
|
|
);
|
|
|
|
$result = $formatter($record);
|
|
|
|
expect(str_contains($result, 'WARNING'))->toBeTrue();
|
|
});
|
|
|
|
it('formats ERROR level', function () {
|
|
$formatter = new DevelopmentFormatter(colorOutput: false);
|
|
|
|
$record = new LogRecord(
|
|
message: 'Error message',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::ERROR,
|
|
timestamp: $this->timestamp
|
|
);
|
|
|
|
$result = $formatter($record);
|
|
|
|
expect(str_contains($result, 'ERROR'))->toBeTrue();
|
|
});
|
|
|
|
it('formats CRITICAL level', function () {
|
|
$formatter = new DevelopmentFormatter(colorOutput: false);
|
|
|
|
$record = new LogRecord(
|
|
message: 'Critical message',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::CRITICAL,
|
|
timestamp: $this->timestamp
|
|
);
|
|
|
|
$result = $formatter($record);
|
|
|
|
expect(str_contains($result, 'CRITICAL'))->toBeTrue();
|
|
});
|
|
|
|
it('formats EMERGENCY level', function () {
|
|
$formatter = new DevelopmentFormatter(colorOutput: false);
|
|
|
|
$record = new LogRecord(
|
|
message: 'Emergency message',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::EMERGENCY,
|
|
timestamp: $this->timestamp
|
|
);
|
|
|
|
$result = $formatter($record);
|
|
|
|
expect(str_contains($result, 'EMERGENCY'))->toBeTrue();
|
|
});
|
|
});
|
|
|
|
describe('readonly behavior', function () {
|
|
it('is a readonly class', function () {
|
|
$reflection = new ReflectionClass(DevelopmentFormatter::class);
|
|
|
|
expect($reflection->isReadOnly())->toBeTrue();
|
|
});
|
|
});
|
|
|
|
describe('edge cases', function () {
|
|
it('handles empty message', function () {
|
|
$formatter = new DevelopmentFormatter(colorOutput: false);
|
|
|
|
$record = new LogRecord(
|
|
message: '',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::INFO,
|
|
timestamp: $this->timestamp
|
|
);
|
|
|
|
$result = $formatter($record);
|
|
|
|
expect($result)->toBeString();
|
|
});
|
|
|
|
it('handles very long message', function () {
|
|
$formatter = new DevelopmentFormatter(colorOutput: false);
|
|
|
|
$longMessage = str_repeat('x', 1000);
|
|
|
|
$record = new LogRecord(
|
|
message: $longMessage,
|
|
context: LogContext::empty(),
|
|
level: LogLevel::INFO,
|
|
timestamp: $this->timestamp
|
|
);
|
|
|
|
$result = $formatter($record);
|
|
|
|
expect(str_contains($result, 'xxx'))->toBeTrue();
|
|
});
|
|
|
|
it('handles unicode characters', function () {
|
|
$formatter = new DevelopmentFormatter(colorOutput: false);
|
|
|
|
$record = new LogRecord(
|
|
message: 'Unicode: 你好 мир 🌍',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::INFO,
|
|
timestamp: $this->timestamp
|
|
);
|
|
|
|
$result = $formatter($record);
|
|
|
|
expect(str_contains($result, '你好'))->toBeTrue();
|
|
expect(str_contains($result, 'мир'))->toBeTrue();
|
|
expect(str_contains($result, '🌍'))->toBeTrue();
|
|
});
|
|
|
|
it('handles null channel', function () {
|
|
$formatter = new DevelopmentFormatter(colorOutput: false);
|
|
|
|
$record = new LogRecord(
|
|
message: 'Test message',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::INFO,
|
|
timestamp: $this->timestamp,
|
|
channel: null
|
|
);
|
|
|
|
$result = $formatter($record);
|
|
|
|
expect($result)->toBeString();
|
|
expect(str_contains($result, 'Test message'))->toBeTrue();
|
|
});
|
|
|
|
it('handles special characters in message', function () {
|
|
$formatter = new DevelopmentFormatter(colorOutput: false);
|
|
|
|
$record = new LogRecord(
|
|
message: 'Message with "quotes" and \'apostrophes\' and \backslashes',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::INFO,
|
|
timestamp: $this->timestamp
|
|
);
|
|
|
|
$result = $formatter($record);
|
|
|
|
expect(str_contains($result, 'quotes'))->toBeTrue();
|
|
});
|
|
});
|
|
});
|