Laravel 13 日志系统详解

日志是应用程序调试和监控的重要组成部分。Laravel 13 提供了强大而灵活的日志系统,基于 Monolog 构建,支持多种日志处理器和通道。

日志配置

基本配置

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
// config/logging.php
return [
'default' => env('LOG_CHANNEL', 'stack'),

'channels' => [
'stack' => [
'driver' => 'stack',
'channels' => ['single', 'slack'],
'ignore_exceptions' => false,
],

'single' => [
'driver' => 'single',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
'replace_placeholders' => true,
],

'daily' => [
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
'days' => 14,
'replace_placeholders' => true,
],

'syslog' => [
'driver' => 'syslog',
'level' => env('LOG_LEVEL', 'debug'),
'facility' => LOG_USER,
],

'errorlog' => [
'driver' => 'errorlog',
'level' => env('LOG_LEVEL', 'debug'),
],

'null' => [
'driver' => 'monolog',
'handler' => Monolog\Handler\NullHandler::class,
],

'emergency' => [
'path' => storage_path('logs/laravel.log'),
],
],
];

日志级别

PSR-3 日志级别

1
2
3
4
5
6
7
8
9
10
use Illuminate\Support\Facades\Log;

Log::emergency($message, $context = []);
Log::alert($message, $context = []);
Log::critical($message, $context = []);
Log::error($message, $context = []);
Log::warning($message, $context = []);
Log::notice($message, $context = []);
Log::info($message, $context = []);
Log::debug($message, $context = []);

级别说明

1
2
3
4
5
6
7
8
Log::emergency('系统不可用');
Log::alert('必须立即采取行动');
Log::critical('严重错误');
Log::error('运行时错误');
Log::warning('警告信息');
Log::notice('普通但重要的事件');
Log::info('感兴趣的信息');
Log::debug('调试信息');

写入日志

基本写入

1
2
3
4
5
use Illuminate\Support\Facades\Log;

Log::info('用户登录', ['user_id' => 1]);
Log::error('支付失败', ['order_id' => 123, 'error' => 'Insufficient funds']);
Log::warning('缓存未命中', ['key' => 'user:1:profile']);

指定通道

1
2
3
Log::channel('daily')->info('每日日志');
Log::channel('slack')->error('发送到 Slack');
Log::stack(['single', 'daily'])->info('写入多个通道');

上下文信息

1
2
3
4
5
6
Log::info('订单创建', [
'order_id' => $order->id,
'user_id' => $user->id,
'total' => $order->total,
'items' => $order->items->count(),
]);

自定义日志通道

Monolog 处理器

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
// config/logging.php
'channels' => [
'custom' => [
'driver' => 'monolog',
'handler' => Monolog\Handler\StreamHandler::class,
'path' => storage_path('logs/custom.log'),
'level' => 'debug',
],

'redis' => [
'driver' => 'monolog',
'handler' => Monolog\Handler\RedisHandler::class,
'constructor' => [
'redisClient' => app('redis'),
'key' => 'laravel:logs',
'level' => 'debug',
],
],

'udp' => [
'driver' => 'monolog',
'handler' => Monolog\Handler\UdpSocketHandler::class,
'constructor' => [
'host' => 'logs.example.com',
'port' => 514,
],
],
],

自定义处理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php

namespace App\Logging;

use Monolog\Handler\AbstractProcessingHandler;
use Monolog\LogRecord;

class DatabaseHandler extends AbstractProcessingHandler
{
protected function write(LogRecord $record): void
{
\DB::table('logs')->insert([
'level' => $record->level->value,
'message' => $record->formatted,
'context' => json_encode($record->context),
'created_at' => now(),
]);
}
}

注册自定义处理器

1
2
3
4
5
6
7
// config/logging.php
'channels' => [
'database' => [
'driver' => 'custom',
'via' => App\Logging\DatabaseLogger::class,
],
],
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php

namespace App\Logging;

use Monolog\Logger;

class DatabaseLogger
{
public function __invoke(array $config): Logger
{
$logger = new Logger('database');
$logger->pushHandler(new DatabaseHandler($config));

return $logger;
}
}

日志格式化

自定义格式化器

1
2
3
4
5
6
7
8
9
// config/logging.php
'channels' => [
'custom' => [
'driver' => 'monolog',
'handler' => Monolog\Handler\StreamHandler::class,
'formatter' => Monolog\Formatter\JsonFormatter::class,
'path' => storage_path('logs/custom.log'),
],
],

自定义格式化类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php

namespace App\Logging\Formatters;

use Monolog\Formatter\NormalizerFormatter;
use Monolog\LogRecord;

class CustomFormatter extends NormalizerFormatter
{
public function format(LogRecord $record): string
{
$data = [
'timestamp' => $record->datetime->format('Y-m-d H:i:s'),
'level' => $record->level->getName(),
'message' => $record->message,
'context' => $record->context,
'extra' => $record->extra,
];

return json_encode($data, JSON_PRETTY_PRINT) . "\n";
}
}

