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

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Framework\Scheduler\Commands;
use App\Framework\Attributes\ConsoleCommand as ConsoleCommandAttribute;
use App\Framework\Console\ConsoleCommand;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\Scheduler\Services\SchedulerService;
@@ -15,15 +16,17 @@ final readonly class SchedulerCommands
{
public function __construct(
private SchedulerService $scheduler
) {}
) {
}
#[ConsoleCommandAttribute(name: 'schedule:list', description: 'List all scheduled tasks')]
#[ConsoleCommand(name: 'schedule:list', description: 'List all scheduled tasks')]
public function listTasks(): void
{
$tasks = $this->scheduler->getScheduledTasks();
if (empty($tasks)) {
echo "No scheduled tasks found\n";
return;
}
@@ -43,7 +46,7 @@ final readonly class SchedulerCommands
}
}
#[ConsoleCommandAttribute(name: 'schedule:stats', description: 'Show scheduler statistics')]
#[ConsoleCommand(name: 'schedule:stats', description: 'Show scheduler statistics')]
public function showStats(): void
{
$stats = $this->scheduler->getStats();
@@ -55,7 +58,7 @@ final readonly class SchedulerCommands
echo "Due Tasks: {$stats['due_tasks']}\n";
echo "Next Execution: " . ($stats['next_execution'] ?? 'None') . "\n\n";
if (!empty($stats['schedule_types'])) {
if (! empty($stats['schedule_types'])) {
echo "Schedule Types:\n";
foreach ($stats['schedule_types'] as $type => $count) {
echo " {$type}: {$count}\n";
@@ -63,13 +66,14 @@ final readonly class SchedulerCommands
}
}
#[ConsoleCommandAttribute(name: 'schedule:due', description: 'Show tasks due for execution')]
#[ConsoleCommand(name: 'schedule:due', description: 'Show tasks due for execution')]
public function showDueTasks(): void
{
$dueTasks = $this->scheduler->getDueTasks();
if (empty($dueTasks)) {
echo "No tasks are currently due for execution\n";
return;
}
@@ -83,13 +87,14 @@ final readonly class SchedulerCommands
}
}
#[ConsoleCommandAttribute(name: 'schedule:run', description: 'Execute all due tasks')]
#[ConsoleCommand(name: 'schedule:run', description: 'Execute all due tasks')]
public function runDueTasks(): void
{
$dueTasks = $this->scheduler->getDueTasks();
if (empty($dueTasks)) {
echo "No tasks are currently due for execution\n";
return;
}
@@ -120,13 +125,14 @@ final readonly class SchedulerCommands
echo "Summary: {$successful} successful, {$failed} failed\n";
}
#[ConsoleCommandAttribute(name: 'schedule:next', description: 'Show next scheduled execution')]
#[ConsoleCommand(name: 'schedule:next', description: 'Show next scheduled execution')]
public function showNext(): void
{
$nextExecution = $this->scheduler->getNextExecution();
if (!$nextExecution) {
if (! $nextExecution) {
echo "No tasks are scheduled for execution\n";
return;
}
@@ -146,7 +152,7 @@ final readonly class SchedulerCommands
}
}
if (!empty($tasksAtTime)) {
if (! empty($tasksAtTime)) {
echo "Tasks:\n";
foreach ($tasksAtTime as $task) {
echo "{$task->id}\n";
@@ -154,7 +160,7 @@ final readonly class SchedulerCommands
}
}
#[ConsoleCommandAttribute(name: 'schedule:check', description: 'Check scheduler health')]
#[ConsoleCommand(name: 'schedule:check', description: 'Check scheduler health')]
public function checkHealth(): void
{
$stats = $this->scheduler->getStats();
@@ -176,7 +182,7 @@ final readonly class SchedulerCommands
}
}
if (!empty($overdueTasks)) {
if (! empty($overdueTasks)) {
echo "\n⚠️ Overdue Tasks:\n";
foreach ($overdueTasks as $overdueTask) {
$task = $overdueTask['task'];
@@ -206,17 +212,20 @@ final readonly class SchedulerCommands
if ($seconds < 3600) {
$minutes = (int) ($seconds / 60);
$remainingSeconds = $seconds % 60;
return $remainingSeconds > 0 ? "{$minutes}m {$remainingSeconds}s" : "{$minutes}m";
}
if ($seconds < 86400) {
$hours = (int) ($seconds / 3600);
$remainingMinutes = (int) (($seconds % 3600) / 60);
return $remainingMinutes > 0 ? "{$hours}h {$remainingMinutes}m" : "{$hours}h";
}
$days = (int) ($seconds / 86400);
$remainingHours = (int) (($seconds % 86400) / 3600);
return $remainingHours > 0 ? "{$days}d {$remainingHours}h" : "{$days}d";
}
}
}

View File

@@ -35,4 +35,4 @@ interface ScheduleInterface
* Convert schedule to array representation
*/
public function toArray(): array;
}
}

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Framework\Scheduler\Schedules;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\Scheduler\Contracts\ScheduleInterface;
use App\Framework\Scheduler\ValueObjects\CronExpression;
@@ -15,7 +16,8 @@ final readonly class CronSchedule implements ScheduleInterface
{
public function __construct(
public CronExpression $cronExpression
) {}
) {
}
public static function fromExpression(string $expression): self
{
@@ -62,7 +64,7 @@ final readonly class CronSchedule implements ScheduleInterface
$from = $from ?? Timestamp::now();
// Start checking from the next minute
$checkTime = $from->add(\App\Framework\Core\ValueObjects\Duration::fromMinutes(1));
$checkTime = $from->add(Duration::fromMinutes(1));
// Round down to the start of the minute
$checkTime = Timestamp::fromDateTime(
@@ -70,14 +72,14 @@ final readonly class CronSchedule implements ScheduleInterface
);
// Check up to one year in the future to avoid infinite loops
$maxTime = $from->add(\App\Framework\Core\ValueObjects\Duration::fromDays(365));
$maxTime = $from->add(Duration::fromDays(365));
while ($checkTime->isBefore($maxTime)) {
if ($this->shouldExecuteAt($checkTime)) {
return $checkTime;
}
$checkTime = $checkTime->add(\App\Framework\Core\ValueObjects\Duration::fromMinutes(1));
$checkTime = $checkTime->add(Duration::fromMinutes(1));
}
return null; // No execution found within the next year
@@ -109,7 +111,7 @@ final readonly class CronSchedule implements ScheduleInterface
return [
'type' => $this->getType(),
'expression' => $this->cronExpression->toString(),
'description' => $this->getDescription()
'description' => $this->getDescription(),
];
}
@@ -137,6 +139,7 @@ final readonly class CronSchedule implements ScheduleInterface
{
// Convert Sunday from 0 to 7 for consistency
$day = $weekday === '0' ? 7 : (int) $weekday;
return $this->matchesField($this->cronExpression->getDayOfWeek(), $day, 0, 7);
}
@@ -150,6 +153,7 @@ final readonly class CronSchedule implements ScheduleInterface
// Handle ranges (e.g., 1-5)
if (str_contains($field, '-')) {
[$start, $end] = explode('-', $field);
return $value >= (int) $start && $value <= (int) $end;
}
@@ -166,6 +170,7 @@ final readonly class CronSchedule implements ScheduleInterface
if (str_contains($base, '-')) {
[$start, $end] = explode('-', $base);
$startValue = (int) $start;
return $value >= $startValue &&
$value <= (int) $end &&
($value - $startValue) % $stepValue === 0;
@@ -173,12 +178,14 @@ final readonly class CronSchedule implements ScheduleInterface
// Single value with step
$baseValue = (int) $base;
return $value >= $baseValue && ($value - $baseValue) % $stepValue === 0;
}
// Handle comma-separated values (e.g., 1,3,5)
if (str_contains($field, ',')) {
$values = array_map('intval', explode(',', $field));
return in_array($value, $values, true);
}
@@ -194,4 +201,4 @@ final readonly class CronSchedule implements ScheduleInterface
return $value === $fieldValue;
}
}
}

