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

@@ -4,9 +4,9 @@ declare(strict_types=1);
namespace App\Framework\Queue\Entities;
use App\Framework\Database\Attributes\Column;
use App\Framework\Database\Attributes\Entity;
use App\Framework\Database\Attributes\Id;
use App\Framework\Database\Attributes\Column;
use App\Framework\Queue\ValueObjects\DeadLetterQueueName;
use App\Framework\Queue\ValueObjects\FailureReason;
use App\Framework\Queue\ValueObjects\JobPayload;
@@ -23,43 +23,32 @@ final readonly class DeadLetterJob
#[Id]
#[Column(name: 'id', type: 'string', length: 26)]
public string $id,
#[Column(name: 'original_job_id', type: 'string', length: 26)]
public string $originalJobId,
#[Column(name: 'dead_letter_queue', type: 'string', length: 100)]
public string $deadLetterQueue,
#[Column(name: 'original_queue', type: 'string', length: 50)]
public string $originalQueue,
#[Column(name: 'job_payload', type: 'text')]
public string $jobPayload,
#[Column(name: 'failure_reason', type: 'text')]
public string $failureReason,
#[Column(name: 'exception_type', type: 'string', length: 255, nullable: true)]
public ?string $exceptionType,
#[Column(name: 'stack_trace', type: 'longtext', nullable: true)]
public ?string $stackTrace,
#[Column(name: 'failed_attempts', type: 'integer')]
public int $failedAttempts,
#[Column(name: 'failed_at', type: 'timestamp')]
public string $failedAt,
#[Column(name: 'moved_to_dlq_at', type: 'timestamp')]
public string $movedToDlqAt,
#[Column(name: 'retry_count', type: 'integer', default: 0)]
public int $retryCount = 0,
#[Column(name: 'last_retry_at', type: 'timestamp', nullable: true)]
public ?string $lastRetryAt = null
) {}
) {
}
public static function fromFailedJob(
JobIndexEntry $failedJob,
@@ -139,7 +128,7 @@ final readonly class DeadLetterJob
'failed_at' => $this->failedAt,
'moved_to_dlq_at' => $this->movedToDlqAt,
'retry_count' => $this->retryCount,
'last_retry_at' => $this->lastRetryAt
'last_retry_at' => $this->lastRetryAt,
];
}
}
}

View File

