日志服务提供者注册
在 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'),
],
],
];
结合提供的 LogManager
、Logger
类和配置文件,看一下记录日志过程:
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.php
的default
定义,如'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 实例:
- 读取配置:
$config = $this->configurationFor($name);
(从config/logging.channels.$name
获取)。 - 选择驱动创建方式:
- 自定义驱动:通过
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
)确保日志不丢失。
关键设计亮点
- 灵活驱动:内置多种驱动(文件/Slack/Syslog),支持自定义扩展。
- 通道堆叠:通过
stack
驱动组合多个日志渠道。 - 事件挂钩:记录时触发
MessageLogged
事件,便于监听。 - 异常回退:自动降级到应急日志,避免系统崩溃。
- 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