laravel日志
Laravel 日志中 formatMessage微妙之处
1、如果是数组 则使用 var_export() 方法,将其转换为数组
2、如果实现了 Jsonable 使用toJson() 方法
3、如果实现了 Arrayable 则可以使用toArray()方法
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;
}Jsonable 接口、契约
namespace Illuminate\Contracts\Support;
interface Jsonable
{
/**
* Convert the object to its JSON representation.
*
* @param int $options
* @return string
*/
public function toJson($options = 0);
}Arrayable 接口、契约
namespace Illuminate\Contracts\Support;
interface Arrayable
{
/**
* Get the instance as an array.
*
* @return array
*/
public function toArray();
}MessageLogged
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;
}
}LogServiceProvider 日志服务提供者
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);
});
}
}Laravel 日志在多请求的时候,记录日志会比较混乱,无法区分出是哪一个请求。
精妙之处,记录下来:
/**
* 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);
}赋值,并判断
如果可执行,直接返回 赋值给$factory
如果不可执行,则通过laravel容器中获取$via 赋值给factory
最后,通过 factory 将config传递进来。 这是精妙之处。犹如-独孤九剑 妙哉,妙哉
Laravel 源码分析:
第一步,将整个app容器注册进来
LogManager.php 位置src/Illuminate/Log/LogManager.php
/**
* Create a new Log manager instance.
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @return void
*/
public function __construct($app)
{
$this->app = $app;
}第二步,通过driver方法 调用获取实例
/**
* Get a log driver instance.
*
* @param string|null $driver
* @return \Psr\Log\LoggerInterface
*/
public function driver($driver = null)
{
return $this->get($driver ?? $this->getDefaultDriver());
}get 方法在LogManager中是一个受保护的对象,不对外提供服务
driver工作原理:
1、通过传递参数$driver 调用get方法
$driver 可以为空,默认调用DefaultDriver return $this->app['config']['logging.default'];
2、get方法接收一个字符串类型值,$name。 name是config/logging.php配置文件中 channels 必须存在
然后调用resolve 方法,通过configurationFor($name) 从配置文件中获取配置,如果没有找到,则抛出异常
如果存在则调用callCustomCreator方法
调用customCreators
/**
* Call a custom driver creator.
*
* @param array $config
* @return mixed
*/
protected function callCustomCreator(array $config)
{
return $this->customCreators[$config['driver']]($this->app, $config);
}
//这个方法是什么意思呢? 就是调用customCreators 数组中的一个值,然后获取到日志实例
//乍一眼看,特别的不容易理解,改一种写法就特别好理解了。 return $this->single($this->app,$config);/**
* 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;
}第三、根据不同的驱动创建实例
/**
* 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),
]);
}第四、写入日志
//src/Illuminate/Log/LogManager.php
public function info($message, array $context = [])
{
$this->driver()->info($message, $context);
}
//src/Illuminate/Log/Logger.php
public function info($message, array $context = [])
{
$this->writeLog(__FUNCTION__, $message, $context);
}
//src/Monolog/Logger.php
/**
* Adds a log record.
*
* @param int $level The logging level
* @param string $message The log message
* @param array $context The log context
* @return bool Whether the record has been processed
*/
public function addRecord(int $level, string $message, array $context = []): bool
{
// check if any handler will handle this message so we can return early and save cycles
$handlerKey = null;
foreach ($this->handlers as $key => $handler) {
if ($handler->isHandling(['level' => $level])) {
$handlerKey = $key;
break;
}
}
if (null === $handlerKey) {
return false;
}
$levelName = static::getLevelName($level);
$record = [
'message' => $message,
'context' => $context,
'level' => $level,
'level_name' => $levelName,
'channel' => $this->name,
'datetime' => new DateTimeImmutable($this->microsecondTimestamps, $this->timezone),
'extra' => [],
];
try {
foreach ($this->processors as $processor) {
$record = call_user_func($processor, $record);
}
// advance the array pointer to the first handler that will handle this record
reset($this->handlers);
while ($handlerKey !== key($this->handlers)) {
next($this->handlers);
}
while ($handler = current($this->handlers)) {
if (true === $handler->handle($record)) {
break;
}
next($this->handlers);
}
} catch (Throwable $e) {
$this->handleException($e, $record);
}
return true;
}
万变不离其宗,最终记录到日志的时候,是使用fopen fwrite fclose 方法实现的。
single 、 daily 这两类的日志,使用的是StreamHandler(src/Monolog/Handler/StreamHandler.php)
StreamHandler 类实现了 AbstractProcessingHandler 中的write 方法。
StreamHandler.php
<?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;
/**
* 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>
*/
class StreamHandler extends AbstractProcessingHandler
{
/** @var resource|null */
protected $stream;
protected $url;
/** @var string|null */
private $errorMessage;
protected $filePermission;
protected $useLocking;
private $dirCreated;
/**
* @param resource|string $stream If a missing path can't be created, an UnexpectedValueException will be thrown on first write
* @param string|int $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
* @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
*
* @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)
{
parent::__construct($level, $bubble);
if (is_resource($stream)) {
$this->stream = $stream;
} elseif (is_string($stream)) {
$this->url = $stream;
} else {
throw new \InvalidArgumentException('A stream must either be a resource or a string.');
}
$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;
}
/**
* {@inheritdoc}
*/
protected function write(array $record): void
{
if (!is_resource($this->stream)) {
if (null === $this->url || '' === $this->url) {
throw new \LogicException('Missing stream url, the stream can not be opened. This may be caused by a premature call to close().');
}
$this->createDir();
$this->errorMessage = null;
set_error_handler([$this, 'customErrorHandler']);
$this->stream = fopen($this->url, 'a');
if ($this->filePermission !== null) {
@chmod($this->url, $this->filePermission);
}
restore_error_handler();
if (!is_resource($this->stream)) {
$this->stream = null;
throw new \UnexpectedValueException(sprintf('The stream or file "%s" could not be opened: '.$this->errorMessage, $this->url));
}
}
if ($this->useLocking) {
// ignoring errors here, there's not much we can do about them
flock($this->stream, LOCK_EX);
}
$this->streamWrite($this->stream, $record);
if ($this->useLocking) {
flock($this->stream, LOCK_UN);
}
}
/**
* Write to stream
* @param resource $stream
* @param array $record
*/
protected function streamWrite($stream, array $record): void
{
fwrite($stream, (string) $record['formatted']);
}
private function customErrorHandler($code, $msg): bool
{
$this->errorMessage = preg_replace('{^(fopen|mkdir)\(.*?\): }', '', $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(): void
{
// Do not try to create dir if it has already been tried.
if ($this->dirCreated) {
return;
}
$dir = $this->getDirFromStream($this->url);
if (null !== $dir && !is_dir($dir)) {
$this->errorMessage = null;
set_error_handler([$this, 'customErrorHandler']);
$status = mkdir($dir, 0777, true);
restore_error_handler();
if (false === $status && !is_dir($dir)) {
throw new \UnexpectedValueException(sprintf('There is no existing directory at "%s" and its not buildable: '.$this->errorMessage, $dir));
}
}
$this->dirCreated = true;
}
}可以看到里面使用到了 创建目录 mkdir chmod fopen fwrite fclose 方法。
laravel的日志到此结束。
其他项说明:
创建emergency级别的日志句柄
/**
* 创建紧急日志处理程序以避免白屏死机
* 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']
);
}
/**获取日志连接配置**/
protected function configurationFor($name)
{
return $this->app['config']["logging.channels.{$name}"];
}