@@ -4,11 +4,11 @@ declare(strict_types=1);
namespace App\Framework\Queue\Entities;
use App\Framework\Database\Attributes\Column;
use App\Framework\Database\Attributes\Entity;
use App\Framework\Database\Attributes\Id;
use App\Framework\Database\Attributes\Column;
use App\Framework\Queue\ValueObjects\JobChain;
use App\Framework\Queue\ValueObjects\ChainExecutionMode;
use App\Framework\Queue\ValueObjects\JobChain;
use App\Framework\Ulid\Ulid;
/**
@@ -21,40 +21,30 @@ final readonly class JobChainEntry
#[Id]
#[Column(name: 'id', type: 'string', length: 26)]
public string $id,
#[Column(name: 'chain_id', type: 'string', length: 26)]
public string $chainId,
#[Column(name: 'name', type: 'string', length: 255)]
public string $name,
#[Column(name: 'job_ids', type: 'json')]
public string $jobIds,
#[Column(name: 'execution_mode', type: 'string', length: 20)]
public string $executionMode,
#[Column(name: 'created_at', type: 'timestamp')]
public string $createdAt,
#[Column(name: 'updated_at', type: 'timestamp')]
public string $updatedAt,
#[Column(name: 'stop_on_failure', type: 'boolean', default: true)]
public bool $stopOnFailure = true,
#[Column(name: 'metadata', type: 'json', nullable: true)]
public ?string $metadata = null,
#[Column(name: 'status', type: 'string', length: 20, default: 'pending')]
public string $status = 'pending',
#[Column(name: 'started_at', type: 'timestamp', nullable: true)]
public ?string $startedAt = null,
#[Column(name: 'completed_at', type: 'timestamp', nullable: true)]
public ?string $completedAt = null
) {}
) {
}
public static function fromJobChain(JobChain $jobChain): self
{
@@ -194,7 +184,7 @@ final readonly class JobChainEntry
'started_at' => $this->startedAt,
'completed_at' => $this->completedAt,
'created_at' => $this->createdAt,
'updated_at' => $this->updatedAt
'updated_at' => $this->updatedAt,
];
}
}
}

View File

@@ -4,11 +4,11 @@ declare(strict_types=1);
namespace App\Framework\Queue\Entities;
use App\Framework\Database\Attributes\Column;
use App\Framework\Database\Attributes\Entity;
use App\Framework\Database\Attributes\Id;
use App\Framework\Database\Attributes\Column;
use App\Framework\Queue\ValueObjects\JobDependency;
use App\Framework\Queue\ValueObjects\DependencyType;
use App\Framework\Queue\ValueObjects\JobDependency;
use App\Framework\Ulid\Ulid;
/**
@@ -21,31 +21,24 @@ final readonly class JobDependencyEntry
#[Id]
#[Column(name: 'id', type: 'string', length: 26)]
public string $id,
#[Column(name: 'dependent_job_id', type: 'string', length: 26)]
public string $dependentJobId,
#[Column(name: 'depends_on_job_id', type: 'string', length: 26)]
public string $dependsOnJobId,
#[Column(name: 'dependency_type', type: 'string', length: 20)]
public string $dependencyType,
#[Column(name: 'created_at', type: 'timestamp')]
public string $createdAt,
#[Column(name: 'updated_at', type: 'timestamp')]
public string $updatedAt,
#[Column(name: 'condition_expression', type: 'text', nullable: true)]
public ?string $conditionExpression = null,
#[Column(name: 'is_satisfied', type: 'boolean', default: false)]
public bool $isSatisfied = false,
#[Column(name: 'satisfied_at', type: 'timestamp', nullable: true)]
public ?string $satisfiedAt = null
) {}
) {
}
public static function fromJobDependency(JobDependency $dependency): self
{
@@ -115,7 +108,7 @@ final readonly class JobDependencyEntry
'is_satisfied' => $this->isSatisfied,
'satisfied_at' => $this->satisfiedAt,
'created_at' => $this->createdAt,
'updated_at' => $this->updatedAt
'updated_at' => $this->updatedAt,
];
}
}
}

View File

@@ -4,11 +4,11 @@ declare(strict_types=1);
namespace App\Framework\Queue\Entities;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\Database\Attributes\Column;
use App\Framework\Database\Attributes\Entity;
use App\Framework\Queue\ValueObjects\JobId;
use App\Framework\Queue\ValueObjects\JobStatus;
use App\Framework\Core\ValueObjects\Timestamp;
/**
* Job History Entry Entity
@@ -21,25 +21,20 @@ final readonly class JobHistoryEntry
public function __construct(
#[Column(name: 'id', primary: true, autoIncrement: true)]
public ?int $id,
#[Column(name: 'job_id')]
public JobId $jobId,
#[Column(name: 'old_status')]
public ?JobStatus $oldStatus,
#[Column(name: 'new_status')]
public JobStatus $newStatus,
#[Column(name: 'error_message')]
public ?string $errorMessage,
#[Column(name: 'changed_at')]
public Timestamp $changedAt,
#[Column(name: 'metadata')]
public ?string $metadata = null
) {}
) {
}
/**
* Create status change entry
@@ -58,7 +53,7 @@ final readonly class JobHistoryEntry
newStatus: $newStatus,
errorMessage: $errorMessage,
changedAt: Timestamp::now(),
metadata: !empty($metadata) ? json_encode($metadata) : null
metadata: ! empty($metadata) ? json_encode($metadata) : null
);
}
@@ -72,6 +67,7 @@ final readonly class JobHistoryEntry
}
$decoded = json_decode($this->metadata, true);
return is_array($decoded) ? $decoded : [];
}
@@ -87,7 +83,7 @@ final readonly class JobHistoryEntry
'new_status' => $this->newStatus->value,
'error_message' => $this->errorMessage,
'changed_at' => $this->changedAt->toFloat(),
'metadata' => $this->metadata
'metadata' => $this->metadata,
];
}
}
}

View File

@@ -4,14 +4,14 @@ declare(strict_types=1);
namespace App\Framework\Queue\Entities;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\Database\Attributes\Column;
use App\Framework\Database\Attributes\Entity;
use App\Framework\Queue\ValueObjects\JobId;
use App\Framework\Queue\ValueObjects\JobStatus;
use App\Framework\Queue\ValueObjects\JobState;
use App\Framework\Queue\ValueObjects\QueueType;
use App\Framework\Queue\ValueObjects\JobStatus;
use App\Framework\Queue\ValueObjects\QueuePriority;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\Queue\ValueObjects\QueueType;
/**
* Job Index Entry Entity
@@ -24,40 +24,30 @@ final readonly class JobIndexEntry
public function __construct(
#[Column(name: 'job_id', primary: true)]
public JobId $jobId,
#[Column(name: 'status')]
public JobStatus $status,
#[Column(name: 'queue_type')]
public QueueType $queueType,
#[Column(name: 'priority')]
public QueuePriority $priority,
#[Column(name: 'attempts')]
public int $attempts,
#[Column(name: 'max_attempts')]
public int $maxAttempts,
#[Column(name: 'created_at')]
public Timestamp $createdAt,
#[Column(name: 'updated_at')]
public Timestamp $updatedAt,
#[Column(name: 'started_at')]
public ?Timestamp $startedAt = null,
#[Column(name: 'completed_at')]
public ?Timestamp $completedAt = null,
#[Column(name: 'scheduled_for')]
public ?Timestamp $scheduledFor = null,
#[Column(name: 'error_message')]
public ?string $errorMessage = null
) {}
) {
}
/**
* Create from JobState
@@ -139,7 +129,7 @@ final readonly class JobIndexEntry
*/
public function needsRetry(): bool
{
if (!$this->canRetry()) {
if (! $this->canRetry()) {
return false;
}
@@ -167,7 +157,7 @@ final readonly class JobIndexEntry
'started_at' => $this->startedAt?->toFloat(),
'completed_at' => $this->completedAt?->toFloat(),
'scheduled_for' => $this->scheduledFor?->toFloat(),
'error_message' => $this->errorMessage
'error_message' => $this->errorMessage,
];
}
}
}

