Monolog 修改

monolog是记录日志的php框架,应用广泛。

github地址:https://github.com/Seldaek/monolog

composer地址:monolog/monolog - Packagist

之前一直用tp,tp保存日志时若是数组或对象,会保留格式输出,大概是每个数据一行,容易看。用原生的monolog的话,会被变成json放到文件中,没有格式,看起来困难。又因为它输入到文件的内容是通过字符串替换实现,所以即使设置var_export()也会变成一行,不是很符合要求。

使用

#Monolog\Logger
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, int> $fiberLogDepth */
			$fiberLogDepth = new \WeakMap();
			$this->fiberLogDepth = $fiberLogDepth;
		}
	}
public function pushHandler(HandlerInterface $handler): self{
		array_unshift($this->handlers, $handler);

		return $this;
	}
public function pushProcessor(callable $callback): self{
		array_unshift($this->processors, $callback);

		return $this;
	}
public function addRecord(int $level, string $message, array $context = [], DateTimeImmutable $datetime = null): bool {
    ……
    foreach ($this->handlers as $handler) {
        if (null === $record) {
			#判断level是否可被记录
            if (!$handler->isHandling(['level' => $level])) {
				continue;
			}
        }
    }
    ……
    #记录日志
    if (true === $handler->handle($record)) {
		break;
	}
    ……
}

所以根据Logger类,可以在构造设置handler或者单独设置,并且没有handler不能记录日志。

$log = new Logger('test1');
$logfile = "logs/20230718.log";
$sream = new StreamHandler($logfile, Logger::DEBUG);
$log->pushHandler($sream);
$log->warning('test1', ['qwe' => '123']);

#记录结果
[2023-07-19T10:53:21.235082+08:00] test1.WARNING: test1 {"qwe":"123"} []

这是经过格式化后的效果。

Monolog\Handler\StreamHandler继承Monolog\Handler\AbstractProcessingHandler,AbstractProcessingHandler继承Monolog\Handler\AbstractHandler。

#Monolog\Handler\AbstractProcessingHandler 
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;
    }

#Monolog\Handler\AbstractHandler
public function isHandling(array $record): bool
    {
        return $record['level'] >= $this->level;
    }

#Monolog\Handler\StreamHandler
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([$this, 'customErrorHandler']);
			$stream = fopen($url, 'a');
			if ($this->filePermission !== null) {
				@chmod($url, $this->filePermission);
			}
			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->streamWrite($stream, $record);

		if ($this->useLocking) {
			flock($stream, LOCK_UN);
		}
	}
protected function streamWrite($stream, array $record): void{
		fwrite($stream, (string) $record['formatted']);
	}

#Monolog\Logger
	public const DEBUG = 100;
    public const INFO = 200;
    public const NOTICE = 250;
    public const WARNING = 300;
    public const ERROR = 400;
    public const CRITICAL = 500;
    public const ALERT = 550;
    public const EMERGENCY = 600;
    

由代码可见,AbstractProcessingHandler::handle()处理格式化并调用父类测level判断,和子类的实际写入,也就是例子中的StreamHandler::write()->streamWrite()。

level判断根据level值的大小判断。所以设置debug都会记录,而设置emergency都不会被记录。

所以改下StreamHandler::streamWrite()应该能改变保存的格式,但是最后输出的内容仍然涉及格式化。

#Monolog\Handler\AbstractProcessingHandler
abstract class AbstractProcessingHandler extends AbstractHandler implements ProcessableHandlerInterface, FormattableHandlerInterface
{
    use ProcessableHandlerTrait;
    use FormattableHandlerTrait;
}

#Monolog\Handler\FormattableHandlerTrait
trait FormattableHandlerTrait
{
    /**
     * @var ?FormatterInterface
     */
    protected $formatter;

    /**
     * {@inheritDoc}
     */
    public function setFormatter(FormatterInterface $formatter): HandlerInterface
    {
        $this->formatter = $formatter;

        return $this;
    }

    /**
     * {@inheritDoc}
     */
    public function getFormatter(): FormatterInterface
    {
        if (!$this->formatter) {
            $this->formatter = $this->getDefaultFormatter();
        }

        return $this->formatter;
    }

