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
44
45
46
47
48
49
50
51
<?php

namespace App\Services\ErrorTracking;

enum ErrorType: string
{
case EXCEPTION = 'exception';
case ERROR = 'error';
case WARNING = 'warning';
case NOTICE = 'notice';
case DEPRECATED = 'deprecated';

public function getSeverity(): string
{
return match ($this) {
self::EXCEPTION => 'high',
self::ERROR => 'high',
self::WARNING => 'medium',
self::NOTICE => 'low',
self::DEPRECATED => 'low',
};
}
}

enum ErrorCategory: string
{
case DATABASE = 'database';
case EXTERNAL_API = 'external_api';
case VALIDATION = 'validation';
case AUTHENTICATION = 'authentication';
case AUTHORIZATION = 'authorization';
case FILE_SYSTEM = 'file_system';
case CACHE = 'cache';
case QUEUE = 'queue';
case UNKNOWN = 'unknown';

public function getLabel(): string
{
return match ($this) {
self::DATABASE => 'Database Error',
self::EXTERNAL_API => 'External API Error',
self::VALIDATION => 'Validation Error',
self::AUTHENTICATION => 'Authentication Error',
self::AUTHORIZATION => 'Authorization Error',
self::FILE_SYSTEM => 'File System Error',
self::CACHE => 'Cache Error',
self::QUEUE => 'Queue Error',
self::UNKNOWN => 'Unknown Error',
};
}
}

错误记录模型

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
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class ErrorRecord extends Model
{
protected $fillable = [
'fingerprint',
'type',
'category',
'severity',
'message',
'exception_class',
'file',
'line',
'stack_trace',
'request_url',
'request_method',
'request_headers',
'request_body',
'user_id',
'session_id',
'ip_address',
'user_agent',
'environment',
'occurred_at',
'resolved_at',
'resolution_note',
];

protected $casts = [
'request_headers' => 'array',
'request_body' => 'array',
'occurred_at' => 'datetime',
'resolved_at' => 'datetime',
];

public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}

public function scopeUnresolved($query)
{
return $query->whereNull('resolved_at');
}

public function scopeByFingerprint($query, string $fingerprint)
{
return $query->where('fingerprint', $fingerprint);
}

public function scopeRecent($query, int $hours = 24)
{
return $query->where('occurred_at', '>=', now()->subHours($hours));
}

public function scopeBySeverity($query, string $severity)
{
return $query->where('severity', $severity);
}

public function resolve(string $note = null): void
{
$this->update([
'resolved_at' => now(),
'resolution_note' => $note,
]);
}

public function isResolved(): bool
{
return $this->resolved_at !== null;
}
}

错误追踪服务

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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
<?php

namespace App\Services\ErrorTracking;

use App\Models\ErrorRecord;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Request;
use Throwable;

