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,139 @@
<?php
declare(strict_types=1);
use App\Application\LiveComponents\Dashboard\FailedJobsState;
describe('FailedJobsState', function () {
it('creates empty state with default values', function () {
$state = FailedJobsState::empty();
expect($state->totalFailedJobs)->toBe(0);
expect($state->failedJobs)->toBe([]);
expect($state->statistics)->toBe([]);
expect($state->lastUpdated)->toBeString();
});
it('creates state from array', function () {
$data = [
'totalFailedJobs' => 15,
'failedJobs' => [
[
'id' => 'job-1',
'queue' => 'default',
'error' => 'Connection timeout',
],
],
'statistics' => [
'total_retries' => 42,
'avg_failure_time' => 123.45,
],
'lastUpdated' => '2024-01-15 12:00:00',
];
$state = FailedJobsState::fromArray($data);
expect($state->totalFailedJobs)->toBe(15);
expect($state->failedJobs)->toHaveCount(1);
expect($state->statistics)->toHaveKey('total_retries');
});
it('converts state to array', function () {
$failedJobs = [
[
'id' => 'job-1',
'queue' => 'default',
'job_type' => 'EmailJob',
'error' => 'SMTP connection failed',
'failed_at' => '2024-01-15 12:00:00',
'attempts' => 3,
],
];
$statistics = [
'total_retries' => 10,
'successful_retries' => 8,
];
$state = new FailedJobsState(
totalFailedJobs: 1,
failedJobs: $failedJobs,
statistics: $statistics,
lastUpdated: '2024-01-15 12:00:00'
);
$array = $state->toArray();
expect($array)->toHaveKey('totalFailedJobs');
expect($array)->toHaveKey('failedJobs');
expect($array)->toHaveKey('statistics');
expect($array['failedJobs'])->toHaveCount(1);
});
it('creates new state with updated failed jobs', function () {
$state = FailedJobsState::empty();
$failedJobs = [
[
'id' => 'job-1',
'error' => 'Database connection lost',
'attempts' => 5,
],
];
$statistics = [
'most_common_error' => 'Database connection lost',
'total_failures_today' => 10,
];
$updatedState = $state->withFailedJobs(
totalFailedJobs: 1,
failedJobs: $failedJobs,
statistics: $statistics
);
// Original unchanged
expect($state->totalFailedJobs)->toBe(0);
expect($state->failedJobs)->toBe([]);
// New state updated
expect($updatedState->totalFailedJobs)->toBe(1);
expect($updatedState->failedJobs)->toHaveCount(1);
expect($updatedState->statistics)->toHaveKey('most_common_error');
expect($updatedState->lastUpdated)->not->toBe($state->lastUpdated);
});
it('is immutable', function () {
$state = new FailedJobsState(
totalFailedJobs: 5,
failedJobs: [],
statistics: [],
lastUpdated: '2024-01-15 12:00:00'
);
$newState = $state->withFailedJobs(
totalFailedJobs: 10,
failedJobs: [],
statistics: []
);
// Original unchanged
expect($state->totalFailedJobs)->toBe(5);
// New instance
expect($newState)->not->toBe($state);
expect($newState->totalFailedJobs)->toBe(10);
});
it('handles empty failed jobs list', function () {
$state = FailedJobsState::empty()->withFailedJobs(
totalFailedJobs: 0,
failedJobs: [],
statistics: []
);
expect($state->failedJobs)->toBe([]);
expect($state->totalFailedJobs)->toBe(0);
expect($state->statistics)->toBe([]);
});
});

View File

