Laravel 日志


Laravel 日志


日志服务提供者注册

在 app 项目实例化时就会调用 registerBaseServiceProviders() 方法注册日志服务提供者 $this->register(new \Illuminate\Log\LogServiceProvider($this));

看下日志服务提供者 Illuminate\Log\LogServiceProvider 的源码:

<?php

namespace Illuminate\Log;

use Illuminate\Support\ServiceProvider;

class LogServiceProvider extends ServiceProvider
{
    /**
     * Register the service provider.
     *
     * @return void
     */
    public function register()
    {
        $this->app->singleton('log', function () {
            return new LogManager($this->app);
        });
    }
}

具体服务是 Illuminate\Log\LogManager,为单例。

日志服务分析

app('log')->info('log', [])Log::info('log', [])logger()->info('log', []) 三种方式记录日志时会初始化。

先看一下流程:用户代码 -> Log门面(代理LogManager) -> LogManager的日志方法(如info) -> 获取通道对应的Logger(Illuminate\Log\Logger实例) -> 调用Logger的info方法 -> 触发事件 -> 调用Monolog实例的info方法 -> Monolog将记录传递给处理器(如StreamHandler) -> 处理器使用格式器格式化记录并写入目标(文件等)。

配置在 app 目录下的 config/logging.php 中:

<?php

use Monolog\Handler\NullHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\SyslogUdpHandler;

return [

    /*
    |--------------------------------------------------------------------------
    | Default Log Channel
    |--------------------------------------------------------------------------
    |
    | This option defines the default log channel that gets used when writing
    | messages to the logs. The name specified in this option should match
    | one of the channels defined in the "channels" configuration array.
    |
    */

    'default' => env('LOG_CHANNEL', 'stack'),

    /*
    |--------------------------------------------------------------------------
    | Log Channels
    |--------------------------------------------------------------------------
    |
    | Here you may configure the log channels for your application. Out of
    | the box, Laravel uses the Monolog PHP logging library. This gives
    | you a variety of powerful log handlers / formatters to utilize.
    |
    | Available Drivers: "single", "daily", "slack", "syslog",
    |                    "errorlog", "monolog",
    |                    "custom", "stack"
    |
    */

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

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

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

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

        'papertrail' => [
            'driver' => 'monolog',
            'level' => 'debug',
            'handler' => SyslogUdpHandler::class,
            'handler_with' => [
                'host' => env('PAPERTRAIL_URL'),
                'port' => env('PAPERTRAIL_PORT'),
            ],
        ],

        'stderr' => [
            'driver' => 'monolog',
            'handler' => StreamHandler::class,
            'formatter' => env('LOG_STDERR_FORMATTER'),
            'with' => [
                'stream' => 'php://stderr',
            ],
        ],

        'syslog' => [
            'driver' => 'syslog',
            'level' => 'debug',
        ],

        'errorlog' => [
            'driver' => 'errorlog',
            'level' => 'debug',
        ],

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

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

];

结合提供的 LogManagerLogger 类和配置文件,看一下记录日志过程:

1. 日志调用入口

当使用 Log::info('Message')logger()->info('Message') 时:

  • Log Facade 代理到 LogManager 实例。
  • LogManager 实现了 Psr\Log\LoggerInterface,调用其方法(如 info())会触发 __call 魔术方法:
public function __call($method, $parameters) {
    return $this->driver()->$method(...$parameters);
}

public function info($message, array $context = [])
{
    $this->driver()->info($message, $context);
}

2. 获取日志驱动(Driver)

  • driver() 方法获取默认驱动(由 config/logging.phpdefault 定义,如 'stack'):
public function driver($driver = null)
{
    return $this->get($driver ?? $this->getDefaultDriver());
}
  • get() 方法解析通道:
    • 优先从缓存 $channels 中获取。
    • 不存在时通过 resolve($name) 创建并缓存。
protected function get($name)
{
    try {
        return $this->channels[$name] ?? with($this->resolve($name), function ($logger) use ($name) {
            return $this->channels[$name] = $this->tap($name, new Logger($logger, $this->app['events']));
        });
    } catch (Throwable $e) {
        return tap($this->createEmergencyLogger(), function ($logger) use ($e) {
            $logger->emergency('Unable to create configured logger. Using emergency logger.', [
                'exception' => $e,
            ]);
        });
    }
}

3. 解析日志通道(Resolve Channel)

resolve($name) 根据配置创建 Monolog 实例:

  1. 读取配置:$config = $this->configurationFor($name);(从 config/logging.channels.$name 获取)。
  2. 选择驱动创建方式:
    • 自定义驱动:通过 extend() 注册的闭包($customCreators)。
    • 内置驱动:调用形如 create{Driver}Driver 的方法(如 createStackDriver)。
    • Monolog 驱动:使用 createMonologDriver 实例化第三方处理器。
protected function configurationFor($name)
{
    return $this->app['config']["logging.channels.{$name}"];
}

public function extend($driver, Closure $callback)
{
    $this->customCreators[$driver] = $callback->bindTo($this, $this);

    return $this;
}

protected function resolve($name)
{
    $config = $this->configurationFor($name);

    if (is_null($config)) {
        throw new InvalidArgumentException("Log [{$name}] is not defined.");
    }

    if (isset($this->customCreators[$config['driver']])) {
        return $this->callCustomCreator($config);
    }

    $driverMethod = 'create'.ucfirst($config['driver']).'Driver';

    if (method_exists($this, $driverMethod)) {
        return $this->{$driverMethod}($config);
    }

    throw new InvalidArgumentException("Driver [{$config['driver']}] is not supported.");
}

4. 创建驱动实例(关键步骤)

示例:stack 驱动

protected function createStackDriver(array $config) {
    // 收集子通道的处理器
    $handlers = collect($config['channels'])->flatMap(function ($channel) {
        return $this->channel($channel)->getHandlers();
    })->all();

    // 忽略异常时用 WhatFailureGroupHandler 包装,确保单个通道失败不会影响其他通道
    if ($config['ignore_exceptions'] ?? false) {
        // WhatFailureGroupHandler 是 Laravel 提供的特殊处理器,用于隔离日志通道之间的故障
        $handlers = [new WhatFailureGroupHandler($handlers)];
    }

    // 返回包含所有处理器的 Monolog 实例
    return new Monolog($this->parseChannel($config), $handlers);
}

public function channel($channel = null)
{
    return $this->driver($channel);
}

protected function createSingleDriver(array $config)
{
    return new Monolog($this->parseChannel($config), [
        $this->prepareHandler(
            // StreamHandler 是 Monolog 提供的基础文件日志处理器
            new StreamHandler(
                $config['path'], 
                $this->level($config),
                $config['bubble'] ?? true,   // 控制日志是否向上冒泡到其他处理器
                $config['permission'] ?? null, 
                $config['locking'] ?? false
            ), $config
        ),
    ]);
}

protected function createMonologDriver(array $config)
{
    if (! is_a($config['handler'], HandlerInterface::class, true)) {
        throw new InvalidArgumentException(
            $config['handler'].' must be an instance of '.HandlerInterface::class
        );
    }

    $with = array_merge(
        ['level' => $this->level($config)],
        $config['with'] ?? [],
        $config['handler_with'] ?? []
    );

    return new Monolog($this->parseChannel($config), [$this->prepareHandler(
        $this->app->make($config['handler'], $with), $config
    )]);
}
  • 递归解析子通道(如 single):
    • single 驱动调用 createSingleDriver,创建 StreamHandler 写入文件。
    • 最终返回包含处理器的 Monolog 实例。

其他驱动创建

  • single:单文件日志(StreamHandler)。
  • daily:按日期轮转(RotatingFileHandler)。
  • slack:发送到 Slack(SlackWebhookHandler)。
  • monolog:自定义 Monolog 处理器(如 SyslogUdpHandler)。

5. 包装为 Laravel Logger

  • 将 Monolog 实例包装到 Illuminate\Log\Logger
protected function get($name) {
    return $this->channels[$name] = $this->tap($name, new Logger($logger, $this->app['events']));
}

protected function tap($name, Logger $logger)
{
    foreach ($this->configurationFor($name)['tap'] ?? [] as $tap) {
        [$class, $arguments] = $this->parseTap($tap);

        $this->app->make($class)->__invoke($logger, ...explode(',', $arguments));
    }

    return $logger;
}
  • tap() 方法应用配置中的 tap 管道(可添加自定义处理逻辑)。

6. 记录日志流程

当调用 $logger->info('Message', $context)

1、格式化消息:

protected function formatMessage($message) {
    if (is_array($message)) return var_export($message, true);
    if ($message instanceof Jsonable) return $message->toJson();
    if ($message instanceof Arrayable) return var_export($message->toArray(), true);
    return $message;
}

2、触发事件:

protected function writeLog($level, $message, $context) {
    $this->fireLogEvent($level, $message, $context); // 分发 MessageLogged 事件
    $this->logger->{$level}($message, $context); // 调用 Monolog 的方法
}

3、Monolog 处理:

  • 调用处理器链(如 StreamHandler->handle())。
  • 处理器使用 LineFormatter 格式化日志(默认包含堆栈跟踪)。
  • 写入目标(文件/Slack/系统日志等)。

7. 异常处理

  • 创建应急日志:当解析配置失败时,调用 createEmergencyLogger()
protected function get($name) {
    try { /* ... */ } catch (Throwable $e) {
        return tap($this->createEmergencyLogger(), function ($logger) use ($e) {
            $logger->emergency('Unable to create configured logger.', ['exception' => $e]);
        });
    }
}
  • 使用默认文件路径(storage/logs/laravel.log)确保日志不丢失。

关键设计亮点

  1. 灵活驱动:内置多种驱动(文件/Slack/Syslog),支持自定义扩展。
  2. 通道堆叠:通过 stack 驱动组合多个日志渠道。
  3. 事件挂钩:记录时触发 MessageLogged 事件,便于监听。
  4. 异常回退:自动降级到应急日志,避免系统崩溃。
  5. PSR-3 兼容:实现标准接口,确保与其他组件兼容。

整个过程体现了 Laravel 日志系统的模块化设计,通过解耦日志管理、通道解析、处理器封装,提供了高扩展性和可靠性。

自定义日志一

laravel中默认的一个日志驱动是single,所有日志信息都保存在logs/laravel.log一个文件中,我们需要把error错误日志写到另一个独立的文件中,这样才能对错误信息一目了然。

下面写一下。一个把错误日志写入文件logs/error_one.log的第一种示例,其中包括日志ID、日志步骤、用户ID、请求URL、请求方法、请求参数、响应状态码、响应数据、异常信息、异常位置、请求时间等。

日志处理器文件:

<?php

namespace App\Logging\Handlers;

use Monolog\Handler\AbstractProcessingHandler;
use Monolog\Logger;
use Monolog\Formatter\LineFormatter;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\File;

class ErrorOneHandler extends AbstractProcessingHandler
{
    /**
     * 日志文件路径
     *
     * @var string
     */
    protected $logPath;

    /**
     * 文件权限
     *
     * @var int|null
     */
    protected $filePermission;

    /**
     * 是否使用文件锁
     *
     * @var bool
     */
    protected $useLocking;

    /**
     * 文件句柄
     *
     * @var resource|null
     */
    protected $fileHandle;

    /**
     * 构造函数
     *
     * @param string $logPath 日志文件路径
     * @param int $level 日志级别
     * @param bool $bubble 是否向上传播
     * @param int|null $filePermission 文件权限
     * @param bool $useLocking 是否使用文件锁
     */
    public function __construct(
        $logPath,
        $level = Logger::ERROR,
        $bubble = true,
        $filePermission = null,
        $useLocking = false
    ) {
        parent::__construct($level, $bubble);

        $this->logPath = $logPath;
        $this->filePermission = $filePermission;
        $this->useLocking = $useLocking;

        // 确保日志目录存在
        $logDir = dirname($logPath);
        if (!File::exists($logDir)) {
            File::makeDirectory($logDir, 0755, true);
        }

        // 设置格式化器
        // $this->setFormatter(new LineFormatter(
        //     "[%datetime%] %channel%.%level_name%: %message% %context% %extra%\n",
        //     null,
        //     true,
        //     true
        // ));

        // 格式化器配置,使用LineFormatter,它可以将记录格式化为一行字符串
        $this->setFormatter(new LineFormatter(
            "[%datetime%] %channel%.%level_name% %context% %extra%\n", // 移除冒号和%message%
            "Y-m-d H:i:s", // 设置日期格式
            true,
            true
        ));
    }

