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

@@ -36,4 +36,4 @@ enum ChainExecutionMode: string
{
return $this === self::CONDITIONAL;
}
}
}

View File

@@ -56,7 +56,7 @@ final readonly class DeadLetterQueueName
throw InvalidDeadLetterQueueNameException::tooLong($this->name, self::MAX_LENGTH);
}
if (!preg_match(self::VALID_PATTERN, $this->name)) {
if (! preg_match(self::VALID_PATTERN, $this->name)) {
throw InvalidDeadLetterQueueNameException::invalidFormat($this->name, self::VALID_PATTERN);
}
}
@@ -65,4 +65,4 @@ final readonly class DeadLetterQueueName
{
return $this->name;
}
}
}

View File

@@ -36,4 +36,4 @@ enum DependencyType: string
{
return $this === self::CONDITIONAL;
}
}
}

View File

@@ -61,7 +61,7 @@ final readonly class FailureReason
return [
'message' => $this->message,
'exception_type' => $this->exceptionType,
'has_stack_trace' => $this->hasStackTrace()
'has_stack_trace' => $this->hasStackTrace(),
];
}
@@ -71,4 +71,4 @@ final readonly class FailureReason
throw new \InvalidArgumentException('Failure reason message cannot be empty');
}
}
}
}

View File

@@ -150,7 +150,7 @@ final readonly class JobBatch
'started_at' => $this->startedAt?->toRfc3339(),
'completed_at' => $this->completedAt?->toRfc3339(),
'failed_at' => $this->failedAt?->toRfc3339(),
'options' => $this->options
'options' => $this->options,
];
}
@@ -191,4 +191,4 @@ final readonly class JobBatch
throw new \InvalidArgumentException('Processed + failed jobs cannot exceed total jobs');
}
}
}
}

View File

@@ -52,4 +52,4 @@ enum JobBatchStatus: string
self::CANCELLED => '🚫'
};
}
}
}

View File

@@ -82,6 +82,7 @@ final readonly class JobChain
public function getJobPosition(string $jobId): ?int
{
$position = array_search($jobId, $this->jobIds, true);
return $position !== false ? $position : null;
}
@@ -138,7 +139,7 @@ final readonly class JobChain
'execution_mode' => $this->executionMode->value,
'stop_on_failure' => $this->stopOnFailure,
'job_count' => $this->getJobCount(),
'metadata' => $this->metadata
'metadata' => $this->metadata,
];
}
@@ -162,4 +163,4 @@ final readonly class JobChain
}
}
}
}
}

View File

@@ -67,7 +67,7 @@ final readonly class JobDependency
'dependent_job_id' => $this->dependentJobId,
'depends_on_job_id' => $this->dependsOnJobId,
'type' => $this->type->value,
'condition' => $this->condition
'condition' => $this->condition,
];
}
@@ -89,4 +89,4 @@ final readonly class JobDependency
throw new \InvalidArgumentException('Conditional dependency must have a condition');
}
}
}
}

View File

@@ -18,7 +18,7 @@ final readonly class JobId
throw new \InvalidArgumentException('JobId cannot be empty');
}
if (!$this->isValidFormat($value)) {
if (! $this->isValidFormat($value)) {
throw new \InvalidArgumentException('Invalid JobId format');
}
}
@@ -102,6 +102,7 @@ final readonly class JobId
{
$ulid = $this->toUlid();
$timestamp = $ulid->getTimestamp();
return \DateTimeImmutable::createFromFormat('U', (string)$timestamp);
}
@@ -161,4 +162,4 @@ final readonly class JobId
{
return $this->value;
}
}
}

View File

