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}"]; }