    /**
     * 写入日志记录
     *
     * @param array $record
     */
    protected function write(array $record): void
    {
        global $log_id;
        global $log_step;

        // 添加用户 ID 到日志上下文
        $content = [
            'log_id' => $log_id,
            'log_step' => $log_step++,
            'user_id' => $this->getUserId(),
            'log_msg' => $record['message'],
            'client_ip' => $this->getClientIp(),
            'request_url' => $this->getRequestUrl(),
            'request_method' => $this->getRequestMethod(),
            'request_params' => $this->getRequestParams(),
            'response_status' => $record['context']['code'] ?? 'N/A',
            'response_data' => $this->formatResponseData($record['context']['responent'] ?? []),
            'exception_message' => $this->getExceptionMessage($record),
            'exception_location' => $this->getExceptionLocation($record),
            'request_time' => date('Y-m-d H:i:s'),
        ];
        $record['context'] = array_merge($content, $record['context']);


        // 格式化日志记录
        $formatted = $this->getFormatter()->format($record);

        // 写入文件
        $this->writeToFile($formatted);
    }

    /**
     * 获取当前用户 ID
     *
     * @return string|int
     */
    protected function getUserId()
    {
        try {
            // 尝试获取认证用户 ID
            // if (Auth::check()) {
            //     return Auth::id();
            // }
            if (!empty(session()->get('admin_id'))) {
                return session()->get('admin_id');
            }

            // 如果是命令行环境,尝试获取当前系统用户
            if (app()->runningInConsole()) {
                return get_current_user() ?: 'console';
            }

            // 如果无法获取用户 ID,返回未知
            return 'unknown';
        } catch (\Exception $e) {
            // 防止因获取用户信息失败导致日志记录失败
            return 'error_getting_user';
        }
    }

    /**
     * 获取客户端IP地址
     *
     * @return string
     */
    protected function getClientIp()
    {
        if (app()->runningInConsole()) {
            return 'console';
        }

        try {
            $request = request();
            if (!$request) {
                return 'N/A';
            }

            // 获取真实IP(考虑代理情况)
            $ip = $request->header('X-Forwarded-For') ??
                $request->header('X-Real-IP') ??
                $request->getClientIp() ?? 'N/A';

            // 如果是多个IP,取第一个
            if (strpos($ip, ',') !== false) {
                $ip = trim(explode(',', $ip)[0]);
            }

            return $ip;
        } catch (\Exception $e) {
            return 'error_getting_ip';
        }
    }

    /**
     * 获取请求URL
     */
    protected function getRequestUrl()
    {
        if (app()->runningInConsole()) {
            return 'console';
        }
        return request() ? request()->fullUrl() : 'N/A';
    }

    /**
     * 获取请求方法
     */
    protected function getRequestMethod()
    {
        if (app()->runningInConsole()) {
            return 'console';
        }
        return request() ? request()->method() : 'N/A';
    }

    /**
     * 获取请求参数(过滤敏感信息)
     */
    protected function getRequestParams()
    {
        if (app()->runningInConsole() || !request()) {
            return [];
        }

        $params = request()->all();
        // 过滤敏感字段
        $sensitive = ['password', 'token', 'secret', '_token'];
        foreach ($sensitive as $key) {
            if (isset($params[$key])) {
                $params[$key] = '***';
            }
        }
        return json_encode($params, JSON_UNESCAPED_UNICODE);
    }

    /**
     * 格式化响应数据
     */
    protected function formatResponseData($data)
    {
        if (empty($data)) {
            return 'N/A';
        }
        // 限制响应数据长度
        $json = json_encode($data, JSON_UNESCAPED_UNICODE);
        return strlen($json) > 500 ? substr($json, 0, 500) . '...' : $json;
    }

    /**
     * 获取异常消息
     */
    protected function getExceptionMessage($record)
    {
        if (isset($record['context']['exception']) && $record['context']['exception'] instanceof \Exception) {
            return $record['context']['exception']->getMessage();
        }
        return 'N/A';
    }

    /**
     * 获取异常位置
     */
    protected function getExceptionLocation($record)
    {
        if (isset($record['context']['exception']) && $record['context']['exception'] instanceof \Exception) {
            $trace = $record['context']['exception']->getTrace()[0] ?? [];
            return isset($trace['file'], $trace['line']) ? 
                $trace['file'] . ':' . $trace['line'] : 'N/A';
        }
        return 'N/A';
    }

    /**
     * 将内容写入文件
     *
     * @param string $data
     */
    protected function writeToFile($data)
    {
        // 打开文件(如果尚未打开)
        if (null === $this->fileHandle) {
            $this->openFile();
        }

        // 使用文件锁(如果需要)
        if ($this->useLocking) {
            flock($this->fileHandle, LOCK_EX);
        }

        // 写入文件
        fwrite($this->fileHandle, $data);

        // 释放文件锁(如果需要)
        if ($this->useLocking) {
            flock($this->fileHandle, LOCK_UN);
        }
    }

    /**
     * 打开日志文件
     */
    protected function openFile()
    {
        // 文件打开模式:追加模式,如果文件不存在则创建
        $fileMode = File::exists($this->logPath) ? 'a' : 'w';

        $this->fileHandle = fopen($this->logPath, $fileMode);
        if (false === $this->fileHandle) {
            throw new \RuntimeException(sprintf('无法打开日志文件 "%s"', $this->logPath));
        }

        // 设置文件权限
        if (null !== $this->filePermission) {
            @chmod($this->logPath, $this->filePermission);
        }
    }

    /**
     * 关闭文件句柄
     */
    protected function closeFile()
    {
        if (null !== $this->fileHandle) {
            fclose($this->fileHandle);
            $this->fileHandle = null;
        }
    }


    // protected function getDefaultFormatter()
    // {
    //     // 使用与 Laravel 相同的格式
    //     return new \Monolog\Formatter\LineFormatter(
    //         "[%datetime%] %channel%.%level_name%: %message% %context% %extra%\n",
    //         null,
    //         true,
    //         true
    //     );
    // }

    /**
     * 关闭处理器
     */
    public function close(): void
    {
        $this->closeFile();
        parent::close();
    }

    /**
     * 析构函数
     */
    public function __destruct()
    {
        try {
            $this->close();
        } catch (\Exception $e) {
            // 避免在析构时抛出异常
        }
    }
}

注册自定义日志处理器。

在 AppServiceProvider 中注册这个自定义处理器:

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        //
    }

    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        //

        // 错误日志处理通道一
        $this->app->make('log')->extend('error_one', function ($app, $config) {
            // 创建处理器实例
            $handler = new \App\Logging\Handlers\ErrorOneHandler(
                $config['path'] ?? storage_path('logs/error_one.log'),
                $this->level($config),
                $config['bubble'] ?? true,
                $config['permission'] ?? null,
                $config['locking'] ?? false
            );

            // 创建日志记录器
            return new \Monolog\Logger($this->parseChannel($config), [$handler]);
        });
    }

    /**
     * 日志等级
     *
     * @param array $config
     * @return void
     */
    protected function level(array $config)
    {
        $level = $config['level'] ?? 'error';

        // 日志级别映射
        $levels = [
            'debug' => \Monolog\Logger::DEBUG,
            'info' => \Monolog\Logger::INFO,
            'notice' => \Monolog\Logger::NOTICE,
            'warning' => \Monolog\Logger::WARNING,
            'error' => \Monolog\Logger::ERROR,
            'critical' => \Monolog\Logger::CRITICAL,
            'alert' => \Monolog\Logger::ALERT,
            'emergency' => \Monolog\Logger::EMERGENCY,
        ];

        return $levels[strtolower($level)] ?? \Monolog\Logger::ERROR;
    }

    /**
     * 解析通道名称
     *
     * @param  array  $config
     * @return string
     */
    protected function parseChannel(array $config)
    {
        return $config['name'] ?? $this->app->environment();
    }
}

在项目入口文件中写入加入以下代码,用于记录具体步骤:

$log_id = date('YmdHis') . uniqid() . mt_rand(100000, 999999);
$log_step = 1;

配置日志通道。

在 config/logging.php 中配置新的错误日志通道:

'channels' => [
    // 创建一个堆栈通道,同时包含常规日志和错误日志
    'stack' => [
        'driver' => 'stack',
        'channels' => ['single', 'error_one'],
        'ignore_exceptions' => false,
    ],

    // 原有的 single 通道保持不变
    'single' => [
        'driver' => 'single',
        'path' => storage_path('logs/laravel.log'),
        'level' => 'debug',
    ],

    // 新增错误日志通道
    'error_one' => [
        'driver' => 'error_one',
        'path' => storage_path('logs/error_one.log'),
        'level' => 'error', // 只记录 error 及以上级别的日志
        'bubble' => false, // 防止日志向上传播到其他处理器
        'permission' => 0664, // 文件权限
        'locking' => false, // 不使用文件锁
    ],
],

生成日志如下:

[2025-08-23 11:58:31] local.ERROR
{
  "log_id": "68a93c6798f7b494589",
  "log_step": 3,
  "user_id": 3,
  "log_msg": "SQLSTATE[42S22]: Column not found: 1054 Unknown column 'created_admin_id-1' in 'where clause' (SQL: select count(*) as aggregate from `fb_user_targeting_template` where `status` >= 0 and `created_admin_id1` = 3)",
  "client_ip": "127.0.0.1",
  "request_url": "http://t2v.cn/admin/fbbatch_user_targeting_template_api?_token=zFMSsEEyos1MxnXC34kqk7CEdxRX69Qb75668y7SY&limit=10&page=1&s=%2F%2Fadmin%2Ffbbatch_user_targeting_template_api&searchParams=%7B%22name%22%3A%22%22%7D",
  "request_method": "GET",
  "request_params": "{\"s\":\"\\/\\/admin\\/fbbatch_user_targeting_template_api\",\"_token\":\"***\",\"page\":\"1\",\"limit\":\"10\",\"searchParams\":\"{\\\"name\\\":\\\"\\\"}\"}",
  "response_status": "N/A",
  "response_data": "N/A",
  "exception_message": "SQLSTATE[42S22]: Column not found: 1054 Unknown column 'created_admin_id-1' in 'where clause' (SQL: select count(*) as aggregate from `fb_user_targeting_template` where `status` >= 0 and `created_admin_id1` = 3)",
  "exception_location": "\develop\\www\\t2v\\vendor\\laravel\\framework\\src\\Illuminate\\Database\\Connection.php:629",
  "request_time": "2025-08-23 11:58:31",
  "exception": "[object] (Illuminate\\Database\\QueryException(code: 42S22): SQLSTATE[42S22]: Column not found: 1054 Unknown column 'created_admin_id-1' in 'where clause' (SQL: select count(*) as aggregate from `fb_user_targeting_template` where `status` >= 0 and `created_admin_id1` = 3) at \develop\\www\\t2v\\vendor\\laravel\\framework\\src\\Illuminate\\Database\\Connection.php:669)\n[previous exception] [object] (PDOException(code: 42S22): SQLSTATE[42S22]: Column not found: 1054 Unknown column 'created_admin_id-1' in 'where clause' at \develop\\www\\t2v\\vendor\\laravel\\framework\\src\\Illuminate\\Database\\Connection.php:331)"
}

自定义日志二

通过上面的自定义日志,我们再写一个自定义日志类,这个日志类所写的日志格式和上面的一样,方便查看。

日志处理器文件:

<?php

namespace App\Logging\Handlers;

use Monolog\Handler\AbstractProcessingHandler;
use Monolog\Logger;
use Monolog\Formatter\LineFormatter;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\File;

class LogOneHandler extends AbstractProcessingHandler
{
    /**
     * 日志文件路径
     *
     * @var string
     */
    protected $logPath;

    /**
     * 文件权限
     *
     * @var int|null
     */
    protected $filePermission;

    /**
     * 是否使用文件锁
     *
     * @var bool
     */
    protected $useLocking;

    /**
     * 文件句柄
     *
     * @var resource|null
     */
    protected $fileHandle;

    /**
     * 构造函数
     *
     * @param string $logPath 日志文件路径
     * @param int $level 日志级别
     * @param bool $bubble 是否向上传播
     * @param int|null $filePermission 文件权限
     * @param bool $useLocking 是否使用文件锁
     */
    public function __construct(
        $logPath,
        $level = Logger::ERROR,
        $bubble = true,
        $filePermission = null,
        $useLocking = false
    ) {
        parent::__construct($level, $bubble);

        $this->logPath = $logPath;
        $this->filePermission = $filePermission;
        $this->useLocking = $useLocking;

        // 确保日志目录存在
        $logDir = dirname($logPath);
        if (!File::exists($logDir)) {
            File::makeDirectory($logDir, 0755, true);
        }

        // 格式化器配置
        $this->setFormatter(new LineFormatter(
            "[%datetime%] %channel%.%level_name% %context% %extra%\n", 
            "Y-m-d H:i:s", // 设置日期格式
            true,
            true
        ));
    }