@@ -4,11 +4,11 @@ declare(strict_types=1);
namespace App\Framework\Queue\ValueObjects;
use App\Framework\Ulid\Ulid;
use App\Framework\DateTime\SystemClock;
use App\Framework\Core\ValueObjects\ClassName;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\DateTime\SystemClock;
use App\Framework\Ulid\Ulid;
/**
* Job Metadata Value Object
@@ -26,7 +26,8 @@ final readonly class JobMetadata
public ?Timestamp $completedAt = null,
public array $tags = [],
public array $extra = []
) {}
) {
}
public static function create(array $data = []): self
{
@@ -49,7 +50,7 @@ final readonly class JobMetadata
return self::create([
'job' => $command,
'type' => 'command',
'tags' => ['command']
'tags' => ['command'],
]);
}
@@ -58,7 +59,7 @@ final readonly class JobMetadata
return self::create([
'job' => $event,
'type' => 'event',
'tags' => ['event']
'tags' => ['event'],
]);
}
@@ -67,7 +68,7 @@ final readonly class JobMetadata
return self::create([
'job' => $email,
'type' => 'email',
'tags' => ['email']
'tags' => ['email'],
]);
}
@@ -102,7 +103,7 @@ final readonly class JobMetadata
public function withTag(string $tag): self
{
$tags = $this->tags;
if (!in_array($tag, $tags, true)) {
if (! in_array($tag, $tags, true)) {
$tags[] = $tag;
}
@@ -206,7 +207,7 @@ final readonly class JobMetadata
$this->isProcessing() => 'processing',
$this->isPending() => 'pending',
default => 'unknown'
}
},
];
}
}
}

View File

@@ -22,7 +22,8 @@ final readonly class JobMetrics
public ?string $completedAt,
public ?string $failedAt,
public array $metadata = []
) {}
) {
}
public static function create(
string $jobId,
@@ -133,6 +134,7 @@ final readonly class JobMetrics
}
$successfulAttempts = $this->status === 'completed' ? 1 : 0;
return Percentage::from(($successfulAttempts / max(1, $this->attempts)) * 100);
}
@@ -148,11 +150,12 @@ final readonly class JobMetrics
public function getDuration(): ?int
{
if (!$this->startedAt) {
if (! $this->startedAt) {
return null;
}
$endTime = $this->completedAt ?? $this->failedAt ?? date('Y-m-d H:i:s');
return strtotime($endTime) - strtotime($this->startedAt);
}
@@ -200,7 +203,7 @@ final readonly class JobMetrics
'started_at' => $this->startedAt,
'completed_at' => $this->completedAt,
'failed_at' => $this->failedAt,
'metadata' => $this->metadata
'metadata' => $this->metadata,
];
}
}
}

View File

@@ -22,7 +22,8 @@ final readonly class JobPayload
public ?Duration $timeout = null,
public ?RetryStrategy $retryStrategy = null,
public ?JobMetadata $metadata = null
) {}
) {
}
public static function create(
object $job,
@@ -198,7 +199,7 @@ final readonly class JobPayload
'has_retry_strategy' => $this->hasRetryStrategy(),
'max_attempts' => $this->retryStrategy?->getMaxAttempts(),
'available_at' => $this->getAvailableTime(),
'metadata' => $this->metadata?->toArray()
'metadata' => $this->metadata?->toArray(),
];
}
}
}

View File

@@ -107,4 +107,4 @@ enum JobPriority: int
self::CRITICAL => 3600 // 60 minutes
};
}
}
}

View File

@@ -60,7 +60,7 @@ final readonly class JobProgress
public function isStarting(): bool
{
return $this->percentage->isZero() && !$this->isFailed();
return $this->percentage->isZero() && ! $this->isFailed();
}
public function withMetadata(array $metadata): self
@@ -90,7 +90,7 @@ final readonly class JobProgress
'metadata' => $this->metadata,
'is_completed' => $this->isCompleted(),
'is_failed' => $this->isFailed(),
'is_starting' => $this->isStarting()
'is_starting' => $this->isStarting(),
];
}
@@ -100,4 +100,4 @@ final readonly class JobProgress
throw new \InvalidArgumentException('Progress message cannot be empty');
}
}
}
}

View File

@@ -4,9 +4,9 @@ declare(strict_types=1);
namespace App\Framework\Queue\ValueObjects;
use App\Framework\StateManagement\SerializableState;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\StateManagement\SerializableState;
/**
* Job state representation for state management
@@ -25,7 +25,8 @@ final readonly class JobState implements SerializableState
public int $attempts = 0,
public int $maxAttempts = 3,
public array $metadata = []
) {}
) {
}
/**
* Create initial job state
@@ -53,7 +54,7 @@ final readonly class JobState implements SerializableState
*/
public function markAsProcessing(): self
{
if (!$this->status->canTransitionTo(JobStatus::PROCESSING)) {
if (! $this->status->canTransitionTo(JobStatus::PROCESSING)) {
throw new \InvalidArgumentException(
"Cannot transition from {$this->status->value} to processing"
);
@@ -79,7 +80,7 @@ final readonly class JobState implements SerializableState
*/
public function markAsCompleted(): self
{
if (!$this->status->canTransitionTo(JobStatus::COMPLETED)) {
if (! $this->status->canTransitionTo(JobStatus::COMPLETED)) {
throw new \InvalidArgumentException(
"Cannot transition from {$this->status->value} to completed"
);
@@ -105,7 +106,7 @@ final readonly class JobState implements SerializableState
*/
public function markAsFailed(string $errorMessage): self
{
if (!$this->status->canTransitionTo(JobStatus::FAILED)) {
if (! $this->status->canTransitionTo(JobStatus::FAILED)) {
throw new \InvalidArgumentException(
"Cannot transition from {$this->status->value} to failed"
);
@@ -135,7 +136,7 @@ final readonly class JobState implements SerializableState
return $this->markAsFailed("Max attempts exceeded: " . $errorMessage);
}
if (!$this->status->canTransitionTo(JobStatus::RETRYING)) {
if (! $this->status->canTransitionTo(JobStatus::RETRYING)) {
throw new \InvalidArgumentException(
"Cannot transition from {$this->status->value} to retrying"
);
@@ -215,7 +216,7 @@ final readonly class JobState implements SerializableState
'error_message' => $this->errorMessage,
'attempts' => $this->attempts,
'max_attempts' => $this->maxAttempts,
'metadata' => $this->metadata
'metadata' => $this->metadata,
];
}
@@ -238,4 +239,4 @@ final readonly class JobState implements SerializableState
metadata: $data['metadata'] ?? []
);
}
}
}

View File

@@ -89,4 +89,4 @@ enum JobStatus: string
{
return in_array($newStatus, $this->getNextPossibleStatuses(), true);
}
}
}

View File

@@ -21,7 +21,7 @@ final readonly class LockKey
}
// Nur alphanumerische Zeichen, Bindestriche, Unterstriche und Punkte erlaubt
if (!preg_match('/^[a-zA-Z0-9\-_.]+$/', $value)) {
if (! preg_match('/^[a-zA-Z0-9\-_.]+$/', $value)) {
throw new \InvalidArgumentException('Lock key contains invalid characters');
}
}
@@ -129,4 +129,4 @@ final readonly class LockKey
{
return fnmatch($pattern, $this->value);
}
}
}

