Laravel 13 提供了强大的任务调度监控能力,本文介绍如何构建完整的调度监控系统。

任务调度监控概述

监控指标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<?php

namespace App\Services\Scheduling;

use Illuminate\Support\Facades\Cache;

class ScheduleMetrics
{
protected string $prefix = 'schedule:metrics:';

public function recordStart(string $taskName): void
{
Cache::put($this->prefix . $taskName . ':started_at', now(), 3600);
Cache::increment($this->prefix . $taskName . ':runs_today');
}

public function recordSuccess(string $taskName, float $duration): void
{
Cache::put($this->prefix . $taskName . ':last_success', now(), 3600);
Cache::increment($this->prefix . $taskName . ':success_count');
Cache::put($this->prefix . $taskName . ':last_duration', $duration, 3600);
}

public function recordFailure(string $taskName, string $error): void
{
Cache::put($this->prefix . $taskName . ':last_failure', now(), 3600);
Cache::increment($this->prefix . $taskName . ':failure_count');
Cache::put($this->prefix . $taskName . ':last_error', $error, 3600);
}

public function getMetrics(string $taskName): array
{
return [
'last_success' => Cache::get($this->prefix . $taskName . ':last_success'),
'last_failure' => Cache::get($this->prefix . $taskName . ':last_failure'),
'success_count' => Cache::get($this->prefix . $taskName . ':success_count', 0),
'failure_count' => Cache::get($this->prefix . $taskName . ':failure_count', 0),
'last_duration' => Cache::get($this->prefix . $taskName . ':last_duration'),
'last_error' => Cache::get($this->prefix . $taskName . ':last_error'),
'runs_today' => Cache::get($this->prefix . $taskName . ':runs_today', 0),
];
}
}

任务执行记录

数据库记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class ScheduledTaskExecution extends Model
{
protected $fillable = [
'task_name',
'command',
'started_at',
'finished_at',
'duration',
'status',
'output',
'error_message',
'memory_usage',
'server_hostname',
];

protected $casts = [
'started_at' => 'datetime',
'finished_at' => 'datetime',
'duration' => 'float',
'memory_usage' => 'integer',
];

public function scopeSuccessful($query)
{
return $query->where('status', 'success');
}

public function scopeFailed($query)
{
return $query->where('status', 'failed');
}

public function scopeToday($query)
{
return $query->whereDate('started_at', today());
}

public function scopeForTask($query, string $taskName)
{
return $query->where('task_name', $taskName);
}
}

执行追踪服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
<?php

namespace App\Services\Scheduling;

use App\Models\ScheduledTaskExecution;
use Illuminate\Support\Facades\Log;

class TaskExecutionTracker
{
protected ?ScheduledTaskExecution $execution = null;
protected float $startTime;
protected int $startMemory;

public function start(string $taskName, string $command): void
{
$this->startTime = microtime(true);
$this->startMemory = memory_get_usage(true);

$this->execution = ScheduledTaskExecution::create([
'task_name' => $taskName,
'command' => $command,
'started_at' => now(),
'status' => 'running',
'server_hostname' => gethostname(),
]);

Log::info("Task started: {$taskName}", [
'execution_id' => $this->execution->id,
]);
}

public function success(string $output = null): void
{
if (!$this->execution) {
return;
}

$this->execution->update([
'finished_at' => now(),
'duration' => microtime(true) - $this->startTime,
'memory_usage' => memory_get_usage(true) - $this->startMemory,
'status' => 'success',
'output' => $output,
]);

Log::info("Task completed: {$this->execution->task_name}", [
'execution_id' => $this->execution->id,
'duration' => $this->execution->duration,
]);
}

public function failure(\Throwable $exception, string $output = null): void
{
if (!$this->execution) {
return;
}

$this->execution->update([
'finished_at' => now(),
'duration' => microtime(true) - $this->startTime,
'memory_usage' => memory_get_usage(true) - $this->startMemory,
'status' => 'failed',
'error_message' => $exception->getMessage(),
'output' => $output,
]);

Log::error("Task failed: {$this->execution->task_name}", [
'execution_id' => $this->execution->id,
'error' => $exception->getMessage(),
]);
}

public function getExecution(): ?ScheduledTaskExecution
{
return $this->execution;
}
}

