Files
michaelschiemer/tests/Unit/Framework/Logging/DefaultLoggerTest.php
Michael Schiemer fc3d7e6357 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.
2025-10-25 19:18:37 +02:00

426 lines
12 KiB
PHP

<?php
declare(strict_types=1);
namespace Tests\Unit\Framework\Logging;
use App\Framework\Logging\DefaultLogger;
use App\Framework\Logging\LogChannel;
use App\Framework\Logging\LogHandler;
use App\Framework\Logging\LogLevel;
use App\Framework\Logging\LogRecord;
use App\Framework\Logging\ValueObjects\LogContext;
beforeEach(function () {
// Reset any static state
});
it('can create a logger with default settings', function () {
$logger = new DefaultLogger();
expect($logger)->toBeInstanceOf(DefaultLogger::class);
expect($logger->getConfiguration()['minLevel'])->toBe(LogLevel::DEBUG->value);
});
it('can create a logger with custom minimum level', function () {
$logger = new DefaultLogger(minLevel: LogLevel::WARNING);
expect($logger->getConfiguration()['minLevel'])->toBe(LogLevel::WARNING->value);
});
it('respects minimum log level and filters lower priority messages', function () {
$mockHandler = new class () implements LogHandler {
public array $handledRecords = [];
public function isHandling(LogRecord $record): bool
{
return true;
}
public function handle(LogRecord $record): void
{
$this->handledRecords[] = $record;
}
};
$logger = new DefaultLogger(
minLevel: LogLevel::WARNING,
handlers: [$mockHandler]
);
// These should be ignored (lower than WARNING)
$logger->debug('Debug message');
$logger->info('Info message');
$logger->notice('Notice message');
// These should be handled (WARNING or higher)
$logger->warning('Warning message');
$logger->error('Error message');
$logger->critical('Critical message');
expect($mockHandler->handledRecords)->toHaveCount(3);
expect($mockHandler->handledRecords[0]->getMessage())->toBe('Warning message');
expect($mockHandler->handledRecords[1]->getMessage())->toBe('Error message');
expect($mockHandler->handledRecords[2]->getMessage())->toBe('Critical message');
});
it('can log messages with all severity levels', function () {
$recordedLevels = [];
$mockHandler = new class ($recordedLevels) implements LogHandler {
public function __construct(private array &$recordedLevels)
{
}
public function isHandling(LogRecord $record): bool
{
return true;
}
public function handle(LogRecord $record): void
{
$this->recordedLevels[] = $record->getLevel()->value;
}
};
$logger = new DefaultLogger(handlers: [$mockHandler]);
$logger->debug('Debug');
$logger->info('Info');
$logger->notice('Notice');
$logger->warning('Warning');
$logger->error('Error');
$logger->critical('Critical');
$logger->alert('Alert');
$logger->emergency('Emergency');
expect($recordedLevels)->toBe([
LogLevel::DEBUG->value,
LogLevel::INFO->value,
LogLevel::NOTICE->value,
LogLevel::WARNING->value,
LogLevel::ERROR->value,
LogLevel::CRITICAL->value,
LogLevel::ALERT->value,
LogLevel::EMERGENCY->value,
]);
});
it('can log with LogContext', function () {
$capturedContext = null;
$mockHandler = new class ($capturedContext) implements LogHandler {
public function __construct(private ?array &$capturedContext)
{
}
public function isHandling(LogRecord $record): bool
{
return true;
}
public function handle(LogRecord $record): void
{
$this->capturedContext = $record->getContext();
}
};
$logger = new DefaultLogger(handlers: [$mockHandler]);
$context = LogContext::withData(['user_id' => 123, 'action' => 'login']);
$logger->info('User action', $context);
expect($capturedContext)->toMatchArray(['user_id' => 123, 'action' => 'login']);
});
it('can log with null context', function () {
$capturedContext = null;
$mockHandler = new class ($capturedContext) implements LogHandler {
public function __construct(private ?array &$capturedContext)
{
}
public function isHandling(LogRecord $record): bool
{
return true;
}
public function handle(LogRecord $record): void
{
$this->capturedContext = $record->getContext();
}
};
$logger = new DefaultLogger(handlers: [$mockHandler]);
// Log without context
$logger->info('Simple message');
expect($capturedContext)->toBe([]);
});
it('can be created without ProcessorManager', function () {
$processedRecord = null;
$mockHandler = new class ($processedRecord) implements LogHandler {
public function __construct(private ?LogRecord &$processedRecord)
{
}
public function isHandling(LogRecord $record): bool
{
return true;
}
public function handle(LogRecord $record): void
{
$this->processedRecord = $record;
}
};
$logger = new DefaultLogger(
handlers: [$mockHandler]
);
$logger->info('Test message');
expect($processedRecord)->toBeInstanceOf(LogRecord::class);
expect($processedRecord->getMessage())->toBe('Test message');
});
it('logs with LogContext including tags', function () {
$capturedContext = null;
$mockHandler = new class ($capturedContext) implements LogHandler {
public function __construct(private ?array &$capturedContext)
{
}
public function isHandling(LogRecord $record): bool
{
return true;
}
public function handle(LogRecord $record): void
{
$this->capturedContext = $record->getContext();
}
};
$logger = new DefaultLogger(
handlers: [$mockHandler]
);
$context = LogContext::withData(['request_id' => 'req-123'])
->addTags('api', 'v2');
$logger->info('API request', $context);
expect($capturedContext)->toMatchArray([
'request_id' => 'req-123',
'_tags' => ['api', 'v2'],
]);
});
it('provides channel loggers for different log channels', function () {
$logger = new DefaultLogger();
// Test that channel() method returns Logger & HasChannel instances
$securityLogger = $logger->channel(LogChannel::SECURITY);
expect($securityLogger->channel)->toBe(LogChannel::SECURITY);
$cacheLogger = $logger->channel(LogChannel::CACHE);
expect($cacheLogger->channel)->toBe(LogChannel::CACHE);
$databaseLogger = $logger->channel(LogChannel::DATABASE);
expect($databaseLogger->channel)->toBe(LogChannel::DATABASE);
$frameworkLogger = $logger->channel(LogChannel::FRAMEWORK);
expect($frameworkLogger->channel)->toBe(LogChannel::FRAMEWORK);
$errorLogger = $logger->channel(LogChannel::ERROR);
expect($errorLogger->channel)->toBe(LogChannel::ERROR);
});
it('can get channel loggers using string names', function () {
$logger = new DefaultLogger();
// Test that channel() also accepts string channel names
$securityLogger = $logger->channel('security');
expect($securityLogger->channel)->toBe(LogChannel::SECURITY);
// Test that same channel returns same instance (singleton per channel)
$securityLogger2 = $logger->channel(LogChannel::SECURITY);
expect($securityLogger2)->toBe($securityLogger);
});
it('can log to specific channels using channel loggers', function () {
$capturedChannel = null;
$capturedMessage = null;
$mockHandler = new class ($capturedChannel, $capturedMessage) implements LogHandler {
public function __construct(
private ?string &$capturedChannel,
private ?string &$capturedMessage
) {
}
public function isHandling(LogRecord $record): bool
{
return true;
}
public function handle(LogRecord $record): void
{
$this->capturedChannel = $record->getChannel();
$this->capturedMessage = $record->getMessage();
}
};
$logger = new DefaultLogger(handlers: [$mockHandler]);
// Log to security channel with LogContext
$context = LogContext::withData(['ip' => '192.168.1.1'])->addTags('security');
$logger->channel(LogChannel::SECURITY)->warning('Unauthorized access attempt', $context);
expect($capturedChannel)->toBe(LogChannel::SECURITY->value);
expect($capturedMessage)->toBe('Unauthorized access attempt');
// Log to database channel without context
$logger->channel(LogChannel::DATABASE)->error('Connection failed');
expect($capturedChannel)->toBe(LogChannel::DATABASE->value);
expect($capturedMessage)->toBe('Connection failed');
});
it('calls multiple handlers when configured', function () {
$handler1Called = false;
$handler2Called = false;
$handler3Called = false;
$handler1 = new class ($handler1Called) implements LogHandler {
public function __construct(private bool &$called)
{
}
public function isHandling(LogRecord $record): bool
{
return true;
}
public function handle(LogRecord $record): void
{
$this->called = true;
}
};
$handler2 = new class ($handler2Called) implements LogHandler {
public function __construct(private bool &$called)
{
}
public function isHandling(LogRecord $record): bool
{
return $record->getLevel()->value >= LogLevel::WARNING->value;
}
public function handle(LogRecord $record): void
{
$this->called = true;
}
};
$handler3 = new class ($handler3Called) implements LogHandler {
public function __construct(private bool &$called)
{
}
public function isHandling(LogRecord $record): bool
{
return false; // Never handles
}
public function handle(LogRecord $record): void
{
$this->called = true;
}
};
$logger = new DefaultLogger(handlers: [$handler1, $handler2, $handler3]);
$logger->error('Test error');
expect($handler1Called)->toBeTrue();
expect($handler2Called)->toBeTrue();
expect($handler3Called)->toBeFalse();
});
it('creates log records with correct timestamp', function () {
$capturedRecord = null;
$mockHandler = new class ($capturedRecord) implements LogHandler {
public function __construct(private ?LogRecord &$capturedRecord)
{
}
public function isHandling(LogRecord $record): bool
{
return true;
}
public function handle(LogRecord $record): void
{
$this->capturedRecord = $record;
}
};
$logger = new DefaultLogger(handlers: [$mockHandler]);
$beforeLog = new \DateTimeImmutable('now', new \DateTimeZone('Europe/Berlin'));
$logger->info('Test timestamp');
$afterLog = new \DateTimeImmutable('now', new \DateTimeZone('Europe/Berlin'));
expect($capturedRecord)->toBeInstanceOf(LogRecord::class);
expect($capturedRecord->getTimestamp())->toBeInstanceOf(\DateTimeImmutable::class);
$timestamp = $capturedRecord->getTimestamp();
expect($timestamp >= $beforeLog)->toBeTrue();
expect($timestamp <= $afterLog)->toBeTrue();
expect($timestamp->getTimezone()->getName())->toBe('Europe/Berlin');
});
it('returns correct configuration information', function () {
$handler1 = new class () implements LogHandler {
public function isHandling(LogRecord $record): bool
{
return true;
}
public function handle(LogRecord $record): void
{
}
};
$handler2 = new class () implements LogHandler {
public function isHandling(LogRecord $record): bool
{
return true;
}
public function handle(LogRecord $record): void
{
}
};
$logger = new DefaultLogger(
minLevel: LogLevel::INFO,
handlers: [$handler1, $handler2]
);
$config = $logger->getConfiguration();
expect($config['minLevel'])->toBe(LogLevel::INFO->value);
expect($config['handlers'])->toHaveCount(2);
expect($config['processors'])->toBeArray();
});