View File

@@ -4,9 +4,9 @@ declare(strict_types=1);
namespace App\Framework\Queue\Entities;
use App\Framework\Database\Attributes\Column;
use App\Framework\Database\Attributes\Entity;
use App\Framework\Database\Attributes\Id;
use App\Framework\Database\Attributes\Column;
use App\Framework\Queue\ValueObjects\JobMetrics;
use App\Framework\Ulid\Ulid;
@@ -17,49 +17,36 @@ final readonly class JobMetricsEntry
#[Id]
#[Column(name: 'id', type: 'string', length: 26)]
public string $id,
#[Column(name: 'job_id', type: 'string', length: 26)]
public string $jobId,
#[Column(name: 'queue_name', type: 'string', length: 100)]
public string $queueName,
#[Column(name: 'status', type: 'string', length: 20)]
public string $status,
#[Column(name: 'attempts', type: 'integer')]
public int $attempts,
#[Column(name: 'max_attempts', type: 'integer')]
public int $maxAttempts,
#[Column(name: 'execution_time_ms', type: 'float')]
public float $executionTimeMs,
#[Column(name: 'memory_usage_bytes', type: 'integer')]
public int $memoryUsageBytes,
#[Column(name: 'created_at', type: 'timestamp')]
public string $createdAt,
#[Column(name: 'updated_at', type: 'timestamp')]
public string $updatedAt,
#[Column(name: 'error_message', type: 'text', nullable: true)]
public ?string $errorMessage = null,
#[Column(name: 'started_at', type: 'timestamp', nullable: true)]
public ?string $startedAt = null,
#[Column(name: 'completed_at', type: 'timestamp', nullable: true)]
public ?string $completedAt = null,
#[Column(name: 'failed_at', type: 'timestamp', nullable: true)]
public ?string $failedAt = null,
#[Column(name: 'metadata', type: 'json', nullable: true)]
public ?string $metadata = null
) {}
) {
}
public static function fromJobMetrics(JobMetrics $metrics): self
{
@@ -80,7 +67,7 @@ final readonly class JobMetricsEntry
startedAt: $metrics->startedAt,
completedAt: $metrics->completedAt,
failedAt: $metrics->failedAt,
metadata: !empty($metrics->metadata) ? json_encode($metrics->metadata) : null
metadata: ! empty($metrics->metadata) ? json_encode($metrics->metadata) : null
);
}
@@ -120,7 +107,7 @@ final readonly class JobMetricsEntry
startedAt: $metrics->startedAt,
completedAt: $metrics->completedAt,
failedAt: $metrics->failedAt,
metadata: !empty($metrics->metadata) ? json_encode($metrics->metadata) : $this->metadata
metadata: ! empty($metrics->metadata) ? json_encode($metrics->metadata) : $this->metadata
);
}
@@ -178,7 +165,7 @@ final readonly class JobMetricsEntry
'started_at' => $this->startedAt,
'completed_at' => $this->completedAt,
'failed_at' => $this->failedAt,
'metadata' => $this->getMetadataArray()
'metadata' => $this->getMetadataArray(),
];
}
}
}

View File