调度监控中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php

namespace App\Console\Middleware;

use App\Services\Scheduling\TaskExecutionTracker;
use Closure;

class MonitorScheduledTask
{
public function __construct(
protected TaskExecutionTracker $tracker
) {}

public function handle($command, Closure $next)
{
$taskName = $command->getName() ?? get_class($command);

$this->tracker->start($taskName, $command->getName() ?? 'unknown');

try {
$result = $next($command);

$this->tracker->success();

return $result;
} catch (\Throwable $e) {
$this->tracker->failure($e);
throw $e;
}
}
}

健康检查

任务健康检查器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<?php

namespace App\Services\Scheduling;

use App\Models\ScheduledTaskExecution;
use Carbon\Carbon;

class TaskHealthChecker
{
public function check(string $taskName, array $config = []): array
{
$config = array_merge([
'max_duration' => 3600,
'max_failures' => 3,
'expected_frequency' => 'daily',
], $config);

$lastExecution = ScheduledTaskExecution::forTask($taskName)
->orderBy('started_at', 'desc')
->first();

$recentFailures = ScheduledTaskExecution::forTask($taskName)
->failed()
->where('started_at', '>=', now()->subHours(24))
->count();

$issues = [];

if (!$lastExecution) {
$issues[] = 'Task has never been executed';
} else {
if ($lastExecution->status === 'failed') {
$issues[] = 'Last execution failed: ' . $lastExecution->error_message;
}

if ($lastExecution->duration > $config['max_duration']) {
$issues[] = "Execution took too long: {$lastExecution->duration}s";
}

$expectedRun = $this->getExpectedLastRun($config['expected_frequency']);
if ($lastExecution->started_at < $expectedRun) {
$issues[] = 'Task has not run as expected';
}
}

if ($recentFailures >= $config['max_failures']) {
$issues[] = "Too many recent failures: {$recentFailures}";
}

return [
'task_name' => $taskName,
'status' => empty($issues) ? 'healthy' : 'unhealthy',
'issues' => $issues,
'last_execution' => $lastExecution?->started_at,
'recent_failures' => $recentFailures,
];
}

protected function getExpectedLastRun(string $frequency): Carbon
{
return match ($frequency) {
'every_minute' => now()->subMinutes(5),
'every_five_minutes' => now()->subMinutes(15),
'hourly' => now()->subHours(2),
'daily' => now()->subHours(25),
'weekly' => now()->subDays(8),
'monthly' => now()->subDays(32),
default => now()->subHours(25),
};
}
}

批量健康检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
<?php

namespace App\Console\Commands;

use App\Services\Scheduling\TaskHealthChecker;
use Illuminate\Console\Command;

class ScheduleHealthCheckCommand extends Command
{
protected $signature = 'schedule:health-check {--alert : Send alerts for unhealthy tasks}';
protected $description = 'Check health of scheduled tasks';

protected array $taskConfigs = [
'reports:generate' => [
'max_duration' => 1800,
'expected_frequency' => 'daily',
],
'backup:full' => [
'max_duration' => 3600,
'expected_frequency' => 'daily',
],
'analytics:aggregate' => [
'max_duration' => 600,
'expected_frequency' => 'hourly',
],
];

public function handle(TaskHealthChecker $checker): int
{
$results = [];
$unhealthy = [];

foreach ($this->taskConfigs as $taskName => $config) {
$result = $checker->check($taskName, $config);
$results[$taskName] = $result;

if ($result['status'] === 'unhealthy') {
$unhealthy[] = $result;
}

$this->displayResult($result);
}

if ($this->option('alert') && !empty($unhealthy)) {
$this->sendAlerts($unhealthy);
}

return empty($unhealthy) ? self::SUCCESS : self::FAILURE;
}

protected function displayResult(array $result): void
{
$status = $result['status'] === 'healthy' ? '<info>✓</info>' : '<error>✗</error>';
$this->line("{$status} {$result['task_name']}");

if (!empty($result['issues'])) {
foreach ($result['issues'] as $issue) {
$this->line(" - {$issue}");
}
}
}

protected function sendAlerts(array $unhealthy): void
{
foreach ($unhealthy as $task) {
$this->error("Alert: {$task['task_name']} is unhealthy");
}
}
}

