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

@@ -2,13 +2,11 @@
declare(strict_types=1);
use App\Framework\Queue\Exceptions\InvalidDeadLetterQueueNameException;
use App\Framework\Queue\ValueObjects\DeadLetterQueueName;
use App\Framework\Queue\ValueObjects\QueueName;
use App\Framework\Queue\ValueObjects\JobId;
use App\Framework\Queue\ValueObjects\JobPayload;
use App\Framework\Queue\ValueObjects\QueuePriority;
use App\Framework\Queue\ValueObjects\FailureReason;
use App\Framework\Queue\Exceptions\InvalidDeadLetterQueueNameException;
use App\Framework\Queue\ValueObjects\QueueName;
describe('DeadLetterQueueName Value Object', function () {
@@ -21,7 +19,7 @@ describe('DeadLetterQueueName Value Object', function () {
'user.registration.failed',
'queue_123_failed',
'a', // minimum length
str_repeat('a', 100) // maximum length
str_repeat('a', 100), // maximum length
];
foreach ($validNames as $name) {
@@ -57,11 +55,11 @@ describe('DeadLetterQueueName Value Object', function () {
'queue?invalid',
'queue/invalid',
'queue~invalid',
'queue`invalid'
'queue`invalid',
];
foreach ($invalidNames as $name) {
expect(fn() => DeadLetterQueueName::fromString($name))
expect(fn () => DeadLetterQueueName::fromString($name))
->toThrow(InvalidDeadLetterQueueNameException::class);
}
});
@@ -166,53 +164,66 @@ describe('Dead Letter Queue Mock Implementation', function () {
beforeEach(function () {
// Create a mock dead letter queue for testing
$this->mockDlq = new class {
$this->mockDlq = new class () {
private array $jobs = [];
private array $stats = [];
public function addFailedJob(array $jobData): void {
public function addFailedJob(array $jobData): void
{
$this->jobs[] = $jobData;
}
public function getJobs(DeadLetterQueueName $queueName, int $limit = 100): array {
public function getJobs(DeadLetterQueueName $queueName, int $limit = 100): array
{
return array_slice(
array_filter($this->jobs, fn($job) => $job['dlq_name'] === $queueName->toString()),
array_filter($this->jobs, fn ($job) => $job['dlq_name'] === $queueName->toString()),
0,
$limit
);
}
public function retryJob(string $jobId): bool {
public function retryJob(string $jobId): bool
{
foreach ($this->jobs as $index => $job) {
if ($job['id'] === $jobId) {
unset($this->jobs[$index]);
return true;
}
}
return false;
}
public function deleteJob(string $jobId): bool {
public function deleteJob(string $jobId): bool
{
foreach ($this->jobs as $index => $job) {
if ($job['id'] === $jobId) {
unset($this->jobs[$index]);
return true;
}
}
return false;
}
public function clearQueue(DeadLetterQueueName $queueName): int {
public function clearQueue(DeadLetterQueueName $queueName): int
{
$initialCount = count($this->jobs);
$this->jobs = array_filter(
$this->jobs,
fn($job) => $job['dlq_name'] !== $queueName->toString()
fn ($job) => $job['dlq_name'] !== $queueName->toString()
);
return $initialCount - count($this->jobs);
}
public function getQueueStats(DeadLetterQueueName $queueName): array {
public function getQueueStats(DeadLetterQueueName $queueName): array
{
$jobs = $this->getJobs($queueName);
return [
'queue_name' => $queueName->toString(),
'total_jobs' => count($jobs),
@@ -221,13 +232,15 @@ describe('Dead Letter Queue Mock Implementation', function () {
'avg_retry_count' => count($jobs) > 0 ? array_sum(array_column($jobs, 'retry_count')) / count($jobs) : 0,
'max_retry_count' => count($jobs) > 0 ? max(array_column($jobs, 'retry_count')) : 0,
'oldest_job' => count($jobs) > 0 ? min(array_column($jobs, 'failed_at')) : null,
'newest_job' => count($jobs) > 0 ? max(array_column($jobs, 'failed_at')) : null
'newest_job' => count($jobs) > 0 ? max(array_column($jobs, 'failed_at')) : null,
];
}
public function getAvailableQueues(): array {
public function getAvailableQueues(): array
{
$queueNames = array_unique(array_column($this->jobs, 'dlq_name'));
return array_map(fn($name) => DeadLetterQueueName::fromString($name), $queueNames);
return array_map(fn ($name) => DeadLetterQueueName::fromString($name), $queueNames);
}
};
});
@@ -248,7 +261,7 @@ describe('Dead Letter Queue Mock Implementation', function () {
'stack_trace' => 'Stack trace here...',
'failed_attempts' => 3,
'failed_at' => date('Y-m-d H:i:s'),
'retry_count' => 0
'retry_count' => 0,
];
$this->mockDlq->addFailedJob($jobData);
@@ -274,7 +287,7 @@ describe('Dead Letter Queue Mock Implementation', function () {
'stack_trace' => 'trace1',
'failed_attempts' => 1,
'failed_at' => date('Y-m-d H:i:s'),
'retry_count' => 0
'retry_count' => 0,
]);
$this->mockDlq->addFailedJob([
@@ -288,7 +301,7 @@ describe('Dead Letter Queue Mock Implementation', function () {
'stack_trace' => 'trace2',
'failed_attempts' => 2,
'failed_at' => date('Y-m-d H:i:s'),
'retry_count' => 0
'retry_count' => 0,
]);
$emailJobs = $this->mockDlq->getJobs($emailDlq);
@@ -315,7 +328,7 @@ describe('Dead Letter Queue Mock Implementation', function () {
'stack_trace' => 'retry_trace',
'failed_attempts' => 1,
'failed_at' => date('Y-m-d H:i:s'),
'retry_count' => 0
'retry_count' => 0,
]);
expect(count($this->mockDlq->getJobs($dlqName)))->toBe(1);
@@ -342,7 +355,7 @@ describe('Dead Letter Queue Mock Implementation', function () {
'stack_trace' => 'delete_trace',
'failed_attempts' => 5,
'failed_at' => date('Y-m-d H:i:s'),
'retry_count' => 0
'retry_count' => 0,
]);
expect(count($this->mockDlq->getJobs($dlqName)))->toBe(1);
@@ -369,7 +382,7 @@ describe('Dead Letter Queue Mock Implementation', function () {
'stack_trace' => "trace_{$i}",
'failed_attempts' => $i,
'failed_at' => date('Y-m-d H:i:s'),
'retry_count' => 0
'retry_count' => 0,
]);
}
@@ -405,7 +418,7 @@ describe('Dead Letter Queue Mock Implementation', function () {
'stack_trace' => "trace_{$index}",
'failed_attempts' => $jobStats['failed_attempts'],
'failed_at' => date('Y-m-d H:i:s', time() - $index * 3600), // Different times
'retry_count' => $jobStats['retry_count']
'retry_count' => $jobStats['retry_count'],
]);
}
@@ -439,7 +452,7 @@ describe('Dead Letter Queue Mock Implementation', function () {
$queues = [
DeadLetterQueueName::fromString('email_failed'),
DeadLetterQueueName::fromString('report_failed'),
DeadLetterQueueName::fromString('background_failed')
DeadLetterQueueName::fromString('background_failed'),
];
foreach ($queues as $index => $queue) {
@@ -454,7 +467,7 @@ describe('Dead Letter Queue Mock Implementation', function () {
'stack_trace' => "trace_{$index}",
'failed_attempts' => 1,
'failed_at' => date('Y-m-d H:i:s'),
'retry_count' => 0
'retry_count' => 0,
]);
}
@@ -462,7 +475,7 @@ describe('Dead Letter Queue Mock Implementation', function () {
expect(count($availableQueues))->toBe(3);
$queueNames = array_map(fn($q) => $q->toString(), $availableQueues);
$queueNames = array_map(fn ($q) => $q->toString(), $availableQueues);
expect($queueNames)->toContain('email_failed');
expect($queueNames)->toContain('report_failed');
expect($queueNames)->toContain('background_failed');
@@ -502,7 +515,7 @@ describe('Dead Letter Queue Mock Implementation', function () {
'stack_trace' => "trace_{$i}",
'failed_attempts' => 1,
'failed_at' => date('Y-m-d H:i:s'),
'retry_count' => 0
'retry_count' => 0,
]);
}
@@ -518,17 +531,20 @@ describe('Dead Letter Queue Mock Implementation', function () {
describe('Dead Letter Queue Integration Scenarios', function () {
beforeEach(function () {
$this->testJob = new class {
$this->testJob = new class () {
public function __construct(
public string $email = 'test@example.com',
public string $subject = 'Test Email'
) {}
) {
}
public function handle(): bool {
public function handle(): bool
{
// Simulate processing that might fail
if ($this->email === 'invalid@test.com') {
throw new \Exception('Invalid email address');
}
return true;
}
};
@@ -537,23 +553,23 @@ describe('Dead Letter Queue Integration Scenarios', function () {
'network_timeout' => [
'reason' => 'Network connection timeout',
'exception' => 'NetworkTimeoutException',
'retryable' => true
'retryable' => true,
],
'invalid_data' => [
'reason' => 'Invalid email format',
'exception' => 'ValidationException',
'retryable' => false
'retryable' => false,
],
'service_unavailable' => [
'reason' => 'External service unavailable',
'exception' => 'ServiceUnavailableException',
'retryable' => true
'retryable' => true,
],
'permission_denied' => [
'reason' => 'Insufficient permissions',
'exception' => 'PermissionException',
'retryable' => false
]
'retryable' => false,
],
];
});
@@ -576,12 +592,12 @@ describe('Dead Letter Queue Integration Scenarios', function () {
it('demonstrates retry strategies for different failure types', function () {
$retryableFailures = array_filter(
$this->failureScenarios,
fn($scenario) => $scenario['retryable']
fn ($scenario) => $scenario['retryable']
);
$nonRetryableFailures = array_filter(
$this->failureScenarios,
fn($scenario) => !$scenario['retryable']
fn ($scenario) => ! $scenario['retryable']
);
expect(count($retryableFailures))->toBeGreaterThan(0);
@@ -602,7 +618,7 @@ describe('Dead Letter Queue Integration Scenarios', function () {
$queueTypes = [
'email' => DeadLetterQueueName::forQueue(QueueName::fromString('email')),
'reports' => DeadLetterQueueName::forQueue(QueueName::fromString('reports')),
'background' => DeadLetterQueueName::forQueue(QueueName::fromString('background'))
'background' => DeadLetterQueueName::forQueue(QueueName::fromString('background')),
];
foreach ($queueTypes as $originalQueue => $dlqName) {
@@ -610,8 +626,8 @@ describe('Dead Letter Queue Integration Scenarios', function () {
}
// Each queue type should have its own DLQ
$queueNames = array_map(fn($dlq) => $dlq->toString(), $queueTypes);
$queueNames = array_map(fn ($dlq) => $dlq->toString(), $queueTypes);
$uniqueNames = array_unique($queueNames);
expect(count($uniqueNames))->toBe(count($queueTypes));
});
});
});