- 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.
426 lines
12 KiB
PHP
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();
|
|
});
|