View File

@@ -18,7 +18,8 @@ final readonly class IntervalSchedule implements ScheduleInterface
public function __construct(
public Duration $interval,
public ?Timestamp $startTime = null
) {}
) {
}
public static function fromEvery(Every $every): self
{
@@ -119,7 +120,7 @@ final readonly class IntervalSchedule implements ScheduleInterface
'type' => $this->getType(),
'interval_seconds' => $this->interval->toSeconds(),
'start_time' => $this->startTime?->toRfc3339(),
'description' => $this->getDescription()
'description' => $this->getDescription(),
];
}
@@ -154,15 +155,18 @@ final readonly class IntervalSchedule implements ScheduleInterface
if ($seconds < 3600) {
$minutes = (int) ($seconds / 60);
return "{$minutes} minute" . ($minutes !== 1 ? 's' : '');
}
if ($seconds < 86400) {
$hours = (int) ($seconds / 3600);
return "{$hours} hour" . ($hours !== 1 ? 's' : '');
}
$days = (int) ($seconds / 86400);
return "{$days} day" . ($days !== 1 ? 's' : '');
}
}
}

View File

@@ -14,7 +14,8 @@ final readonly class OneTimeSchedule implements ScheduleInterface
{
public function __construct(
public Timestamp $executeAt
) {}
) {
}
public static function at(Timestamp $timestamp): self
{
@@ -24,6 +25,7 @@ final readonly class OneTimeSchedule implements ScheduleInterface
public static function in(int $seconds): self
{
$executeTime = Timestamp::fromFloat(time() + $seconds);
return new self(executeAt: $executeTime);
}
@@ -59,7 +61,7 @@ final readonly class OneTimeSchedule implements ScheduleInterface
return [
'type' => $this->getType(),
'execute_at' => $this->executeAt->format('c'),
'description' => $this->getDescription()
'description' => $this->getDescription(),
];
}
}
}

