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,30 @@
<?php
declare(strict_types=1);
namespace App\Framework\Queue\Exceptions;
use App\Framework\Exception\Core\QueueErrorCode;
use App\Framework\Exception\ExceptionContext;
/**
* Exception thrown when attempting to complete or update steps after all steps are completed
*/
final class AllStepsCompletedException extends QueueException
{
public static function forJob(string $jobId, int $totalSteps): self
{
$context = ExceptionContext::forOperation('step.complete', 'StepProgressTracker')
->withData([
'job_id' => $jobId,
'total_steps' => $totalSteps,
'current_step_index' => $totalSteps,
]);
return self::create(
QueueErrorCode::INVALID_STATE,
"All {$totalSteps} steps have already been completed for job '{$jobId}'",
$context
);
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace App\Framework\Queue\Exceptions;
use App\Framework\Exception\Core\QueueErrorCode;
use App\Framework\Exception\ExceptionContext;
/**
* Exception thrown when a job chain cannot be found
*/
final class ChainNotFoundException extends QueueException
{
public static function byId(string $chainId): self
{
$context = ExceptionContext::forOperation('chain.lookup', 'JobChainManager')
->withData([
'chain_id' => $chainId,
'search_type' => 'by_id',
]);
return self::create(
QueueErrorCode::CHAIN_NOT_FOUND,
"Chain with ID '{$chainId}' not found",
$context
);
}
public static function byName(string $name): self
{
$context = ExceptionContext::forOperation('chain.lookup', 'JobChainManager')
->withData([
'chain_name' => $name,
'search_type' => 'by_name',
]);
return self::create(
QueueErrorCode::CHAIN_NOT_FOUND,
"Chain with name '{$name}' not found",
$context
);
}
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace App\Framework\Queue\Exceptions;
use App\Framework\Exception\Core\QueueErrorCode;
use App\Framework\Exception\ExceptionContext;
/**
* Exception thrown when circular dependencies are detected in job chains
*/
final class CircularDependencyException extends QueueException
{
public static function inChain(string $chainId, array $circularDependencies = []): self
{
$context = ExceptionContext::forOperation('chain.validate', 'JobChainExecutionCoordinator')
->withData([
'chain_id' => $chainId,
'circular_dependencies' => $circularDependencies,
]);
return self::create(
QueueErrorCode::CIRCULAR_DEPENDENCY,
"Chain '{$chainId}' has circular dependencies",
$context
);
}
}

View File

@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace App\Framework\Queue\Exceptions;
use App\Framework\Exception\Core\QueueErrorCode;
use App\Framework\Exception\ExceptionContext;
/**
* Exception thrown when a job chain is in an invalid state for the requested operation
*/
final class InvalidChainStateException extends QueueException
{
public static function notPending(string $chainId, string $currentStatus): self
{
$context = ExceptionContext::forOperation('chain.start', 'JobChainManager')
->withData([
'chain_id' => $chainId,
'current_status' => $currentStatus,
'required_status' => 'pending',
]);
return self::create(
QueueErrorCode::INVALID_STATE,
"Chain '{$chainId}' is not in pending status (current: {$currentStatus})",
$context
);
}
public static function alreadyCompleted(string $chainId): self
{
$context = ExceptionContext::forOperation('chain.modify', 'JobChainManager')
->withData([
'chain_id' => $chainId,
'current_status' => 'completed',
]);
return self::create(
QueueErrorCode::INVALID_STATE,
"Chain '{$chainId}' is already completed and cannot be modified",
$context
);
}
public static function alreadyFailed(string $chainId): self
{
$context = ExceptionContext::forOperation('chain.modify', 'JobChainManager')
->withData([
'chain_id' => $chainId,
'current_status' => 'failed',
]);
return self::create(
QueueErrorCode::INVALID_STATE,
"Chain '{$chainId}' has failed and cannot be modified",
$context
);
}
}

View File

@@ -5,42 +5,42 @@ declare(strict_types=1);
namespace App\Framework\Queue\Exceptions;
use App\Framework\Exception\FrameworkException;
use App\Framework\Exception\ErrorCode;
use App\Framework\Exception\ValidationErrorCode;
final class InvalidDeadLetterQueueNameException extends FrameworkException
{
public static function tooShort(string $name, int $minLength): self
{
return self::create(
ErrorCode::VAL_BUSINESS_RULE_VIOLATION,
ValidationErrorCode::BUSINESS_RULE_VIOLATION,
"Dead letter queue name '{$name}' is too short. Minimum length is {$minLength} characters."
)->withData([
'name' => $name,
'actual_length' => strlen($name),
'min_length' => $minLength
'min_length' => $minLength,
]);
}
public static function tooLong(string $name, int $maxLength): self
{
return self::create(
ErrorCode::VAL_BUSINESS_RULE_VIOLATION,
ValidationErrorCode::BUSINESS_RULE_VIOLATION,
"Dead letter queue name '{$name}' is too long. Maximum length is {$maxLength} characters."
)->withData([
'name' => $name,
'actual_length' => strlen($name),
'max_length' => $maxLength
'max_length' => $maxLength,
]);
}
public static function invalidFormat(string $name, string $pattern): self
{
return self::create(
ErrorCode::VAL_BUSINESS_RULE_VIOLATION,
ValidationErrorCode::BUSINESS_RULE_VIOLATION,
"Dead letter queue name '{$name}' contains invalid characters. Only alphanumeric characters, underscores, hyphens, and dots are allowed."
)->withData([
'name' => $name,
'valid_pattern' => $pattern
'valid_pattern' => $pattern,
]);
}
}
}

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace App\Framework\Queue\Exceptions;
use App\Framework\Exception\Core\QueueErrorCode;
use App\Framework\Exception\ExceptionContext;
use App\Framework\Queue\ValueObjects\JobId;
/**
* Exception thrown when a job cannot be found
*/
final class JobNotFoundException extends QueueException
{
public static function byId(JobId $jobId): self
{
$context = ExceptionContext::forOperation('job.lookup', 'JobPersistenceLayer')
->withData([
'job_id' => $jobId->toString(),
'search_type' => 'by_id',
]);
return self::create(
QueueErrorCode::JOB_NOT_FOUND,
"Job with ID '{$jobId->toString()}' not found",
$context
);
}
public static function inQueue(JobId $jobId, string $queueName): self
{
$context = ExceptionContext::forOperation('job.lookup', 'Queue')
->withData([
'job_id' => $jobId->toString(),
'queue_name' => $queueName,
'search_type' => 'in_queue',
]);
return self::create(
QueueErrorCode::JOB_NOT_FOUND,
"Job '{$jobId->toString()}' not found in queue '{$queueName}'",
$context
);
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\Framework\Queue\Exceptions;
use App\Framework\Exception\FrameworkException;
/**
* Base exception for Queue module
*
* All Queue-related exceptions should extend this class.
*/
class QueueException extends FrameworkException
{
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace App\Framework\Queue\Exceptions;
use App\Framework\Exception\Core\QueueErrorCode;
use App\Framework\Exception\ExceptionContext;
/**
* Exception thrown when Redis PHP extension is not loaded
*
* This is typically caught and handled with graceful fallback to FileQueue.
*/
final class RedisExtensionNotLoadedException extends QueueException
{
public static function notLoaded(): self
{
$context = ExceptionContext::forOperation('queue.init', 'QueueInitializer')
->withData([
'required_extension' => 'redis',
'loaded_extensions' => get_loaded_extensions(),
'fallback_available' => 'FileQueue',
]);
return self::fromContext(
'Redis PHP extension is not loaded',
$context,
QueueErrorCode::WORKER_UNAVAILABLE
);
}
}