@@ -4,11 +4,11 @@ declare(strict_types=1);
namespace App\Framework\Queue\Entities;
use App\Framework\Core\ValueObjects\Percentage;
use App\Framework\Database\Attributes\Column;
use App\Framework\Database\Attributes\Entity;
use App\Framework\Database\Attributes\Id;
use App\Framework\Database\Attributes\Column;
use App\Framework\Queue\ValueObjects\JobProgress;
use App\Framework\Core\ValueObjects\Percentage;
use App\Framework\Ulid\Ulid;
/**
@@ -21,31 +21,24 @@ final readonly class JobProgressEntry
#[Id]
#[Column(name: 'id', type: 'string', length: 26)]
public string $id,
#[Column(name: 'job_id', type: 'string', length: 26)]
public string $jobId,
#[Column(name: 'percentage', type: 'decimal', precision: 5, scale: 2)]
public float $percentage,
#[Column(name: 'message', type: 'text')]
public string $message,
#[Column(name: 'step_name', type: 'string', length: 100, nullable: true)]
public ?string $stepName,
#[Column(name: 'metadata', type: 'json', nullable: true)]
public ?string $metadata,
#[Column(name: 'updated_at', type: 'timestamp')]
public string $updatedAt,
#[Column(name: 'is_completed', type: 'boolean', default: false)]
public bool $isCompleted = false,
#[Column(name: 'is_failed', type: 'boolean', default: false)]
public bool $isFailed = false
) {}
) {
}
public static function fromJobProgress(string $jobId, JobProgress $progress, ?string $stepName = null): self
{
@@ -100,7 +93,7 @@ final readonly class JobProgressEntry
'metadata' => $this->getMetadataArray(),
'updated_at' => $this->updatedAt,
'is_completed' => $this->isCompleted,
'is_failed' => $this->isFailed
'is_failed' => $this->isFailed,
];
}
}
}

View File

@@ -4,10 +4,10 @@ declare(strict_types=1);
namespace App\Framework\Queue\Entities;
use App\Framework\Queue\ValueObjects\WorkerId;
use App\Framework\Queue\ValueObjects\QueueName;
use App\Framework\Core\ValueObjects\Percentage;
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Core\ValueObjects\Percentage;
use App\Framework\Queue\ValueObjects\QueueName;
use App\Framework\Queue\ValueObjects\WorkerId;
/**
* Worker Entity für Distributed Job Processing
@@ -136,6 +136,7 @@ final readonly class Worker
return true;
}
}
return false;
}
@@ -144,7 +145,7 @@ final readonly class Worker
*/
public function isHealthy(): bool
{
if (!$this->isActive) {
if (! $this->isActive) {
return false;
}
@@ -204,7 +205,7 @@ final readonly class Worker
'id' => $this->id->toString(),
'hostname' => $this->hostname,
'process_id' => $this->processId,
'queues' => array_map(fn(QueueName $queue) => $queue->toString(), $this->queues),
'queues' => array_map(fn (QueueName $queue) => $queue->toString(), $this->queues),
'max_jobs' => $this->maxJobs,
'current_jobs' => $this->currentJobs,
'is_active' => $this->isActive,
@@ -216,7 +217,7 @@ final readonly class Worker
'registered_at' => $this->registeredAt->format('Y-m-d H:i:s'),
'last_heartbeat' => $this->lastHeartbeat?->format('Y-m-d H:i:s'),
'capabilities' => $this->capabilities,
'version' => $this->version
'version' => $this->version,
];
}
@@ -229,7 +230,7 @@ final readonly class Worker
'id' => $this->id->toString(),
'hostname' => $this->hostname,
'process_id' => $this->processId,
'queues' => json_encode(array_map(fn(QueueName $queue) => $queue->toString(), $this->queues)),
'queues' => json_encode(array_map(fn (QueueName $queue) => $queue->toString(), $this->queues)),
'max_jobs' => $this->maxJobs,
'current_jobs' => $this->currentJobs,
'is_active' => $this->isActive,
@@ -238,7 +239,7 @@ final readonly class Worker
'registered_at' => $this->registeredAt->format('Y-m-d H:i:s'),
'last_heartbeat' => $this->lastHeartbeat?->format('Y-m-d H:i:s'),
'capabilities' => json_encode($this->capabilities),
'version' => $this->version
'version' => $this->version,
];
}
@@ -248,13 +249,14 @@ final readonly class Worker
public static function fromArray(array $data): self
{
$queueStrings = json_decode($data['queues'], true);
$queues = array_map(function(string $queueString) {
$queues = array_map(function (string $queueString) {
// Parse queue string zurück zu QueueName
// Annahme: Format ist "type.name" oder "tenant.type.name"
$parts = explode('.', $queueString);
if (count($parts) >= 2) {
return QueueName::default(); // Vereinfacht - könnte erweitert werden
}
return QueueName::default();
}, $queueStrings);
@@ -274,4 +276,4 @@ final readonly class Worker
version: $data['version'] ?? '1.0.0'
);
}
}
}