Files
michaelschiemer/tests/Unit/Framework/Logging/LogViewerTest.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

260 lines
8.7 KiB
PHP

<?php
declare(strict_types=1);
use App\Framework\Filesystem\ValueObjects\FilePath;
use App\Framework\Logging\LogLevel;
use App\Framework\Logging\LogViewer;
use App\Framework\Logging\ValueObjects\LogName;
use App\Framework\Logging\ValueObjects\LogReadResult;
use App\Framework\Logging\ValueObjects\LogSearchResult;
use App\Framework\Logging\ValueObjects\LogViewerConfig;
describe('LogViewer', function () {
beforeEach(function () {
// Create test log directory structure
$this->testDir = '/tmp/logviewer_test_' . uniqid();
mkdir($this->testDir, 0777, true);
mkdir($this->testDir . '/app', 0777, true);
mkdir($this->testDir . '/debug', 0777, true);
// Create test log files
file_put_contents(
$this->testDir . '/app/error.log',
"[2024-01-15 10:30:45] local.ERROR: Database connection failed\n" .
"[2024-01-15 10:31:00] local.WARNING: Cache miss\n" .
"[2024-01-15 10:31:30] local.INFO: User logged in\n"
);
file_put_contents(
$this->testDir . '/debug/test.log',
"[2024-01-15 10:32:00] local.DEBUG: Debug message\n" .
"[2024-01-15 10:32:30] local.ERROR: Test error\n"
);
// Create config
$this->config = new LogViewerConfig(
storageLogsPath: $this->testDir,
logDirectories: ['app', 'debug'],
defaultLimit: 100
);
$this->logViewer = new LogViewer($this->config);
});
afterEach(function () {
// Cleanup test directory
if (is_dir($this->testDir)) {
array_map('unlink', glob($this->testDir . '/*/*.log'));
rmdir($this->testDir . '/app');
rmdir($this->testDir . '/debug');
rmdir($this->testDir);
}
});
it('gets available logs', function () {
$logs = $this->logViewer->getAvailableLogs();
expect($logs)->toBeArray();
expect($logs)->toHaveKey('app_error');
expect($logs)->toHaveKey('debug_test');
expect($logs['app_error']['name'])->toBeInstanceOf(LogName::class);
expect($logs['app_error']['path'])->toBeInstanceOf(FilePath::class);
expect($logs['app_error'])->toHaveKey('size');
expect($logs['app_error'])->toHaveKey('modified');
expect($logs['app_error']['readable'])->toBeTrue();
});
it('reads log with default limit', function () {
$result = $this->logViewer->readLog('app_error');
expect($result)->toBeInstanceOf(LogReadResult::class);
expect($result->entries)->toHaveCount(3);
expect($result->logName->value)->toBe('app_error');
});
it('reads log with custom limit', function () {
$result = $this->logViewer->readLog('app_error', limit: 2);
expect($result->entries)->toHaveCount(2);
expect($result->limit)->toBe(2);
});
it('reads log with level filter', function () {
$result = $this->logViewer->readLog('app_error', level: LogLevel::ERROR);
expect($result->entries)->toHaveCount(1);
expect($result->entries[0]->level)->toBe(LogLevel::ERROR);
});
it('reads log with string level filter', function () {
$result = $this->logViewer->readLog('app_error', level: 'ERROR');
expect($result->entries)->toHaveCount(1);
expect($result->entries[0]->level)->toBe(LogLevel::ERROR);
});
it('reads log with search filter', function () {
$result = $this->logViewer->readLog('app_error', search: 'Database');
expect($result->entries)->toHaveCount(1);
expect($result->entries[0]->message)->toContain('Database');
});
it('reads log with combined filters', function () {
$result = $this->logViewer->readLog(
'app_error',
level: LogLevel::ERROR,
search: 'Database'
);
expect($result->entries)->toHaveCount(1);
expect($result->entries[0]->level)->toBe(LogLevel::ERROR);
expect($result->entries[0]->message)->toContain('Database');
});
it('accepts LogName value object', function () {
$logName = LogName::fromString('app_error');
$result = $this->logViewer->readLog($logName);
expect($result->entries)->toHaveCount(3);
});
it('throws on non-existent log', function () {
$this->logViewer->readLog('nonexistent_log');
})->throws(\InvalidArgumentException::class, "Log 'nonexistent_log' not found");
it('tails log with default lines', function () {
$result = $this->logViewer->tailLog('app_error');
expect($result)->toBeInstanceOf(LogReadResult::class);
expect($result->entries)->toHaveCount(3);
// Entries should be in reverse order (most recent first)
expect($result->entries[0]->message)->toContain('User logged in');
});
it('tails log with custom line count', function () {
$result = $this->logViewer->tailLog('app_error', lines: 2);
expect($result->entries)->toHaveCount(2);
expect($result->limit)->toBe(2);
});
it('searches across multiple logs', function () {
$result = $this->logViewer->searchLogs('error');
expect($result)->toBeInstanceOf(LogSearchResult::class);
expect($result->totalMatches)->toBeGreaterThan(0);
expect($result->filesSearched)->toBeGreaterThan(0);
});
it('searches specific logs', function () {
$result = $this->logViewer->searchLogs(
'error',
logNames: ['app_error']
);
expect($result->filesSearched)->toBe(1);
});
it('searches with level filter', function () {
$result = $this->logViewer->searchLogs(
'error',
level: LogLevel::ERROR
);
$allEntries = $result->getAllEntries();
foreach ($allEntries as $entry) {
expect($entry->level)->toBe(LogLevel::ERROR);
}
});
it('streams log entries in batches', function () {
$batchSize = 2;
$batches = [];
foreach ($this->logViewer->streamLog('app_error', batchSize: $batchSize) as $batch) {
$batches[] = $batch;
}
expect($batches)->toHaveCount(2); // 3 entries / 2 per batch = 2 batches
expect($batches[0]['entries_in_batch'])->toBe(2);
expect($batches[1]['entries_in_batch'])->toBe(1);
expect($batches[1]['is_final'])->toBeTrue();
});
it('streams log with level filter', function () {
$batches = [];
foreach ($this->logViewer->streamLog('app_error', level: LogLevel::ERROR) as $batch) {
$batches[] = $batch;
}
$totalEntries = array_sum(array_column($batches, 'entries_in_batch'));
expect($totalEntries)->toBe(1);
});
it('streams log with search filter', function () {
$batches = [];
foreach ($this->logViewer->streamLog('app_error', search: 'Database') as $batch) {
$batches[] = $batch;
}
$totalEntries = array_sum(array_column($batches, 'entries_in_batch'));
expect($totalEntries)->toBe(1);
});
it('handles empty log file', function () {
// Create empty log file
file_put_contents($this->testDir . '/app/empty.log', '');
$result = $this->logViewer->readLog('app_empty');
expect($result->isEmpty())->toBeTrue();
expect($result->entries)->toBe([]);
});
it('handles log with unstructured lines', function () {
// Create log with mixed structured and unstructured lines
file_put_contents(
$this->testDir . '/app/mixed.log',
"[2024-01-15 10:30:45] local.ERROR: Structured error\n" .
"Unstructured log line\n" .
"Another unstructured line\n"
);
$result = $this->logViewer->readLog('app_mixed');
expect($result->entries)->toHaveCount(3);
expect($result->entries[0]->parsed)->toBeTrue();
expect($result->entries[1]->parsed)->toBeFalse();
expect($result->entries[2]->parsed)->toBeFalse();
});
it('handles log with unknown level', function () {
file_put_contents(
$this->testDir . '/app/unknown.log',
"[2024-01-15 10:30:45] local.UNKNOWN: Unknown level\n"
);
$result = $this->logViewer->readLog('app_unknown');
expect($result->entries)->toHaveCount(1);
// Should fallback to INFO for unknown levels
expect($result->entries[0]->level)->toBe(LogLevel::INFO);
});
it('skips unreadable log files', function () {
// Create unreadable file (can't actually test this without root permissions)
// Just verify that getAvailableLogs filters correctly
$logs = $this->logViewer->getAvailableLogs();
foreach ($logs as $log) {
expect($log['readable'])->toBeTrue();
}
});
});