    /**
     * 写入日志记录
     *
     * @param array $record
     */
    protected function write(array $record): void
    {
        global $log_id;
        global $log_step;

        // 添加用户 ID 到日志上下文
        $content = [
            'log_id' => $log_id,
            'log_step' => $log_step++,
            'user_id' => $this->getUserId(),
            'log_msg' => $record['message'],
            'client_ip' => $this->getClientIp(),
            'request_url' => $this->getRequestUrl(),
            'request_method' => $this->getRequestMethod(),
            'request_params' => $this->getRequestParams(),
            'response_status' => $record['context']['code'] ?? 'N/A',
            'response_data' => $this->formatResponseData($record['context']['responent'] ?? []),
            'exception_message' => $this->getExceptionMessage($record),
            'exception_location' => $this->getExceptionLocation($record),
            'request_time' => date('Y-m-d H:i:s'),
        ];
        $record['context'] = array_merge($content, $record['context']);

        // 格式化日志记录
        $formatted = $this->getFormatter()->format($record);

        // 写入文件
        $this->writeToFile($formatted);
    }

    /**
     * 获取当前用户 ID
     *
     * @return string|int
     */
    protected function getUserId()
    {
        try {
            // 尝试获取认证用户 ID
            // if (Auth::check()) {
            //     return Auth::id();
            // }
            if (!empty(session()->get('admin_id'))) {
                return session()->get('admin_id');
            }

            if (app()->runningInConsole()) {
                return get_current_user() ?: 'console';
            }

            return 'unknown';
        } catch (\Exception $e) {
            return 'error_getting_user';
        }
    }

    /**
     * 获取客户端IP地址
     *
     * @return string
     */
    protected function getClientIp()
    {
        if (app()->runningInConsole()) {
            return 'console';
        }

        try {
            $request = request();
            if (!$request) {
                return 'N/A';
            }

            // 获取真实IP(考虑代理情况)
            $ip = $request->header('X-Forwarded-For') ??
                $request->header('X-Real-IP') ??
                $request->getClientIp() ?? 'N/A';

            // 如果是多个IP,取第一个
            if (strpos($ip, ',') !== false) {
                $ip = trim(explode(',', $ip)[0]);
            }

            return $ip;
        } catch (\Exception $e) {
            return 'error_getting_ip';
        }
    }

    /**
     * 获取请求URL
     */
    protected function getRequestUrl()
    {
        if (app()->runningInConsole()) {
            return 'console';
        }
        return request() ? request()->fullUrl() : 'N/A';
    }

    /**
     * 获取请求方法
     */
    protected function getRequestMethod()
    {
        if (app()->runningInConsole()) {
            return 'console';
        }
        return request() ? request()->method() : 'N/A';
    }

    /**
     * 获取请求参数(过滤敏感信息)
     */
    protected function getRequestParams()
    {
        if (app()->runningInConsole() || !request()) {
            return [];
        }

        $params = request()->all();
        // 过滤敏感字段
        $sensitive = ['password', 'token', 'secret', '_token'];
        foreach ($sensitive as $key) {
            if (isset($params[$key])) {
                $params[$key] = '***';
            }
        }
        return json_encode($params, JSON_UNESCAPED_UNICODE);
    }

    /**
     * 格式化响应数据
     */
    protected function formatResponseData($data)
    {
        if (empty($data)) {
            return 'N/A';
        }
        // 限制响应数据长度
        $json = json_encode($data, JSON_UNESCAPED_UNICODE);
        return strlen($json) > 500 ? substr($json, 0, 500) . '...' : $json;
    }

    /**
     * 获取异常消息
     */
    protected function getExceptionMessage($record)
    {
        if (isset($record['context']['exception']) && $record['context']['exception'] instanceof \Exception) {
            return $record['context']['exception']->getMessage();
        }
        return 'N/A';
    }

    /**
     * 获取异常位置
     */
    protected function getExceptionLocation($record)
    {
        if (isset($record['context']['exception']) && $record['context']['exception'] instanceof \Exception) {
            $trace = $record['context']['exception']->getTrace()[0] ?? [];
            return isset($trace['file'], $trace['line']) ? 
                $trace['file'] . ':' . $trace['line'] : 'N/A';
        }
        return 'N/A';
    }

    /**
     * 将内容写入文件
     *
     * @param string $data
     */
    protected function writeToFile($data)
    {
        // 打开文件(如果尚未打开)
        if (null === $this->fileHandle) {
            $this->openFile();
        }

        // 使用文件锁(如果需要)
        if ($this->useLocking) {
            flock($this->fileHandle, LOCK_EX);
        }

        // 写入文件
        fwrite($this->fileHandle, $data);

        // 释放文件锁(如果需要)
        if ($this->useLocking) {
            flock($this->fileHandle, LOCK_UN);
        }
    }

    /**
     * 打开日志文件
     */
    protected function openFile()
    {
        // 文件打开模式:追加模式,如果文件不存在则创建
        $fileMode = File::exists($this->logPath) ? 'a' : 'w';

        $this->fileHandle = fopen($this->logPath, $fileMode);
        if (false === $this->fileHandle) {
            throw new \RuntimeException(sprintf('无法打开日志文件 "%s"', $this->logPath));
        }

        // 设置文件权限
        if (null !== $this->filePermission) {
            @chmod($this->logPath, $this->filePermission);
        }
    }

    /**
     * 关闭文件句柄
     */
    protected function closeFile()
    {
        if (null !== $this->fileHandle) {
            fclose($this->fileHandle);
            $this->fileHandle = null;
        }
    }

    /**
     * 关闭处理器
     */
    public function close(): void
    {
        $this->closeFile();
        parent::close();
    }

    /**
     * 析构函数
     */
    public function __destruct()
    {
        try {
            $this->close();
        } catch (\Exception $e) {
            // 避免在析构时抛出异常
        }
    }
}

注册自定义日志处理器。

在 AppServiceProvider 中注册这个自定义处理器:

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        //
    }

    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        //

        // 错误日志处理通道一
        $this->app->make('log')->extend('log_one', function ($app, $config) {
            // 创建处理器实例
            $handler = new \App\Logging\Handlers\LogOneHandler(
                $config['path'] ?? storage_path('logs/log_one.log'),
                $config['level'] ?? 'info',
                $config['bubble'] ?? true,
                $config['permission'] ?? null,
                $config['locking'] ?? false
            );

            // 创建日志记录器。$config['name'] 通道名称
            return new \Monolog\Logger($config['name'] ?? $this->app->environment(), [$handler]);
        });
    }
}

在项目入口文件中加入以下代码,用于记录具体步骤:

$log_id = date('YmdHis') . uniqid() . mt_rand(100000, 999999);
$log_step = 1;

配置日志通道。

在 config/logging.php 中配置新的错误日志通道:

'default' => env('LOG_CHANNEL', 'stack'),

'channels' => [
    // 创建一个堆栈通道,同时包含常规日志和错误日志
    'stack' => [
        'driver' => 'stack',
        'channels' => ['daily', 'log_one'],
        'ignore_exceptions' => false,
    ],

    // 日日志通道
    'daily' => [
        'driver' => 'daily',
        'path' => storage_path('logs/laravel.log'),
        'level' => 'debug',
        'days' => 14,
    ],

    // 日志一通道
    'log_one' => [
        'driver' => 'log_one',
        'path' => storage_path('logs/log_one.log'),
        'level' => 'info', 
        'bubble' => true, 
        'permission' => 0664, 
        'locking' => false, 
    ],
],

生成日志如下:

[2025-08-23 17:50:28] local.INFO
{
  "log_id": "68a98ee498c36981732",
  "log_step": 2,
  "user_id": 3,
  "log_msg": "App\\Http\\Controllers\\Controller::ret",
  "client_ip": "127.0.0.1",
  "request_url": "http://t2v.cn/admin/fbbatch_user_targeting_template_api?_token=0uatWLQXGMxrZ5uDiihOlxd3WRQxSdfnDiE1USG5&limit=10&page=1&s=%2F%2Fadmin%2Ffbbatch_user_targeting_template_api&searchParams=%7B%22name%22%3A%22%22%7D",
  "request_method": "GET",
  "request_params": "{\"s\":\"\\/\\/admin\\/fbbatch_user_targeting_template_api\",\"_token\":\"***\",\"page\":\"1\",\"limit\":\"10\",\"searchParams\":\"{\\\"name\\\":\\\"\\\"}\"}",
  "response_status": 0,
  "response_data": "{\"path\":\"admin\\/fbbatch_user_targeting_template_api\"}",
  "exception_message": "N/A",
  "exception_location": "N/A",
  "request_time": "2025-08-23 17:50:28",
  "code": 0,
  "responent": {
    "path": "admin/fbbatch_user_targeting_template_api"
  }
}

Elasticsearch日志

使用 Monolog\Logger 的Elasticsearch处理器来记录日志。

<?php

namespace App\Logging\Handlers;

use Monolog\Logger;
use Monolog\Handler\ElasticsearchHandler;
use Elasticsearch\ClientBuilder;
use Illuminate\Support\Facades\Auth;

class LaravelElasticsearchHandler extends ElasticsearchHandler
{
    /**
     * @var array
     */
    protected $defaultOptions;

    /**
     * 构造函数
     *
     * @param array $config Elasticsearch 配置
     * @param string $level 日志级别
     * @param bool $bubble 是否向上传播
     */
    public function __construct(array $config, $level = Logger::DEBUG, $bubble = true)
    {
        // 设置默认选项
        $this->defaultOptions = [
            'index' => $config['index'] ?? 'laravel_logs',
            'type' => $config['type'] ?? '_doc',
            'ignore_error' => $config['ignore_error'] ?? false,
        ];

        // 创建 Elasticsearch 客户端
        $client = ClientBuilder::create()
            ->setHosts($config['hosts'] ?? ['localhost:9200'])
            ->build();

        parent::__construct($client, $this->defaultOptions, $level, $bubble);
    }

    /**
     * 处理日志记录,添加用户信息
     *
     * @param array $record
     * @return bool
     */
    public function handle(array $record): bool
    {
        // 添加用户 ID 到日志上下文
        $record['context']['user_id'] = $this->getUserId();

        // 添加环境信息
        $record['context']['env'] = app()->environment();

        try {
            return parent::handle($record);
        } catch (\Exception $e) {
            // Elasticsearch 不可用时,回退到文件日志
            $this->fallbackToFileLog($record, $e);
            return false;
        }
    }

    /**
     * 获取当前用户 ID
     *
     * @return string|int
     */
    protected function getUserId()
    {
        try {
            // 尝试获取认证用户 ID
            if (Auth::check()) {
                return Auth::id();
            }

            // 如果是命令行环境,尝试获取当前系统用户
            if (app()->runningInConsole()) {
                return get_current_user() ?: 'console';
            }

            // 如果无法获取用户 ID,返回未知
            return 'unknown';
        } catch (\Exception $e) {
            // 防止因获取用户信息失败导致日志记录失败
            return 'error_getting_user';
        }
    }

    /**
     * 回退到文件日志
     *
     * @param array $record
     * @param \Exception $exception
     */
    protected function fallbackToFileLog(array $record, \Exception $exception)
    {
        try {
            // 使用 Laravel 的文件日志
            $message = sprintf(
                'Elasticsearch logging failed: %s. Original message: %s',
                $exception->getMessage(),
                $record['message']
            );
            
            Log::channel('single')->error($message, [
                'original_context' => $record['context'],
                'exception' => $exception->getTraceAsString()
            ]);
        } catch (\Exception $fallbackException) {
            // 终极回退:系统错误日志
            error_log(sprintf(
                "Elasticsearch logging failed and fallback also failed: %s\nOriginal message: %s",
                $exception->getMessage(),
                $record['message']
            ));
        }
    }
}

在 AppServiceProvider 中注册 Elasticsearch 日志驱动:

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Logging\Handlers\LaravelElasticsearchHandler;
use Monolog\Logger;
use Monolog\Handler\BufferHandler;
use Monolog\Handler\WhatFailureGroupHandler;

class AppServiceProvider extends ServiceProvider
{
    /**
     * 启动服务
     *
     * @return void
     */
    public function boot()
    {
        // 扩展日志系统,添加 Elasticsearch 驱动
        $this->app->make('log')->extend('elasticsearch', function ($app, $config) {
            // 创建处理器实例
            $handler = new LaravelElasticsearchHandler(
                $config, // 传递所有配置
                $this->level($config),
                $config['bubble'] ?? true
            );

            // 包装在 WhatFailureGroupHandler 中,
            // 即使 Elasticsearch 不可用,日志记录也不会抛出异常,而是静默失败(但会记录内部错误日志)
            // $handler = new WhatFailureGroupHandler([
            //     new LaravelElasticsearchHandler(
            //         $config, // 传递所有配置
            //         $this->level($config),
            //         $config['bubble'] ?? true
            //     )
            // ]);

            // 添加缓冲,每100条日志批量发送一次
            $bufferedHandler = new BufferHandler($handler, 100);

            // 创建日志记录器
            return new Logger($this->parseChannel($config), [$bufferedHandler]);
        });
    }

