- 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.
508 lines
19 KiB
PHP
508 lines
19 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\Core\ValueObjects\Byte;
|
|
use App\Framework\Logging\Commands\LogHealthCheckCommand;
|
|
use App\Framework\Logging\Handlers\RotatingFileHandler;
|
|
use App\Framework\Logging\LogLevel;
|
|
use App\Framework\Logging\LogRecord;
|
|
use App\Framework\Logging\ValueObjects\LogContext;
|
|
use App\Framework\Console\ConsoleInput;
|
|
use App\Framework\Console\ConsoleOutput;
|
|
use App\Framework\Console\ExitCode;
|
|
|
|
/**
|
|
* Helper function: Recursively delete directory and all contents
|
|
*/
|
|
function recursiveDelete(string $dir): void {
|
|
if (!is_dir($dir)) {
|
|
return;
|
|
}
|
|
|
|
$files = array_diff(scandir($dir), ['.', '..']);
|
|
foreach ($files as $file) {
|
|
$path = $dir . '/' . $file;
|
|
is_dir($path) ? recursiveDelete($path) : unlink($path);
|
|
}
|
|
rmdir($dir);
|
|
}
|
|
|
|
/**
|
|
* Integration Tests für die gesamte Logging-Infrastruktur
|
|
*
|
|
* Testet End-to-End Workflows:
|
|
* - RotatingFileHandler mit realen Dateien
|
|
* - LogHealthCheckCommand Integration
|
|
* - Logging-Pipeline mit Rotation und Monitoring
|
|
* - Docker-Volume-Interaktion
|
|
*/
|
|
describe('Logging Infrastructure Integration', function () {
|
|
beforeEach(function () {
|
|
$this->testDir = sys_get_temp_dir() . '/logging_integration_test_' . uniqid();
|
|
mkdir($this->testDir, 0777, true);
|
|
mkdir($this->testDir . '/storage/logs', 0777, true);
|
|
mkdir($this->testDir . '/storage/logs/app', 0777, true);
|
|
mkdir($this->testDir . '/storage/logs/debug', 0777, true);
|
|
mkdir($this->testDir . '/storage/logs/security', 0777, true);
|
|
});
|
|
|
|
afterEach(function () {
|
|
// Cleanup all test directories and files
|
|
recursiveDelete($this->testDir);
|
|
});
|
|
|
|
describe('RotatingFileHandler Integration', function () {
|
|
it('handles complete rotation lifecycle with real files', function () {
|
|
$logFile = $this->testDir . '/storage/logs/app.log';
|
|
|
|
// Create handler with very small size limit for testing
|
|
$handler = RotatingFileHandler::withSizeRotation(
|
|
$logFile,
|
|
maxFileSize: Byte::fromBytes(200),
|
|
maxFiles: 3,
|
|
compress: false // Disable compression for easier testing
|
|
);
|
|
|
|
// Write logs until rotation occurs
|
|
for ($i = 0; $i < 10; $i++) {
|
|
$record = new LogRecord(
|
|
message: "Log entry #{$i} - " . str_repeat('X', 50),
|
|
context: LogContext::empty(),
|
|
level: LogLevel::INFO,
|
|
timestamp: new DateTimeImmutable()
|
|
);
|
|
|
|
$handler->handle($record);
|
|
}
|
|
|
|
// Verify rotation occurred
|
|
expect(file_exists($logFile))->toBeTrue();
|
|
expect(file_exists($logFile . '.1'))->toBeTrue();
|
|
|
|
// Verify current log file is within size limit (with some buffer for timestamps)
|
|
$currentSize = filesize($logFile);
|
|
expect($currentSize)->toBeLessThan(400); // Allow buffer for log formatting
|
|
|
|
// Verify rotated file exists and contains data
|
|
$rotatedContent = file_get_contents($logFile . '.1');
|
|
expect($rotatedContent)->not->toBeEmpty();
|
|
expect($rotatedContent)->toContain('Log entry');
|
|
});
|
|
|
|
it('integrates daily rotation with file system', function () {
|
|
$logFile = $this->testDir . '/storage/logs/daily.log';
|
|
|
|
// Create old log file from yesterday
|
|
file_put_contents($logFile, "Yesterday's log entry\n");
|
|
touch($logFile, strtotime('yesterday'));
|
|
|
|
// Create handler with daily rotation
|
|
$handler = RotatingFileHandler::daily($logFile, maxFiles: 7, compress: false);
|
|
|
|
// Write new log entry
|
|
$record = new LogRecord(
|
|
message: "Today's log entry",
|
|
context: LogContext::empty(),
|
|
level: LogLevel::INFO,
|
|
timestamp: new DateTimeImmutable()
|
|
);
|
|
|
|
$handler->handle($record);
|
|
|
|
// Verify rotation occurred
|
|
expect(file_exists($logFile . '.1'))->toBeTrue();
|
|
|
|
// Verify old content is in rotated file
|
|
$rotatedContent = file_get_contents($logFile . '.1');
|
|
expect($rotatedContent)->toContain("Yesterday's log");
|
|
|
|
// Verify new log only contains today's entry
|
|
$currentContent = file_get_contents($logFile);
|
|
expect($currentContent)->toContain("Today's log");
|
|
expect($currentContent)->not->toContain("Yesterday's log");
|
|
});
|
|
|
|
it('handles production configuration correctly', function () {
|
|
$logFile = $this->testDir . '/storage/logs/production.log';
|
|
|
|
$handler = RotatingFileHandler::production($logFile);
|
|
|
|
// Verify INFO level logs are handled
|
|
$infoRecord = new LogRecord(
|
|
message: 'Production info log',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::INFO,
|
|
timestamp: new DateTimeImmutable()
|
|
);
|
|
|
|
expect($handler->isHandling($infoRecord))->toBeTrue();
|
|
$handler->handle($infoRecord);
|
|
expect(file_exists($logFile))->toBeTrue();
|
|
|
|
$content = file_get_contents($logFile);
|
|
expect($content)->toContain('Production info log');
|
|
|
|
// Verify DEBUG logs are NOT handled by production handler
|
|
$debugRecord = new LogRecord(
|
|
message: 'Debug log should not appear',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::DEBUG,
|
|
timestamp: new DateTimeImmutable()
|
|
);
|
|
|
|
expect($handler->isHandling($debugRecord))->toBeFalse();
|
|
});
|
|
|
|
it('maintains rotation across multiple log files in subdirectories', function () {
|
|
$appLog = $this->testDir . '/storage/logs/app/application.log';
|
|
$debugLog = $this->testDir . '/storage/logs/debug/debug.log';
|
|
$securityLog = $this->testDir . '/storage/logs/security/security.log';
|
|
|
|
// Create handlers for different log types
|
|
$appHandler = RotatingFileHandler::withSizeRotation($appLog, maxFileSize: Byte::fromBytes(100));
|
|
$debugHandler = RotatingFileHandler::withSizeRotation($debugLog, maxFileSize: Byte::fromBytes(100));
|
|
$securityHandler = RotatingFileHandler::withSizeRotation($securityLog, maxFileSize: Byte::fromBytes(100));
|
|
|
|
// Write logs to all handlers
|
|
for ($i = 0; $i < 5; $i++) {
|
|
$record = new LogRecord(
|
|
message: str_repeat('X', 30),
|
|
context: LogContext::empty(),
|
|
level: LogLevel::INFO,
|
|
timestamp: new DateTimeImmutable()
|
|
);
|
|
|
|
$appHandler->handle($record);
|
|
$debugHandler->handle($record);
|
|
$securityHandler->handle($record);
|
|
}
|
|
|
|
// Verify all logs exist
|
|
expect(file_exists($appLog))->toBeTrue();
|
|
expect(file_exists($debugLog))->toBeTrue();
|
|
expect(file_exists($securityLog))->toBeTrue();
|
|
|
|
// At least one should have triggered rotation
|
|
$rotationOccurred = file_exists($appLog . '.1')
|
|
|| file_exists($debugLog . '.1')
|
|
|| file_exists($securityLog . '.1');
|
|
|
|
expect($rotationOccurred)->toBeTrue();
|
|
});
|
|
});
|
|
|
|
describe('LogHealthCheckCommand Integration', function () {
|
|
it('performs complete health check on log infrastructure', function () {
|
|
// Save current directory and change to test directory
|
|
$originalDir = getcwd();
|
|
chdir($this->testDir);
|
|
|
|
try {
|
|
// Create command
|
|
$command = new LogHealthCheckCommand();
|
|
|
|
// Create input without options
|
|
$output = new ConsoleOutput();
|
|
$input = new ConsoleInput(['test'], $output, null);
|
|
|
|
// Capture output
|
|
ob_start();
|
|
$exitCode = $command->execute($input);
|
|
$output = ob_get_clean();
|
|
|
|
// Verify all checks were performed
|
|
expect($output)->toContain('Checking log directories');
|
|
expect($output)->toContain('Checking write permissions');
|
|
expect($output)->toContain('Checking disk space');
|
|
expect($output)->toContain('Checking log files');
|
|
expect($output)->toContain('Summary');
|
|
|
|
// Should pass all checks (directories exist, permissions OK, disk space OK, no large files)
|
|
expect($exitCode)->toBe(ExitCode::SUCCESS);
|
|
expect($output)->toContain('All checks passed');
|
|
} finally {
|
|
// Restore original directory
|
|
chdir($originalDir);
|
|
}
|
|
});
|
|
|
|
it('detects missing directories and can auto-fix with --fix-permissions', function () {
|
|
// Remove debug directory
|
|
rmdir($this->testDir . '/storage/logs/debug');
|
|
|
|
$originalDir = getcwd();
|
|
chdir($this->testDir);
|
|
|
|
try {
|
|
// First run without fix - should fail
|
|
$command1 = new LogHealthCheckCommand();
|
|
$output = new ConsoleOutput();
|
|
$input1 = new ConsoleInput(['test'], $output, null);
|
|
|
|
ob_start();
|
|
$exitCode1 = $command1->execute($input1);
|
|
$output1 = ob_get_clean();
|
|
|
|
expect($exitCode1)->toBe(ExitCode::GENERAL_ERROR);
|
|
expect($output1)->toContain('(missing)');
|
|
|
|
// Second run with --fix-permissions
|
|
$command2 = new LogHealthCheckCommand();
|
|
$output2 = new ConsoleOutput();
|
|
$input2 = new ConsoleInput(['test', '--fix-permissions'], $output2, null);
|
|
|
|
ob_start();
|
|
$exitCode2 = $command2->execute($input2);
|
|
$output2 = ob_get_clean();
|
|
|
|
// Should pass after fix
|
|
expect($exitCode2)->toBe(ExitCode::SUCCESS);
|
|
expect(is_dir($this->testDir . '/storage/logs/debug'))->toBeTrue();
|
|
} finally {
|
|
chdir($originalDir);
|
|
}
|
|
});
|
|
|
|
it('reports detailed information with --detailed flag', function () {
|
|
// Create some log files
|
|
file_put_contents($this->testDir . '/storage/logs/test1.log', str_repeat('X', 1000));
|
|
file_put_contents($this->testDir . '/storage/logs/test2.log', str_repeat('Y', 2000));
|
|
|
|
$originalDir = getcwd();
|
|
chdir($this->testDir);
|
|
|
|
try {
|
|
$command = new LogHealthCheckCommand();
|
|
$output = new ConsoleOutput();
|
|
$input = new ConsoleInput(['test', '--detailed'], $output, null);
|
|
|
|
ob_start();
|
|
$command->execute($input);
|
|
$output = ob_get_clean();
|
|
|
|
// Verify detailed output shows file information
|
|
expect($output)->toContain('test1.log');
|
|
expect($output)->toContain('test2.log');
|
|
expect($output)->toContain('Size:');
|
|
expect($output)->toContain('Modified:');
|
|
} finally {
|
|
chdir($originalDir);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Full Logging Pipeline Integration', function () {
|
|
it('completes full workflow: write logs -> rotate -> health check', function () {
|
|
$logFile = $this->testDir . '/storage/logs/app/workflow.log';
|
|
|
|
// Step 1: Write logs with rotation
|
|
$handler = RotatingFileHandler::withSizeRotation(
|
|
$logFile,
|
|
maxFileSize: Byte::fromBytes(500),
|
|
maxFiles: 3,
|
|
compress: false
|
|
);
|
|
|
|
for ($i = 0; $i < 20; $i++) {
|
|
$record = new LogRecord(
|
|
message: "Workflow log entry #{$i} - " . str_repeat('X', 50),
|
|
context: LogContext::empty(),
|
|
level: LogLevel::INFO,
|
|
timestamp: new DateTimeImmutable()
|
|
);
|
|
|
|
$handler->handle($record);
|
|
}
|
|
|
|
// Verify logs and rotation
|
|
expect(file_exists($logFile))->toBeTrue();
|
|
expect(file_exists($logFile . '.1'))->toBeTrue();
|
|
|
|
// Step 2: Run health check
|
|
$originalDir = getcwd();
|
|
chdir($this->testDir);
|
|
|
|
try {
|
|
$command = new LogHealthCheckCommand();
|
|
$output = new ConsoleOutput();
|
|
$input = new ConsoleInput(['test', '--detailed'], $output, null);
|
|
|
|
ob_start();
|
|
$exitCode = $command->execute($input);
|
|
$healthOutput = ob_get_clean();
|
|
|
|
// Verify health check passes
|
|
expect($exitCode)->toBe(ExitCode::SUCCESS);
|
|
expect($healthOutput)->toContain('log file(s)'); // At least 2 files (workflow.log + workflow.log.1)
|
|
expect($healthOutput)->toContain('All checks passed');
|
|
} finally {
|
|
chdir($originalDir);
|
|
}
|
|
});
|
|
|
|
it('handles concurrent logging with multiple handlers and health monitoring', function () {
|
|
$appLog = $this->testDir . '/storage/logs/app/app.log';
|
|
$debugLog = $this->testDir . '/storage/logs/debug/debug.log';
|
|
$securityLog = $this->testDir . '/storage/logs/security/security.log';
|
|
|
|
// Create multiple handlers
|
|
$handlers = [
|
|
RotatingFileHandler::production($appLog),
|
|
RotatingFileHandler::withSizeRotation($debugLog, maxFileSize: Byte::fromKilobytes(1)),
|
|
RotatingFileHandler::daily($securityLog),
|
|
];
|
|
|
|
// Write logs concurrently to all handlers
|
|
foreach ($handlers as $handler) {
|
|
for ($i = 0; $i < 10; $i++) {
|
|
$record = new LogRecord(
|
|
message: "Concurrent log entry #{$i}",
|
|
context: LogContext::empty(),
|
|
level: LogLevel::INFO,
|
|
timestamp: new DateTimeImmutable()
|
|
);
|
|
|
|
$handler->handle($record);
|
|
}
|
|
}
|
|
|
|
// Verify all logs exist
|
|
expect(file_exists($appLog))->toBeTrue();
|
|
expect(file_exists($debugLog))->toBeTrue();
|
|
expect(file_exists($securityLog))->toBeTrue();
|
|
|
|
// Run health check on entire infrastructure
|
|
$originalDir = getcwd();
|
|
chdir($this->testDir);
|
|
|
|
try {
|
|
$command = new LogHealthCheckCommand();
|
|
$output = new ConsoleOutput();
|
|
$input = new ConsoleInput(['test'], $output, null);
|
|
|
|
ob_start();
|
|
$exitCode = $command->execute($input);
|
|
$healthOutput = ob_get_clean();
|
|
|
|
// Should pass all checks
|
|
expect($exitCode)->toBe(ExitCode::SUCCESS);
|
|
expect($healthOutput)->toContain('log file(s)'); // At least 3 log files
|
|
} finally {
|
|
chdir($originalDir);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Performance and Scalability Integration', function () {
|
|
it('handles high-volume logging efficiently', function () {
|
|
$logFile = $this->testDir . '/storage/logs/performance.log';
|
|
|
|
$handler = RotatingFileHandler::withSizeRotation(
|
|
$logFile,
|
|
maxFileSize: Byte::fromKilobytes(10),
|
|
maxFiles: 5
|
|
);
|
|
|
|
$startTime = microtime(true);
|
|
|
|
// Write 1000 log entries
|
|
for ($i = 0; $i < 1000; $i++) {
|
|
$record = new LogRecord(
|
|
message: "Performance test entry #{$i}",
|
|
context: LogContext::empty(),
|
|
level: LogLevel::INFO,
|
|
timestamp: new DateTimeImmutable()
|
|
);
|
|
|
|
$handler->handle($record);
|
|
}
|
|
|
|
$endTime = microtime(true);
|
|
$executionTime = $endTime - $startTime;
|
|
|
|
// Should complete in reasonable time (< 5 seconds for 1000 entries)
|
|
expect($executionTime)->toBeLessThan(5.0);
|
|
|
|
// Verify logs were written and rotated
|
|
expect(file_exists($logFile))->toBeTrue();
|
|
|
|
// Count rotation files (should have multiple due to small size limit)
|
|
$rotationCount = 0;
|
|
for ($i = 1; $i <= 5; $i++) {
|
|
if (file_exists($logFile . '.' . $i)) {
|
|
$rotationCount++;
|
|
}
|
|
}
|
|
|
|
expect($rotationCount)->toBeGreaterThan(0);
|
|
});
|
|
|
|
it('maintains performance with health checks on large log directories', function () {
|
|
// Create many small log files
|
|
for ($i = 0; $i < 50; $i++) {
|
|
file_put_contents(
|
|
$this->testDir . "/storage/logs/file{$i}.log",
|
|
str_repeat('X', 100)
|
|
);
|
|
}
|
|
|
|
$originalDir = getcwd();
|
|
chdir($this->testDir);
|
|
|
|
try {
|
|
$command = new LogHealthCheckCommand();
|
|
$output = new ConsoleOutput();
|
|
$input = new ConsoleInput(['test', '--detailed'], $output, null);
|
|
|
|
$startTime = microtime(true);
|
|
|
|
ob_start();
|
|
$exitCode = $command->execute($input);
|
|
$healthOutput = ob_get_clean();
|
|
|
|
$endTime = microtime(true);
|
|
$executionTime = $endTime - $startTime;
|
|
|
|
// Should complete quickly even with 50 files (< 2 seconds)
|
|
expect($executionTime)->toBeLessThan(2.0);
|
|
expect($exitCode)->toBe(ExitCode::SUCCESS);
|
|
expect($healthOutput)->toContain('Found 50 log file(s)');
|
|
} finally {
|
|
chdir($originalDir);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Docker Volume Integration (Simulated)', function () {
|
|
it('works with host-mounted log directories', function () {
|
|
// Simulate host mount scenario with correct permissions
|
|
$mountedLogDir = $this->testDir . '/storage/logs';
|
|
chmod($mountedLogDir, 0777);
|
|
|
|
$logFile = $mountedLogDir . '/docker-test.log';
|
|
|
|
$handler = RotatingFileHandler::production($logFile);
|
|
|
|
// Write logs as if running in Docker container
|
|
for ($i = 0; $i < 5; $i++) {
|
|
$record = new LogRecord(
|
|
message: "Docker container log #{$i}",
|
|
context: LogContext::empty(),
|
|
level: LogLevel::INFO,
|
|
timestamp: new DateTimeImmutable()
|
|
);
|
|
|
|
$handler->handle($record);
|
|
}
|
|
|
|
// Verify logs are accessible from "host" (same directory in this test)
|
|
expect(file_exists($logFile))->toBeTrue();
|
|
expect(is_readable($logFile))->toBeTrue();
|
|
|
|
$content = file_get_contents($logFile);
|
|
expect($content)->toContain('Docker container log');
|
|
});
|
|
});
|
|
});
|