日志

简介

为了帮你了解应用里发生了什么,Laravel 提供了一套完善的日志服务,可以把日志写到文件、系统错误日志,甚至发到 Slack 通知整个团队。

Laravel 的日志基于「通道(channel)」来组织。每个通道代表一种特定的日志写入方式。比如 single 通道把日志写到一个文件里,slack 通道把日志消息发到 Slack。日志消息可以根据严重程度写入不同的通道。

底层用的是 Monolog 库,它支持多种强大的日志处理器(handler)。Laravel 让你很方便地配置这些处理器,可以随意组合来定制应用的日志处理方式。

配置

控制应用日志行为的所有配置项都在 config/logging.php 配置文件里。这个文件让你配置应用的日志通道,建议仔细看看每个可用通道及其选项。下面介绍几个常用选项。

默认情况下,Laravel 记录日志时用的是 stack 通道。stack 通道用来把多个日志通道聚合到一个通道里。更多关于构建 stack 的信息,参见下文。

可用通道驱动

每个日志通道由一个「驱动」驱动。驱动决定了日志消息实际记录的方式和位置。以下日志通道驱动在每个 Laravel 应用中都可以使用。大部分驱动在你应用的 config/logging.php 配置文件里已经有了对应条目,建议看看这个文件熟悉一下:

名称 说明
custom 调用指定工厂来创建通道的驱动。
daily 基于 RotatingFileHandler 的 Monolog 驱动,按天轮转。
errorlog 基于 ErrorLogHandler 的 Monolog 驱动。
monolog Monolog 工厂驱动,可以使用任何受支持的 Monolog 处理器。
papertrail 基于 SyslogUdpHandler 的 Monolog 驱动。
single 基于单个文件或路径的日志通道(StreamHandler)。
slack 基于 SlackWebhookHandler 的 Monolog 驱动。
stack 用于创建「多通道」通道的包装器。
syslog 基于 SyslogHandler 的 Monolog 驱动。

信息

想了解 monolog 和 custom 驱动,可以看看高级通道定制的文档。

配置通道名称

默认情况下,Monolog 实例化时会带一个和当前环境匹配的「通道名称」,比如 production 或 local。想改这个值,可以在通道配置里加一个 name 选项:

'stack' => [
    'driver' => 'stack',
    'name' => 'channel-name',
    'channels' => ['single', 'slack'],
],

通道前置条件

配置 Single 和 Daily 通道

single 和 daily 通道有三个可选配置项:bubble、permission 和 locking。

名称 说明 默认值
bubble 指示消息被处理后是否冒泡传递给其他通道。 true
locking 写入日志文件前尝试加锁。 false
permission 日志文件的权限。 0644

此外,daily 通道的保留策略可以通过 LOG_DAILY_DAYS 环境变量或 days 配置项来配置。

名称 说明 默认值
days 每日日志文件保留的天数。 14

配置 Papertrail 通道

papertrail 通道需要 host 和 port 配置项。可以通过 PAPERTRAIL_URL 和 PAPERTRAIL_PORT 环境变量来定义。这些值可以从 Papertrail 获取。

配置 Slack 通道

slack 通道需要 url 配置项。这个值可以通过 LOG_SLACK_WEBHOOK_URL 环境变量来定义。这个 URL 应该匹配你为 Slack 团队配置的 incoming webhook URL。

默认情况下,Slack 只会收到 critical 及以上级别的日志;不过你可以通过 LOG_LEVEL 环境变量或在 Slack 日志通道的配置数组里修改 level 配置项来调整。

记录弃用警告

PHP、Laravel 和其他库经常会通知用户某些功能已被弃用,将在未来版本中移除。如果你想记录这些弃用警告,可以通过 LOG_DEPRECATIONS_CHANNEL 环境变量或应用的 config/logging.php 配置文件指定首选的 deprecations 日志通道:

'deprecations' => [
    'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
    'trace' => env('LOG_DEPRECATIONS_TRACE', false),
],

'channels' => [
    // ...
]

或者,你也可以定义一个名为 deprecations 的日志通道。如果存在这个名字的日志通道,它就会自动用来记录弃用警告:

'channels' => [
    'deprecations' => [
        'driver' => 'single',
        'path' => storage_path('logs/php-deprecation-warnings.log'),
    ],
],

构建 Log Stack

前面提到过,stack 驱动可以把多个通道组合成一个日志通道,方便使用。来看一个生产应用中可能见过的示例配置:

'channels' => [
    'stack' => [
        'driver' => 'stack',
        'channels' => ['syslog', 'slack'], // [tl! add]
        'ignore_exceptions' => false,
    ],

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

    'slack' => [
        'driver' => 'slack',
        'url' => env('LOG_SLACK_WEBHOOK_URL'),
        'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'),
        'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),
        'level' => env('LOG_LEVEL', 'critical'),
        'replace_placeholders' => true,
    ],
],

来拆解一下这个配置。首先,stack 通道通过 channels 选项聚合了另外两个通道:syslog 和 slack。所以记录日志消息时,这两个通道都有机会记录。不过,正如下面会讲到的,这些通道是否真的记录消息,取决于消息的严重程度即「级别」。