View File

@@ -84,7 +84,7 @@ final readonly class ProgressStep
'completed' => $this->completed,
'completed_at' => $this->completedAt,
'status' => $this->getStatus(),
'metadata' => $this->metadata
'metadata' => $this->metadata,
];
}
@@ -98,8 +98,8 @@ final readonly class ProgressStep
throw new \InvalidArgumentException('Step description cannot be empty');
}
if ($this->completed && !$this->completedAt) {
if ($this->completed && ! $this->completedAt) {
throw new \InvalidArgumentException('Completed steps must have a completion timestamp');
}
}
}
}

View File

@@ -22,7 +22,8 @@ final readonly class QueueMetrics
public Percentage $successRate,
public string $measuredAt,
public array $additionalMetrics = []
) {}
) {
}
public static function calculate(
string $queueName,
@@ -212,7 +213,7 @@ final readonly class QueueMetrics
'is_healthy' => $this->isHealthy(),
'bottleneck_indicators' => $this->getBottleneckIndicators(),
'measured_at' => $this->measuredAt,
'additional_metrics' => $this->additionalMetrics
'additional_metrics' => $this->additionalMetrics,
];
}
}
}

View File

@@ -20,7 +20,7 @@ final readonly class QueueName
throw new \InvalidArgumentException('Queue name cannot be empty');
}
if (!preg_match('/^[a-z0-9\-_.]+$/i', $value)) {
if (! preg_match('/^[a-z0-9\-_.]+$/i', $value)) {
throw new \InvalidArgumentException(
sprintf('Queue name "%s" contains invalid characters. Only alphanumeric, dash, underscore and dot allowed.', $value)
);
@@ -70,6 +70,7 @@ final readonly class QueueName
public function toString(): string
{
$base = "{$this->type->value}.{$this->value}";
return $this->tenant ? "{$this->tenant}.{$base}" : $base;
}
@@ -98,4 +99,4 @@ final readonly class QueueName
{
return str_replace('.', '/', $this->toString());
}
}
}

View File

@@ -104,4 +104,4 @@ final readonly class QueuePriority
default => sprintf('custom(%d)', $this->value)
};
}
}
}

View File

@@ -4,8 +4,6 @@ declare(strict_types=1);
namespace App\Framework\Queue\ValueObjects;
use App\Framework\Ulid\Ulid;
/**
* Value Object representing a unique Worker identifier
*/
@@ -35,6 +33,7 @@ final readonly class WorkerId
{
// Create a deterministic ID based on hostname and PID
$identifier = sprintf('%s_%d_%s', $hostname, $pid, uniqid());
return new self(substr(md5($identifier), 0, 16));
}
@@ -85,4 +84,4 @@ final readonly class WorkerId
{
return $this->value;
}
}
}