- 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.
257 lines
8.3 KiB
PHP
257 lines
8.3 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;
|
|
use App\Framework\Logging\ValueObjects\LogSearchResult;
|
|
|
|
describe('LogSearchResult', function () {
|
|
beforeEach(function () {
|
|
$this->logPath1 = FilePath::create('/tmp/app.log');
|
|
$this->logPath2 = FilePath::create('/tmp/debug.log');
|
|
|
|
$this->result1 = LogReadResult::fromEntries(
|
|
logName: LogName::fromString('app_error'),
|
|
logPath: $this->logPath1,
|
|
entries: [
|
|
LogEntry::fromParsedLine(
|
|
timestamp: '2024-01-15 10:30:45',
|
|
level: 'ERROR',
|
|
messageWithContext: 'Database connection failed',
|
|
raw: '[2024-01-15 10:30:45] local.ERROR: Database connection failed',
|
|
sourcePath: $this->logPath1
|
|
),
|
|
LogEntry::fromParsedLine(
|
|
timestamp: '2024-01-15 10:31:00',
|
|
level: 'WARNING',
|
|
messageWithContext: 'Database slow query',
|
|
raw: '[2024-01-15 10:31:00] local.WARNING: Database slow query',
|
|
sourcePath: $this->logPath1
|
|
),
|
|
],
|
|
limit: 100
|
|
);
|
|
|
|
$this->result2 = LogReadResult::fromEntries(
|
|
logName: LogName::fromString('debug_log'),
|
|
logPath: $this->logPath2,
|
|
entries: [
|
|
LogEntry::fromParsedLine(
|
|
timestamp: '2024-01-15 10:32:00',
|
|
level: 'INFO',
|
|
messageWithContext: 'Database query executed',
|
|
raw: '[2024-01-15 10:32:00] local.INFO: Database query executed',
|
|
sourcePath: $this->logPath2
|
|
),
|
|
],
|
|
limit: 100
|
|
);
|
|
});
|
|
|
|
it('creates from results', function () {
|
|
$searchResult = LogSearchResult::fromResults(
|
|
'database',
|
|
[$this->result1, $this->result2]
|
|
);
|
|
|
|
expect($searchResult->searchTerm)->toBe('database');
|
|
expect($searchResult->results)->toHaveCount(2);
|
|
expect($searchResult->totalMatches)->toBe(3);
|
|
expect($searchResult->filesSearched)->toBe(2);
|
|
});
|
|
|
|
it('creates empty result', function () {
|
|
$searchResult = LogSearchResult::empty('test');
|
|
|
|
expect($searchResult->searchTerm)->toBe('test');
|
|
expect($searchResult->results)->toBe([]);
|
|
expect($searchResult->totalMatches)->toBe(0);
|
|
expect($searchResult->filesSearched)->toBe(0);
|
|
expect($searchResult->isEmpty())->toBeTrue();
|
|
});
|
|
|
|
it('checks for matches', function () {
|
|
$searchResult = LogSearchResult::fromResults(
|
|
'database',
|
|
[$this->result1, $this->result2]
|
|
);
|
|
|
|
expect($searchResult->hasMatches())->toBeTrue();
|
|
expect($searchResult->isEmpty())->toBeFalse();
|
|
});
|
|
|
|
it('gets all entries', function () {
|
|
$searchResult = LogSearchResult::fromResults(
|
|
'database',
|
|
[$this->result1, $this->result2]
|
|
);
|
|
|
|
$allEntries = $searchResult->getAllEntries();
|
|
|
|
expect($allEntries)->toHaveCount(3);
|
|
expect($allEntries[0])->toBeInstanceOf(LogEntry::class);
|
|
});
|
|
|
|
it('filters by log name', function () {
|
|
$searchResult = LogSearchResult::fromResults(
|
|
'database',
|
|
[$this->result1, $this->result2]
|
|
);
|
|
|
|
$filtered = $searchResult->filterByLogName(LogName::fromString('app_error'));
|
|
|
|
expect($filtered->results)->toHaveCount(1);
|
|
expect($filtered->totalMatches)->toBe(2);
|
|
});
|
|
|
|
it('filters by minimum level', function () {
|
|
$searchResult = LogSearchResult::fromResults(
|
|
'database',
|
|
[$this->result1, $this->result2]
|
|
);
|
|
|
|
$filtered = $searchResult->filterByMinimumLevel(LogLevel::WARNING);
|
|
|
|
expect($filtered->totalMatches)->toBe(2); // ERROR and WARNING only
|
|
});
|
|
|
|
it('sorts by most recent', function () {
|
|
$searchResult = LogSearchResult::fromResults(
|
|
'database',
|
|
[$this->result1, $this->result2]
|
|
);
|
|
|
|
$sorted = $searchResult->sortByMostRecent();
|
|
|
|
expect($sorted->results)->toHaveCount(2);
|
|
// Most recent should be first
|
|
expect($sorted->results[0]->last()->message)->toContain('executed');
|
|
});
|
|
|
|
it('takes limited results', function () {
|
|
$searchResult = LogSearchResult::fromResults(
|
|
'database',
|
|
[$this->result1, $this->result2]
|
|
);
|
|
|
|
$limited = $searchResult->take(1);
|
|
|
|
expect($limited->results)->toHaveCount(1);
|
|
expect($limited->totalMatches)->toBe(2);
|
|
expect($limited->filesSearched)->toBe(1);
|
|
});
|
|
|
|
it('groups by level', function () {
|
|
$searchResult = LogSearchResult::fromResults(
|
|
'database',
|
|
[$this->result1, $this->result2]
|
|
);
|
|
|
|
$grouped = $searchResult->groupByLevel();
|
|
|
|
expect($grouped)->toBeArray();
|
|
expect($grouped)->toHaveKey('ERROR');
|
|
expect($grouped)->toHaveKey('WARNING');
|
|
expect($grouped)->toHaveKey('INFO');
|
|
expect($grouped['ERROR'])->toHaveCount(1);
|
|
expect($grouped['WARNING'])->toHaveCount(1);
|
|
expect($grouped['INFO'])->toHaveCount(1);
|
|
});
|
|
|
|
it('gets statistics', function () {
|
|
$searchResult = LogSearchResult::fromResults(
|
|
'database',
|
|
[$this->result1, $this->result2]
|
|
);
|
|
|
|
$stats = $searchResult->getStatistics();
|
|
|
|
expect($stats)->toBeArray();
|
|
expect($stats)->toHaveKey('search_term');
|
|
expect($stats)->toHaveKey('total_matches');
|
|
expect($stats)->toHaveKey('files_searched');
|
|
expect($stats)->toHaveKey('level_distribution');
|
|
expect($stats)->toHaveKey('files_with_matches');
|
|
|
|
expect($stats['search_term'])->toBe('database');
|
|
expect($stats['total_matches'])->toBe(3);
|
|
expect($stats['files_searched'])->toBe(2);
|
|
expect($stats['level_distribution'])->toBeArray();
|
|
});
|
|
|
|
it('converts to array', function () {
|
|
$searchResult = LogSearchResult::fromResults(
|
|
'database',
|
|
[$this->result1, $this->result2]
|
|
);
|
|
|
|
$array = $searchResult->toArray();
|
|
|
|
expect($array)->toBeArray();
|
|
expect($array)->toHaveKey('search_term');
|
|
expect($array)->toHaveKey('results');
|
|
expect($array)->toHaveKey('total_matches');
|
|
expect($array)->toHaveKey('files_searched');
|
|
expect($array)->toHaveKey('statistics');
|
|
|
|
expect($array['results'])->toBeArray();
|
|
expect($array['results'])->toHaveCount(2);
|
|
});
|
|
|
|
it('gets summary', function () {
|
|
$searchResult = LogSearchResult::fromResults(
|
|
'database',
|
|
[$this->result1, $this->result2]
|
|
);
|
|
|
|
$summary = $searchResult->getSummary();
|
|
|
|
expect($summary)->toBeString();
|
|
expect($summary)->toContain('database');
|
|
expect($summary)->toContain('3');
|
|
expect($summary)->toContain('2');
|
|
});
|
|
|
|
it('gets empty summary', function () {
|
|
$searchResult = LogSearchResult::empty('test');
|
|
|
|
$summary = $searchResult->getSummary();
|
|
|
|
expect($summary)->toBeString();
|
|
expect($summary)->toContain('No matches');
|
|
expect($summary)->toContain('test');
|
|
});
|
|
|
|
it('creates iterator', function () {
|
|
$searchResult = LogSearchResult::fromResults(
|
|
'database',
|
|
[$this->result1, $this->result2]
|
|
);
|
|
|
|
$iterator = $searchResult->getIterator();
|
|
|
|
expect($iterator)->toBeInstanceOf(\ArrayIterator::class);
|
|
|
|
$count = 0;
|
|
foreach ($iterator as $result) {
|
|
expect($result)->toBeInstanceOf(LogReadResult::class);
|
|
$count++;
|
|
}
|
|
|
|
expect($count)->toBe(2);
|
|
});
|
|
|
|
it('validates results array contains only LogReadResult', function () {
|
|
new LogSearchResult(
|
|
searchTerm: 'test',
|
|
results: ['invalid'],
|
|
totalMatches: 0,
|
|
filesSearched: 0
|
|
);
|
|
})->throws(\InvalidArgumentException::class, 'All results must be LogReadResult instances');
|
|
});
|