    /**
     * 解析日志级别
     *
     * @param  array  $config
     * @return int
     */
    protected function level(array $config)
    {
        $level = $config['level'] ?? 'debug';

        // 日志级别映射
        $levels = [
            'debug' => Logger::DEBUG,
            'info' => Logger::INFO,
            'notice' => Logger::NOTICE,
            'warning' => Logger::WARNING,
            'error' => Logger::ERROR,
            'critical' => Logger::CRITICAL,
            'alert' => Logger::ALERT,
            'emergency' => Logger::EMERGENCY,
        ];

        return $levels[strtolower($level)] ?? Logger::DEBUG;
    }

    /**
     * 解析通道名称
     *
     * @param  array  $config
     * @return string
     */
    protected function parseChannel(array $config)
    {
        return $config['name'] ?? $this->app->environment();
    }
}

配置日志通道:

'channels' => [
    // 其他通道...

    'elasticsearch' => [
        'driver' => 'elasticsearch',
        'hosts' => [
            env('ELASTICSEARCH_HOST', 'localhost:9200'),
        ],
        'index' => env('ELASTICSEARCH_INDEX', 'laravel_logs'),
        'type' => '_doc', // Elasticsearch 7.x 及以上版本使用 _doc
        'level' => 'debug',
        'bubble' => true,
        'ignore_error' => false, // 是否忽略 Elasticsearch 错误
    ],

    // 创建一个堆栈通道,同时包含文件日志和 Elasticsearch 日志
    'stack' => [
        'driver' => 'stack',
        'channels' => ['single', 'elasticsearch'],
        'ignore_exceptions' => false,
    ],
],

配置环境变量:

ELASTICSEARCH_HOST=localhost:9200
ELASTICSEARCH_INDEX=laravel_logs
LOG_CHANNEL=stack

创建 Elasticsearch 索引模板(可选)。为了确保日志数据有正确的映射,可以创建一个索引模板:

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Elasticsearch\ClientBuilder;

class CreateElasticsearchIndexTemplate extends Command
{
    protected $signature = 'elasticsearch:create-index-template';
    protected $description = 'Create Elasticsearch index template for logs';

    public function handle()
    {
        $client = ClientBuilder::create()
            ->setHosts([env('ELASTICSEARCH_HOST', 'localhost:9200')])
            ->build();

        $params = [
            'name' => 'laravel_logs_template',
            'body' => [
                'index_patterns' => ['laravel_logs*'],
                'template' => [
                    'mappings' => [
                        'properties' => [
                            '@timestamp' => ['type' => 'date'],
                            'message' => ['type' => 'text'],
                            'channel' => ['type' => 'keyword'],
                            'level' => ['type' => 'keyword'],
                            'context' => ['type' => 'object'],
                            'extra' => ['type' => 'object'],
                            'user_id' => ['type' => 'keyword'],
                            'env' => ['type' => 'keyword'],
                        ]
                    ]
                ]
            ]
        ];

        try {
            $response = $client->indices()->putTemplate($params);
            $this->info('Elasticsearch index template created successfully');
        } catch (\Exception $e) {
            $this->error('Failed to create Elasticsearch index template: ' . $e->getMessage());
        }
    }
}

源码

LogServiceProvider 类

Illuminate\Log\LogServiceProvider 源码:

<?php

namespace Illuminate\Log;

use Illuminate\Support\ServiceProvider;

class LogServiceProvider extends ServiceProvider
{
    /**
     * Register the service provider.
     *
     * @return void
     */
    public function register()
    {
        $this->app->singleton('log', function () {
            return new LogManager($this->app);
        });
    }
}

LogManager 类

Illuminate\Log\LogManager 源码:

<?php

namespace Illuminate\Log;

use Closure;
use Illuminate\Support\Str;
use InvalidArgumentException;
use Monolog\Formatter\LineFormatter;
use Monolog\Handler\ErrorLogHandler;
use Monolog\Handler\FormattableHandlerInterface;
use Monolog\Handler\HandlerInterface;
use Monolog\Handler\RotatingFileHandler;
use Monolog\Handler\SlackWebhookHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\SyslogHandler;
use Monolog\Handler\WhatFailureGroupHandler;
use Monolog\Logger as Monolog;
use Psr\Log\LoggerInterface;
use Throwable;

class LogManager implements LoggerInterface
{
    use ParsesLogConfiguration;

    /**
     * The application instance.
     *
     * @var \Illuminate\Contracts\Foundation\Application
     */
    protected $app;

    /**
     * The array of resolved channels.
     *
     * @var array
     */
    protected $channels = [];

    /**
     * The registered custom driver creators.
     *
     * @var array
     */
    protected $customCreators = [];

    /**
     * The standard date format to use when writing logs.
     *
     * @var string
     */
    protected $dateFormat = 'Y-m-d H:i:s';

    /**
     * Create a new Log manager instance.
     *
     * @param  \Illuminate\Contracts\Foundation\Application  $app
     * @return void
     */
    public function __construct($app)
    {
        $this->app = $app;
    }

    /**
     * Create a new, on-demand aggregate logger instance.
     *
     * @param  array  $channels
     * @param  string|null  $channel
     * @return \Psr\Log\LoggerInterface
     */
    public function stack(array $channels, $channel = null)
    {
        return new Logger(
            $this->createStackDriver(compact('channels', 'channel')),
            $this->app['events']
        );
    }

    /**
     * Get a log channel instance.
     *
     * @param  string|null  $channel
     * @return \Psr\Log\LoggerInterface
     */
    public function channel($channel = null)
    {
        return $this->driver($channel);
    }

    /**
     * Get a log driver instance.
     *
     * @param  string|null  $driver
     * @return \Psr\Log\LoggerInterface
     */
    public function driver($driver = null)
    {
        return $this->get($driver ?? $this->getDefaultDriver());
    }

    /**
     * @return array
     */
    public function getChannels()
    {
        return $this->channels;
    }

    /**
     * Attempt to get the log from the local cache.
     *
     * @param  string  $name
     * @return \Psr\Log\LoggerInterface
     */
    protected function get($name)
    {
        try {
            return $this->channels[$name] ?? with($this->resolve($name), function ($logger) use ($name) {
                return $this->channels[$name] = $this->tap($name, new Logger($logger, $this->app['events']));
            });
        } catch (Throwable $e) {
            return tap($this->createEmergencyLogger(), function ($logger) use ($e) {
                $logger->emergency('Unable to create configured logger. Using emergency logger.', [
                    'exception' => $e,
                ]);
            });
        }
    }

    /**
     * Apply the configured taps for the logger.
     *
     * @param  string  $name
     * @param  \Illuminate\Log\Logger  $logger
     * @return \Illuminate\Log\Logger
     */
    protected function tap($name, Logger $logger)
    {
        foreach ($this->configurationFor($name)['tap'] ?? [] as $tap) {
            [$class, $arguments] = $this->parseTap($tap);

            $this->app->make($class)->__invoke($logger, ...explode(',', $arguments));
        }

        return $logger;
    }

    /**
     * Parse the given tap class string into a class name and arguments string.
     *
     * @param  string  $tap
     * @return array
     */
    protected function parseTap($tap)
    {
        return Str::contains($tap, ':') ? explode(':', $tap, 2) : [$tap, ''];
    }

    /**
     * Create an emergency log handler to avoid white screens of death.
     *
     * @return \Psr\Log\LoggerInterface
     */
    protected function createEmergencyLogger()
    {
        $config = $this->configurationFor('emergency');

        $handler = new StreamHandler(
            $config['path'] ?? $this->app->storagePath().'/logs/laravel.log',
            $this->level(['level' => 'debug'])
        );

        return new Logger(
            new Monolog('laravel', $this->prepareHandlers([$handler])),
            $this->app['events']
        );
    }

    /**
     * Resolve the given log instance by name.
     *
     * @param  string  $name
     * @return \Psr\Log\LoggerInterface
     *
     * @throws \InvalidArgumentException
     */
    protected function resolve($name)
    {
        $config = $this->configurationFor($name);

        if (is_null($config)) {
            throw new InvalidArgumentException("Log [{$name}] is not defined.");
        }

        if (isset($this->customCreators[$config['driver']])) {
            return $this->callCustomCreator($config);
        }

        $driverMethod = 'create'.ucfirst($config['driver']).'Driver';

        if (method_exists($this, $driverMethod)) {
            return $this->{$driverMethod}($config);
        }

        throw new InvalidArgumentException("Driver [{$config['driver']}] is not supported.");
    }

    /**
     * Call a custom driver creator.
     *
     * @param  array  $config
     * @return mixed
     */
    protected function callCustomCreator(array $config)
    {
        return $this->customCreators[$config['driver']]($this->app, $config);
    }

    /**
     * Create a custom log driver instance.
     *
     * @param  array  $config
     * @return \Psr\Log\LoggerInterface
     */
    protected function createCustomDriver(array $config)
    {
        $factory = is_callable($via = $config['via']) ? $via : $this->app->make($via);

        return $factory($config);
    }

    /**
     * Create an aggregate log driver instance.
     *
     * @param  array  $config
     * @return \Psr\Log\LoggerInterface
     */
    protected function createStackDriver(array $config)
    {
        $handlers = collect($config['channels'])->flatMap(function ($channel) {
            return $this->channel($channel)->getHandlers();
        })->all();

        if ($config['ignore_exceptions'] ?? false) {
            $handlers = [new WhatFailureGroupHandler($handlers)];
        }

        return new Monolog($this->parseChannel($config), $handlers);
    }

    /**
     * Create an instance of the single file log driver.
     *
     * @param  array  $config
     * @return \Psr\Log\LoggerInterface
     */
    protected function createSingleDriver(array $config)
    {
        return new Monolog($this->parseChannel($config), [
            $this->prepareHandler(
                new StreamHandler(
                    $config['path'], $this->level($config),
                    $config['bubble'] ?? true, $config['permission'] ?? null, $config['locking'] ?? false
                ), $config
            ),
        ]);
    }

    /**
     * Create an instance of the daily file log driver.
     *
     * @param  array  $config
     * @return \Psr\Log\LoggerInterface
     */
    protected function createDailyDriver(array $config)
    {
        return new Monolog($this->parseChannel($config), [
            $this->prepareHandler(new RotatingFileHandler(
                $config['path'], $config['days'] ?? 7, $this->level($config),
                $config['bubble'] ?? true, $config['permission'] ?? null, $config['locking'] ?? false
            ), $config),
        ]);
    }

    /**
     * Create an instance of the Slack log driver.
     *
     * @param  array  $config
     * @return \Psr\Log\LoggerInterface
     */
    protected function createSlackDriver(array $config)
    {
        return new Monolog($this->parseChannel($config), [
            $this->prepareHandler(new SlackWebhookHandler(
                $config['url'],
                $config['channel'] ?? null,
                $config['username'] ?? 'Laravel',
                $config['attachment'] ?? true,
                $config['emoji'] ?? ':boom:',
                $config['short'] ?? false,
                $config['context'] ?? true,
                $this->level($config),
                $config['bubble'] ?? true,
                $config['exclude_fields'] ?? []
            ), $config),
        ]);
    }

    /**
     * Create an instance of the syslog log driver.
     *
     * @param  array  $config
     * @return \Psr\Log\LoggerInterface
     */
    protected function createSyslogDriver(array $config)
    {
        return new Monolog($this->parseChannel($config), [
            $this->prepareHandler(new SyslogHandler(
                Str::snake($this->app['config']['app.name'], '-'),
                $config['facility'] ?? LOG_USER, $this->level($config)
            ), $config),
        ]);
    }

    /**
     * Create an instance of the "error log" log driver.
     *
     * @param  array  $config
     * @return \Psr\Log\LoggerInterface
     */
    protected function createErrorlogDriver(array $config)
    {
        return new Monolog($this->parseChannel($config), [
            $this->prepareHandler(new ErrorLogHandler(
                $config['type'] ?? ErrorLogHandler::OPERATING_SYSTEM, $this->level($config)
            )),
        ]);
    }

    /**
     * Create an instance of any handler available in Monolog.
     *
     * @param  array  $config
     * @return \Psr\Log\LoggerInterface
     *
     * @throws \InvalidArgumentException
     * @throws \Illuminate\Contracts\Container\BindingResolutionException
     */
    protected function createMonologDriver(array $config)
    {
        if (! is_a($config['handler'], HandlerInterface::class, true)) {
            throw new InvalidArgumentException(
                $config['handler'].' must be an instance of '.HandlerInterface::class
            );
        }

        $with = array_merge(
            ['level' => $this->level($config)],
            $config['with'] ?? [],
            $config['handler_with'] ?? []
        );

        return new Monolog($this->parseChannel($config), [$this->prepareHandler(
            $this->app->make($config['handler'], $with), $config
        )]);
    }

    /**
     * Prepare the handlers for usage by Monolog.
     *
     * @param  array  $handlers
     * @return array
     */
    protected function prepareHandlers(array $handlers)
    {
        foreach ($handlers as $key => $handler) {
            $handlers[$key] = $this->prepareHandler($handler);
        }

        return $handlers;
    }