日志级别

注意上面示例中 syslog 和 slack 通道配置里的 level 选项。这个选项决定了消息要达到的最低「级别」才会被该通道记录。驱动 Laravel 日志服务的 Monolog 提供了 RFC 5424 规范 定义的所有日志级别。按严重程度从高到低依次是:emergency、alert、critical、error、warning、notice、info 和 debug。

假设我们用 debug 方法记录一条消息:

Log::debug('An informational message.');

按照上面的配置,syslog 通道会把消息写入系统日志;但因为错误消息不是 critical 或更高级别,所以不会发到 Slack。但如果我们记录一条 emergency 消息,它会同时写入系统日志和 Slack,因为 emergency 级别高于两个通道的最低级别阈值:

Log::emergency('The system is down!');

写日志

你可以用 Log 门面来写日志。前面提到过,日志器提供了 RFC 5424 规范定义的八个日志级别:emergency、alert、critical、error、warning、notice、info 和 debug:

use Illuminate\Support\Facades\Log;

Log::emergency($message);
Log::alert($message);
Log::critical($message);
Log::error($message);
Log::warning($message);
Log::notice($message);
Log::info($message);
Log::debug($message);

调用这些方法就能记录对应级别的日志。默认情况下,消息会写入 logging 配置文件中配置的默认日志通道:

<?php

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Support\Facades\Log;
use Illuminate\View\View;

class UserController extends Controller
{
    /**
     * Show the profile for the given user.
     */
    public function show(string $id): View
    {
        Log::info('Showing the user profile for user: {id}', ['id' => $id]);

        return view('user.profile', [
            'user' => User::findOrFail($id)
        ]);
    }
}

上下文信息

可以给日志方法传一个上下文数据数组。这些上下文数据会被格式化后跟日志消息一起显示:

use Illuminate\Support\Facades\Log;

Log::info('User {id} failed to login.', ['id' => $user->id]);

有时候你可能想给某个通道的所有后续日志条目都附带一些上下文信息。比如,你可能想记录每个请求关联的请求 ID。为此,可以调用 Log 门面的 withContext() 方法:

<?php

namespace App\Http\Middleware;

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

class AssignRequestId
{
    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next): Response
    {
        $requestId = (string) Str::uuid();

        Log::withContext([
            'request-id' => $requestId
        ]);

        $response = $next($request);

        $response->headers->set('Request-Id', $requestId);

        return $response;
    }
}

如果想在_所有_日志通道之间共享上下文信息,可以调用 Log::shareContext() 方法。这个方法会把上下文信息提供给所有已创建的通道以及后续创建的通道:

<?php

namespace App\Http\Middleware;

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

class AssignRequestId
{
    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next): Response
    {
        $requestId = (string) Str::uuid();

        Log::shareContext([
            'request-id' => $requestId
        ]);

        // ...
    }
}

信息

如果你需要在处理队列任务时共享日志上下文,可以使用任务中间件。

写入指定通道

有时候你可能想把消息写到应用默认通道之外的其他通道。可以用 Log 门面的 channel() 方法获取配置文件中定义的任意通道并写入:

use Illuminate\Support\Facades\Log;

Log::channel('slack')->info('Something happened!');

如果想创建一个由多个通道组成的临时日志栈,可以用 stack() 方法:

Log::stack(['single', 'slack'])->info('Something happened!');

按需通道

也可以在运行时提供配置来创建按需通道,而不需要在应用的 logging 配置文件中预先定义。为此,可以给 Log 门面的 build() 方法传一个配置数组:

use Illuminate\Support\Facades\Log;

Log::build([
  'driver' => 'single',
  'path' => storage_path('logs/custom.log'),
])->info('Something happened!');

你还可以把按需通道加入按需日志栈。把按需通道实例放到传给 stack() 方法的数组里即可:

use Illuminate\Support\Facades\Log;

$channel = Log::build([
  'driver' => 'single',
  'path' => storage_path('logs/custom.log'),
]);

Log::stack(['slack', $channel])->info('Something happened!');

Monolog 通道定制

为通道定制 Monolog

有时候你可能需要对现有通道的 Monolog 配置进行完全控制。比如,你可能想为 Laravel 内置的 single 通道配置一个自定义的 Monolog FormatterInterface 实现。

首先,在通道配置中定义一个 tap 数组。tap 数组应该包含一系列类名,这些类会在 Monolog 实例创建后有机会定制(即「tap into」)它。这些类放在哪里没有固定约定,你可以在应用里随意建个目录来放:

'single' => [
    'driver' => 'single',
    'tap' => [App\Logging\CustomizeFormatter::class],
    'path' => storage_path('logs/laravel.log'),
    'level' => env('LOG_LEVEL', 'debug'),
    'replace_placeholders' => true,
],

配置好 tap 选项后,就可以定义定制 Monolog 实例的类了。这个类只需要一个方法:__invoke(),接收一个 Illuminate\Log\Logger 实例。Illuminate\Log\Logger 实例会把所有方法调用代理到底层的 Monolog 实例:

<?php

namespace App\Logging;