class ErrorTracker
{
protected ErrorContextCollector $contextCollector;
protected ErrorFingerprinter $fingerprinter;
protected ErrorNotifier $notifier;

public function __construct(
ErrorContextCollector $contextCollector,
ErrorFingerprinter $fingerprinter,
ErrorNotifier $notifier
) {
$this->contextCollector = $contextCollector;
$this->fingerprinter = $fingerprinter;
$this->notifier = $notifier;
}

public function capture(Throwable $exception, array $context = []): ErrorRecord
{
$fingerprint = $this->fingerprinter->generate($exception);
$category = $this->categorize($exception);

$record = ErrorRecord::create([
'fingerprint' => $fingerprint,
'type' => ErrorType::EXCEPTION->value,
'category' => $category->value,
'severity' => $this->determineSeverity($exception),
'message' => $exception->getMessage(),
'exception_class' => get_class($exception),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
'stack_trace' => $exception->getTraceAsString(),
'request_url' => Request::fullUrl(),
'request_method' => Request::method(),
'request_headers' => $this->filterHeaders(Request::headers()->all()),
'request_body' => $this->filterRequestBody(Request::all()),
'user_id' => Auth::id(),
'session_id' => session()->getId(),
'ip_address' => Request::ip(),
'user_agent' => Request::userAgent(),
'environment' => app()->environment(),
'occurred_at' => now(),
]);

$this->notifier->notifyIfNeeded($record, $exception);

return $record;
}

protected function categorize(Throwable $exception): ErrorCategory
{
$class = get_class($exception);

return match (true) {
str_contains($class, 'Database') ||
str_contains($class, 'Query') ||
str_contains($class, 'SQL') => ErrorCategory::DATABASE,

str_contains($class, 'Http') ||
str_contains($class, 'Guzzle') ||
str_contains($class, 'Api') => ErrorCategory::EXTERNAL_API,

str_contains($class, 'Validation') ||
str_contains($class, 'Validator') => ErrorCategory::VALIDATION,

str_contains($class, 'Authentication') ||
str_contains($class, 'Auth') => ErrorCategory::AUTHENTICATION,

str_contains($class, 'Authorization') ||
str_contains($class, 'Permission') => ErrorCategory::AUTHORIZATION,

str_contains($class, 'File') ||
str_contains($class, 'Storage') => ErrorCategory::FILE_SYSTEM,

str_contains($class, 'Cache') => ErrorCategory::CACHE,

str_contains($class, 'Queue') ||
str_contains($class, 'Job') => ErrorCategory::QUEUE,

default => ErrorCategory::UNKNOWN,
};
}

protected function determineSeverity(Throwable $exception): string
{
if ($exception instanceof \Throwable && method_exists($exception, 'getSeverity')) {
$severity = $exception->getSeverity();

return match ($severity) {
E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR => 'critical',
E_WARNING, E_CORE_WARNING, E_COMPILE_WARNING, E_USER_WARNING => 'high',
E_NOTICE, E_USER_NOTICE, E_STRICT, E_RECOVERABLE_ERROR => 'medium',
E_DEPRECATED, E_USER_DEPRECATED => 'low',
default => 'medium',
};
}

$class = get_class($exception);

if (str_contains($class, 'Fatal') || str_contains($class, 'Critical')) {
return 'critical';
}

if (str_contains($class, 'Error')) {
return 'high';
}

return 'medium';
}

protected function filterHeaders(array $headers): array
{
$sensitive = ['authorization', 'cookie', 'php-auth-pw', 'x-api-key'];

return collect($headers)
->map(function ($value, $key) use ($sensitive) {
if (in_array(strtolower($key), $sensitive)) {
return '[FILTERED]';
}
return $value;
})
->toArray();
}

protected function filterRequestBody(array $body): array
{
$sensitive = ['password', 'password_confirmation', 'token', 'secret', 'api_key'];

return collect($body)
->map(function ($value, $key) use ($sensitive) {
if (in_array(strtolower($key), $sensitive)) {
return '[FILTERED]';
}
return $value;
})
->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\ErrorTracking;

use Throwable;

class ErrorFingerprinter
{
public function generate(Throwable $exception): string
{
$components = [
get_class($exception),
$exception->getMessage(),
$exception->getFile(),
$exception->getLine(),
];

$stackTrace = $exception->getTrace();

$relevantFrames = array_slice($stackTrace, 0, 5);

foreach ($relevantFrames as $frame) {
$components[] = ($frame['class'] ?? '') . ($frame['type'] ?? '') . ($frame['function'] ?? '');
}

return md5(implode('|', $components));
}

public function generateGroupingKey(Throwable $exception): string
{
$components = [
get_class($exception),
$this->normalizeMessage($exception->getMessage()),
];

$stackTrace = $exception->getTrace();

$appFrames = array_filter($stackTrace, function ($frame) {
$file = $frame['file'] ?? '';
return str_contains($file, app_path());
});

foreach (array_slice($appFrames, 0, 3) as $frame) {
$components[] = $frame['file'] ?? '';
$components[] = $frame['line'] ?? '';
}

return md5(implode('|', $components));
}

protected function normalizeMessage(string $message): string
{
$message = preg_replace('/\d+/', '*', $message);
$message = preg_replace('/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/i', '*', $message);
$message = preg_replace('/0x[a-f0-9]+/i', '*', $message);

return $message;
}
}

错误上下文收集

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
93
94
95
96
97
98
99
100
101
102
103
104
<?php

namespace App\Services\ErrorTracking;

use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Request;

class ErrorContextCollector
{
public function collect(): array
{
return [
'environment' => app()->environment(),
'php_version' => PHP_VERSION,
'laravel_version' => app()->version(),
'timestamp' => now()->toIso8601String(),
'request' => $this->collectRequest(),
'user' => $this->collectUser(),
'server' => $this->collectServer(),
'runtime' => $this->collectRuntime(),
];
}

protected function collectRequest(): array
{
return [
'url' => Request::fullUrl(),
'method' => Request::method(),
'route' => Request::route()?->getName(),
'action' => Request::route()?->getActionName(),
'headers' => $this->filterSensitive(Request::headers()->all()),
'query' => Request::query()->all(),
'body' => $this->filterSensitive(Request::all()),
'ip' => Request::ip(),
'user_agent' => Request::userAgent(),
];
}

protected function collectUser(): ?array
{
$user = Auth::user();

if (!$user) {
return null;
}

return [
'id' => $user->id,
'email' => $user->email,
'name' => $user->name ?? null,
'roles' => $user->roles->pluck('name')->toArray() ?? [],
];
}

protected function collectServer(): array
{
return [
'hostname' => gethostname(),
'os' => PHP_OS_FAMILY,
'server_software' => $_SERVER['SERVER_SOFTWARE'] ?? null,
'document_root' => $_SERVER['DOCUMENT_ROOT'] ?? null,
];
}

protected function collectRuntime(): array
{
return [
'memory_usage' => memory_get_usage(true),
'memory_peak' => memory_get_peak_usage(true),
'execution_time' => defined('LARAVEL_START')
? microtime(true) - LARAVEL_START
: null,
];
}

protected function filterSensitive(array $data): array
{
$sensitiveKeys = [
'password',
'password_confirmation',
'token',
'secret',
'api_key',
'authorization',
'cookie',
];

return collect($data)
->map(function ($value, $key) use ($sensitiveKeys) {
foreach ($sensitiveKeys as $sensitive) {
if (str_contains(strtolower($key), $sensitive)) {
return '[FILTERED]';
}
}

if (is_array($value)) {
return $this->filterSensitive($value);
}

return $value;
})
->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
88
89
90
91
92
93
94
95
96
97
98
99
100
<?php

namespace App\Services\ErrorTracking;

use App\Models\ErrorRecord;
use App\Notifications\CriticalErrorNotification;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Facades\RateLimiter;

class ErrorNotifier
{
protected array $config;

public function __construct()
{
$this->config = config('error_tracking.notifications', []);
}

public function notifyIfNeeded(ErrorRecord $record, $exception): void
{
if (!$this->shouldNotify($record)) {
return;
}

$this->sendNotification($record);
}

protected function shouldNotify(ErrorRecord $record): bool
{
if (!$this->config['enabled'] ?? false) {
return false;
}

$minSeverity = $this->config['min_severity'] ?? 'high';
$severityLevels = ['low' => 1, 'medium' => 2, 'high' => 3, 'critical' => 4];

if (($severityLevels[$record->severity] ?? 0) < ($severityLevels[$minSeverity] ?? 0)) {
return false;
}

$rateLimitKey = "error_notify:{$record->fingerprint}";
$maxNotifications = $this->config['rate_limit'] ?? 5;

if (RateLimiter::tooManyAttempts($rateLimitKey, $maxNotifications)) {
return false;
}

RateLimiter::hit($rateLimitKey, 3600);

return true;
}

protected function sendNotification(ErrorRecord $record): void
{
$channels = $this->config['channels'] ?? ['mail'];

foreach ($channels as $channel) {
match ($channel) {
'mail' => $this->sendMailNotification($record),
'slack' => $this->sendSlackNotification($record),
'webhook' => $this->sendWebhookNotification($record),
default => null,
};
}
}

protected function sendMailNotification(ErrorRecord $record): void
{
$recipients = $this->config['mail_recipients'] ?? ['admin@example.com'];

Notification::route('mail', $recipients)
->notify(new CriticalErrorNotification($record));
}

protected function sendSlackNotification(ErrorRecord $record): void
{
$webhook = $this->config['slack_webhook'] ?? null;

if (!$webhook) {
return;
}

Notification::route('slack', $webhook)
->notify(new CriticalErrorNotification($record));
}

protected function sendWebhookNotification(ErrorRecord $record): void
{
$webhook = $this->config['webhook_url'] ?? null;

if (!$webhook) {
return;
}

\Illuminate\Support\Facades\Http::post($webhook, [
'event' => 'error_occurred',
'data' => $record->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
<?php

namespace App\Services\ErrorTracking;

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

class ErrorAggregator
{
public function getGroupedErrors(int $hours = 24): array
{
return ErrorRecord::select([
'fingerprint',
'exception_class',
'message',
'file',
'line',
'severity',
DB::raw('COUNT(*) as occurrence_count'),
DB::raw('MAX(occurred_at) as last_occurrence'),
DB::raw('MIN(occurred_at) as first_occurrence'),
])
->recent($hours)
->groupBy('fingerprint', 'exception_class', 'message', 'file', 'line', 'severity')
->orderByDesc('occurrence_count')
->get()
->toArray();
}

public function getErrorTrend(int $days = 7): array
{
return ErrorRecord::select([
DB::raw('DATE(occurred_at) as date'),
DB::raw('COUNT(*) as total'),
DB::raw('SUM(CASE WHEN severity = "critical" THEN 1 ELSE 0 END) as critical'),
DB::raw('SUM(CASE WHEN severity = "high" THEN 1 ELSE 0 END) as high'),
DB::raw('SUM(CASE WHEN severity = "medium" THEN 1 ELSE 0 END) as medium'),
])
->where('occurred_at', '>=', now()->subDays($days))
->groupBy('date')
->orderBy('date')
->get()
->toArray();
}

public function getTopErrors(int $limit = 10): array
{
return ErrorRecord::select([
'fingerprint',
'exception_class',
'message',
DB::raw('COUNT(*) as count'),
])
->unresolved()
->recent(24)
->groupBy('fingerprint', 'exception_class', 'message')
->orderByDesc('count')
->limit($limit)
->get()
->toArray();
}

public function getAffectedUsers(string $fingerprint): array
{
return ErrorRecord::select(['user_id', DB::raw('COUNT(*) as count')])
->byFingerprint($fingerprint)
->whereNotNull('user_id')
->groupBy('user_id')
->orderByDesc('count')
->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
<?php

namespace App\Http\Middleware;

use App\Services\ErrorTracking\ErrorTracker;
use Closure;
use Throwable;

class TrackErrors
{
public function __construct(
protected ErrorTracker $tracker
) {}

public function handle($request, Closure $next)
{
try {
return $next($request);
} catch (Throwable $e) {
$this->tracker->capture($e, [
'request_id' => $request->header('X-Request-ID'),
]);

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
<?php

namespace App\Console\Commands;

use App\Services\ErrorTracking\ErrorAggregator;
use Illuminate\Console\Command;

class ErrorReportCommand extends Command
{
protected $signature = 'error:report
{--hours=24 : Hours to include in report}
{--format=table : Output format}';
protected $description = 'Generate error tracking report';

public function handle(ErrorAggregator $aggregator): int
{
$hours = (int) $this->option('hours');

$this->info("=== Error Report (Last {$hours} hours) ===");
$this->newLine();

$grouped = $aggregator->getGroupedErrors($hours);

$this->info('Top Errors:');
$this->table(
['Count', 'Type', 'Message', 'Severity'],
collect($grouped)->take(10)->map(fn($error) => [
$error['occurrence_count'],
class_basename($error['exception_class']),
str_limit($error['message'], 50),
$error['severity'],
])
);

$this->newLine();

$trend = $aggregator->getErrorTrend(7);

$this->info('7-Day Trend:');
$this->table(
['Date', 'Total', 'Critical', 'High', 'Medium'],
collect($trend)->map(fn($day) => [
$day['date'],
$day['total'],
$day['critical'],
$day['high'],
$day['medium'],
])
);

return self::SUCCESS;
}
}

总结

Laravel 13 的错误追踪系统需要结合错误分类、指纹生成、上下文收集和通知机制来构建。通过完善的错误追踪体系,可以快速发现、定位和解决生产环境中的问题,提高系统稳定性。