    /**
     * Prepare the handler for usage by Monolog.
     *
     * @param  \Monolog\Handler\HandlerInterface  $handler
     * @param  array  $config
     * @return \Monolog\Handler\HandlerInterface
     */
    protected function prepareHandler(HandlerInterface $handler, array $config = [])
    {
        $isHandlerFormattable = false;

        if (Monolog::API === 1) {
            $isHandlerFormattable = true;
        } elseif (Monolog::API === 2 && $handler instanceof FormattableHandlerInterface) {
            $isHandlerFormattable = true;
        }

        if ($isHandlerFormattable && ! isset($config['formatter'])) {
            $handler->setFormatter($this->formatter());
        } elseif ($isHandlerFormattable && $config['formatter'] !== 'default') {
            $handler->setFormatter($this->app->make($config['formatter'], $config['formatter_with'] ?? []));
        }

        return $handler;
    }

    /**
     * Get a Monolog formatter instance.
     *
     * @return \Monolog\Formatter\FormatterInterface
     */
    protected function formatter()
    {
        return tap(new LineFormatter(null, $this->dateFormat, true, true), function ($formatter) {
            $formatter->includeStacktraces();
        });
    }

    /**
     * Get fallback log channel name.
     *
     * @return string
     */
    protected function getFallbackChannelName()
    {
        return $this->app->bound('env') ? $this->app->environment() : 'production';
    }

    /**
     * Get the log connection configuration.
     *
     * @param  string  $name
     * @return array
     */
    protected function configurationFor($name)
    {
        return $this->app['config']["logging.channels.{$name}"];
    }

    /**
     * Get the default log driver name.
     *
     * @return string
     */
    public function getDefaultDriver()
    {
        return $this->app['config']['logging.default'];
    }

    /**
     * Set the default log driver name.
     *
     * @param  string  $name
     * @return void
     */
    public function setDefaultDriver($name)
    {
        $this->app['config']['logging.default'] = $name;
    }

    /**
     * Register a custom driver creator Closure.
     *
     * @param  string  $driver
     * @param  \Closure  $callback
     * @return $this
     */
    public function extend($driver, Closure $callback)
    {
        $this->customCreators[$driver] = $callback->bindTo($this, $this);

        return $this;
    }

    /**
     * Unset the given channel instance.
     *
     * @param  string|null  $driver
     * @return $this
     */
    public function forgetChannel($driver = null)
    {
        $driver = $driver ?? $this->getDefaultDriver();

        if (isset($this->channels[$driver])) {
            unset($this->channels[$driver]);
        }
    }

    /**
     * System is unusable.
     *
     * @param  string  $message
     * @param  array  $context
     * @return void
     */
    public function emergency($message, array $context = [])
    {
        $this->driver()->emergency($message, $context);
    }

    /**
     * Action must be taken immediately.
     *
     * Example: Entire website down, database unavailable, etc. This should
     * trigger the SMS alerts and wake you up.
     *
     * @param  string  $message
     * @param  array  $context
     * @return void
     */
    public function alert($message, array $context = [])
    {
        $this->driver()->alert($message, $context);
    }

    /**
     * Critical conditions.
     *
     * Example: Application component unavailable, unexpected exception.
     *
     * @param  string  $message
     * @param  array  $context
     * @return void
     */
    public function critical($message, array $context = [])
    {
        $this->driver()->critical($message, $context);
    }

    /**
     * Runtime errors that do not require immediate action but should typically
     * be logged and monitored.
     *
     * @param  string  $message
     * @param  array  $context
     * @return void
     */
    public function error($message, array $context = [])
    {
        $this->driver()->error($message, $context);
    }

    /**
     * Exceptional occurrences that are not errors.
     *
     * Example: Use of deprecated APIs, poor use of an API, undesirable things
     * that are not necessarily wrong.
     *
     * @param  string  $message
     * @param  array  $context
     * @return void
     */
    public function warning($message, array $context = [])
    {
        $this->driver()->warning($message, $context);
    }

    /**
     * Normal but significant events.
     *
     * @param  string  $message
     * @param  array  $context
     * @return void
     */
    public function notice($message, array $context = [])
    {
        $this->driver()->notice($message, $context);
    }

    /**
     * Interesting events.
     *
     * Example: User logs in, SQL logs.
     *
     * @param  string  $message
     * @param  array  $context
     * @return void
     */
    public function info($message, array $context = [])
    {
        $this->driver()->info($message, $context);
    }

    /**
     * Detailed debug information.
     *
     * @param  string  $message
     * @param  array  $context
     * @return void
     */
    public function debug($message, array $context = [])
    {
        $this->driver()->debug($message, $context);
    }

    /**
     * Logs with an arbitrary level.
     *
     * @param  mixed  $level
     * @param  string  $message
     * @param  array  $context
     * @return void
     */
    public function log($level, $message, array $context = [])
    {
        $this->driver()->log($level, $message, $context);
    }

    /**
     * Dynamically call the default driver instance.
     *
     * @param  string  $method
     * @param  array  $parameters
     * @return mixed
     */
    public function __call($method, $parameters)
    {
        return $this->driver()->$method(...$parameters);
    }
}

ParsesLogConfiguration 类

Illuminate\Log\ParsesLogConfiguration 源码:

<?php

namespace Illuminate\Log;

use InvalidArgumentException;
use Monolog\Logger as Monolog;

trait ParsesLogConfiguration
{
    /**
     * The Log levels.
     *
     * @var array
     */
    protected $levels = [
        'debug' => Monolog::DEBUG,
        'info' => Monolog::INFO,
        'notice' => Monolog::NOTICE,
        'warning' => Monolog::WARNING,
        'error' => Monolog::ERROR,
        'critical' => Monolog::CRITICAL,
        'alert' => Monolog::ALERT,
        'emergency' => Monolog::EMERGENCY,
    ];

    /**
     * Get fallback log channel name.
     *
     * @return string
     */
    abstract protected function getFallbackChannelName();

    /**
     * Parse the string level into a Monolog constant.
     *
     * @param  array  $config
     * @return int
     *
     * @throws \InvalidArgumentException
     */
    protected function level(array $config)
    {
        $level = $config['level'] ?? 'debug';

        if (isset($this->levels[$level])) {
            return $this->levels[$level];
        }

        throw new InvalidArgumentException('Invalid log level.');
    }

    /**
     * Extract the log channel from the given configuration.
     *
     * @param  array  $config
     * @return string
     */
    protected function parseChannel(array $config)
    {
        return $config['name'] ?? $this->getFallbackChannelName();
    }
}

Logger 类

Illuminate\Log\Logger 源码:

<?php

namespace Illuminate\Log;

use Closure;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Contracts\Support\Jsonable;
use Illuminate\Log\Events\MessageLogged;
use Psr\Log\LoggerInterface;
use RuntimeException;

class Logger implements LoggerInterface
{
    /**
     * The underlying logger implementation.
     *
     * @var \Psr\Log\LoggerInterface
     */
    protected $logger;

    /**
     * The event dispatcher instance.
     *
     * @var \Illuminate\Contracts\Events\Dispatcher|null
     */
    protected $dispatcher;

    /**
     * Create a new log writer instance.
     *
     * @param  \Psr\Log\LoggerInterface  $logger
     * @param  \Illuminate\Contracts\Events\Dispatcher|null  $dispatcher
     * @return void
     */
    public function __construct(LoggerInterface $logger, Dispatcher $dispatcher = null)
    {
        $this->logger = $logger;
        $this->dispatcher = $dispatcher;
    }

    /**
     * Log an emergency message to the logs.
     *
     * @param  string  $message
     * @param  array  $context
     * @return void
     */
    public function emergency($message, array $context = [])
    {
        $this->writeLog(__FUNCTION__, $message, $context);
    }

    /**
     * Log an alert message to the logs.
     *
     * @param  string  $message
     * @param  array  $context
     * @return void
     */
    public function alert($message, array $context = [])
    {
        $this->writeLog(__FUNCTION__, $message, $context);
    }

    /**
     * Log a critical message to the logs.
     *
     * @param  string  $message
     * @param  array  $context
     * @return void
     */
    public function critical($message, array $context = [])
    {
        $this->writeLog(__FUNCTION__, $message, $context);
    }

    /**
     * Log an error message to the logs.
     *
     * @param  string  $message
     * @param  array  $context
     * @return void
     */
    public function error($message, array $context = [])
    {
        $this->writeLog(__FUNCTION__, $message, $context);
    }

    /**
     * Log a warning message to the logs.
     *
     * @param  string  $message
     * @param  array  $context
     * @return void
     */
    public function warning($message, array $context = [])
    {
        $this->writeLog(__FUNCTION__, $message, $context);
    }

    /**
     * Log a notice to the logs.
     *
     * @param  string  $message
     * @param  array  $context
     * @return void
     */
    public function notice($message, array $context = [])
    {
        $this->writeLog(__FUNCTION__, $message, $context);
    }

    /**
     * Log an informational message to the logs.
     *
     * @param  string  $message
     * @param  array  $context
     * @return void
     */
    public function info($message, array $context = [])
    {
        $this->writeLog(__FUNCTION__, $message, $context);
    }

    /**
     * Log a debug message to the logs.
     *
     * @param  string  $message
     * @param  array  $context
     * @return void
     */
    public function debug($message, array $context = [])
    {
        $this->writeLog(__FUNCTION__, $message, $context);
    }

    /**
     * Log a message to the logs.
     *
     * @param  string  $level
     * @param  string  $message
     * @param  array  $context
     * @return void
     */
    public function log($level, $message, array $context = [])
    {
        $this->writeLog($level, $message, $context);
    }

    /**
     * Dynamically pass log calls into the writer.
     *
     * @param  string  $level
     * @param  string  $message
     * @param  array  $context
     * @return void
     */
    public function write($level, $message, array $context = [])
    {
        $this->writeLog($level, $message, $context);
    }

    /**
     * Write a message to the log.
     *
     * @param  string  $level
     * @param  string  $message
     * @param  array  $context
     * @return void
     */
    protected function writeLog($level, $message, $context)
    {
        $this->fireLogEvent($level, $message = $this->formatMessage($message), $context);

        $this->logger->{$level}($message, $context);
    }

    /**
     * Register a new callback handler for when a log event is triggered.
     *
     * @param  \Closure  $callback
     * @return void
     *
     * @throws \RuntimeException
     */
    public function listen(Closure $callback)
    {
        if (! isset($this->dispatcher)) {
            throw new RuntimeException('Events dispatcher has not been set.');
        }

        $this->dispatcher->listen(MessageLogged::class, $callback);
    }

    /**
     * Fires a log event.
     *
     * @param  string  $level
     * @param  string  $message
     * @param  array  $context
     * @return void
     */
    protected function fireLogEvent($level, $message, array $context = [])
    {
        // If the event dispatcher is set, we will pass along the parameters to the
        // log listeners. These are useful for building profilers or other tools
        // that aggregate all of the log messages for a given "request" cycle.
        if (isset($this->dispatcher)) {
            $this->dispatcher->dispatch(new MessageLogged($level, $message, $context));
        }
    }

    /**
     * Format the parameters for the logger.
     *
     * @param  mixed  $message
     * @return mixed
     */
    protected function formatMessage($message)
    {
        if (is_array($message)) {
            return var_export($message, true);
        } elseif ($message instanceof Jsonable) {
            return $message->toJson();
        } elseif ($message instanceof Arrayable) {
            return var_export($message->toArray(), true);
        }

        return $message;
    }

    /**
     * Get the underlying logger implementation.
     *
     * @return \Psr\Log\LoggerInterface
     */
    public function getLogger()
    {
        return $this->logger;
    }

    /**
     * Get the event dispatcher instance.
     *
     * @return \Illuminate\Contracts\Events\Dispatcher
     */
    public function getEventDispatcher()
    {
        return $this->dispatcher;
    }

    /**
     * Set the event dispatcher instance.
     *
     * @param  \Illuminate\Contracts\Events\Dispatcher  $dispatcher
     * @return void
     */
    public function setEventDispatcher(Dispatcher $dispatcher)
    {
        $this->dispatcher = $dispatcher;
    }

    /**
     * Dynamically proxy method calls to the underlying logger.
     *
     * @param  string  $method
     * @param  array  $parameters
     * @return mixed
     */
    public function __call($method, $parameters)
    {
        return $this->logger->{$method}(...$parameters);
    }
}

MessageLogged 类

Illuminate\Log\Events\MessageLogged 源码:

<?php

namespace Illuminate\Log\Events;

class MessageLogged
{
    /**
     * The log "level".
     *
     * @var string
     */
    public $level;

    /**
     * The log message.
     *
     * @var string
     */
    public $message;

    /**
     * The log context.
     *
     * @var array
     */
    public $context;

    /**
     * Create a new event instance.
     *
     * @param  string  $level
     * @param  string  $message
     * @param  array  $context
     * @return void
     */
    public function __construct($level, $message, array $context = [])
    {
        $this->level = $level;
        $this->message = $message;
        $this->context = $context;
    }
}

