- 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.
308 lines
10 KiB
PHP
308 lines
10 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\Core\ValueObjects\Byte;
|
|
use App\Framework\Logging\Handlers\RotatingFileHandler;
|
|
use App\Framework\Logging\LogLevel;
|
|
use App\Framework\Logging\LogRecord;
|
|
use App\Framework\Logging\ValueObjects\LogContext;
|
|
|
|
describe('RotatingFileHandler', function () {
|
|
beforeEach(function () {
|
|
$this->testDir = sys_get_temp_dir() . '/rotating_handler_test_' . uniqid();
|
|
mkdir($this->testDir, 0777, true);
|
|
$this->testLogFile = $this->testDir . '/test.log';
|
|
});
|
|
|
|
afterEach(function () {
|
|
// Clean up test files
|
|
$files = glob($this->testDir . '/*');
|
|
if ($files) {
|
|
foreach ($files as $file) {
|
|
if (is_file($file)) {
|
|
unlink($file);
|
|
}
|
|
}
|
|
}
|
|
if (is_dir($this->testDir)) {
|
|
rmdir($this->testDir);
|
|
}
|
|
});
|
|
|
|
describe('withSizeRotation()', function () {
|
|
it('creates handler with size-based rotation', function () {
|
|
$handler = RotatingFileHandler::withSizeRotation(
|
|
$this->testLogFile,
|
|
maxFileSize: Byte::fromKilobytes(1),
|
|
maxFiles: 3
|
|
);
|
|
|
|
expect($handler)->toBeInstanceOf(RotatingFileHandler::class);
|
|
|
|
// Write small log
|
|
$record = new LogRecord(
|
|
message: 'Test message',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::INFO,
|
|
timestamp: new DateTimeImmutable()
|
|
);
|
|
|
|
$handler->handle($record);
|
|
|
|
expect(file_exists($this->testLogFile))->toBeTrue();
|
|
});
|
|
|
|
it('rotates log when size limit exceeded', function () {
|
|
$handler = RotatingFileHandler::withSizeRotation(
|
|
$this->testLogFile,
|
|
maxFileSize: Byte::fromBytes(100), // Very small size
|
|
maxFiles: 3
|
|
);
|
|
|
|
// Write multiple large messages to exceed size
|
|
for ($i = 0; $i < 10; $i++) {
|
|
$record = new LogRecord(
|
|
message: str_repeat('X', 50), // 50 bytes per message
|
|
context: LogContext::empty(),
|
|
level: LogLevel::INFO,
|
|
timestamp: new DateTimeImmutable()
|
|
);
|
|
|
|
$handler->handle($record);
|
|
}
|
|
|
|
// Check that rotation happened (should have .1 file)
|
|
expect(file_exists($this->testLogFile . '.1'))->toBeTrue();
|
|
});
|
|
});
|
|
|
|
describe('daily()', function () {
|
|
it('creates handler with daily rotation strategy', function () {
|
|
$handler = RotatingFileHandler::daily($this->testLogFile);
|
|
|
|
expect($handler)->toBeInstanceOf(RotatingFileHandler::class);
|
|
|
|
$strategy = $handler->getRotationStrategy();
|
|
expect($strategy['time_based'])->toBe('daily');
|
|
expect($strategy['size_based'])->toBeTrue();
|
|
});
|
|
|
|
it('rotates log when file is from previous day', function () {
|
|
// Create old log file
|
|
file_put_contents($this->testLogFile, "Old log content\n");
|
|
|
|
// Set file modification time to yesterday
|
|
touch($this->testLogFile, strtotime('yesterday'));
|
|
|
|
$handler = RotatingFileHandler::daily($this->testLogFile);
|
|
|
|
// Write new log
|
|
$record = new LogRecord(
|
|
message: 'New log entry',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::INFO,
|
|
timestamp: new DateTimeImmutable()
|
|
);
|
|
|
|
$handler->handle($record);
|
|
|
|
// File should have been rotated
|
|
expect(file_exists($this->testLogFile . '.1'))->toBeTrue();
|
|
|
|
// New file should contain only new entry
|
|
$content = file_get_contents($this->testLogFile);
|
|
expect($content)->toContain('New log entry');
|
|
expect($content)->not->toContain('Old log content');
|
|
});
|
|
|
|
it('does not rotate log when file is from today', function () {
|
|
// Create log file
|
|
file_put_contents($this->testLogFile, "Today's log\n");
|
|
|
|
$handler = RotatingFileHandler::daily($this->testLogFile);
|
|
|
|
// Write new log
|
|
$record = new LogRecord(
|
|
message: 'Another entry',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::INFO,
|
|
timestamp: new DateTimeImmutable()
|
|
);
|
|
|
|
$handler->handle($record);
|
|
|
|
// No rotation should have occurred
|
|
expect(file_exists($this->testLogFile . '.1'))->toBeFalse();
|
|
|
|
// File should contain both entries
|
|
$content = file_get_contents($this->testLogFile);
|
|
expect($content)->toContain("Today's log");
|
|
expect($content)->toContain('Another entry');
|
|
});
|
|
});
|
|
|
|
describe('weekly()', function () {
|
|
it('creates handler with weekly rotation strategy', function () {
|
|
$handler = RotatingFileHandler::weekly($this->testLogFile);
|
|
|
|
$strategy = $handler->getRotationStrategy();
|
|
expect($strategy['time_based'])->toBe('weekly');
|
|
});
|
|
|
|
it('rotates log when file is from previous week', function () {
|
|
// Create old log file
|
|
file_put_contents($this->testLogFile, "Old week log\n");
|
|
|
|
// Set file modification time to last week
|
|
touch($this->testLogFile, strtotime('last week'));
|
|
|
|
$handler = RotatingFileHandler::weekly($this->testLogFile);
|
|
|
|
// Write new log
|
|
$record = new LogRecord(
|
|
message: 'New week entry',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::INFO,
|
|
timestamp: new DateTimeImmutable()
|
|
);
|
|
|
|
$handler->handle($record);
|
|
|
|
// File should have been rotated
|
|
expect(file_exists($this->testLogFile . '.1'))->toBeTrue();
|
|
});
|
|
});
|
|
|
|
describe('monthly()', function () {
|
|
it('creates handler with monthly rotation strategy', function () {
|
|
$handler = RotatingFileHandler::monthly($this->testLogFile);
|
|
|
|
$strategy = $handler->getRotationStrategy();
|
|
expect($strategy['time_based'])->toBe('monthly');
|
|
});
|
|
|
|
it('rotates log when file is from previous month', function () {
|
|
// Create old log file
|
|
file_put_contents($this->testLogFile, "Old month log\n");
|
|
|
|
// Set file modification time to last month
|
|
touch($this->testLogFile, strtotime('last month'));
|
|
|
|
$handler = RotatingFileHandler::monthly($this->testLogFile);
|
|
|
|
// Write new log
|
|
$record = new LogRecord(
|
|
message: 'New month entry',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::INFO,
|
|
timestamp: new DateTimeImmutable()
|
|
);
|
|
|
|
$handler->handle($record);
|
|
|
|
// File should have been rotated
|
|
expect(file_exists($this->testLogFile . '.1'))->toBeTrue();
|
|
});
|
|
});
|
|
|
|
describe('production()', function () {
|
|
it('creates handler with production-optimized settings', function () {
|
|
$handler = RotatingFileHandler::production($this->testLogFile);
|
|
|
|
expect($handler)->toBeInstanceOf(RotatingFileHandler::class);
|
|
|
|
$strategy = $handler->getRotationStrategy();
|
|
expect($strategy['time_based'])->toBe('daily');
|
|
expect($strategy['size_based'])->toBeTrue();
|
|
});
|
|
|
|
it('uses INFO as default level for production', function () {
|
|
$handler = RotatingFileHandler::production($this->testLogFile);
|
|
|
|
// DEBUG should not be handled
|
|
$debugRecord = new LogRecord(
|
|
message: 'Debug message',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::DEBUG,
|
|
timestamp: new DateTimeImmutable()
|
|
);
|
|
|
|
expect($handler->isHandling($debugRecord))->toBeFalse();
|
|
|
|
// INFO should be handled
|
|
$infoRecord = new LogRecord(
|
|
message: 'Info message',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::INFO,
|
|
timestamp: new DateTimeImmutable()
|
|
);
|
|
|
|
expect($handler->isHandling($infoRecord))->toBeTrue();
|
|
});
|
|
});
|
|
|
|
describe('withMaxSize()', function () {
|
|
it('allows configuring max size after creation', function () {
|
|
$handler = RotatingFileHandler::daily($this->testLogFile);
|
|
|
|
$handler->withMaxSize(Byte::fromBytes(50), maxFiles: 5);
|
|
|
|
expect($handler)->toBeInstanceOf(RotatingFileHandler::class);
|
|
});
|
|
});
|
|
|
|
describe('getRotationStrategy()', function () {
|
|
it('returns rotation strategy info', function () {
|
|
$handler = RotatingFileHandler::daily($this->testLogFile);
|
|
|
|
$strategy = $handler->getRotationStrategy();
|
|
|
|
expect($strategy)->toHaveKeys(['time_based', 'size_based', 'last_check']);
|
|
expect($strategy['time_based'])->toBe('daily');
|
|
expect($strategy['size_based'])->toBeTrue();
|
|
});
|
|
|
|
it('returns none for size-only rotation', function () {
|
|
$handler = RotatingFileHandler::withSizeRotation($this->testLogFile);
|
|
|
|
$strategy = $handler->getRotationStrategy();
|
|
|
|
expect($strategy['time_based'])->toBe('none');
|
|
});
|
|
});
|
|
|
|
describe('caching behavior', function () {
|
|
it('caches rotation checks for performance', function () {
|
|
$handler = RotatingFileHandler::daily($this->testLogFile);
|
|
|
|
// First write
|
|
$record1 = new LogRecord(
|
|
message: 'First message',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::INFO,
|
|
timestamp: new DateTimeImmutable()
|
|
);
|
|
$handler->handle($record1);
|
|
|
|
$strategy1 = $handler->getRotationStrategy();
|
|
$lastCheck1 = $strategy1['last_check'];
|
|
|
|
// Immediate second write (within cache window)
|
|
$record2 = new LogRecord(
|
|
message: 'Second message',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::INFO,
|
|
timestamp: new DateTimeImmutable()
|
|
);
|
|
$handler->handle($record2);
|
|
|
|
$strategy2 = $handler->getRotationStrategy();
|
|
$lastCheck2 = $strategy2['last_check'];
|
|
|
|
// Last check should be same (cached)
|
|
expect($lastCheck1)->toBe($lastCheck2);
|
|
});
|
|
});
|
|
});
|