@@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
use App\Application\LiveComponents\Dashboard\QueueStatsState;
describe('QueueStatsState', function () {
it('creates empty state with default values', function () {
$state = QueueStatsState::empty();
expect($state->currentQueueSize)->toBe(0);
expect($state->totalJobs)->toBe(0);
expect($state->successfulJobs)->toBe(0);
expect($state->failedJobs)->toBe(0);
expect($state->successRate)->toBe(0.0);
expect($state->avgExecutionTimeMs)->toBe(0.0);
expect($state->lastUpdated)->toBeString();
});
it('creates state from array', function () {
$data = [
'currentQueueSize' => 42,
'totalJobs' => 1000,
'successfulJobs' => 950,
'failedJobs' => 50,
'successRate' => 95.0,
'avgExecutionTimeMs' => 123.45,
'lastUpdated' => '2024-01-15 12:00:00',
];
$state = QueueStatsState::fromArray($data);
expect($state->currentQueueSize)->toBe(42);
expect($state->totalJobs)->toBe(1000);
expect($state->successfulJobs)->toBe(950);
expect($state->failedJobs)->toBe(50);
expect($state->successRate)->toBe(95.0);
expect($state->avgExecutionTimeMs)->toBe(123.45);
expect($state->lastUpdated)->toBe('2024-01-15 12:00:00');
});
it('converts state to array', function () {
$state = new QueueStatsState(
currentQueueSize: 10,
totalJobs: 100,
successfulJobs: 90,
failedJobs: 10,
successRate: 90.0,
avgExecutionTimeMs: 50.5,
lastUpdated: '2024-01-15 12:00:00'
);
$array = $state->toArray();
expect($array)->toBe([
'currentQueueSize' => 10,
'totalJobs' => 100,
'successfulJobs' => 90,
'failedJobs' => 10,
'successRate' => 90.0,
'avgExecutionTimeMs' => 50.5,
'lastUpdated' => '2024-01-15 12:00:00',
]);
});
it('creates new state with updated stats', function () {
$state = QueueStatsState::empty();
$updatedState = $state->withStats(
currentQueueSize: 5,
totalJobs: 50,
successfulJobs: 45,
failedJobs: 5,
successRate: 90.0,
avgExecutionTimeMs: 75.0
);
// Original state unchanged (immutable)
expect($state->currentQueueSize)->toBe(0);
expect($state->totalJobs)->toBe(0);
// New state has updated values
expect($updatedState->currentQueueSize)->toBe(5);
expect($updatedState->totalJobs)->toBe(50);
expect($updatedState->successfulJobs)->toBe(45);
expect($updatedState->failedJobs)->toBe(5);
expect($updatedState->successRate)->toBe(90.0);
expect($updatedState->avgExecutionTimeMs)->toBe(75.0);
expect($updatedState->lastUpdated)->not->toBe($state->lastUpdated);
});
it('handles zero division in success rate gracefully', function () {
$state = new QueueStatsState(
currentQueueSize: 0,
totalJobs: 0,
successfulJobs: 0,
failedJobs: 0,
successRate: 0.0,
avgExecutionTimeMs: 0.0,
lastUpdated: '2024-01-15 12:00:00'
);
expect($state->successRate)->toBe(0.0);
});
it('is immutable', function () {
$state = new QueueStatsState(
currentQueueSize: 10,
totalJobs: 100,
successfulJobs: 90,
failedJobs: 10,
successRate: 90.0,
avgExecutionTimeMs: 50.0,
lastUpdated: '2024-01-15 12:00:00'
);
$newState = $state->withStats(
currentQueueSize: 20,
totalJobs: 200,
successfulJobs: 180,
failedJobs: 20,
successRate: 90.0,
avgExecutionTimeMs: 60.0
);
// Original unchanged
expect($state->currentQueueSize)->toBe(10);
expect($state->totalJobs)->toBe(100);
// New instance created
expect($newState)->not->toBe($state);
expect($newState->currentQueueSize)->toBe(20);
expect($newState->totalJobs)->toBe(200);
});
});

View File

