作为程序员一定要保持良好的睡眠,才能好编程

Laravel中 日志源码分析

发布时间:2020-05-01


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

image.png


万变不离其宗,最终记录到日志的时候,是使用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}"];
}