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.
This commit is contained in:
2025-10-25 19:18:37 +02:00
parent caa85db796
commit fc3d7e6357
83016 changed files with 378904 additions and 20919 deletions

View File

@@ -0,0 +1,235 @@
<?php
declare(strict_types=1);
use App\Application\LiveComponents\Dashboard\WorkerHealthComponent;
use App\Application\LiveComponents\Dashboard\WorkerHealthState;
use App\Framework\LiveComponents\ValueObjects\ComponentId;
use App\Framework\Queue\Services\WorkerRegistry;
use App\Framework\Queue\Entities\Worker;
use App\Framework\Queue\ValueObjects\WorkerId;
use App\Framework\Core\ValueObjects\Timestamp;
describe('WorkerHealthComponent', function () {
beforeEach(function () {
$this->workerRegistry = Mockery::mock(WorkerRegistry::class);
$this->componentId = ComponentId::create('worker-health', 'test');
$this->initialState = WorkerHealthState::empty();
});
afterEach(function () {
Mockery::close();
});
it('polls and updates state with worker health data', function () {
$now = Timestamp::now();
$oneMinuteAgo = $now->sub(Duration::fromSeconds(60));
$worker1 = new Worker(
id: WorkerId::generate(),
hostname: 'server-01',
processId: 12345,
startedAt: $oneMinuteAgo,
lastHeartbeat: $now,
status: 'active',
currentJobs: 2,
maxJobs: 10,
memoryUsageMb: 128.5,
cpuUsage: 45.2
);
$worker2 = new Worker(
id: WorkerId::generate(),
hostname: 'server-02',
processId: 67890,
startedAt: $oneMinuteAgo,
lastHeartbeat: $now->sub(Duration::fromMinutes(5)), // Unhealthy - old heartbeat
status: 'active',
currentJobs: 0,
maxJobs: 10,
memoryUsageMb: 64.0,
cpuUsage: 98.5 // Unhealthy - high CPU
);
$this->workerRegistry->shouldReceive('findActiveWorkers')
->once()
->andReturn([$worker1, $worker2]);
$component = new WorkerHealthComponent(
id: $this->componentId,
state: $this->initialState,
workerRegistry: $this->workerRegistry
);
$newState = $component->poll();
expect($newState)->toBeInstanceOf(WorkerHealthState::class);
expect($newState->activeWorkers)->toBe(2);
expect($newState->totalWorkers)->toBe(2);
expect($newState->jobsInProgress)->toBe(2); // Only worker1 has jobs
expect($newState->workerDetails)->toHaveCount(2);
// Worker 1 should be healthy
expect($newState->workerDetails[0]['healthy'])->toBeTrue();
expect($newState->workerDetails[0]['hostname'])->toBe('server-01');
// Worker 2 should be unhealthy
expect($newState->workerDetails[1]['healthy'])->toBeFalse();
});
it('identifies healthy workers correctly', function () {
$now = Timestamp::now();
$healthyWorker = new Worker(
id: WorkerId::generate(),
hostname: 'healthy-server',
processId: 11111,
startedAt: $now->sub(Duration::fromMinutes(5)),
lastHeartbeat: $now->sub(Duration::fromSeconds(30)), // Recent heartbeat
status: 'active',
currentJobs: 5,
maxJobs: 10,
memoryUsageMb: 100.0,
cpuUsage: 50.0 // Normal CPU
);
$this->workerRegistry->shouldReceive('findActiveWorkers')
->once()
->andReturn([$healthyWorker]);
$component = new WorkerHealthComponent(
id: $this->componentId,
state: $this->initialState,
workerRegistry: $this->workerRegistry
);
$newState = $component->poll();
expect($newState->workerDetails[0]['healthy'])->toBeTrue();
});
it('identifies unhealthy workers by stale heartbeat', function () {
$now = Timestamp::now();
$staleWorker = new Worker(
id: WorkerId::generate(),
hostname: 'stale-server',
processId: 22222,
startedAt: $now->sub(Duration::fromMinutes(10)),
lastHeartbeat: $now->sub(Duration::fromMinutes(3)), // Stale heartbeat
status: 'active',
currentJobs: 2,
maxJobs: 10,
memoryUsageMb: 80.0,
cpuUsage: 30.0
);
$this->workerRegistry->shouldReceive('findActiveWorkers')
->once()
->andReturn([$staleWorker]);
$component = new WorkerHealthComponent(
id: $this->componentId,
state: $this->initialState,
workerRegistry: $this->workerRegistry
);
$newState = $component->poll();
expect($newState->workerDetails[0]['healthy'])->toBeFalse();
});
it('identifies unhealthy workers by high CPU', function () {
$now = Timestamp::now();
$highCpuWorker = new Worker(
id: WorkerId::generate(),
hostname: 'high-cpu-server',
processId: 33333,
startedAt: $now->sub(Duration::fromMinutes(5)),
lastHeartbeat: $now, // Recent heartbeat
status: 'active',
currentJobs: 8,
maxJobs: 10,
memoryUsageMb: 200.0,
cpuUsage: 96.5 // High CPU
);
$this->workerRegistry->shouldReceive('findActiveWorkers')
->once()
->andReturn([$highCpuWorker]);
$component = new WorkerHealthComponent(
id: $this->componentId,
state: $this->initialState,
workerRegistry: $this->workerRegistry
);
$newState = $component->poll();
expect($newState->workerDetails[0]['healthy'])->toBeFalse();
});
it('has correct poll interval', function () {
$component = new WorkerHealthComponent(
id: $this->componentId,
state: $this->initialState,
workerRegistry: $this->workerRegistry
);
expect($component->getPollInterval())->toBe(5000);
});
it('returns correct render data', function () {
$workerDetails = [
[
'id' => 'worker-1',
'hostname' => 'server-01',
'healthy' => true,
'jobs' => 5,
],
];
$state = new WorkerHealthState(
activeWorkers: 1,
totalWorkers: 1,
jobsInProgress: 5,
workerDetails: $workerDetails,
lastUpdated: '2024-01-15 12:00:00'
);
$component = new WorkerHealthComponent(
id: $this->componentId,
state: $state,
workerRegistry: $this->workerRegistry
);
$renderData = $component->getRenderData();
expect($renderData->templatePath)->toBe('livecomponent-worker-health');
expect($renderData->data)->toHaveKey('componentId');
expect($renderData->data)->toHaveKey('pollInterval');
expect($renderData->data['pollInterval'])->toBe(5000);
expect($renderData->data['activeWorkers'])->toBe(1);
expect($renderData->data['jobsInProgress'])->toBe(5);
});
it('handles no active workers', function () {
$this->workerRegistry->shouldReceive('findActiveWorkers')
->once()
->andReturn([]);
$component = new WorkerHealthComponent(
id: $this->componentId,
state: $this->initialState,
workerRegistry: $this->workerRegistry
);
$newState = $component->poll();
expect($newState->activeWorkers)->toBe(0);
expect($newState->totalWorkers)->toBe(0);
expect($newState->jobsInProgress)->toBe(0);
expect($newState->workerDetails)->toBe([]);
});
});