View File

@@ -21,7 +21,8 @@ final class SchedulerService
public function __construct(
private readonly Logger $logger
) {}
) {
}
/**
* Register a scheduled task
@@ -39,7 +40,7 @@ final class SchedulerService
'task_id' => $taskId,
'schedule_type' => $schedule->getType(),
'description' => $schedule->getDescription(),
'next_execution' => $this->scheduledTasks[$taskId]->nextExecution?->format('c')
'next_execution' => $this->scheduledTasks[$taskId]->nextExecution?->format('c'),
]));
}
@@ -48,14 +49,14 @@ final class SchedulerService
*/
public function unschedule(string $taskId): bool
{
if (!isset($this->scheduledTasks[$taskId])) {
if (! isset($this->scheduledTasks[$taskId])) {
return false;
}
unset($this->scheduledTasks[$taskId]);
$this->logger->info('Task unscheduled', LogContext::withData([
'task_id' => $taskId
'task_id' => $taskId,
]));
return true;
@@ -119,7 +120,7 @@ final class SchedulerService
try {
$this->logger->info('Executing scheduled task', LogContext::withData([
'task_id' => $task->id,
'schedule_type' => $task->schedule->getType()
'schedule_type' => $task->schedule->getType(),
]));
// Execute the task
@@ -134,7 +135,7 @@ final class SchedulerService
$this->logger->info('Scheduled task completed successfully', LogContext::withData([
'task_id' => $task->id,
'execution_time_seconds' => $duration,
'next_execution' => $nextExecution?->format('c')
'next_execution' => $nextExecution?->format('c'),
]));
return TaskExecutionResult::success(
@@ -150,7 +151,7 @@ final class SchedulerService
$this->logger->error('Scheduled task failed', LogContext::withData([
'task_id' => $task->id,
'error' => $e->getMessage(),
'execution_time_seconds' => $duration
'execution_time_seconds' => $duration,
]));
// Still update next execution time for recurring tasks
@@ -184,7 +185,7 @@ final class SchedulerService
$nextExecution = null;
foreach ($this->scheduledTasks as $task) {
if ($task->nextExecution && (!$nextExecution || $task->nextExecution->isBefore($nextExecution))) {
if ($task->nextExecution && (! $nextExecution || $task->nextExecution->isBefore($nextExecution))) {
$nextExecution = $task->nextExecution;
}
}
@@ -201,7 +202,7 @@ final class SchedulerService
'total_tasks' => count($this->scheduledTasks),
'due_tasks' => count($this->getDueTasks()),
'next_execution' => $this->getNextExecution()?->format('c'),
'schedule_types' => []
'schedule_types' => [],
];
foreach ($this->scheduledTasks as $task) {
@@ -211,4 +212,4 @@ final class SchedulerService
return $stats;
}
}
}

View File

@@ -170,6 +170,7 @@ final readonly class CronExpression
if ($start < $min || $start > $max || $end < $min || $end > $max || $start > $end) {
throw new \InvalidArgumentException("Invalid range in {$fieldName}: {$field}");
}
return;
}
@@ -189,6 +190,7 @@ final readonly class CronExpression
if ($step[0] !== '*') {
$this->validateField($step[0], $min, $max, $fieldName);
}
return;
}
@@ -198,11 +200,12 @@ final readonly class CronExpression
foreach ($values as $value) {
$this->validateField(trim($value), $min, $max, $fieldName);
}
return;
}
// Single numeric value
if (!is_numeric($field)) {
if (! is_numeric($field)) {
throw new \InvalidArgumentException("Invalid {$fieldName} value: {$field}");
}
@@ -218,4 +221,4 @@ final readonly class CronExpression
return; // 7 is valid for Sunday
}
}
}
}