Handler 类

<?php declare(strict_types=1);

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

/**
 * Base Handler class providing basic close() support as well as handleBatch
 *
 * @author Jordi Boggiano <j.boggiano@seld.be>
 */
abstract class Handler implements HandlerInterface
{
    /**
     * {@inheritDoc}
     */
    public function handleBatch(array $records): void
    {
        foreach ($records as $record) {
            $this->handle($record);
        }
    }

    /**
     * {@inheritDoc}
     */
    public function close(): void
    {
    }

    public function __destruct()
    {
        try {
            $this->close();
        } catch (\Throwable $e) {
            // do nothing
        }
    }

    public function __sleep()
    {
        $this->close();

        $reflClass = new \ReflectionClass($this);

        $keys = [];
        foreach ($reflClass->getProperties() as $reflProp) {
            if (!$reflProp->isStatic()) {
                $keys[] = $reflProp->getName();
            }
        }

        return $keys;
    }
}

NullHandler 类

<?php declare(strict_types=1);

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Monolog\Logger;
use Psr\Log\LogLevel;

/**
 * Blackhole
 *
 * Any record it can handle will be thrown away. This can be used
 * to put on top of an existing stack to override it temporarily.
 *
 * @author Jordi Boggiano <j.boggiano@seld.be>
 *
 * @phpstan-import-type Level from \Monolog\Logger
 * @phpstan-import-type LevelName from \Monolog\Logger
 */
class NullHandler extends Handler
{
    /**
     * @var int
     */
    private $level;

    /**
     * @param string|int $level The minimum logging level at which this handler will be triggered
     *
     * @phpstan-param Level|LevelName|LogLevel::* $level
     */
    public function __construct($level = Logger::DEBUG)
    {
        $this->level = Logger::toMonologLevel($level);
    }

    /**
     * {@inheritDoc}
     */
    public function isHandling(array $record): bool
    {
        return $record['level'] >= $this->level;
    }

    /**
     * {@inheritDoc}
     */
    public function handle(array $record): bool
    {
        return $record['level'] >= $this->level;
    }
}

AbstractHandler 类

<?php declare(strict_types=1);

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Monolog\Logger;
use Monolog\ResettableInterface;
use Psr\Log\LogLevel;

/**
 * Base Handler class providing basic level/bubble support
 *
 * @author Jordi Boggiano <j.boggiano@seld.be>
 *
 * @phpstan-import-type Level from \Monolog\Logger
 * @phpstan-import-type LevelName from \Monolog\Logger
 */
abstract class AbstractHandler extends Handler implements ResettableInterface
{
    /**
     * @var int
     * @phpstan-var Level
     */
    protected $level = Logger::DEBUG;
    /** @var bool */
    protected $bubble = true;

    /**
     * @param int|string $level  The minimum logging level at which this handler will be triggered
     * @param bool       $bubble Whether the messages that are handled can bubble up the stack or not
     *
     * @phpstan-param Level|LevelName|LogLevel::* $level
     */
    public function __construct($level = Logger::DEBUG, bool $bubble = true)
    {
        $this->setLevel($level);
        $this->bubble = $bubble;
    }

    /**
     * {@inheritDoc}
     */
    public function isHandling(array $record): bool
    {
        return $record['level'] >= $this->level;
    }

    /**
     * Sets minimum logging level at which this handler will be triggered.
     *
     * @param  Level|LevelName|LogLevel::* $level Level or level name
     * @return self
     */
    public function setLevel($level): self
    {
        $this->level = Logger::toMonologLevel($level);

        return $this;
    }

    /**
     * Gets minimum logging level at which this handler will be triggered.
     *
     * @return int
     *
     * @phpstan-return Level
     */
    public function getLevel(): int
    {
        return $this->level;
    }

    /**
     * Sets the bubbling behavior.
     *
     * @param  bool $bubble true means that this handler allows bubbling.
     *                      false means that bubbling is not permitted.
     * @return self
     */
    public function setBubble(bool $bubble): self
    {
        $this->bubble = $bubble;

        return $this;
    }

    /**
     * Gets the bubbling behavior.
     *
     * @return bool true means that this handler allows bubbling.
     *              false means that bubbling is not permitted.
     */
    public function getBubble(): bool
    {
        return $this->bubble;
    }

    /**
     * {@inheritDoc}
     */
    public function reset()
    {
    }
}

AbstractProcessingHandler 类

<?php declare(strict_types=1);

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

/**
 * Base Handler class providing the Handler structure, including processors and formatters
 *
 * Classes extending it should (in most cases) only implement write($record)
 *
 * @author Jordi Boggiano <j.boggiano@seld.be>
 * @author Christophe Coevoet <stof@notk.org>
 *
 * @phpstan-import-type LevelName from \Monolog\Logger
 * @phpstan-import-type Level from \Monolog\Logger
 * @phpstan-import-type Record from \Monolog\Logger
 * @phpstan-type FormattedRecord array{message: string, context: mixed[], level: Level, level_name: LevelName, channel: string, datetime: \DateTimeImmutable, extra: mixed[], formatted: mixed}
 */
abstract class AbstractProcessingHandler extends AbstractHandler implements ProcessableHandlerInterface, FormattableHandlerInterface
{
    use ProcessableHandlerTrait;
    use FormattableHandlerTrait;

    /**
     * {@inheritDoc}
     */
    public function handle(array $record): bool
    {
        if (!$this->isHandling($record)) {
            return false;
        }

        if ($this->processors) {
            /** @var Record $record */
            $record = $this->processRecord($record);
        }

        $record['formatted'] = $this->getFormatter()->format($record);

        $this->write($record);

        return false === $this->bubble;
    }

    /**
     * Writes the record down to the log of the implementing handler
     *
     * @phpstan-param FormattedRecord $record
     */
    abstract protected function write(array $record): void;

    /**
     * @return void
     */
    public function reset()
    {
        parent::reset();

        $this->resetProcessors();
    }
}

ElasticsearchHandler 类

<?php declare(strict_types=1);

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Elastic\Elasticsearch\Response\Elasticsearch;
use Throwable;
use RuntimeException;
use Monolog\Logger;
use Monolog\Formatter\FormatterInterface;
use Monolog\Formatter\ElasticsearchFormatter;
use InvalidArgumentException;
use Elasticsearch\Common\Exceptions\RuntimeException as ElasticsearchRuntimeException;
use Elasticsearch\Client;
use Elastic\Elasticsearch\Exception\InvalidArgumentException as ElasticInvalidArgumentException;
use Elastic\Elasticsearch\Client as Client8;

/**
 * Elasticsearch handler
 *
 * @link https://www.elastic.co/guide/en/elasticsearch/client/php-api/current/index.html
 *
 * Simple usage example:
 *
 *    $client = \Elasticsearch\ClientBuilder::create()
 *        ->setHosts($hosts)
 *        ->build();
 *
 *    $options = array(
 *        'index' => 'elastic_index_name',
 *        'type'  => 'elastic_doc_type',
 *    );
 *    $handler = new ElasticsearchHandler($client, $options);
 *    $log = new Logger('application');
 *    $log->pushHandler($handler);
 *
 * @author Avtandil Kikabidze <akalongman@gmail.com>
 */
class ElasticsearchHandler extends AbstractProcessingHandler
{
    /**
     * @var Client|Client8
     */
    protected $client;

    /**
     * @var mixed[] Handler config options
     */
    protected $options = [];

    /**
     * @var bool
     */
    private $needsType;

    /**
     * @param Client|Client8 $client  Elasticsearch Client object
     * @param mixed[]        $options Handler configuration
     */
    public function __construct($client, array $options = [], $level = Logger::DEBUG, bool $bubble = true)
    {
        if (!$client instanceof Client && !$client instanceof Client8) {
            throw new \TypeError('Elasticsearch\Client or Elastic\Elasticsearch\Client instance required');
        }

        parent::__construct($level, $bubble);
        $this->client = $client;
        $this->options = array_merge(
            [
                'index'        => 'monolog', // Elastic index name
                'type'         => '_doc',    // Elastic document type
                'ignore_error' => false,     // Suppress Elasticsearch exceptions
            ],
            $options
        );

        if ($client instanceof Client8 || $client::VERSION[0] === '7') {
            $this->needsType = false;
            // force the type to _doc for ES8/ES7
            $this->options['type'] = '_doc';
        } else {
            $this->needsType = true;
        }
    }

    /**
     * {@inheritDoc}
     */
    protected function write(array $record): void
    {
        $this->bulkSend([$record['formatted']]);
    }

    /**
     * {@inheritDoc}
     */
    public function setFormatter(FormatterInterface $formatter): HandlerInterface
    {
        if ($formatter instanceof ElasticsearchFormatter) {
            return parent::setFormatter($formatter);
        }

        throw new InvalidArgumentException('ElasticsearchHandler is only compatible with ElasticsearchFormatter');
    }

    /**
     * Getter options
     *
     * @return mixed[]
     */
    public function getOptions(): array
    {
        return $this->options;
    }

    /**
     * {@inheritDoc}
     */
    protected function getDefaultFormatter(): FormatterInterface
    {
        return new ElasticsearchFormatter($this->options['index'], $this->options['type']);
    }

    /**
     * {@inheritDoc}
     */
    public function handleBatch(array $records): void
    {
        $documents = $this->getFormatter()->formatBatch($records);
        $this->bulkSend($documents);
    }

    /**
     * Use Elasticsearch bulk API to send list of documents
     *
     * @param  array[]           $records Records + _index/_type keys
     * @throws \RuntimeException
     */
    protected function bulkSend(array $records): void
    {
        try {
            $params = [
                'body' => [],
            ];

            foreach ($records as $record) {
                $params['body'][] = [
                    'index' => $this->needsType ? [
                        '_index' => $record['_index'],
                        '_type'  => $record['_type'],
                    ] : [
                        '_index' => $record['_index'],
                    ],
                ];
                unset($record['_index'], $record['_type']);

                $params['body'][] = $record;
            }

            /** @var Elasticsearch */
            $responses = $this->client->bulk($params);

            if ($responses['errors'] === true) {
                throw $this->createExceptionFromResponses($responses);
            }
        } catch (Throwable $e) {
            if (! $this->options['ignore_error']) {
                throw new RuntimeException('Error sending messages to Elasticsearch', 0, $e);
            }
        }
    }

    /**
     * Creates elasticsearch exception from responses array
     *
     * Only the first error is converted into an exception.
     *
     * @param mixed[]|Elasticsearch $responses returned by $this->client->bulk()
     */
    protected function createExceptionFromResponses($responses): Throwable
    {
        // @phpstan-ignore offsetAccess.nonOffsetAccessible
        foreach ($responses['items'] ?? [] as $item) {
            if (isset($item['index']['error'])) {
                return $this->createExceptionFromError($item['index']['error']);
            }
        }

        if (class_exists(ElasticInvalidArgumentException::class)) {
            return new ElasticInvalidArgumentException('Elasticsearch failed to index one or more records.');
        }

        return new ElasticsearchRuntimeException('Elasticsearch failed to index one or more records.');
    }

    /**
     * Creates elasticsearch exception from error array
     *
     * @param mixed[] $error
     */
    protected function createExceptionFromError(array $error): Throwable
    {
        $previous = isset($error['caused_by']) ? $this->createExceptionFromError($error['caused_by']) : null;

        if (class_exists(ElasticInvalidArgumentException::class)) {
            return new ElasticInvalidArgumentException($error['type'] . ': ' . $error['reason'], 0, $previous);
        }

        return new ElasticsearchRuntimeException($error['type'] . ': ' . $error['reason'], 0, $previous);
    }
}

StreamHandler 类

<?php declare(strict_types=1);

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog\Handler;

use Monolog\Logger;
use Monolog\Utils;

/**
 * Stores to any stream resource
 *
 * Can be used to store into php://stderr, remote and local files, etc.
 *
 * @author Jordi Boggiano <j.boggiano@seld.be>
 *
 * @phpstan-import-type FormattedRecord from AbstractProcessingHandler
 */
class StreamHandler extends AbstractProcessingHandler
{
    /** @const int */
    protected const MAX_CHUNK_SIZE = 2147483647;
    /** @const int 10MB */
    protected const DEFAULT_CHUNK_SIZE = 10 * 1024 * 1024;
    /** @var int */
    protected $streamChunkSize;
    /** @var resource|null */
    protected $stream;
    /** @var ?string */
    protected $url = null;
    /** @var ?string */
    private $errorMessage = null;
    /** @var ?int */
    protected $filePermission;
    /** @var bool */
    protected $useLocking;
	/** @var string */
    protected $fileOpenMode;
    /** @var true|null */
    private $dirCreated = null;
    /** @var bool */
    private $retrying = false;

