feat(Production): Complete production deployment infrastructure

- 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.
This commit is contained in:
2025-10-25 19:18:37 +02:00
parent caa85db796
commit fc3d7e6357
83016 changed files with 378904 additions and 20919 deletions

View File

@@ -0,0 +1,544 @@
<?php
declare(strict_types=1);
use App\Framework\Logging\Formatter\LineFormatter;
use App\Framework\Logging\LogLevel;
use App\Framework\Logging\LogRecord;
use App\Framework\Logging\ValueObjects\LogContext;
describe('LineFormatter', function () {
beforeEach(function () {
$this->timestamp = new DateTimeImmutable('2024-01-15 10:30:45', new DateTimeZone('Europe/Berlin'));
});
describe('constructor', function () {
it('accepts default format and timestamp format', function () {
$formatter = new LineFormatter();
expect($formatter instanceof LineFormatter)->toBeTrue();
});
it('accepts custom format', function () {
$formatter = new LineFormatter(
format: '{level}: {message}'
);
expect($formatter instanceof LineFormatter)->toBeTrue();
});
it('accepts custom timestamp format', function () {
$formatter = new LineFormatter(
format: '[{timestamp}] {message}',
timestampFormat: 'H:i:s'
);
expect($formatter instanceof LineFormatter)->toBeTrue();
});
});
describe('default format', function () {
it('formats with default format string', function () {
$formatter = new LineFormatter();
$record = new LogRecord(
message: 'Test message',
context: LogContext::empty(),
level: LogLevel::INFO,
timestamp: $this->timestamp
);
$result = $formatter($record);
expect($result)->toBeString();
expect($result)->toContain('2024-01-15');
expect($result)->toContain('INFO');
expect($result)->toContain('Test message');
});
it('includes channel in output', function () {
$formatter = new LineFormatter();
$record = new LogRecord(
message: 'Test message',
context: LogContext::empty(),
level: LogLevel::INFO,
timestamp: $this->timestamp,
channel: 'security'
);
$result = $formatter($record);
expect($result)->toContain('security');
});
it('uses default channel "app" when none provided', function () {
$formatter = new LineFormatter();
$record = new LogRecord(
message: 'Test message',
context: LogContext::empty(),
level: LogLevel::INFO,
timestamp: $this->timestamp
);
$result = $formatter($record);
expect($result)->toContain('app');
});
it('includes context as JSON', function () {
$formatter = new LineFormatter();
$record = new LogRecord(
message: 'Test message',
context: LogContext::withData([
'user_id' => 123,
'action' => 'login'
]),
level: LogLevel::INFO,
timestamp: $this->timestamp
);
$result = $formatter($record);
expect($result)->toContain('"user_id":123');
expect($result)->toContain('"action":"login"');
});
it('handles empty context', function () {
$formatter = new LineFormatter();
$record = new LogRecord(
message: 'Test message',
context: LogContext::empty(),
level: LogLevel::INFO,
timestamp: $this->timestamp
);
$result = $formatter($record);
// Empty context should result in empty context string
expect($result)->toBeString();
expect($result)->toContain('Test message');
});
});
describe('custom format', function () {
it('uses custom format string', function () {
$formatter = new LineFormatter(
format: '{level}: {message}'
);
$record = new LogRecord(
message: 'Custom format',
context: LogContext::empty(),
level: LogLevel::WARNING,
timestamp: $this->timestamp
);
$result = $formatter($record);
expect($result)->toBe('WARNING: Custom format');
});
it('supports timestamp-only format', function () {
$formatter = new LineFormatter(
format: '[{timestamp}]',
timestampFormat: 'Y-m-d H:i:s'
);
$record = new LogRecord(
message: 'Message',
context: LogContext::empty(),
level: LogLevel::INFO,
timestamp: $this->timestamp
);
$result = $formatter($record);
expect($result)->toBe('[2024-01-15 10:30:45]');
});
it('supports message-only format', function () {
$formatter = new LineFormatter(
format: '{message}'
);
$record = new LogRecord(
message: 'Simple message',
context: LogContext::empty(),
level: LogLevel::DEBUG,
timestamp: $this->timestamp
);
$result = $formatter($record);
expect($result)->toBe('Simple message');
});
it('supports complex custom format', function () {
$formatter = new LineFormatter(
format: '[{timestamp}] {channel}.{level}: {message} {context}'
);
$record = new LogRecord(
message: 'Complex',
context: LogContext::withData(['key' => 'value']),
level: LogLevel::ERROR,
timestamp: $this->timestamp,
channel: 'api'
);
$result = $formatter($record);
expect($result)->toContain('[2024-01-15');
expect($result)->toContain('api.ERROR');
expect($result)->toContain('Complex');
expect($result)->toContain('"key":"value"');
});
});
describe('timestamp formatting', function () {
it('uses default timestamp format', function () {
$formatter = new LineFormatter(
format: '{timestamp}'
);
$record = new LogRecord(
message: 'Message',
context: LogContext::empty(),
level: LogLevel::INFO,
timestamp: $this->timestamp
);
$result = $formatter($record);
// Default format is Y-m-d H:i:s
expect($result)->toBe('2024-01-15 10:30:45');
});
it('uses custom timestamp format', function () {
$formatter = new LineFormatter(
format: '{timestamp}',
timestampFormat: 'd/m/Y H:i'
);
$record = new LogRecord(
message: 'Message',
context: LogContext::empty(),
level: LogLevel::INFO,
timestamp: $this->timestamp
);
$result = $formatter($record);
expect($result)->toBe('15/01/2024 10:30');
});
it('supports ISO 8601 timestamp format', function () {
$formatter = new LineFormatter(
format: '{timestamp}',
timestampFormat: 'c'
);
$record = new LogRecord(
message: 'Message',
context: LogContext::empty(),
level: LogLevel::INFO,
timestamp: $this->timestamp
);
$result = $formatter($record);
expect($result)->toContain('2024-01-15T10:30:45');
});
it('supports microseconds in timestamp', function () {
$formatter = new LineFormatter(
format: '{timestamp}',
timestampFormat: 'Y-m-d H:i:s.u'
);
$record = new LogRecord(
message: 'Message',
context: LogContext::empty(),
level: LogLevel::INFO,
timestamp: $this->timestamp
);
$result = $formatter($record);
expect($result)->toContain('2024-01-15 10:30:45.');
});
});
describe('context handling', function () {
it('formats context as JSON with unescaped slashes', function () {
$formatter = new LineFormatter(
format: '{context}'
);
$record = new LogRecord(
message: 'Message',
context: LogContext::withData([
'url' => 'https://example.com/path',
'file' => '/var/www/html/index.php'
]),
level: LogLevel::INFO,
timestamp: $this->timestamp
);
$result = $formatter($record);
// JSON_UNESCAPED_SLASHES should be used
expect($result)->toContain('https://example.com/path');
expect($result)->toContain('/var/www/html/index.php');
// Slashes should not be escaped
expect(str_contains($result, '\\/'))->toBeFalse();
});
it('combines context and extras', function () {
$formatter = new LineFormatter(
format: '{context}'
);
$record = (new LogRecord(
message: 'Message',
context: LogContext::withData(['context_key' => 'context_value']),
level: LogLevel::INFO,
timestamp: $this->timestamp
))->addExtra('extra_key', 'extra_value');
$result = $formatter($record);
expect($result)->toContain('"context_key":"context_value"');
expect($result)->toContain('"extra_key":"extra_value"');
});
it('handles nested context arrays', function () {
$formatter = new LineFormatter(
format: '{context}'
);
$record = new LogRecord(
message: 'Message',
context: LogContext::withData([
'user' => [
'id' => 123,
'name' => 'John Doe',
'roles' => ['admin', 'user']
]
]),
level: LogLevel::INFO,
timestamp: $this->timestamp
);
$result = $formatter($record);
expect($result)->toContain('"user"');
expect($result)->toContain('"id":123');
expect($result)->toContain('"name":"John Doe"');
expect($result)->toContain('["admin","user"]');
});
it('handles special characters in context', function () {
$formatter = new LineFormatter(
format: '{context}'
);
$record = new LogRecord(
message: 'Message',
context: LogContext::withData([
'text' => 'Special chars: "quotes", \'apostrophes\', \\backslashes'
]),
level: LogLevel::INFO,
timestamp: $this->timestamp
);
$result = $formatter($record);
expect($result)->toBeString();
expect($result)->toContain('Special chars');
});
});
describe('log levels', function () {
it('formats DEBUG level', function () {
$formatter = new LineFormatter(format: '{level}');
$record = new LogRecord(
message: 'Message',
context: LogContext::empty(),
level: LogLevel::DEBUG,
timestamp: $this->timestamp
);
expect($formatter($record))->toBe('DEBUG');
});
it('formats INFO level', function () {
$formatter = new LineFormatter(format: '{level}');
$record = new LogRecord(
message: 'Message',
context: LogContext::empty(),
level: LogLevel::INFO,
timestamp: $this->timestamp
);
expect($formatter($record))->toBe('INFO');
});
it('formats WARNING level', function () {
$formatter = new LineFormatter(format: '{level}');
$record = new LogRecord(
message: 'Message',
context: LogContext::empty(),
level: LogLevel::WARNING,
timestamp: $this->timestamp
);
expect($formatter($record))->toBe('WARNING');
});
it('formats ERROR level', function () {
$formatter = new LineFormatter(format: '{level}');
$record = new LogRecord(
message: 'Message',
context: LogContext::empty(),
level: LogLevel::ERROR,
timestamp: $this->timestamp
);
expect($formatter($record))->toBe('ERROR');
});
it('formats CRITICAL level', function () {
$formatter = new LineFormatter(format: '{level}');
$record = new LogRecord(
message: 'Message',
context: LogContext::empty(),
level: LogLevel::CRITICAL,
timestamp: $this->timestamp
);
expect($formatter($record))->toBe('CRITICAL');
});
it('formats ALERT level', function () {
$formatter = new LineFormatter(format: '{level}');
$record = new LogRecord(
message: 'Message',
context: LogContext::empty(),
level: LogLevel::ALERT,
timestamp: $this->timestamp
);
expect($formatter($record))->toBe('ALERT');
});
it('formats EMERGENCY level', function () {
$formatter = new LineFormatter(format: '{level}');
$record = new LogRecord(
message: 'Message',
context: LogContext::empty(),
level: LogLevel::EMERGENCY,
timestamp: $this->timestamp
);
expect($formatter($record))->toBe('EMERGENCY');
});
});
describe('readonly behavior', function () {
it('is a readonly class', function () {
$reflection = new ReflectionClass(LineFormatter::class);
expect($reflection->isReadOnly())->toBeTrue();
});
});
describe('edge cases', function () {
it('handles empty message', function () {
$formatter = new LineFormatter(format: '{message}');
$record = new LogRecord(
message: '',
context: LogContext::empty(),
level: LogLevel::INFO,
timestamp: $this->timestamp
);
expect($formatter($record))->toBe('');
});
it('handles very long message', function () {
$formatter = new LineFormatter(format: '{message}');
$longMessage = str_repeat('x', 10000);
$record = new LogRecord(
message: $longMessage,
context: LogContext::empty(),
level: LogLevel::INFO,
timestamp: $this->timestamp
);
$result = $formatter($record);
expect($result)->toHaveLength(10000);
});
it('handles unicode characters in message', function () {
$formatter = new LineFormatter(format: '{message}');
$record = new LogRecord(
message: 'Unicode: 你好 мир 🌍',
context: LogContext::empty(),
level: LogLevel::INFO,
timestamp: $this->timestamp
);
$result = $formatter($record);
expect($result)->toContain('你好');
expect($result)->toContain('мир');
expect($result)->toContain('🌍');
});
it('handles null channel gracefully', function () {
$formatter = new LineFormatter(format: '{channel}');
$record = new LogRecord(
message: 'Message',
context: LogContext::empty(),
level: LogLevel::INFO,
timestamp: $this->timestamp,
channel: null
);
$result = $formatter($record);
expect($result)->toBe('app'); // Default channel
});
it('handles format with no placeholders', function () {
$formatter = new LineFormatter(format: 'static text');
$record = new LogRecord(
message: 'Message',
context: LogContext::empty(),
level: LogLevel::INFO,
timestamp: $this->timestamp
);
expect($formatter($record))->toBe('static text');
});
});
});