View File

@@ -122,7 +122,7 @@ final readonly class JobSchedule
return [
'scheduled_at' => $this->scheduledAt?->toRfc3339(),
'not_before' => $this->notBefore?->toRfc3339(),
'not_after' => $this->notAfter?->toRfc3339()
'not_after' => $this->notAfter?->toRfc3339(),
];
}
@@ -149,4 +149,4 @@ final readonly class JobSchedule
throw new \InvalidArgumentException('scheduledAt cannot be after notAfter');
}
}
}
}

View File

@@ -20,7 +20,7 @@ final readonly class ScheduledTask
public ?Timestamp $lastExecution = null,
public int $executionCount = 0
) {
if (!is_callable($this->task)) {
if (! is_callable($this->task)) {
throw new \InvalidArgumentException('Task must be callable');
}
}
@@ -29,7 +29,7 @@ final readonly class ScheduledTask
{
$at = $at ?? Timestamp::now();
if (!$this->nextExecution) {
if (! $this->nextExecution) {
return false;
}
@@ -67,7 +67,7 @@ final readonly class ScheduledTask
'schedule' => $this->schedule->toArray(),
'next_execution' => $this->nextExecution?->toRfc3339(),
'last_execution' => $this->lastExecution?->toRfc3339(),
'execution_count' => $this->executionCount
'execution_count' => $this->executionCount,
];
}
}
}

View File

@@ -19,7 +19,8 @@ final readonly class TaskExecutionResult
public ?\Throwable $error = null,
public ?Timestamp $nextExecution = null,
public ?Timestamp $executedAt = null
) {}
) {
}
public static function success(
string $taskId,
@@ -60,7 +61,7 @@ final readonly class TaskExecutionResult
public function isFailure(): bool
{
return !$this->success;
return ! $this->success;
}
public function hasNextExecution(): bool
@@ -82,7 +83,7 @@ final readonly class TaskExecutionResult
'executed_at' => $this->executedAt->toRfc3339(),
'next_execution' => $this->nextExecution?->toRfc3339(),
'error_message' => $this->getErrorMessage(),
'has_result' => $this->result !== null
'has_result' => $this->result !== null,
];
}
}
}