    /**
     * @param resource|string $stream         If a missing path can't be created, an UnexpectedValueException will be thrown on first write
     * @param int|null        $filePermission Optional file permissions (default (0644) are only for owner read/write)
     * @param bool            $useLocking     Try to lock log file before doing any writes
     * @param string          $fileOpenMode   The fopen() mode used when opening a file, if $stream is a file path
     *
     * @throws \InvalidArgumentException If stream is not a resource or string
     */
    public function __construct($stream, $level = Logger::DEBUG, bool $bubble = true, ?int $filePermission = null, bool $useLocking = false, $fileOpenMode = 'a')
    {
        parent::__construct($level, $bubble);

        if (($phpMemoryLimit = Utils::expandIniShorthandBytes(ini_get('memory_limit'))) !== false) {
            if ($phpMemoryLimit > 0) {
                // use max 10% of allowed memory for the chunk size, and at least 100KB
                $this->streamChunkSize = min(static::MAX_CHUNK_SIZE, max((int) ($phpMemoryLimit / 10), 100 * 1024));
            } else {
                // memory is unlimited, set to the default 10MB
                $this->streamChunkSize = static::DEFAULT_CHUNK_SIZE;
            }
        } else {
            // no memory limit information, set to the default 10MB
            $this->streamChunkSize = static::DEFAULT_CHUNK_SIZE;
        }

        if (is_resource($stream)) {
            $this->stream = $stream;

            stream_set_chunk_size($this->stream, $this->streamChunkSize);
        } elseif (is_string($stream)) {
            $this->url = Utils::canonicalizePath($stream);
        } else {
            throw new \InvalidArgumentException('A stream must either be a resource or a string.');
        }

        $this->fileOpenMode = $fileOpenMode;
        $this->filePermission = $filePermission;
        $this->useLocking = $useLocking;
    }

    /**
     * {@inheritDoc}
     */
    public function close(): void
    {
        if ($this->url && is_resource($this->stream)) {
            fclose($this->stream);
        }
        $this->stream = null;
        $this->dirCreated = null;
    }

    /**
     * Return the currently active stream if it is open
     *
     * @return resource|null
     */
    public function getStream()
    {
        return $this->stream;
    }

    /**
     * Return the stream URL if it was configured with a URL and not an active resource
     *
     * @return string|null
     */
    public function getUrl(): ?string
    {
        return $this->url;
    }

    /**
     * @return int
     */
    public function getStreamChunkSize(): int
    {
        return $this->streamChunkSize;
    }

    /**
     * {@inheritDoc}
     */
    protected function write(array $record): void
    {
        if (!is_resource($this->stream)) {
            $url = $this->url;
            if (null === $url || '' === $url) {
                throw new \LogicException('Missing stream url, the stream can not be opened. This may be caused by a premature call to close().' . Utils::getRecordMessageForException($record));
            }
            $this->createDir($url);
            $this->errorMessage = null;
            set_error_handler(function (...$args) {
                return $this->customErrorHandler(...$args);
            });
            try {
                $stream = fopen($url, $this->fileOpenMode);
                if ($this->filePermission !== null) {
                    @chmod($url, $this->filePermission);
                }
            } finally {
                restore_error_handler();
            }
            if (!is_resource($stream)) {
                $this->stream = null;

                throw new \UnexpectedValueException(sprintf('The stream or file "%s" could not be opened in append mode: '.$this->errorMessage, $url) . Utils::getRecordMessageForException($record));
            }
            stream_set_chunk_size($stream, $this->streamChunkSize);
            $this->stream = $stream;
        }

        $stream = $this->stream;
        if (!is_resource($stream)) {
            throw new \LogicException('No stream was opened yet' . Utils::getRecordMessageForException($record));
        }

        if ($this->useLocking) {
            // ignoring errors here, there's not much we can do about them
            flock($stream, LOCK_EX);
        }

        $this->errorMessage = null;
        set_error_handler(function (...$args) {
            return $this->customErrorHandler(...$args);
        });
        try {
            $this->streamWrite($stream, $record);
        } finally {
            restore_error_handler();
        }
        if ($this->errorMessage !== null) {
            $error = $this->errorMessage;
            // close the resource if possible to reopen it, and retry the failed write
            if (!$this->retrying && $this->url !== null && $this->url !== 'php://memory') {
                $this->retrying = true;
                $this->close();
                $this->write($record);

                return;
            }

            throw new \UnexpectedValueException('Writing to the log file failed: '.$error . Utils::getRecordMessageForException($record));
        }

        $this->retrying = false;
        if ($this->useLocking) {
            flock($stream, LOCK_UN);
        }
    }

    /**
     * Write to stream
     * @param resource $stream
     * @param array    $record
     *
     * @phpstan-param FormattedRecord $record
     */
    protected function streamWrite($stream, array $record): void
    {
        fwrite($stream, (string) $record['formatted']);
    }

    private function customErrorHandler(int $code, string $msg): bool
    {
        $this->errorMessage = preg_replace('{^(fopen|mkdir|fwrite)\(.*?\): }', '', $msg);

        return true;
    }

    private function getDirFromStream(string $stream): ?string
    {
        $pos = strpos($stream, '://');
        if ($pos === false) {
            return dirname($stream);
        }

        if ('file://' === substr($stream, 0, 7)) {
            return dirname(substr($stream, 7));
        }

        return null;
    }

    private function createDir(string $url): void
    {
        // Do not try to create dir if it has already been tried.
        if ($this->dirCreated) {
            return;
        }

        $dir = $this->getDirFromStream($url);
        if (null !== $dir && !is_dir($dir)) {
            $this->errorMessage = null;
            set_error_handler(function (...$args) {
                return $this->customErrorHandler(...$args);
            });
            $status = mkdir($dir, 0777, true);
            restore_error_handler();
            if (false === $status && !is_dir($dir) && strpos((string) $this->errorMessage, 'File exists') === false) {
                throw new \UnexpectedValueException(sprintf('There is no existing directory at "%s" and it could not be created: '.$this->errorMessage, $dir));
            }
        }
        $this->dirCreated = true;
    }
}

Monolog/Logger 类

<?php declare(strict_types=1);

/*
 * This file is part of the Monolog package.
 *
 * (c) Jordi Boggiano <j.boggiano@seld.be>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Monolog;

use DateTimeZone;
use Monolog\Handler\HandlerInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\InvalidArgumentException;
use Psr\Log\LogLevel;
use Throwable;
use Stringable;

/**
 * Monolog log channel
 *
 * It contains a stack of Handlers and a stack of Processors,
 * and uses them to store records that are added to it.
 *
 * @author Jordi Boggiano <j.boggiano@seld.be>
 *
 * @phpstan-type Level Logger::DEBUG|Logger::INFO|Logger::NOTICE|Logger::WARNING|Logger::ERROR|Logger::CRITICAL|Logger::ALERT|Logger::EMERGENCY
 * @phpstan-type LevelName 'DEBUG'|'INFO'|'NOTICE'|'WARNING'|'ERROR'|'CRITICAL'|'ALERT'|'EMERGENCY'
 * @phpstan-type Record array{message: string, context: mixed[], level: Level, level_name: LevelName, channel: string, datetime: \DateTimeImmutable, extra: mixed[]}
 */
class Logger implements LoggerInterface, ResettableInterface
{
    /**
     * Detailed debug information
     */
    public const DEBUG = 100;

    /**
     * Interesting events
     *
     * Examples: User logs in, SQL logs.
     */
    public const INFO = 200;

    /**
     * Uncommon events
     */
    public const NOTICE = 250;

    /**
     * Exceptional occurrences that are not errors
     *
     * Examples: Use of deprecated APIs, poor use of an API,
     * undesirable things that are not necessarily wrong.
     */
    public const WARNING = 300;

    /**
     * Runtime errors
     */
    public const ERROR = 400;

    /**
     * Critical conditions
     *
     * Example: Application component unavailable, unexpected exception.
     */
    public const CRITICAL = 500;

    /**
     * Action must be taken immediately
     *
     * Example: Entire website down, database unavailable, etc.
     * This should trigger the SMS alerts and wake you up.
     */
    public const ALERT = 550;

    /**
     * Urgent alert.
     */
    public const EMERGENCY = 600;

    /**
     * Monolog API version
     *
     * This is only bumped when API breaks are done and should
     * follow the major version of the library
     *
     * @var int
     */
    public const API = 2;

    /**
     * This is a static variable and not a constant to serve as an extension point for custom levels
     *
     * @var array<int, string> $levels Logging levels with the levels as key
     *
     * @phpstan-var array<Level, LevelName> $levels Logging levels with the levels as key
     */
    protected static $levels = [
        self::DEBUG     => 'DEBUG',
        self::INFO      => 'INFO',
        self::NOTICE    => 'NOTICE',
        self::WARNING   => 'WARNING',
        self::ERROR     => 'ERROR',
        self::CRITICAL  => 'CRITICAL',
        self::ALERT     => 'ALERT',
        self::EMERGENCY => 'EMERGENCY',
    ];

    /**
     * Mapping between levels numbers defined in RFC 5424 and Monolog ones
     *
     * @phpstan-var array<int, Level> $rfc_5424_levels
     */
    private const RFC_5424_LEVELS = [
        7 => self::DEBUG,
        6 => self::INFO,
        5 => self::NOTICE,
        4 => self::WARNING,
        3 => self::ERROR,
        2 => self::CRITICAL,
        1 => self::ALERT,
        0 => self::EMERGENCY,
    ];

    /**
     * @var string
     */
    protected $name;

    /**
     * The handler stack
     *
     * @var HandlerInterface[]
     */
    protected $handlers;

    /**
     * Processors that will process all log records
     *
     * To process records of a single handler instead, add the processor on that specific handler
     *
     * @var callable[]
     */
    protected $processors;

    /**
     * @var bool
     */
    protected $microsecondTimestamps = true;

    /**
     * @var DateTimeZone
     */
    protected $timezone;

    /**
     * @var callable|null
     */
    protected $exceptionHandler;

    /**
     * @var int Keeps track of depth to prevent infinite logging loops
     */
    private $logDepth = 0;

    /**
     * @var \WeakMap<\Fiber<mixed, mixed, mixed, mixed>, int> Keeps track of depth inside fibers to prevent infinite logging loops
     */
    private $fiberLogDepth;

    /**
     * @var bool Whether to detect infinite logging loops
     *
     * This can be disabled via {@see useLoggingLoopDetection} if you have async handlers that do not play well with this
     */
    private $detectCycles = true;

    /**
     * @psalm-param array<callable(array): array> $processors
     *
     * @param string             $name       The logging channel, a simple descriptive name that is attached to all log records
     * @param HandlerInterface[] $handlers   Optional stack of handlers, the first one in the array is called first, etc.
     * @param callable[]         $processors Optional array of processors
     * @param DateTimeZone|null  $timezone   Optional timezone, if not provided date_default_timezone_get() will be used
     */
    public function __construct(string $name, array $handlers = [], array $processors = [], ?DateTimeZone $timezone = null)
    {
        $this->name = $name;
        $this->setHandlers($handlers);
        $this->processors = $processors;
        $this->timezone = $timezone ?: new DateTimeZone(date_default_timezone_get() ?: 'UTC');

        if (\PHP_VERSION_ID >= 80100) {
            // Local variable for phpstan, see https://github.com/phpstan/phpstan/issues/6732#issuecomment-1111118412
            /** @var \WeakMap<\Fiber<mixed, mixed, mixed, mixed>, int> $fiberLogDepth */
            $fiberLogDepth = new \WeakMap();
            $this->fiberLogDepth = $fiberLogDepth;
        }
    }

    public function getName(): string
    {
        return $this->name;
    }

    /**
     * Return a new cloned instance with the name changed
     */
    public function withName(string $name): self
    {
        $new = clone $this;
        $new->name = $name;

        return $new;
    }

    /**
     * Pushes a handler on to the stack.
     */
    public function pushHandler(HandlerInterface $handler): self
    {
        array_unshift($this->handlers, $handler);

        return $this;
    }

    /**
     * Pops a handler from the stack
     *
     * @throws \LogicException If empty handler stack
     */
    public function popHandler(): HandlerInterface
    {
        if (!$this->handlers) {
            throw new \LogicException('You tried to pop from an empty handler stack.');
        }

        return array_shift($this->handlers);
    }

    /**
     * Set handlers, replacing all existing ones.
     *
     * If a map is passed, keys will be ignored.
     *
     * @param HandlerInterface[] $handlers
     */
    public function setHandlers(array $handlers): self
    {
        $this->handlers = [];
        foreach (array_reverse($handlers) as $handler) {
            $this->pushHandler($handler);
        }

        return $this;
    }

    /**
     * @return HandlerInterface[]
     */
    public function getHandlers(): array
    {
        return $this->handlers;
    }

    /**
     * Adds a processor on to the stack.
     */
    public function pushProcessor(callable $callback): self
    {
        array_unshift($this->processors, $callback);

        return $this;
    }