监控仪表板数据

统计服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
<?php

namespace App\Services\Scheduling;

use App\Models\ScheduledTaskExecution;
use Illuminate\Support\Facades\DB;

class ScheduleStatistics
{
public function getOverview(): array
{
$today = today();

return [
'total_executions_today' => ScheduledTaskExecution::whereDate('started_at', $today)->count(),
'successful_today' => ScheduledTaskExecution::whereDate('started_at', $today)
->successful()
->count(),
'failed_today' => ScheduledTaskExecution::whereDate('started_at', $today)
->failed()
->count(),
'average_duration_today' => ScheduledTaskExecution::whereDate('started_at', $today)
->whereNotNull('duration')
->avg('duration'),
];
}

public function getTaskStatistics(string $taskName, int $days = 7): array
{
$startDate = now()->subDays($days);

$executions = ScheduledTaskExecution::forTask($taskName)
->where('started_at', '>=', $startDate)
->get();

return [
'total_executions' => $executions->count(),
'successful' => $executions->where('status', 'success')->count(),
'failed' => $executions->where('status', 'failed')->count(),
'average_duration' => $executions->avg('duration'),
'max_duration' => $executions->max('duration'),
'min_duration' => $executions->min('duration'),
'average_memory' => $executions->avg('memory_usage'),
'success_rate' => $executions->count() > 0
? round($executions->where('status', 'success')->count() / $executions->count() * 100, 2)
: 0,
];
}

public function getDailyTrend(int $days = 30): array
{
return ScheduledTaskExecution::select(
DB::raw('DATE(started_at) as date'),
DB::raw('COUNT(*) as total'),
DB::raw('SUM(CASE WHEN status = "success" THEN 1 ELSE 0 END) as successful'),
DB::raw('SUM(CASE WHEN status = "failed" THEN 1 ELSE 0 END) as failed'),
DB::raw('AVG(duration) as avg_duration')
)
->where('started_at', '>=', now()->subDays($days))
->groupBy('date')
->orderBy('date')
->get()
->toArray();
}

public function getSlowestTasks(int $limit = 10): array
{
return ScheduledTaskExecution::select('task_name', DB::raw('AVG(duration) as avg_duration'))
->where('started_at', '>=', now()->subDays(7))
->groupBy('task_name')
->orderByDesc('avg_duration')
->limit($limit)
->get()
->toArray();
}

public function getMostFailingTasks(int $limit = 10): array
{
return ScheduledTaskExecution::select(
'task_name',
DB::raw('COUNT(*) as total'),
DB::raw('SUM(CASE WHEN status = "failed" THEN 1 ELSE 0 END) as failures')
)
->where('started_at', '>=', now()->subDays(7))
->groupBy('task_name')
->having('failures', '>', 0)
->orderByDesc('failures')
->limit($limit)
->get()
->toArray();
}
}

告警系统

告警配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<?php

namespace App\Services\Scheduling;

class AlertConfig
{
protected array $rules = [];

public function addRule(string $name, callable $condition, callable $handler): self
{
$this->rules[$name] = [
'condition' => $condition,
'handler' => $handler,
];

return $this;
}

public function check(array $metrics): array
{
$alerts = [];

foreach ($this->rules as $name => $rule) {
if ($rule['condition']($metrics)) {
$alerts[] = [
'rule' => $name,
'handler' => $rule['handler'],
];
}
}

return $alerts;
}

public static function default(): self
{
$config = new self();

$config->addRule(
'high_failure_rate',
fn($m) => $m['failure_count'] > 3,
fn($m) => new Alert('high_failure_rate', "High failure rate detected", $m)
);

$config->addRule(
'slow_execution',
fn($m) => $m['last_duration'] > 300,
fn($m) => new Alert('slow_execution', "Slow execution detected", $m)
);

$config->addRule(
'task_stuck',
fn($m) => $m['last_success'] && now()->diffInHours($m['last_success']) > 25,
fn($m) => new Alert('task_stuck', "Task appears stuck", $m)
);

return $config;
}
}