@@ -0,0 +1,172 @@
<?php
declare(strict_types=1);
use App\Application\LiveComponents\Dashboard\SchedulerState;
describe('SchedulerState', function () {
it('creates empty state with default values', function () {
$state = SchedulerState::empty();
expect($state->totalScheduledTasks)->toBe(0);
expect($state->dueTasks)->toBe(0);
expect($state->upcomingTasks)->toBe([]);
expect($state->nextExecution)->toBeNull();
expect($state->statistics)->toBe([]);
expect($state->lastUpdated)->toBeString();
});
it('creates state from array', function () {
$data = [
'totalScheduledTasks' => 10,
'dueTasks' => 2,
'upcomingTasks' => [
[
'id' => 'task-1',
'schedule_type' => 'cron',
'next_run' => '2024-01-15 13:00:00',
],
],
'nextExecution' => '2024-01-15 13:00:00',
'statistics' => [
'total_executions_today' => 50,
],
'lastUpdated' => '2024-01-15 12:00:00',
];
$state = SchedulerState::fromArray($data);
expect($state->totalScheduledTasks)->toBe(10);
expect($state->dueTasks)->toBe(2);
expect($state->upcomingTasks)->toHaveCount(1);
expect($state->nextExecution)->toBe('2024-01-15 13:00:00');
});
it('converts state to array', function () {
$upcomingTasks = [
[
'id' => 'task-1',
'schedule_type' => 'interval',
'next_run' => '2024-01-15 13:00:00',
'next_run_relative' => 'in 1 hour',
'is_due' => false,
],
];
$statistics = [
'successful_executions' => 45,
'failed_executions' => 5,
];
$state = new SchedulerState(
totalScheduledTasks: 5,
dueTasks: 1,
upcomingTasks: $upcomingTasks,
nextExecution: '2024-01-15 13:00:00',
statistics: $statistics,
lastUpdated: '2024-01-15 12:00:00'
);
$array = $state->toArray();
expect($array)->toHaveKey('totalScheduledTasks');
expect($array)->toHaveKey('dueTasks');
expect($array)->toHaveKey('upcomingTasks');
expect($array)->toHaveKey('nextExecution');
expect($array)->toHaveKey('statistics');
expect($array['upcomingTasks'])->toHaveCount(1);
});
it('creates new state with updated scheduler data', function () {
$state = SchedulerState::empty();
$upcomingTasks = [
[
'id' => 'backup-task',
'schedule_type' => 'cron',
'next_run' => '2024-01-15 02:00:00',
'is_due' => true,
],
];
$statistics = [
'tasks_executed_today' => 20,
'average_execution_time' => 1.5,
];
$updatedState = $state->withSchedulerData(
totalScheduledTasks: 3,
dueTasks: 1,
upcomingTasks: $upcomingTasks,
nextExecution: '2024-01-15 02:00:00',
statistics: $statistics
);
// Original unchanged
expect($state->totalScheduledTasks)->toBe(0);
expect($state->upcomingTasks)->toBe([]);
expect($state->nextExecution)->toBeNull();
// New state updated
expect($updatedState->totalScheduledTasks)->toBe(3);
expect($updatedState->dueTasks)->toBe(1);
expect($updatedState->upcomingTasks)->toHaveCount(1);
expect($updatedState->nextExecution)->toBe('2024-01-15 02:00:00');
expect($updatedState->statistics)->toHaveKey('tasks_executed_today');
expect($updatedState->lastUpdated)->not->toBe($state->lastUpdated);
});
it('is immutable', function () {
$state = new SchedulerState(
totalScheduledTasks: 10,
dueTasks: 2,
upcomingTasks: [],
nextExecution: '2024-01-15 12:00:00',
statistics: [],
lastUpdated: '2024-01-15 11:00:00'
);
$newState = $state->withSchedulerData(
totalScheduledTasks: 15,
dueTasks: 3,
upcomingTasks: [],
nextExecution: '2024-01-15 13:00:00',
statistics: []
);
// Original unchanged
expect($state->totalScheduledTasks)->toBe(10);
expect($state->dueTasks)->toBe(2);
// New instance
expect($newState)->not->toBe($state);
expect($newState->totalScheduledTasks)->toBe(15);
expect($newState->dueTasks)->toBe(3);
});
it('handles null next execution', function () {
$state = SchedulerState::empty()->withSchedulerData(
totalScheduledTasks: 0,
dueTasks: 0,
upcomingTasks: [],
nextExecution: null,
statistics: []
);
expect($state->nextExecution)->toBeNull();
expect($state->totalScheduledTasks)->toBe(0);
});
it('handles empty upcoming tasks', function () {
$state = SchedulerState::empty()->withSchedulerData(
totalScheduledTasks: 0,
dueTasks: 0,
upcomingTasks: [],
nextExecution: null,
statistics: []
);
expect($state->upcomingTasks)->toBe([]);
expect($state->statistics)->toBe([]);
});
});