日志处理器

Slack 通知

1
2
3
4
5
6
7
8
9
10
// config/logging.php
'channels' => [
'slack' => [
'driver' => 'slack',
'url' => env('LOG_SLACK_WEBHOOK_URL'),
'username' => 'Laravel Log',
'emoji' => ':boom:',
'level' => 'critical',
],
],

Papertrail

1
2
3
4
5
6
7
8
9
10
'channels' => [
'papertrail' => [
'driver' => 'monolog',
'handler' => Monolog\Handler\SyslogUdpHandler::class,
'constructor' => [
'host' => 'logs.papertrailapp.com',
'port' => 12345,
],
],
],

Loggly

1
2
3
4
5
6
7
8
9
'channels' => [
'loggly' => [
'driver' => 'monolog',
'handler' => Monolog\Handler\LogglyHandler::class,
'constructor' => [
'token' => env('LOGGLY_TOKEN'),
],
],
],

日志中间件

请求日志中间件

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

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;

class LogRequests
{
public function handle(Request $request, Closure $next): Response
{
$startTime = microtime(true);

$response = $next($request);

$duration = round((microtime(true) - $startTime) * 1000, 2);

Log::channel('requests')->info('HTTP Request', [
'method' => $request->method(),
'url' => $request->fullUrl(),
'ip' => $request->ip(),
'user_agent' => $request->userAgent(),
'status' => $response->getStatusCode(),
'duration_ms' => $duration,
'user_id' => $request->user()?->id,
]);

return $response;
}
}

日志上下文

全局上下文

1
2
3
4
5
6
7
8
9
10
// AppServiceProvider.php
use Illuminate\Support\Facades\Log;

public function boot(): void
{
Log::shareContext([
'environment' => app()->environment(),
'app_version' => config('app.version'),
]);
}

请求上下文

1
2
3
4
5
// 中间件中添加上下文
Log::withContext([
'request_id' => Str::uuid()->toString(),
'user_id' => $request->user()?->id,
]);

清除上下文

1
2
Log::withoutContext()->info('No context');
Log::flushSharedContext();

日志处理

日志处理器

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

namespace App\Services;

use Illuminate\Support\Facades\Log;

class LogService
{
public function logException(\Throwable $exception, array $context = []): void
{
Log::error($exception->getMessage(), array_merge($context, [
'exception' => get_class($exception),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
'trace' => $exception->getTraceAsString(),
]));
}

public function logPerformance(string $operation, float $duration, array $context = []): void
{
Log::channel('performance')->info($operation, array_merge($context, [
'duration_ms' => round($duration * 1000, 2),
]));
}
}

日志查看

使用 Artisan 命令

1
2
php artisan log:clear
php artisan log:view

自定义日志命令

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\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;

class LogView extends Command
{
protected $signature = 'log:view {--lines=100}';
protected $description = 'View recent log entries';

public function handle(): int
{
$path = storage_path('logs/laravel.log');

if (! File::exists($path)) {
$this->error('Log file not found');
return 1;
}

$content = File::tail($path, $this->option('lines'));
$this->output->write($content);

return 0;
}
}

最佳实践

1. 使用合适的日志级别

1
2
3
4
5
6
7
8
// 好的做法
Log::error('数据库连接失败', ['error' => $e->getMessage()]);
Log::info('用户登录', ['user_id' => $user->id]);
Log::debug('SQL 查询', ['sql' => $query->toSql()]);

// 不好的做法
Log::info('数据库连接失败');
Log::debug('用户登录');

2. 添加有意义的上下文

1
2
3
4
5
6
7
8
9
10
// 好的做法
Log::error('支付失败', [
'order_id' => $order->id,
'user_id' => $user->id,
'amount' => $order->total,
'error' => $e->getMessage(),
]);

// 不好的做法
Log::error('支付失败');

3. 使用不同的通道

1
2
3
4
5
6
7
8
9
// 好的做法
Log::channel('security')->warning('可疑登录', ['ip' => $ip]);
Log::channel('performance')->info('慢查询', ['sql' => $sql]);
Log::channel('audit')->info('数据修改', ['changes' => $changes]);

// 不好的做法
Log::info('可疑登录');
Log::info('慢查询');
Log::info('数据修改');

4. 日志轮转

1
2
3
4
5
6
7
8
// 使用 daily 通道自动轮转
'channels' => [
'daily' => [
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),
'days' => 14,
],
],

总结

Laravel 13 的日志系统提供了强大而灵活的日志记录能力。通过合理使用日志级别、上下文信息和多通道配置,可以构建出完善的日志体系,帮助开发者快速定位问题、监控系统状态。记住选择合适的日志级别,添加有意义的上下文信息,并使用不同的通道来分离不同类型的日志。