- 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.
643 lines
19 KiB
PHP
643 lines
19 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\Logging\LogLevel;
|
|
use App\Framework\Logging\LogProcessor;
|
|
use App\Framework\Logging\LogRecord;
|
|
use App\Framework\Logging\ProcessorManager;
|
|
use App\Framework\Logging\ValueObjects\LogContext;
|
|
|
|
describe('ProcessorManager', function () {
|
|
beforeEach(function () {
|
|
$this->timestamp = new DateTimeImmutable('2024-01-15 10:30:45', new DateTimeZone('Europe/Berlin'));
|
|
});
|
|
|
|
describe('constructor', function () {
|
|
it('accepts no processors', function () {
|
|
$manager = new ProcessorManager();
|
|
|
|
expect($manager instanceof ProcessorManager)->toBeTrue();
|
|
});
|
|
|
|
it('accepts single processor', function () {
|
|
$processor = new class implements LogProcessor {
|
|
public function processRecord(LogRecord $record): LogRecord
|
|
{
|
|
return $record;
|
|
}
|
|
|
|
public function getPriority(): int
|
|
{
|
|
return 10;
|
|
}
|
|
|
|
public function getName(): string
|
|
{
|
|
return 'test';
|
|
}
|
|
};
|
|
|
|
$manager = new ProcessorManager($processor);
|
|
|
|
expect($manager instanceof ProcessorManager)->toBeTrue();
|
|
});
|
|
|
|
it('accepts multiple processors', function () {
|
|
$processor1 = new class implements LogProcessor {
|
|
public function processRecord(LogRecord $record): LogRecord
|
|
{
|
|
return $record;
|
|
}
|
|
|
|
public function getPriority(): int
|
|
{
|
|
return 10;
|
|
}
|
|
|
|
public function getName(): string
|
|
{
|
|
return 'test1';
|
|
}
|
|
};
|
|
|
|
$processor2 = new class implements LogProcessor {
|
|
public function processRecord(LogRecord $record): LogRecord
|
|
{
|
|
return $record;
|
|
}
|
|
|
|
public function getPriority(): int
|
|
{
|
|
return 20;
|
|
}
|
|
|
|
public function getName(): string
|
|
{
|
|
return 'test2';
|
|
}
|
|
};
|
|
|
|
$manager = new ProcessorManager($processor1, $processor2);
|
|
|
|
expect($manager instanceof ProcessorManager)->toBeTrue();
|
|
});
|
|
});
|
|
|
|
describe('addProcessor()', function () {
|
|
it('returns new instance with added processor', function () {
|
|
$manager = new ProcessorManager();
|
|
|
|
$processor = new class implements LogProcessor {
|
|
public function processRecord(LogRecord $record): LogRecord
|
|
{
|
|
return $record;
|
|
}
|
|
|
|
public function getPriority(): int
|
|
{
|
|
return 10;
|
|
}
|
|
|
|
public function getName(): string
|
|
{
|
|
return 'new_processor';
|
|
}
|
|
};
|
|
|
|
$newManager = $manager->addProcessor($processor);
|
|
|
|
// Should return new instance (readonly pattern)
|
|
expect($newManager !== $manager)->toBeTrue();
|
|
});
|
|
|
|
it('adds processor to existing list', function () {
|
|
$processor1 = new class implements LogProcessor {
|
|
public function processRecord(LogRecord $record): LogRecord
|
|
{
|
|
return $record;
|
|
}
|
|
|
|
public function getPriority(): int
|
|
{
|
|
return 10;
|
|
}
|
|
|
|
public function getName(): string
|
|
{
|
|
return 'first';
|
|
}
|
|
};
|
|
|
|
$manager = new ProcessorManager($processor1);
|
|
|
|
$processor2 = new class implements LogProcessor {
|
|
public function processRecord(LogRecord $record): LogRecord
|
|
{
|
|
return $record;
|
|
}
|
|
|
|
public function getPriority(): int
|
|
{
|
|
return 20;
|
|
}
|
|
|
|
public function getName(): string
|
|
{
|
|
return 'second';
|
|
}
|
|
};
|
|
|
|
$newManager = $manager->addProcessor($processor2);
|
|
|
|
$list = $newManager->getProcessorList();
|
|
expect(count($list))->toBe(2);
|
|
expect(isset($list['first']))->toBeTrue();
|
|
expect(isset($list['second']))->toBeTrue();
|
|
});
|
|
|
|
it('maintains priority sorting after adding processor', function () {
|
|
$lowPriority = new class implements LogProcessor {
|
|
public function processRecord(LogRecord $record): LogRecord
|
|
{
|
|
return $record->addExtra('low', true);
|
|
}
|
|
|
|
public function getPriority(): int
|
|
{
|
|
return 5;
|
|
}
|
|
|
|
public function getName(): string
|
|
{
|
|
return 'low';
|
|
}
|
|
};
|
|
|
|
$manager = new ProcessorManager($lowPriority);
|
|
|
|
$highPriority = new class implements LogProcessor {
|
|
public function processRecord(LogRecord $record): LogRecord
|
|
{
|
|
return $record->addExtra('high', true);
|
|
}
|
|
|
|
public function getPriority(): int
|
|
{
|
|
return 20;
|
|
}
|
|
|
|
public function getName(): string
|
|
{
|
|
return 'high';
|
|
}
|
|
};
|
|
|
|
$newManager = $manager->addProcessor($highPriority);
|
|
|
|
$record = new LogRecord(
|
|
message: 'Test',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::INFO,
|
|
timestamp: $this->timestamp
|
|
);
|
|
|
|
$processed = $newManager->processRecord($record);
|
|
|
|
// High priority should run first
|
|
$extras = $processed->getExtras();
|
|
expect(isset($extras['high']))->toBeTrue();
|
|
expect(isset($extras['low']))->toBeTrue();
|
|
});
|
|
});
|
|
|
|
describe('processRecord()', function () {
|
|
it('returns original record when no processors', function () {
|
|
$manager = new ProcessorManager();
|
|
|
|
$record = new LogRecord(
|
|
message: 'Test message',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::INFO,
|
|
timestamp: $this->timestamp
|
|
);
|
|
|
|
$processed = $manager->processRecord($record);
|
|
|
|
expect($processed->getMessage())->toBe('Test message');
|
|
});
|
|
|
|
it('applies single processor to record', function () {
|
|
$processor = new class implements LogProcessor {
|
|
public function processRecord(LogRecord $record): LogRecord
|
|
{
|
|
return $record->addExtra('processed', true);
|
|
}
|
|
|
|
public function getPriority(): int
|
|
{
|
|
return 10;
|
|
}
|
|
|
|
public function getName(): string
|
|
{
|
|
return 'test_processor';
|
|
}
|
|
};
|
|
|
|
$manager = new ProcessorManager($processor);
|
|
|
|
$record = new LogRecord(
|
|
message: 'Test',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::INFO,
|
|
timestamp: $this->timestamp
|
|
);
|
|
|
|
$processed = $manager->processRecord($record);
|
|
|
|
$extras = $processed->getExtras();
|
|
expect(isset($extras['processed']))->toBeTrue();
|
|
expect($extras['processed'])->toBe(true);
|
|
});
|
|
|
|
it('applies multiple processors in priority order', function () {
|
|
$processor1 = new class implements LogProcessor {
|
|
public function processRecord(LogRecord $record): LogRecord
|
|
{
|
|
return $record->addExtra('order', 'first');
|
|
}
|
|
|
|
public function getPriority(): int
|
|
{
|
|
return 20; // Higher priority = runs first
|
|
}
|
|
|
|
public function getName(): string
|
|
{
|
|
return 'high_priority';
|
|
}
|
|
};
|
|
|
|
$processor2 = new class implements LogProcessor {
|
|
public function processRecord(LogRecord $record): LogRecord
|
|
{
|
|
$extras = $record->getExtras();
|
|
// Should have 'order' from first processor
|
|
return $record->addExtra('second_saw_first', isset($extras['order']));
|
|
}
|
|
|
|
public function getPriority(): int
|
|
{
|
|
return 10; // Lower priority = runs second
|
|
}
|
|
|
|
public function getName(): string
|
|
{
|
|
return 'low_priority';
|
|
}
|
|
};
|
|
|
|
$manager = new ProcessorManager($processor1, $processor2);
|
|
|
|
$record = new LogRecord(
|
|
message: 'Test',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::INFO,
|
|
timestamp: $this->timestamp
|
|
);
|
|
|
|
$processed = $manager->processRecord($record);
|
|
|
|
$extras = $processed->getExtras();
|
|
expect($extras['order'])->toBe('first');
|
|
expect($extras['second_saw_first'])->toBeTrue();
|
|
});
|
|
|
|
it('chains processor transformations', function () {
|
|
$addPrefix = new class implements LogProcessor {
|
|
public function processRecord(LogRecord $record): LogRecord
|
|
{
|
|
return $record->withMessage('[PREFIX] ' . $record->getMessage());
|
|
}
|
|
|
|
public function getPriority(): int
|
|
{
|
|
return 20;
|
|
}
|
|
|
|
public function getName(): string
|
|
{
|
|
return 'prefix';
|
|
}
|
|
};
|
|
|
|
$addSuffix = new class implements LogProcessor {
|
|
public function processRecord(LogRecord $record): LogRecord
|
|
{
|
|
return $record->withMessage($record->getMessage() . ' [SUFFIX]');
|
|
}
|
|
|
|
public function getPriority(): int
|
|
{
|
|
return 10;
|
|
}
|
|
|
|
public function getName(): string
|
|
{
|
|
return 'suffix';
|
|
}
|
|
};
|
|
|
|
$manager = new ProcessorManager($addPrefix, $addSuffix);
|
|
|
|
$record = new LogRecord(
|
|
message: 'Message',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::INFO,
|
|
timestamp: $this->timestamp
|
|
);
|
|
|
|
$processed = $manager->processRecord($record);
|
|
|
|
expect($processed->getMessage())->toBe('[PREFIX] Message [SUFFIX]');
|
|
});
|
|
});
|
|
|
|
describe('getProcessorList()', function () {
|
|
it('returns empty array when no processors', function () {
|
|
$manager = new ProcessorManager();
|
|
|
|
$list = $manager->getProcessorList();
|
|
|
|
expect($list)->toBeArray();
|
|
expect($list)->toBeEmpty();
|
|
});
|
|
|
|
it('returns array with processor names as keys', function () {
|
|
$processor1 = new class implements LogProcessor {
|
|
public function processRecord(LogRecord $record): LogRecord
|
|
{
|
|
return $record;
|
|
}
|
|
|
|
public function getPriority(): int
|
|
{
|
|
return 10;
|
|
}
|
|
|
|
public function getName(): string
|
|
{
|
|
return 'first_processor';
|
|
}
|
|
};
|
|
|
|
$processor2 = new class implements LogProcessor {
|
|
public function processRecord(LogRecord $record): LogRecord
|
|
{
|
|
return $record;
|
|
}
|
|
|
|
public function getPriority(): int
|
|
{
|
|
return 20;
|
|
}
|
|
|
|
public function getName(): string
|
|
{
|
|
return 'second_processor';
|
|
}
|
|
};
|
|
|
|
$manager = new ProcessorManager($processor1, $processor2);
|
|
|
|
$list = $manager->getProcessorList();
|
|
|
|
expect(array_key_exists('first_processor', $list))->toBeTrue();
|
|
expect(array_key_exists('second_processor', $list))->toBeTrue();
|
|
});
|
|
|
|
it('returns priorities as values', function () {
|
|
$processor = new class implements LogProcessor {
|
|
public function processRecord(LogRecord $record): LogRecord
|
|
{
|
|
return $record;
|
|
}
|
|
|
|
public function getPriority(): int
|
|
{
|
|
return 42;
|
|
}
|
|
|
|
public function getName(): string
|
|
{
|
|
return 'test';
|
|
}
|
|
};
|
|
|
|
$manager = new ProcessorManager($processor);
|
|
|
|
$list = $manager->getProcessorList();
|
|
|
|
expect($list['test'])->toBe(42);
|
|
});
|
|
});
|
|
|
|
describe('hasProcessor()', function () {
|
|
it('returns false when no processors', function () {
|
|
$manager = new ProcessorManager();
|
|
|
|
expect($manager->hasProcessor('any_name'))->toBeFalse();
|
|
});
|
|
|
|
it('returns true when processor exists', function () {
|
|
$processor = new class implements LogProcessor {
|
|
public function processRecord(LogRecord $record): LogRecord
|
|
{
|
|
return $record;
|
|
}
|
|
|
|
public function getPriority(): int
|
|
{
|
|
return 10;
|
|
}
|
|
|
|
public function getName(): string
|
|
{
|
|
return 'existing_processor';
|
|
}
|
|
};
|
|
|
|
$manager = new ProcessorManager($processor);
|
|
|
|
expect($manager->hasProcessor('existing_processor'))->toBeTrue();
|
|
});
|
|
|
|
it('returns false when processor does not exist', function () {
|
|
$processor = new class implements LogProcessor {
|
|
public function processRecord(LogRecord $record): LogRecord
|
|
{
|
|
return $record;
|
|
}
|
|
|
|
public function getPriority(): int
|
|
{
|
|
return 10;
|
|
}
|
|
|
|
public function getName(): string
|
|
{
|
|
return 'existing';
|
|
}
|
|
};
|
|
|
|
$manager = new ProcessorManager($processor);
|
|
|
|
expect($manager->hasProcessor('nonexistent'))->toBeFalse();
|
|
});
|
|
});
|
|
|
|
describe('getProcessor()', function () {
|
|
it('returns null when no processors', function () {
|
|
$manager = new ProcessorManager();
|
|
|
|
expect($manager->getProcessor('any_name'))->toBeNull();
|
|
});
|
|
|
|
it('returns processor when exists', function () {
|
|
$processor = new class implements LogProcessor {
|
|
public function processRecord(LogRecord $record): LogRecord
|
|
{
|
|
return $record;
|
|
}
|
|
|
|
public function getPriority(): int
|
|
{
|
|
return 10;
|
|
}
|
|
|
|
public function getName(): string
|
|
{
|
|
return 'findme';
|
|
}
|
|
};
|
|
|
|
$manager = new ProcessorManager($processor);
|
|
|
|
$found = $manager->getProcessor('findme');
|
|
|
|
expect($found !== null)->toBeTrue();
|
|
expect($found->getName())->toBe('findme');
|
|
});
|
|
|
|
it('returns null when processor does not exist', function () {
|
|
$processor = new class implements LogProcessor {
|
|
public function processRecord(LogRecord $record): LogRecord
|
|
{
|
|
return $record;
|
|
}
|
|
|
|
public function getPriority(): int
|
|
{
|
|
return 10;
|
|
}
|
|
|
|
public function getName(): string
|
|
{
|
|
return 'existing';
|
|
}
|
|
};
|
|
|
|
$manager = new ProcessorManager($processor);
|
|
|
|
expect($manager->getProcessor('nonexistent'))->toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('readonly behavior', function () {
|
|
it('is a readonly class', function () {
|
|
$reflection = new ReflectionClass(ProcessorManager::class);
|
|
|
|
expect($reflection->isReadOnly())->toBeTrue();
|
|
});
|
|
});
|
|
|
|
describe('priority sorting', function () {
|
|
it('sorts processors by priority descending', function () {
|
|
$lowPriority = new class implements LogProcessor {
|
|
public function processRecord(LogRecord $record): LogRecord
|
|
{
|
|
return $record->addExtra('low', true);
|
|
}
|
|
|
|
public function getPriority(): int
|
|
{
|
|
return 5;
|
|
}
|
|
|
|
public function getName(): string
|
|
{
|
|
return 'low';
|
|
}
|
|
};
|
|
|
|
$mediumPriority = new class implements LogProcessor {
|
|
public function processRecord(LogRecord $record): LogRecord
|
|
{
|
|
return $record->addExtra('medium', true);
|
|
}
|
|
|
|
public function getPriority(): int
|
|
{
|
|
return 15;
|
|
}
|
|
|
|
public function getName(): string
|
|
{
|
|
return 'medium';
|
|
}
|
|
};
|
|
|
|
$highPriority = new class implements LogProcessor {
|
|
public function processRecord(LogRecord $record): LogRecord
|
|
{
|
|
return $record->addExtra('high', true);
|
|
}
|
|
|
|
public function getPriority(): int
|
|
{
|
|
return 25;
|
|
}
|
|
|
|
public function getName(): string
|
|
{
|
|
return 'high';
|
|
}
|
|
};
|
|
|
|
// Add in random order
|
|
$manager = new ProcessorManager($lowPriority, $highPriority, $mediumPriority);
|
|
|
|
$record = new LogRecord(
|
|
message: 'Test',
|
|
context: LogContext::empty(),
|
|
level: LogLevel::INFO,
|
|
timestamp: $this->timestamp
|
|
);
|
|
|
|
$processed = $manager->processRecord($record);
|
|
|
|
// All extras should be present (processors were executed)
|
|
$extras = $processed->getExtras();
|
|
expect(isset($extras['high']))->toBeTrue();
|
|
expect(isset($extras['medium']))->toBeTrue();
|
|
expect(isset($extras['low']))->toBeTrue();
|
|
|
|
// Verify they all have value true
|
|
expect($extras['high'] === true)->toBeTrue();
|
|
expect($extras['medium'] === true)->toBeTrue();
|
|
expect($extras['low'] === true)->toBeTrue();
|
|
});
|
|
});
|
|
});
|