use Illuminate\Log\Logger;
use Monolog\Formatter\LineFormatter;

class CustomizeFormatter
{
    /**
     * Customize the given logger instance.
     */
    public function __invoke(Logger $logger): void
    {
        foreach ($logger->getHandlers() as $handler) {
            $handler->setFormatter(new LineFormatter(
                '[%datetime%] %channel%.%level_name%: %message% %context% %extra%'
            ));
        }
    }
}

信息

所有「tap」类都由服务容器解析,所以它们需要的构造函数依赖会自动注入。

创建 Monolog 处理器通道

Monolog 有很多可用的处理器,Laravel 并没有为每一个都内置通道。有时候你可能想创建一个自定义通道,仅仅是为了使用某个没有对应 Laravel 日志驱动的 Monolog 处理器。用 monolog 驱动可以轻松创建这类通道。

使用 monolog 驱动时,handler 配置选项用来指定要实例化哪个处理器。可选地,处理器需要的构造函数参数可以通过 handler_with 配置选项指定:

'logentries' => [
    'driver'  => 'monolog',
    'handler' => Monolog\Handler\SyslogUdpHandler::class,
    'handler_with' => [
        'host' => 'my.logentries.internal.datahubhost.company.com',
        'port' => '10000',
    ],
],

Monolog 格式化器

使用 monolog 驱动时,Monolog 的 LineFormatter 会作为默认格式化器。不过,你可以用 formatter 和 formatter_with 配置选项来自定义传给处理器的格式化器类型:

'browser' => [
    'driver' => 'monolog',
    'handler' => Monolog\Handler\BrowserConsoleHandler::class,
    'formatter' => Monolog\Formatter\HtmlFormatter::class,
    'formatter_with' => [
        'dateFormat' => 'Y-m-d',
    ],
],

如果你用的 Monolog 处理器能够自己提供格式化器,可以把 formatter 配置选项设为 default:

'newrelic' => [
    'driver' => 'monolog',
    'handler' => Monolog\Handler\NewRelicHandler::class,
    'formatter' => 'default',
],

Monolog 处理器

Monolog 还可以在记录日志前处理消息。你可以创建自己的处理器,也可以使用 Monolog 已有的处理器。

如果你想为 monolog 驱动自定义处理器,在通道配置中加一个 processors 配置值:

'memory' => [
    'driver' => 'monolog',
    'handler' => Monolog\Handler\StreamHandler::class,
    'handler_with' => [
        'stream' => 'php://stderr',
    ],
    'processors' => [
        // Simple syntax...
        Monolog\Processor\MemoryUsageProcessor::class,

        // With options...
        [
            'processor' => Monolog\Processor\PsrLogMessageProcessor::class,
            'with' => ['removeUsedContextFields' => true],
        ],
    ],
],

通过工厂创建自定义通道

如果你想定义一个完全自定义的通道,对 Monolog 的实例化和配置进行完全控制,可以在 config/logging.php 配置文件中指定 custom 驱动类型。你的配置应该包含一个 via 选项,其值是要调用来创建 Monolog 实例的工厂类名:

'channels' => [
    'example-custom-channel' => [
        'driver' => 'custom',
        'via' => App\Logging\CreateCustomLogger::class,
    ],
],

配置好 custom 驱动通道后,就可以定义创建 Monolog 实例的类了。这个类只需要一个 __invoke() 方法,返回 Monolog logger 实例。该方法接收通道配置数组作为唯一参数:

<?php

namespace App\Logging;

use Monolog\Logger;

class CreateCustomLogger
{
    /**
     * Create a custom Monolog instance.
     */
    public function __invoke(array $config): Logger
    {
        return new Logger(/* ... */);
    }
}

用 Pail 实时追踪日志

经常需要实时追踪应用的日志。比如调试问题时,或者监控应用日志中的特定错误类型时。

Laravel Pail 是一个包,让你可以直接从命令行轻松查看 Laravel 应用的日志文件。和标准的 tail 命令不同,Pail 可以配合任何日志驱动使用,包括 Laravel Nightwatch、Sentry 或 Flare。此外,Pail 还提供了一组实用的过滤器,帮你快速找到需要的内容。

安装

注意

Laravel Pail 需要 PCNTL PHP 扩展。

首先用 Composer 包管理器把 Pail 装到项目中:

composer require --dev laravel/pail

用法

运行 pail 命令开始追踪日志:

php artisan pail

增加输出详细程度、避免截断(…),用 -v 选项:

php artisan pail -v

要最大详细程度并显示异常堆栈跟踪,用 -vv 选项:

php artisan pail -vv

随时按 Ctrl+C 停止追踪日志。

过滤日志

--filter

用 --filter 选项按类型、文件、消息和堆栈跟踪内容过滤日志:

php artisan pail --filter="QueryException"

--message

要只按消息内容过滤日志,用 --message 选项:

php artisan pail --message="User created"

--level

--level 选项可以按日志级别过滤日志:

php artisan pail --level=error

--user

只想显示某个已认证用户操作期间写入的日志,把用户 ID 传给 --user 选项:

php artisan pail --user=1