View File

@@ -0,0 +1,145 @@
<?php
declare(strict_types=1);
use App\Application\LiveComponents\Dashboard\WorkerHealthState;
describe('WorkerHealthState', function () {
it('creates empty state with default values', function () {
$state = WorkerHealthState::empty();
expect($state->activeWorkers)->toBe(0);
expect($state->totalWorkers)->toBe(0);
expect($state->jobsInProgress)->toBe(0);
expect($state->workerDetails)->toBe([]);
expect($state->lastUpdated)->toBeString();
});
it('creates state from array', function () {
$data = [
'activeWorkers' => 3,
'totalWorkers' => 5,
'jobsInProgress' => 7,
'workerDetails' => [
[
'id' => 'worker-1',
'hostname' => 'server-01',
'healthy' => true,
],
],
'lastUpdated' => '2024-01-15 12:00:00',
];
$state = WorkerHealthState::fromArray($data);
expect($state->activeWorkers)->toBe(3);
expect($state->totalWorkers)->toBe(5);
expect($state->jobsInProgress)->toBe(7);
expect($state->workerDetails)->toHaveCount(1);
expect($state->workerDetails[0]['id'])->toBe('worker-1');
});
it('converts state to array', function () {
$workerDetails = [
[
'id' => 'worker-1',
'hostname' => 'server-01',
'process_id' => 12345,
'healthy' => true,
'jobs' => 2,
'max_jobs' => 10,
],
];
$state = new WorkerHealthState(
activeWorkers: 1,
totalWorkers: 3,
jobsInProgress: 2,
workerDetails: $workerDetails,
lastUpdated: '2024-01-15 12:00:00'
);
$array = $state->toArray();
expect($array)->toHaveKey('activeWorkers');
expect($array)->toHaveKey('totalWorkers');
expect($array)->toHaveKey('jobsInProgress');
expect($array)->toHaveKey('workerDetails');
expect($array['workerDetails'])->toHaveCount(1);
});
it('creates new state with updated worker health', function () {
$state = WorkerHealthState::empty();
$workerDetails = [
[
'id' => 'worker-1',
'hostname' => 'server-01',
'healthy' => true,
'jobs' => 5,
],
[
'id' => 'worker-2',
'hostname' => 'server-02',
'healthy' => false,
'jobs' => 0,
],
];
$updatedState = $state->withWorkerHealth(
activeWorkers: 2,
totalWorkers: 2,
jobsInProgress: 5,
workerDetails: $workerDetails
);
// Original unchanged
expect($state->activeWorkers)->toBe(0);
expect($state->workerDetails)->toBe([]);
// New state updated
expect($updatedState->activeWorkers)->toBe(2);
expect($updatedState->totalWorkers)->toBe(2);
expect($updatedState->jobsInProgress)->toBe(5);
expect($updatedState->workerDetails)->toHaveCount(2);
expect($updatedState->lastUpdated)->not->toBe($state->lastUpdated);
});
it('is immutable', function () {
$state = new WorkerHealthState(
activeWorkers: 5,
totalWorkers: 10,
jobsInProgress: 15,
workerDetails: [],
lastUpdated: '2024-01-15 12:00:00'
);
$newState = $state->withWorkerHealth(
activeWorkers: 6,
totalWorkers: 10,
jobsInProgress: 20,
workerDetails: []
);
// Original unchanged
expect($state->activeWorkers)->toBe(5);
expect($state->jobsInProgress)->toBe(15);
// New instance
expect($newState)->not->toBe($state);
expect($newState->activeWorkers)->toBe(6);
expect($newState->jobsInProgress)->toBe(20);
});
it('handles empty worker details array', function () {
$state = WorkerHealthState::empty()->withWorkerHealth(
activeWorkers: 0,
totalWorkers: 0,
jobsInProgress: 0,
workerDetails: []
);
expect($state->workerDetails)->toBe([]);
expect($state->activeWorkers)->toBe(0);
});
});