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

@@ -2,147 +2,258 @@
declare(strict_types=1);
namespace Tests\Unit\Framework\Logging;
use App\Framework\Logging\ChannelLogger;
use App\Framework\Logging\DefaultChannelLogger;
use App\Framework\Logging\HasChannel;
use App\Framework\Logging\LogChannel;
use App\Framework\Logging\Logger;
use App\Framework\Logging\LogLevel;
use App\Framework\Logging\SupportsChannels;
use App\Framework\Logging\ValueObjects\LogContext;
use PHPUnit\Framework\TestCase;
final class DefaultChannelLoggerTest extends TestCase
{
private Logger $mockLogger;
describe('DefaultChannelLogger', function () {
beforeEach(function () {
// Create a test logger that records calls
$this->logCalls = [];
$this->testLogger = new class($this->logCalls) implements Logger, SupportsChannels {
public function __construct(private array &$logCalls)
{
}
private ChannelLogger $channelLogger;
public function debug(string $message, ?LogContext $context = null): void
{
$this->logToChannel(LogChannel::FRAMEWORK, LogLevel::DEBUG, $message, $context);
}
protected function setUp(): void
{
$this->mockLogger = $this->createMock(Logger::class);
$this->channelLogger = new DefaultChannelLogger($this->mockLogger, LogChannel::SECURITY);
}
public function info(string $message, ?LogContext $context = null): void
{
$this->logToChannel(LogChannel::FRAMEWORK, LogLevel::INFO, $message, $context);
}
public function test_implements_channel_logger_interface(): void
{
expect($this->channelLogger)->toBeInstanceOf(ChannelLogger::class);
}
public function notice(string $message, ?LogContext $context = null): void
{
$this->logToChannel(LogChannel::FRAMEWORK, LogLevel::NOTICE, $message, $context);
}
public function test_can_get_channel(): void
{
expect($this->channelLogger->getChannel())->toBe(LogChannel::SECURITY);
}
public function warning(string $message, ?LogContext $context = null): void
{
$this->logToChannel(LogChannel::FRAMEWORK, LogLevel::WARNING, $message, $context);
}
public function test_debug_delegates_to_parent_logger(): void
{
$this->mockLogger
->expects($this->once())
->method('logToChannel')
->with(LogChannel::SECURITY, LogLevel::DEBUG, 'Test message', ['key' => 'value']);
public function error(string $message, ?LogContext $context = null): void
{
$this->logToChannel(LogChannel::ERROR, LogLevel::ERROR, $message, $context);
}
$this->channelLogger->debug('Test message', ['key' => 'value']);
}
public function critical(string $message, ?LogContext $context = null): void
{
$this->logToChannel(LogChannel::ERROR, LogLevel::CRITICAL, $message, $context);
}
public function test_info_delegates_to_parent_logger(): void
{
$this->mockLogger
->expects($this->once())
->method('logToChannel')
->with(LogChannel::SECURITY, LogLevel::INFO, 'Test message', ['key' => 'value']);
public function alert(string $message, ?LogContext $context = null): void
{
$this->logToChannel(LogChannel::ERROR, LogLevel::ALERT, $message, $context);
}
$this->channelLogger->info('Test message', ['key' => 'value']);
}
public function emergency(string $message, ?LogContext $context = null): void
{
$this->logToChannel(LogChannel::ERROR, LogLevel::EMERGENCY, $message, $context);
}
public function test_warning_delegates_to_parent_logger(): void
{
$this->mockLogger
->expects($this->once())
->method('logToChannel')
->with(LogChannel::SECURITY, LogLevel::WARNING, 'Test message', ['key' => 'value']);
public function log(LogLevel $level, string $message, ?LogContext $context = null): void
{
$this->logToChannel(LogChannel::FRAMEWORK, $level, $message, $context);
}
$this->channelLogger->warning('Test message', ['key' => 'value']);
}
public function logToChannel(
LogChannel $channel,
LogLevel $level,
string $message,
?LogContext $context = null
): void {
$this->logCalls[] = [
'channel' => $channel,
'level' => $level,
'message' => $message,
'context' => $context
];
}
public function test_error_delegates_to_parent_logger(): void
{
$this->mockLogger
->expects($this->once())
->method('logToChannel')
->with(LogChannel::SECURITY, LogLevel::ERROR, 'Test message', ['key' => 'value']);
public function channel(LogChannel|string $channel): Logger&HasChannel
{
$logChannel = $channel instanceof LogChannel ? $channel : LogChannel::from($channel);
return new DefaultChannelLogger($this, $logChannel);
}
};
$this->channelLogger->error('Test message', ['key' => 'value']);
}
$this->channelLogger = new DefaultChannelLogger($this->testLogger, LogChannel::SECURITY);
});
public function test_critical_delegates_to_parent_logger(): void
{
$this->mockLogger
->expects($this->once())
->method('logToChannel')
->with(LogChannel::SECURITY, LogLevel::CRITICAL, 'Test message', ['key' => 'value']);
describe('interface implementation', function () {
it('implements Logger and HasChannel interfaces', function () {
expect($this->channelLogger)->toBeInstanceOf(Logger::class);
expect($this->channelLogger)->toBeInstanceOf(HasChannel::class);
});
$this->channelLogger->critical('Test message', ['key' => 'value']);
}
it('has correct channel property', function () {
expect($this->channelLogger->channel)->toBe(LogChannel::SECURITY);
});
});
public function test_alert_delegates_to_parent_logger(): void
{
$this->mockLogger
->expects($this->once())
->method('logToChannel')
->with(LogChannel::SECURITY, LogLevel::ALERT, 'Test message', ['key' => 'value']);
describe('log level delegation', function () {
it('delegates debug to parent logger', function () {
$this->channelLogger->debug('Test message', LogContext::withData(['key' => 'value']));
$this->channelLogger->alert('Test message', ['key' => 'value']);
}
expect(count($this->logCalls))->toBe(1);
expect($this->logCalls[0]['channel'])->toBe(LogChannel::SECURITY);
expect($this->logCalls[0]['level'])->toBe(LogLevel::DEBUG);
expect($this->logCalls[0]['message'])->toBe('Test message');
});
public function test_emergency_delegates_to_parent_logger(): void
{
$this->mockLogger
->expects($this->once())
->method('logToChannel')
->with(LogChannel::SECURITY, LogLevel::EMERGENCY, 'Test message', ['key' => 'value']);
it('delegates info to parent logger', function () {
$this->channelLogger->info('Test message', LogContext::withData(['key' => 'value']));
$this->channelLogger->emergency('Test message', ['key' => 'value']);
}
expect(count($this->logCalls))->toBe(1);
expect($this->logCalls[0]['level'])->toBe(LogLevel::INFO);
});
public function test_log_delegates_to_parent_logger(): void
{
$this->mockLogger
->expects($this->once())
->method('logToChannel')
->with(LogChannel::SECURITY, LogLevel::WARNING, 'Test message', ['key' => 'value']);
it('delegates warning to parent logger', function () {
$this->channelLogger->warning('Test message', LogContext::withData(['key' => 'value']));
$this->channelLogger->log(LogLevel::WARNING, 'Test message', ['key' => 'value']);
}
expect(count($this->logCalls))->toBe(1);
expect($this->logCalls[0]['level'])->toBe(LogLevel::WARNING);
});
public function test_supports_log_context_objects(): void
{
$logContext = LogContext::withData(['user_id' => 123]);
it('delegates error to parent logger', function () {
$this->channelLogger->error('Test message', LogContext::withData(['key' => 'value']));
$this->mockLogger
->expects($this->once())
->method('logToChannel')
->with(LogChannel::SECURITY, LogLevel::INFO, 'Test message', $logContext);
expect(count($this->logCalls))->toBe(1);
expect($this->logCalls[0]['level'])->toBe(LogLevel::ERROR);
});
$this->channelLogger->info('Test message', $logContext);
}
it('delegates critical to parent logger', function () {
$this->channelLogger->critical('Test message', LogContext::withData(['key' => 'value']));
public function test_supports_empty_context(): void
{
$this->mockLogger
->expects($this->once())
->method('logToChannel')
->with(LogChannel::SECURITY, LogLevel::INFO, 'Test message', []);
expect(count($this->logCalls))->toBe(1);
expect($this->logCalls[0]['level'])->toBe(LogLevel::CRITICAL);
});
$this->channelLogger->info('Test message');
}
it('delegates alert to parent logger', function () {
$this->channelLogger->alert('Test message', LogContext::withData(['key' => 'value']));
public function test_different_channels_work_independently(): void
{
$cacheLogger = new DefaultChannelLogger($this->mockLogger, LogChannel::CACHE);
$dbLogger = new DefaultChannelLogger($this->mockLogger, LogChannel::DATABASE);
expect(count($this->logCalls))->toBe(1);
expect($this->logCalls[0]['level'])->toBe(LogLevel::ALERT);
});
expect($cacheLogger->getChannel())->toBe(LogChannel::CACHE);
expect($dbLogger->getChannel())->toBe(LogChannel::DATABASE);
expect($this->channelLogger->getChannel())->toBe(LogChannel::SECURITY);
}
}
it('delegates emergency to parent logger', function () {
$this->channelLogger->emergency('Test message', LogContext::withData(['key' => 'value']));
expect(count($this->logCalls))->toBe(1);
expect($this->logCalls[0]['level'])->toBe(LogLevel::EMERGENCY);
});
it('delegates generic log to parent logger', function () {
$this->channelLogger->log(LogLevel::WARNING, 'Test message', LogContext::withData(['key' => 'value']));
expect(count($this->logCalls))->toBe(1);
expect($this->logCalls[0]['level'])->toBe(LogLevel::WARNING);
expect($this->logCalls[0]['message'])->toBe('Test message');
});
});
describe('context handling', function () {
it('supports log context objects', function () {
$logContext = LogContext::withData(['user_id' => 123]);
$this->channelLogger->info('Test message', $logContext);
expect(count($this->logCalls))->toBe(1);
expect($this->logCalls[0]['context'])->toBeInstanceOf(LogContext::class);
});
it('supports empty context', function () {
$this->channelLogger->info('Test message');
expect(count($this->logCalls))->toBe(1);
expect($this->logCalls[0]['message'])->toBe('Test message');
});
});
describe('channel independence', function () {
it('different channels work independently', function () {
$logCalls = [];
$testLogger = new class($logCalls) implements Logger, SupportsChannels {
public function __construct(private array &$logCalls)
{
}
public function debug(string $message, ?LogContext $context = null): void
{
$this->logToChannel(LogChannel::FRAMEWORK, LogLevel::DEBUG, $message, $context);
}
public function info(string $message, ?LogContext $context = null): void
{
$this->logToChannel(LogChannel::FRAMEWORK, LogLevel::INFO, $message, $context);
}
public function notice(string $message, ?LogContext $context = null): void
{
$this->logToChannel(LogChannel::FRAMEWORK, LogLevel::NOTICE, $message, $context);
}
public function warning(string $message, ?LogContext $context = null): void
{
$this->logToChannel(LogChannel::FRAMEWORK, LogLevel::WARNING, $message, $context);
}
public function error(string $message, ?LogContext $context = null): void
{
$this->logToChannel(LogChannel::ERROR, LogLevel::ERROR, $message, $context);
}
public function critical(string $message, ?LogContext $context = null): void
{
$this->logToChannel(LogChannel::ERROR, LogLevel::CRITICAL, $message, $context);
}
public function alert(string $message, ?LogContext $context = null): void
{
$this->logToChannel(LogChannel::ERROR, LogLevel::ALERT, $message, $context);
}
public function emergency(string $message, ?LogContext $context = null): void
{
$this->logToChannel(LogChannel::ERROR, LogLevel::EMERGENCY, $message, $context);
}
public function log(LogLevel $level, string $message, ?LogContext $context = null): void
{
$this->logToChannel(LogChannel::FRAMEWORK, $level, $message, $context);
}
public function logToChannel(
LogChannel $channel,
LogLevel $level,
string $message,
?LogContext $context = null
): void {
$this->logCalls[] = ['channel' => $channel];
}
public function channel(LogChannel|string $channel): Logger&HasChannel
{
$logChannel = $channel instanceof LogChannel ? $channel : LogChannel::from($channel);
return new DefaultChannelLogger($this, $logChannel);
}
};
$cacheLogger = new DefaultChannelLogger($testLogger, LogChannel::CACHE);
$dbLogger = new DefaultChannelLogger($testLogger, LogChannel::DATABASE);
$securityLogger = new DefaultChannelLogger($testLogger, LogChannel::SECURITY);
expect($cacheLogger->channel)->toBe(LogChannel::CACHE);
expect($dbLogger->channel)->toBe(LogChannel::DATABASE);
expect($securityLogger->channel)->toBe(LogChannel::SECURITY);
});
});
});