- 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.
269 lines
8.0 KiB
PHP
269 lines
8.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\Filesystem\ValueObjects\FilePath;
|
|
use App\Framework\Logging\LogLevel;
|
|
use App\Framework\Logging\ValueObjects\LogEntry;
|
|
use App\Framework\Logging\ValueObjects\LogName;
|
|
use App\Framework\Logging\ValueObjects\LogReadResult;
|
|
|
|
describe('LogReadResult', function () {
|
|
beforeEach(function () {
|
|
$this->logName = LogName::fromString('app_error');
|
|
$this->logPath = FilePath::create('/tmp/test.log');
|
|
|
|
$this->entries = [
|
|
LogEntry::fromParsedLine(
|
|
timestamp: '2024-01-15 10:30:45',
|
|
level: 'ERROR',
|
|
messageWithContext: 'Database error',
|
|
raw: '[2024-01-15 10:30:45] local.ERROR: Database error',
|
|
sourcePath: $this->logPath
|
|
),
|
|
LogEntry::fromParsedLine(
|
|
timestamp: '2024-01-15 10:31:00',
|
|
level: 'WARNING',
|
|
messageWithContext: 'Cache miss',
|
|
raw: '[2024-01-15 10:31:00] local.WARNING: Cache miss',
|
|
sourcePath: $this->logPath
|
|
),
|
|
LogEntry::fromParsedLine(
|
|
timestamp: '2024-01-15 10:31:30',
|
|
level: 'INFO',
|
|
messageWithContext: 'User logged in',
|
|
raw: '[2024-01-15 10:31:30] local.INFO: User logged in',
|
|
sourcePath: $this->logPath
|
|
),
|
|
];
|
|
});
|
|
|
|
it('creates from entries', function () {
|
|
$result = LogReadResult::fromEntries(
|
|
logName: $this->logName,
|
|
logPath: $this->logPath,
|
|
entries: $this->entries,
|
|
limit: 100
|
|
);
|
|
|
|
expect($result->logName)->toBe($this->logName);
|
|
expect($result->logPath)->toBe($this->logPath);
|
|
expect($result->entries)->toHaveCount(3);
|
|
expect($result->totalEntries)->toBe(3);
|
|
expect($result->limit)->toBe(100);
|
|
});
|
|
|
|
it('checks if empty', function () {
|
|
$result = LogReadResult::fromEntries(
|
|
logName: $this->logName,
|
|
logPath: $this->logPath,
|
|
entries: [],
|
|
limit: 100
|
|
);
|
|
|
|
expect($result->isEmpty())->toBeTrue();
|
|
expect($result->hasEntries())->toBeFalse();
|
|
});
|
|
|
|
it('filters by log level', function () {
|
|
$result = LogReadResult::fromEntries(
|
|
logName: $this->logName,
|
|
logPath: $this->logPath,
|
|
entries: $this->entries,
|
|
limit: 100
|
|
);
|
|
|
|
$filtered = $result->filterByLevel(LogLevel::ERROR);
|
|
|
|
expect($filtered->entries)->toHaveCount(1);
|
|
expect($filtered->entries[0]->level)->toBe(LogLevel::ERROR);
|
|
});
|
|
|
|
it('filters by search term', function () {
|
|
$result = LogReadResult::fromEntries(
|
|
logName: $this->logName,
|
|
logPath: $this->logPath,
|
|
entries: $this->entries,
|
|
limit: 100
|
|
);
|
|
|
|
$filtered = $result->filterBySearch('database');
|
|
|
|
expect($filtered->entries)->toHaveCount(1);
|
|
expect($filtered->entries[0]->message)->toContain('Database');
|
|
});
|
|
|
|
it('filters by minimum log level', function () {
|
|
$result = LogReadResult::fromEntries(
|
|
logName: $this->logName,
|
|
logPath: $this->logPath,
|
|
entries: $this->entries,
|
|
limit: 100
|
|
);
|
|
|
|
$filtered = $result->filterByMinimumLevel(LogLevel::WARNING);
|
|
|
|
expect($filtered->entries)->toHaveCount(2); // ERROR and WARNING
|
|
expect($filtered->first()->level)->toBe(LogLevel::ERROR);
|
|
expect($filtered->last()->level)->toBe(LogLevel::WARNING);
|
|
});
|
|
|
|
it('gets first entry', function () {
|
|
$result = LogReadResult::fromEntries(
|
|
logName: $this->logName,
|
|
logPath: $this->logPath,
|
|
entries: $this->entries,
|
|
limit: 100
|
|
);
|
|
|
|
$first = $result->first();
|
|
|
|
expect($first)->not->toBeNull();
|
|
expect($first->level)->toBe(LogLevel::ERROR);
|
|
});
|
|
|
|
it('gets last entry', function () {
|
|
$result = LogReadResult::fromEntries(
|
|
logName: $this->logName,
|
|
logPath: $this->logPath,
|
|
entries: $this->entries,
|
|
limit: 100
|
|
);
|
|
|
|
$last = $result->last();
|
|
|
|
expect($last)->not->toBeNull();
|
|
expect($last->level)->toBe(LogLevel::INFO);
|
|
});
|
|
|
|
it('returns null for first/last on empty result', function () {
|
|
$result = LogReadResult::fromEntries(
|
|
logName: $this->logName,
|
|
logPath: $this->logPath,
|
|
entries: [],
|
|
limit: 100
|
|
);
|
|
|
|
expect($result->first())->toBeNull();
|
|
expect($result->last())->toBeNull();
|
|
});
|
|
|
|
it('takes limited entries', function () {
|
|
$result = LogReadResult::fromEntries(
|
|
logName: $this->logName,
|
|
logPath: $this->logPath,
|
|
entries: $this->entries,
|
|
limit: 100
|
|
);
|
|
|
|
$limited = $result->take(2);
|
|
|
|
expect($limited->entries)->toHaveCount(2);
|
|
expect($limited->totalEntries)->toBe(2);
|
|
});
|
|
|
|
it('skips entries', function () {
|
|
$result = LogReadResult::fromEntries(
|
|
logName: $this->logName,
|
|
logPath: $this->logPath,
|
|
entries: $this->entries,
|
|
limit: 100
|
|
);
|
|
|
|
$skipped = $result->skip(1);
|
|
|
|
expect($skipped->entries)->toHaveCount(2);
|
|
expect($skipped->first()->level)->toBe(LogLevel::WARNING);
|
|
});
|
|
|
|
it('reverses entry order', function () {
|
|
$result = LogReadResult::fromEntries(
|
|
logName: $this->logName,
|
|
logPath: $this->logPath,
|
|
entries: $this->entries,
|
|
limit: 100
|
|
);
|
|
|
|
$reversed = $result->reverse();
|
|
|
|
expect($reversed->first()->level)->toBe(LogLevel::INFO);
|
|
expect($reversed->last()->level)->toBe(LogLevel::ERROR);
|
|
});
|
|
|
|
it('gets level statistics', function () {
|
|
$result = LogReadResult::fromEntries(
|
|
logName: $this->logName,
|
|
logPath: $this->logPath,
|
|
entries: $this->entries,
|
|
limit: 100
|
|
);
|
|
|
|
$stats = $result->getLevelStatistics();
|
|
|
|
expect($stats)->toBeArray();
|
|
expect($stats['ERROR'])->toBe(1);
|
|
expect($stats['WARNING'])->toBe(1);
|
|
expect($stats['INFO'])->toBe(1);
|
|
});
|
|
|
|
it('gets metadata', function () {
|
|
$result = LogReadResult::fromEntries(
|
|
logName: $this->logName,
|
|
logPath: $this->logPath,
|
|
entries: $this->entries,
|
|
limit: 100,
|
|
search: 'test',
|
|
levelFilter: LogLevel::ERROR
|
|
);
|
|
|
|
$metadata = $result->getMetadata();
|
|
|
|
expect($metadata)->toBeArray();
|
|
expect($metadata['log_name'])->toBe('app_error');
|
|
expect($metadata['total_entries'])->toBe(3);
|
|
expect($metadata['limit'])->toBe(100);
|
|
expect($metadata['search'])->toBe('test');
|
|
expect($metadata['level_filter'])->toBe('ERROR');
|
|
});
|
|
|
|
it('converts to array', function () {
|
|
$result = LogReadResult::fromEntries(
|
|
logName: $this->logName,
|
|
logPath: $this->logPath,
|
|
entries: $this->entries,
|
|
limit: 100
|
|
);
|
|
|
|
$array = $result->toArray();
|
|
|
|
expect($array)->toHaveKey('log_name');
|
|
expect($array)->toHaveKey('log_path');
|
|
expect($array)->toHaveKey('entries');
|
|
expect($array)->toHaveKey('total_entries');
|
|
expect($array)->toHaveKey('metadata');
|
|
expect($array['entries'])->toBeArray();
|
|
expect($array['entries'])->toHaveCount(3);
|
|
});
|
|
|
|
it('creates iterator', function () {
|
|
$result = LogReadResult::fromEntries(
|
|
logName: $this->logName,
|
|
logPath: $this->logPath,
|
|
entries: $this->entries,
|
|
limit: 100
|
|
);
|
|
|
|
$iterator = $result->getIterator();
|
|
|
|
expect($iterator)->toBeInstanceOf(\ArrayIterator::class);
|
|
|
|
$count = 0;
|
|
foreach ($iterator as $entry) {
|
|
expect($entry)->toBeInstanceOf(LogEntry::class);
|
|
$count++;
|
|
}
|
|
|
|
expect($count)->toBe(3);
|
|
});
|
|
});
|