告警通知

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<?php

namespace App\Services\Scheduling;

use App\Mail\ScheduleAlertMail;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Notification;

class AlertNotifier
{
protected array $channels;

public function __construct()
{
$this->channels = config('scheduling.alert_channels', ['mail']);
}

public function send(Alert $alert): void
{
foreach ($this->channels as $channel) {
match ($channel) {
'mail' => $this->sendMail($alert),
'slack' => $this->sendSlack($alert),
'webhook' => $this->sendWebhook($alert),
default => null,
};
}
}

protected function sendMail(Alert $alert): void
{
$recipients = config('scheduling.alert_emails', ['admin@example.com']);

Mail::to($recipients)
->send(new ScheduleAlertMail($alert));
}

protected function sendSlack(Alert $alert): void
{
Notification::route('slack', config('scheduling.slack_webhook'))
->notify(new ScheduleAlertNotification($alert));
}

protected function sendWebhook(Alert $alert): void
{
$webhookUrl = config('scheduling.alert_webhook');

if ($webhookUrl) {
Http::post($webhookUrl, $alert->toArray());
}
}
}

监控命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
<?php

namespace App\Console\Commands;

use App\Services\Scheduling\ScheduleStatistics;
use Illuminate\Console\Command;

class ScheduleMonitorCommand extends Command
{
protected $signature = 'schedule:monitor
{--task= : Specific task to monitor}
{--days=7 : Number of days to analyze}
{--format=table : Output format (table|json)}';
protected $description = 'Monitor scheduled task performance';

public function handle(ScheduleStatistics $stats): int
{
if ($task = $this->option('task')) {
return $this->monitorTask($stats, $task);
}

return $this->monitorAll($stats);
}

protected function monitorTask(ScheduleStatistics $stats, string $task): int
{
$statistics = $stats->getTaskStatistics($task, (int) $this->option('days'));

if ($this->option('format') === 'json') {
$this->line(json_encode($statistics, JSON_PRETTY_PRINT));
return self::SUCCESS;
}

$this->info("Task: {$task}");
$this->newLine();

$this->table(
['Metric', 'Value'],
[
['Total Executions', $statistics['total_executions']],
['Successful', $statistics['successful']],
['Failed', $statistics['failed']],
['Success Rate', $statistics['success_rate'] . '%'],
['Avg Duration', round($statistics['average_duration'] ?? 0, 2) . 's'],
['Max Duration', round($statistics['max_duration'] ?? 0, 2) . 's'],
['Avg Memory', $this->formatBytes($statistics['average_memory'] ?? 0)],
]
);

return self::SUCCESS;
}

protected function monitorAll(ScheduleStatistics $stats): int
{
$overview = $stats->getOverview();
$slowest = $stats->getSlowestTasks();
$failing = $stats->getMostFailingTasks();

$this->info('=== Schedule Overview ===');
$this->table(
['Metric', 'Value'],
[
['Total Today', $overview['total_executions_today']],
['Successful Today', $overview['successful_today']],
['Failed Today', $overview['failed_today']],
['Avg Duration', round($overview['average_duration_today'] ?? 0, 2) . 's'],
]
);

$this->newLine();
$this->info('=== Slowest Tasks (Last 7 Days) ===');
$this->table(['Task', 'Avg Duration'], $slowest);

$this->newLine();
$this->info('=== Most Failing Tasks (Last 7 Days) ===');
$this->table(['Task', 'Total', 'Failures'], $failing);

return self::SUCCESS;
}

protected function formatBytes(int $bytes): string
{
$units = ['B', 'KB', 'MB', 'GB'];
$power = $bytes > 0 ? floor(log($bytes, 1024)) : 0;
return number_format($bytes / pow(1024, $power), 2) . ' ' . $units[$power];
}
}

总结

Laravel 13 的任务调度监控系统需要结合数据库记录、健康检查、统计分析和告警系统来构建。通过完善的监控体系,可以及时发现和解决任务调度中的问题,确保后台任务稳定运行。