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,413 @@
<?php
declare(strict_types=1);
use App\Framework\Logging\Handlers\FileHandler;
use App\Framework\Logging\LogLevel;
use App\Framework\Logging\LogRecord;
use App\Framework\Logging\ValueObjects\LogContext;
describe('FileHandler', function () {
beforeEach(function () {
// Create test directory
$this->testDir = sys_get_temp_dir() . '/logger_test_' . uniqid();
mkdir($this->testDir, 0777, true);
$this->testLogFile = $this->testDir . '/test.log';
});
afterEach(function () {
// Clean up test files
if (file_exists($this->testLogFile)) {
unlink($this->testLogFile);
}
if (is_dir($this->testDir)) {
rmdir($this->testDir);
}
});
describe('constructor', function () {
it('creates log file directory if it does not exist', function () {
$logFile = $this->testDir . '/nested/dir/test.log';
$handler = new FileHandler($logFile);
expect(is_dir(dirname($logFile)))->toBeTrue();
});
it('accepts LogLevel enum as minLevel', function () {
$handler = new FileHandler($this->testLogFile, LogLevel::ERROR);
$record = new LogRecord(
message: 'test message',
context: LogContext::empty(),
level: LogLevel::WARNING,
timestamp: new DateTimeImmutable(),
channel: 'test'
);
expect($handler->isHandling($record))->toBeFalse();
});
it('accepts int as minLevel', function () {
$handler = new FileHandler($this->testLogFile, 400); // ERROR level
$record = new LogRecord(
message: 'test message',
context: LogContext::empty(),
level: LogLevel::WARNING,
timestamp: new DateTimeImmutable(),
channel: 'test'
);
expect($handler->isHandling($record))->toBeFalse();
});
it('defaults to DEBUG level when no minLevel specified', function () {
$handler = new FileHandler($this->testLogFile);
$record = new LogRecord(
message: 'test message',
context: LogContext::empty(),
level: LogLevel::DEBUG,
timestamp: new DateTimeImmutable(),
channel: 'test'
);
expect($handler->isHandling($record))->toBeTrue();
});
});
describe('isHandling()', function () {
it('returns true when record level is above minLevel', function () {
$handler = new FileHandler($this->testLogFile, LogLevel::WARNING);
$record = new LogRecord(
message: 'test message',
context: LogContext::empty(),
level: LogLevel::ERROR,
timestamp: new DateTimeImmutable(),
channel: 'test'
);
expect($handler->isHandling($record))->toBeTrue();
});
it('returns true when record level equals minLevel', function () {
$handler = new FileHandler($this->testLogFile, LogLevel::WARNING);
$record = new LogRecord(
message: 'test message',
context: LogContext::empty(),
level: LogLevel::WARNING,
timestamp: new DateTimeImmutable(),
channel: 'test'
);
expect($handler->isHandling($record))->toBeTrue();
});
it('returns false when record level is below minLevel', function () {
$handler = new FileHandler($this->testLogFile, LogLevel::ERROR);
$record = new LogRecord(
message: 'test message',
context: LogContext::empty(),
level: LogLevel::WARNING,
timestamp: new DateTimeImmutable(),
channel: 'test'
);
expect($handler->isHandling($record))->toBeFalse();
});
});
describe('handle()', function () {
it('writes log entry to file', function () {
$handler = new FileHandler($this->testLogFile);
$record = new LogRecord(
message: 'Test log message',
context: LogContext::empty(),
level: LogLevel::INFO,
timestamp: new DateTimeImmutable(),
channel: 'test'
);
$handler->handle($record);
expect(file_exists($this->testLogFile))->toBeTrue();
$content = file_get_contents($this->testLogFile);
expect($content)->toContain('Test log message');
});
it('includes level name in output', function () {
$handler = new FileHandler($this->testLogFile);
$record = new LogRecord(
message: 'Test error',
context: LogContext::empty(),
level: LogLevel::ERROR,
timestamp: new DateTimeImmutable(),
channel: 'test'
);
$handler->handle($record);
$content = file_get_contents($this->testLogFile);
expect($content)->toContain('ERROR');
});
it('includes timestamp in output', function () {
$handler = new FileHandler($this->testLogFile);
$record = new LogRecord(
message: 'Test message',
context: LogContext::empty(),
level: LogLevel::INFO,
timestamp: new DateTimeImmutable(),
channel: 'test'
);
$handler->handle($record);
$content = file_get_contents($this->testLogFile);
// Check that a timestamp pattern exists (e.g., 2024-10-20 or similar)
expect($content)->toMatch('/\d{4}-\d{2}-\d{2}/');
});
it('includes channel in output', function () {
$handler = new FileHandler($this->testLogFile);
$record = new LogRecord(
message: 'Test message',
context: LogContext::empty(),
level: LogLevel::INFO,
timestamp: new DateTimeImmutable(),
channel: 'security'
);
$handler->handle($record);
$content = file_get_contents($this->testLogFile);
expect($content)->toContain('[security]');
});
it('includes request_id when present in extras', function () {
$handler = new FileHandler($this->testLogFile);
$record = (new LogRecord(
message: 'Test message',
context: LogContext::empty(),
level: LogLevel::INFO,
timestamp: new DateTimeImmutable(),
channel: 'test'
))->addExtra('request_id', 'req-123');
$handler->handle($record);
$content = file_get_contents($this->testLogFile);
expect($content)->toContain('[req-123]');
});
it('appends to existing file', function () {
$handler = new FileHandler($this->testLogFile);
$record1 = new LogRecord(
message: 'First message',
context: LogContext::empty(),
level: LogLevel::INFO,
timestamp: new DateTimeImmutable(),
channel: 'test'
);
$record2 = new LogRecord(
message: 'Second message',
context: LogContext::empty(),
level: LogLevel::INFO,
timestamp: new DateTimeImmutable(),
channel: 'test'
);
$handler->handle($record1);
$handler->handle($record2);
$content = file_get_contents($this->testLogFile);
expect($content)->toContain('First message');
expect($content)->toContain('Second message');
// Check that we have two lines
$lines = explode(PHP_EOL, trim($content));
expect(count($lines))->toBe(2);
});
it('uses custom output format when specified', function () {
$handler = new FileHandler(
$this->testLogFile,
outputFormat: '{level_name}: {message}'
);
$record = new LogRecord(
message: 'Custom format test',
context: LogContext::empty(),
level: LogLevel::ERROR,
timestamp: new DateTimeImmutable(),
channel: 'test'
);
$handler->handle($record);
$content = file_get_contents($this->testLogFile);
expect($content)->toContain('ERROR: Custom format test');
});
});
describe('setMinLevel()', function () {
it('updates minimum level with LogLevel', function () {
$handler = new FileHandler($this->testLogFile, LogLevel::DEBUG);
$handler->setMinLevel(LogLevel::ERROR);
$record = new LogRecord(
message: 'test message',
context: LogContext::empty(),
level: LogLevel::WARNING,
timestamp: new DateTimeImmutable(),
channel: 'test'
);
expect($handler->isHandling($record))->toBeFalse();
});
it('updates minimum level with int', function () {
$handler = new FileHandler($this->testLogFile, LogLevel::DEBUG);
$handler->setMinLevel(400); // ERROR
$record = new LogRecord(
message: 'test message',
context: LogContext::empty(),
level: LogLevel::WARNING,
timestamp: new DateTimeImmutable(),
channel: 'test'
);
expect($handler->isHandling($record))->toBeFalse();
});
it('returns self for fluent API', function () {
$handler = new FileHandler($this->testLogFile);
$result = $handler->setMinLevel(LogLevel::ERROR);
expect($result)->toBe($handler);
});
});
describe('setOutputFormat()', function () {
it('updates output format', function () {
$handler = new FileHandler($this->testLogFile);
$handler->setOutputFormat('{level_name} - {message}');
$record = new LogRecord(
message: 'New format',
context: LogContext::empty(),
level: LogLevel::INFO,
timestamp: new DateTimeImmutable(),
channel: 'test'
);
$handler->handle($record);
$content = file_get_contents($this->testLogFile);
expect($content)->toContain('INFO - New format');
});
it('returns self for fluent API', function () {
$handler = new FileHandler($this->testLogFile);
$result = $handler->setOutputFormat('{message}');
expect($result)->toBe($handler);
});
});
describe('setLogFile()', function () {
it('updates log file path', function () {
$handler = new FileHandler($this->testLogFile);
$newLogFile = $this->testDir . '/new-test.log';
$handler->setLogFile($newLogFile);
$record = new LogRecord(
message: 'New file test',
context: LogContext::empty(),
level: LogLevel::INFO,
timestamp: new DateTimeImmutable(),
channel: 'test'
);
$handler->handle($record);
expect(file_exists($newLogFile))->toBeTrue();
$content = file_get_contents($newLogFile);
expect($content)->toContain('New file test');
// Cleanup
if (file_exists($newLogFile)) {
unlink($newLogFile);
}
});
it('creates directory for new log file', function () {
$handler = new FileHandler($this->testLogFile);
$newLogFile = $this->testDir . '/nested/new/test.log';
$handler->setLogFile($newLogFile);
expect(is_dir(dirname($newLogFile)))->toBeTrue();
// Cleanup
if (file_exists($newLogFile)) {
unlink($newLogFile);
}
// Clean directories from deepest to shallowest
if (is_dir($this->testDir . '/nested/new')) {
rmdir($this->testDir . '/nested/new');
}
if (is_dir($this->testDir . '/nested')) {
rmdir($this->testDir . '/nested');
}
});
it('returns self for fluent API', function () {
$handler = new FileHandler($this->testLogFile);
$result = $handler->setLogFile($this->testDir . '/other.log');
expect($result)->toBe($handler);
});
});
describe('fluent interface', function () {
it('supports chaining methods', function () {
$handler = new FileHandler($this->testLogFile);
$handler
->setMinLevel(LogLevel::WARNING)
->setOutputFormat('{level_name}: {message}')
->setLogFile($this->testLogFile);
$record = new LogRecord(
message: 'Chained config',
context: LogContext::empty(),
level: LogLevel::ERROR,
timestamp: new DateTimeImmutable(),
channel: 'test'
);
expect($handler->isHandling($record))->toBeTrue();
$handler->handle($record);
$content = file_get_contents($this->testLogFile);
expect($content)->toContain('ERROR: Chained config');
});
});
});