    /**
     * Gets the default formatter.
     *
     * Overwrite this if the LineFormatter is not a good default for your handler.
     */
    protected function getDefaultFormatter(): FormatterInterface
    {
        return new LineFormatter();
    }
}

#Monolog\Handler\ProcessableHandlerTrait
trait ProcessableHandlerTrait
{
    /**
     * @var callable[]
     * @phpstan-var array
     */
    protected $processors = [];

    /**
     * {@inheritDoc}
     */
    public function pushProcessor(callable $callback): HandlerInterface
    {
        array_unshift($this->processors, $callback);

        return $this;
    }

    /**
     * {@inheritDoc}
     */
    public function popProcessor(): callable
    {
        if (!$this->processors) {
            throw new \LogicException('You tried to pop from an empty processor stack.');
        }

        return array_shift($this->processors);
    }

    /**
     * Processes a record.
     *
     * @phpstan-param  Record $record
     * @phpstan-return Record
     */
    protected function processRecord(array $record): array
    {
        foreach ($this->processors as $processor) {
            $record = $processor($record);
        }

        return $record;
    }

    protected function resetProcessors(): void
    {
        foreach ($this->processors as $processor) {
            if ($processor instanceof ResettableInterface) {
                $processor->reset();
            }
        }
    }

AbstractProcessingHandler使用ProcessableHandlerTrait和FormattableHandlerTrait。因为handler和process都是数组,这两者都是对数组的操作,handler会设置默认Monolog\Formatter\LineFormatter类。

#Monolog\Formatter\LineFormatter
class LineFormatter extends NormalizerFormatter
{
     public const SIMPLE_FORMAT = "[%datetime%] %channel%.%level_name%: %message% %context% %extra%\n";
    ……
      public function __construct(?string $format = null, ?string $dateFormat = null, bool $allowInlineLineBreaks = false, bool $ignoreEmptyContextAndExtra = false, bool $includeStacktraces = false)
    {
        $this->format = $format === null ? static::SIMPLE_FORMAT : $format;
        ……
        parent::__construct($dateFormat);
    }
     public function format(array $record): string
    {
        $vars = parent::format($record);

        $output = $this->format;

        foreach ($vars['extra'] as $var => $val) {
            if (false !== strpos($output, '%extra.'.$var.'%')) {
                $output = str_replace('%extra.'.$var.'%', $this->stringify($val), $output);
                unset($vars['extra'][$var]);
            }
        }

        foreach ($vars['context'] as $var => $val) {
            if (false !== strpos($output, '%context.'.$var.'%')) {
                $output = str_replace('%context.'.$var.'%', $this->stringify($val), $output);
                unset($vars['context'][$var]);
            }
        }

        if ($this->ignoreEmptyContextAndExtra) {
            if (empty($vars['context'])) {
                unset($vars['context']);
                $output = str_replace('%context%', '', $output);
            }

            if (empty($vars['extra'])) {
                unset($vars['extra']);
                $output = str_replace('%extra%', '', $output);
            }
        }

        foreach ($vars as $var => $val) {
            if (false !== strpos($output, '%'.$var.'%')) {
                $output = str_replace('%'.$var.'%', $this->stringify($val), $output);
            }
        }

        // remove leftover %extra.xxx% and %context.xxx% if any
        if (false !== strpos($output, '%')) {
            $output = preg_replace('/%(?:extra|context)\..+?%/', '', $output);
            if (null === $output) {
                $pcreErrorCode = preg_last_error();
                throw new \RuntimeException('Failed to run preg_replace: ' . $pcreErrorCode . ' / ' . Utils::pcreLastErrorMessage($pcreErrorCode));
            }
        }

        return $output;
    }
}

#Monolog\Formatter\NormalizerFormatter
class NormalizerFormatter implements FormatterInterface
{
    public const SIMPLE_DATE = "Y-m-d\TH:i:sP";
    ……
      public function __construct(?string $dateFormat = null)
    {
        $this->dateFormat = null === $dateFormat ? static::SIMPLE_DATE : $dateFormat;
        if (!function_exists('json_encode')) {
            throw new \RuntimeException('PHP\'s json extension is required to use Monolog\'s NormalizerFormatter');
        }
    }
    public function format(array $record)
    {
        return $this->normalize($record);
    }
     protected function normalize($data, int $depth = 0)
    {
        if ($depth > $this->maxNormalizeDepth) {
            return 'Over ' . $this->maxNormalizeDepth . ' levels deep, aborting normalization';
        }

        if (null === $data || is_scalar($data)) {
            if (is_float($data)) {
                if (is_infinite($data)) {
                    return ($data > 0 ? '' : '-') . 'INF';
                }
                if (is_nan($data)) {
                    return 'NaN';
                }
            }

            return $data;
        }

        if (is_array($data)) {
            $normalized = [];

            $count = 1;
            foreach ($data as $key => $value) {
                if ($count++ > $this->maxNormalizeItemCount) {
                    $normalized['...'] = 'Over ' . $this->maxNormalizeItemCount . ' items ('.count($data).' total), aborting normalization';
                    break;
                }

                $normalized[$key] = $this->normalize($value, $depth + 1);
            }

            return $normalized;    
        }
        ……
    }
    
}

LineFormatter设置默认格式,先调用父类格式化,之后再根据格式替换字符。时间格式是通过父类格式化。

父类NormalizerFormatter,设置默认时间格式。通过normalize函数递归调用格式化数据。大于最大层数maxNormalizeItemCount=1000,可能会显示异常。

最终修改

$autoloadfile = "vendor/autoload.php";
require_once $autoloadfile;

use Monolog\Handler\StreamHandler;
use Monolog\Logger;
use Monolog\Formatter\LineFormatter;

class myStreamHandler extends StreamHandler {
	protected function streamWrite($stream, array $record): void{
		$str = (string) $record['formatted'] . PHP_EOL . var_export($record['context'], true). PHP_EOL;
		fwrite($stream, $str);
	}
}

$logfile = "logs/20230718.log";
$log = new Logger('test1');

$dateFormat = "Y-m-d H:i:s";
$output = "%datetime%||%channel%||%level_name%\n%message%\n%extra%";
$formatter = new LineFormatter($output, $dateFormat);

//$sream = new StreamHandler($logfile, Logger::DEBUG);
$sream = new myStreamHandler($logfile, Logger::DEBUG);
$sream->setFormatter($formatter);

$log->pushHandler($sream);
$log->pushProcessor(function ($record) {
    var_dump($record);
    $record['extra']['test'] ='test' ;    
    return $record;
});


#执行输出
array(7) {
  'message' =>
  string(5) "test1"
  'context' =>
  array(1) {
    'qwe' =>
    string(3) "123"
  }
  'level' =>
  int(300)
  'level_name' =>
  string(7) "WARNING"
  'channel' =>
  string(5) "test1"
  'datetime' =>
  class Monolog\DateTimeImmutable#7 (4) {
    private $useMicroseconds =>
    bool(true)
    public $date =>
    string(26) "2023-07-19 15:15:31.218972"
    public $timezone_type =>
    int(3)
    public $timezone =>
    string(13) "Asia/Shanghai"
  }
  'extra' =>
  array(1) {
    'test' =>
    string(4) "test"
  }
}

#日志写入
2023-07-19 15:18:09||test1||WARNING
test1
{"test":"test"}
array (
  'qwe' => '123',
)

总结

monolog设计采用多层继承,将统一使用的方法抽象出类,其子类负责呈上启下的流程调用,其子类的子类则通过方法重写负责具体实现。

数据格式化可以使用HtmlFormatter或者JsonFormatter可以自己定义。

handler可以使用RedisHandler、PHPConsoleHandler等,根据构造不同穿的参数不同,功能很多,还能发送email。

比如process可以在write调用之前处理缓存数据,例如 MemoryPeakUsageProcessor设置内存用量 ,可以对extra参数设置逻辑数据。

你可能感兴趣的:(php)