    /**
     * Removes the processor on top of the stack and returns it.
     *
     * @throws \LogicException If empty processor stack
     * @return callable
     */
    public function popProcessor(): callable
    {
        if (!$this->processors) {
            throw new \LogicException('You tried to pop from an empty processor stack.');
        }

        return array_shift($this->processors);
    }

    /**
     * @return callable[]
     */
    public function getProcessors(): array
    {
        return $this->processors;
    }

    /**
     * Control the use of microsecond resolution timestamps in the 'datetime'
     * member of new records.
     *
     * As of PHP7.1 microseconds are always included by the engine, so
     * there is no performance penalty and Monolog 2 enabled microseconds
     * by default. This function lets you disable them though in case you want
     * to suppress microseconds from the output.
     *
     * @param bool $micro True to use microtime() to create timestamps
     */
    public function useMicrosecondTimestamps(bool $micro): self
    {
        $this->microsecondTimestamps = $micro;

        return $this;
    }

    public function useLoggingLoopDetection(bool $detectCycles): self
    {
        $this->detectCycles = $detectCycles;

        return $this;
    }

    /**
     * Adds a log record.
     *
     * @param  int               $level    The logging level (a Monolog or RFC 5424 level)
     * @param  string            $message  The log message
     * @param  mixed[]           $context  The log context
     * @param  DateTimeImmutable $datetime Optional log date to log into the past or future
     * @return bool              Whether the record has been processed
     *
     * @phpstan-param Level $level
     */
    public function addRecord(int $level, string $message, array $context = [], ?DateTimeImmutable $datetime = null): bool
    {
        if (isset(self::RFC_5424_LEVELS[$level])) {
            $level = self::RFC_5424_LEVELS[$level];
        }

        if ($this->detectCycles) {
            if (\PHP_VERSION_ID >= 80100 && $fiber = \Fiber::getCurrent()) {
                // @phpstan-ignore offsetAssign.dimType
                $this->fiberLogDepth[$fiber] = $this->fiberLogDepth[$fiber] ?? 0;
                $logDepth = ++$this->fiberLogDepth[$fiber];
            } else {
                $logDepth = ++$this->logDepth;
            }
        } else {
            $logDepth = 0;
        }

        if ($logDepth === 3) {
            $this->warning('A possible infinite logging loop was detected and aborted. It appears some of your handler code is triggering logging, see the previous log record for a hint as to what may be the cause.');
            return false;
        } elseif ($logDepth >= 5) { // log depth 4 is let through, so we can log the warning above
            return false;
        }

        try {
            $record = null;

            foreach ($this->handlers as $handler) {
                if (null === $record) {
                    // skip creating the record as long as no handler is going to handle it
                    if (!$handler->isHandling(['level' => $level])) {
                        continue;
                    }

                    $levelName = static::getLevelName($level);

                    $record = [
                        'message' => $message,
                        'context' => $context,
                        'level' => $level,
                        'level_name' => $levelName,
                        'channel' => $this->name,
                        'datetime' => $datetime ?? new DateTimeImmutable($this->microsecondTimestamps, $this->timezone),
                        'extra' => [],
                    ];

                    try {
                        foreach ($this->processors as $processor) {
                            $record = $processor($record);
                        }
                    } catch (Throwable $e) {
                        $this->handleException($e, $record);

                        return true;
                    }
                }

                // once the record exists, send it to all handlers as long as the bubbling chain is not interrupted
                try {
                    if (true === $handler->handle($record)) {
                        break;
                    }
                } catch (Throwable $e) {
                    $this->handleException($e, $record);

                    return true;
                }
            }
        } finally {
            if ($this->detectCycles) {
                if (isset($fiber)) {
                    $this->fiberLogDepth[$fiber]--;
                } else {
                    $this->logDepth--;
                }
            }
        }

        return null !== $record;
    }

    /**
     * Ends a log cycle and frees all resources used by handlers.
     *
     * Closing a Handler means flushing all buffers and freeing any open resources/handles.
     * Handlers that have been closed should be able to accept log records again and re-open
     * themselves on demand, but this may not always be possible depending on implementation.
     *
     * This is useful at the end of a request and will be called automatically on every handler
     * when they get destructed.
     */
    public function close(): void
    {
        foreach ($this->handlers as $handler) {
            $handler->close();
        }
    }

    /**
     * Ends a log cycle and resets all handlers and processors to their initial state.
     *
     * Resetting a Handler or a Processor means flushing/cleaning all buffers, resetting internal
     * state, and getting it back to a state in which it can receive log records again.
     *
     * This is useful in case you want to avoid logs leaking between two requests or jobs when you
     * have a long running process like a worker or an application server serving multiple requests
     * in one process.
     */
    public function reset(): void
    {
        foreach ($this->handlers as $handler) {
            if ($handler instanceof ResettableInterface) {
                $handler->reset();
            }
        }

        foreach ($this->processors as $processor) {
            if ($processor instanceof ResettableInterface) {
                $processor->reset();
            }
        }
    }

    /**
     * Gets all supported logging levels.
     *
     * @return array<string, int> Assoc array with human-readable level names => level codes.
     * @phpstan-return array<LevelName, Level>
     */
    public static function getLevels(): array
    {
        return array_flip(static::$levels);
    }

    /**
     * Gets the name of the logging level.
     *
     * @throws \Psr\Log\InvalidArgumentException If level is not defined
     *
     * @phpstan-param  Level     $level
     * @phpstan-return LevelName
     */
    public static function getLevelName(int $level): string
    {
        if (!isset(static::$levels[$level])) {
            throw new InvalidArgumentException('Level "'.$level.'" is not defined, use one of: '.implode(', ', array_keys(static::$levels)));
        }

        return static::$levels[$level];
    }

    /**
     * Converts PSR-3 levels to Monolog ones if necessary
     *
     * @param  string|int                        $level Level number (monolog) or name (PSR-3)
     * @throws \Psr\Log\InvalidArgumentException If level is not defined
     *
     * @phpstan-param  Level|LevelName|LogLevel::* $level
     * @phpstan-return Level
     */
    public static function toMonologLevel($level): int
    {
        if (is_string($level)) {
            if (is_numeric($level)) {
                /** @phpstan-ignore-next-line */
                return intval($level);
            }

            // Contains chars of all log levels and avoids using strtoupper() which may have
            // strange results depending on locale (for example, "i" will become "İ" in Turkish locale)
            $upper = strtr($level, 'abcdefgilmnortuwy', 'ABCDEFGILMNORTUWY');
            if (defined(__CLASS__.'::'.$upper)) {
                return constant(__CLASS__ . '::' . $upper);
            }

            throw new InvalidArgumentException('Level "'.$level.'" is not defined, use one of: '.implode(', ', array_keys(static::$levels) + static::$levels));
        }

        if (!is_int($level)) {
            throw new InvalidArgumentException('Level "'.var_export($level, true).'" is not defined, use one of: '.implode(', ', array_keys(static::$levels) + static::$levels));
        }

        return $level;
    }

    /**
     * Checks whether the Logger has a handler that listens on the given level
     *
     * @phpstan-param Level $level
     */
    public function isHandling(int $level): bool
    {
        $record = [
            'level' => $level,
        ];

        foreach ($this->handlers as $handler) {
            if ($handler->isHandling($record)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Set a custom exception handler that will be called if adding a new record fails
     *
     * The callable will receive an exception object and the record that failed to be logged
     */
    public function setExceptionHandler(?callable $callback): self
    {
        $this->exceptionHandler = $callback;

        return $this;
    }

    public function getExceptionHandler(): ?callable
    {
        return $this->exceptionHandler;
    }

    /**
     * Adds a log record at an arbitrary level.
     *
     * This method allows for compatibility with common interfaces.
     *
     * @param mixed             $level   The log level (a Monolog, PSR-3 or RFC 5424 level)
     * @param string|Stringable $message The log message
     * @param mixed[]           $context The log context
     *
     * @phpstan-param Level|LevelName|LogLevel::* $level
     */
    public function log($level, $message, array $context = []): void
    {
        if (!is_int($level) && !is_string($level)) {
            throw new \InvalidArgumentException('$level is expected to be a string or int');
        }

        if (isset(self::RFC_5424_LEVELS[$level])) {
            $level = self::RFC_5424_LEVELS[$level];
        }

        $level = static::toMonologLevel($level);

        $this->addRecord($level, (string) $message, $context);
    }

    /**
     * Adds a log record at the DEBUG level.
     *
     * This method allows for compatibility with common interfaces.
     *
     * @param string|Stringable $message The log message
     * @param mixed[]           $context The log context
     */
    public function debug($message, array $context = []): void
    {
        $this->addRecord(static::DEBUG, (string) $message, $context);
    }

    /**
     * Adds a log record at the INFO level.
     *
     * This method allows for compatibility with common interfaces.
     *
     * @param string|Stringable $message The log message
     * @param mixed[]           $context The log context
     */
    public function info($message, array $context = []): void
    {
        $this->addRecord(static::INFO, (string) $message, $context);
    }

    /**
     * Adds a log record at the NOTICE level.
     *
     * This method allows for compatibility with common interfaces.
     *
     * @param string|Stringable $message The log message
     * @param mixed[]           $context The log context
     */
    public function notice($message, array $context = []): void
    {
        $this->addRecord(static::NOTICE, (string) $message, $context);
    }

    /**
     * Adds a log record at the WARNING level.
     *
     * This method allows for compatibility with common interfaces.
     *
     * @param string|Stringable $message The log message
     * @param mixed[]           $context The log context
     */
    public function warning($message, array $context = []): void
    {
        $this->addRecord(static::WARNING, (string) $message, $context);
    }

    /**
     * Adds a log record at the ERROR level.
     *
     * This method allows for compatibility with common interfaces.
     *
     * @param string|Stringable $message The log message
     * @param mixed[]           $context The log context
     */
    public function error($message, array $context = []): void
    {
        $this->addRecord(static::ERROR, (string) $message, $context);
    }

    /**
     * Adds a log record at the CRITICAL level.
     *
     * This method allows for compatibility with common interfaces.
     *
     * @param string|Stringable $message The log message
     * @param mixed[]           $context The log context
     */
    public function critical($message, array $context = []): void
    {
        $this->addRecord(static::CRITICAL, (string) $message, $context);
    }

    /**
     * Adds a log record at the ALERT level.
     *
     * This method allows for compatibility with common interfaces.
     *
     * @param string|Stringable $message The log message
     * @param mixed[]           $context The log context
     */
    public function alert($message, array $context = []): void
    {
        $this->addRecord(static::ALERT, (string) $message, $context);
    }

    /**
     * Adds a log record at the EMERGENCY level.
     *
     * This method allows for compatibility with common interfaces.
     *
     * @param string|Stringable $message The log message
     * @param mixed[]           $context The log context
     */
    public function emergency($message, array $context = []): void
    {
        $this->addRecord(static::EMERGENCY, (string) $message, $context);
    }

    /**
     * Sets the timezone to be used for the timestamp of log records.
     */
    public function setTimezone(DateTimeZone $tz): self
    {
        $this->timezone = $tz;

        return $this;
    }

    /**
     * Returns the timezone to be used for the timestamp of log records.
     */
    public function getTimezone(): DateTimeZone
    {
        return $this->timezone;
    }

    /**
     * Delegates exception management to the custom exception handler,
     * or throws the exception if no custom handler is set.
     *
     * @param array $record
     * @phpstan-param Record $record
     */
    protected function handleException(Throwable $e, array $record): void
    {
        if (!$this->exceptionHandler) {
            throw $e;
        }

        ($this->exceptionHandler)($e, $record);
    }

    /**
     * @return array<string, mixed>
     */
    public function __serialize(): array
    {
        return [
            'name' => $this->name,
            'handlers' => $this->handlers,
            'processors' => $this->processors,
            'microsecondTimestamps' => $this->microsecondTimestamps,
            'timezone' => $this->timezone,
            'exceptionHandler' => $this->exceptionHandler,
            'logDepth' => $this->logDepth,
            'detectCycles' => $this->detectCycles,
        ];
    }

    /**
     * @param array<string, mixed> $data
     */
    public function __unserialize(array $data): void
    {
        foreach (['name', 'handlers', 'processors', 'microsecondTimestamps', 'timezone', 'exceptionHandler', 'logDepth', 'detectCycles'] as $property) {
            if (isset($data[$property])) {
                $this->$property = $data[$property];
            }
        }

        if (\PHP_VERSION_ID >= 80100) {
            // Local variable for phpstan, see https://github.com/phpstan/phpstan/issues/6732#issuecomment-1111118412
            /** @var \WeakMap<\Fiber<mixed, mixed, mixed, mixed>, int> $fiberLogDepth */
            $fiberLogDepth = new \WeakMap();
            $this->fiberLogDepth = $fiberLogDepth;
        }
    }
}

参考资料

Laravel 6 中文文档 https://learnku.com/docs/laravel/6.x/facades/5146

Laravel 实用小技巧——日志告警功能应该怎么做 https://learnku.com/articles/82018


返回