TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Log\Writer;
|
||||
|
||||
use TYPO3\CMS\Core\Log\Exception\InvalidLogWriterConfigurationException;
|
||||
use TYPO3\CMS\Core\Security\BlockSerializationTrait;
|
||||
|
||||
/**
|
||||
* Abstract implementation of a log writer
|
||||
*/
|
||||
abstract class AbstractWriter implements WriterInterface
|
||||
{
|
||||
use BlockSerializationTrait;
|
||||
|
||||
/**
|
||||
* Constructs this log writer
|
||||
*
|
||||
* @param array $options Configuration options - depends on the actual log writer
|
||||
* @throws \TYPO3\CMS\Core\Log\Exception\InvalidLogWriterConfigurationException
|
||||
*/
|
||||
public function __construct(array $options = [])
|
||||
{
|
||||
foreach ($options as $optionKey => $optionValue) {
|
||||
$methodName = 'set' . ucfirst($optionKey);
|
||||
if (method_exists($this, $methodName)) {
|
||||
$this->{$methodName}($optionValue);
|
||||
} else {
|
||||
throw new InvalidLogWriterConfigurationException('Invalid LogWriter configuration option "' . $optionKey . '" for log writer of type "' . static::class . '"', 1321696152);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpolates context values into the message placeholders.
|
||||
*/
|
||||
protected function interpolate(string $message, array $context = []): string
|
||||
{
|
||||
// Build a replacement array with braces around the context keys.
|
||||
$replace = [];
|
||||
foreach ($context as $key => $val) {
|
||||
if (!is_array($val) && !is_null($val) && (!is_object($val) || method_exists($val, '__toString'))) {
|
||||
$replace['{' . $key . '}'] = $this->formatContextValue($val);
|
||||
}
|
||||
}
|
||||
|
||||
// Interpolate replacement values into the message and return.
|
||||
return strtr($message, $replace);
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape or quote a value from the context appropriate for the output.
|
||||
*
|
||||
* Note: In some output cases, escaping should not be done here but later on output,
|
||||
* such as if it's being written to a database for later display.
|
||||
*
|
||||
* @param string $value
|
||||
*/
|
||||
protected function formatContextValue(string $value): string
|
||||
{
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats an exception into a string.
|
||||
*
|
||||
* The format here is nearly the same as just casting an exception to a string,
|
||||
* but omits the full class namespace and stack trace, as those get very long.
|
||||
*/
|
||||
protected function formatException(\Throwable $ex): string
|
||||
{
|
||||
$classname = get_class($ex);
|
||||
if ($pos = strrpos($classname, '\\')) {
|
||||
$classname = substr($classname, $pos + 1);
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'- %s: %s, in file %s:%s',
|
||||
$classname,
|
||||
$ex->getMessage(),
|
||||
$ex->getFile(),
|
||||
$ex->getLine(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Log\Writer;
|
||||
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Log\LogLevel;
|
||||
use TYPO3\CMS\Core\Log\LogRecord;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Log writer that writes the log records into a database table.
|
||||
*/
|
||||
class DatabaseWriter extends AbstractWriter
|
||||
{
|
||||
/**
|
||||
* Writes the log record
|
||||
*
|
||||
* @param LogRecord $record Log record
|
||||
* @return \TYPO3\CMS\Core\Log\Writer\WriterInterface $this
|
||||
*/
|
||||
public function writeLog(LogRecord $record)
|
||||
{
|
||||
try {
|
||||
// Avoid ConnectionPool usage prior boot completion (see #96291).
|
||||
if (!GeneralUtility::getContainer()->get('boot.state')->complete) {
|
||||
return $this;
|
||||
}
|
||||
} catch (\LogicException $e) {
|
||||
// LogicException will be thrown if the container isn't available yet.
|
||||
return $this;
|
||||
}
|
||||
|
||||
$data = '';
|
||||
$context = $record->getData();
|
||||
if (!empty($context)) {
|
||||
// Fold an exception into the message, and string-ify it into context so it can be jsonified.
|
||||
if (isset($context['exception']) && $context['exception'] instanceof \Throwable) {
|
||||
$context['exception'] = (string)$context['exception'];
|
||||
}
|
||||
$data = json_encode($context);
|
||||
}
|
||||
|
||||
$fieldValues = [
|
||||
'request_id' => $record->getRequestId(),
|
||||
'time_micro' => $record->getCreated(),
|
||||
'component' => $record->getComponent(),
|
||||
'level' => LogLevel::normalizeLevel($record->getLevel()),
|
||||
'message' => $record->getMessage(),
|
||||
'data' => $data,
|
||||
];
|
||||
|
||||
// sys_log uses tstamp for garbage collection via TableGarbageCollectionTask.
|
||||
// Without it, tstamp defaults to 0 (1970-01-01), causing immediate deletion (see #109290).
|
||||
$fieldValues['tstamp'] = (int)$record->getCreated();
|
||||
|
||||
GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getConnectionForTable('sys_log')
|
||||
->insert('sys_log', $fieldValues);
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Log\Writer\Enum;
|
||||
|
||||
enum Interval: string
|
||||
{
|
||||
case DAILY = 'daily';
|
||||
case WEEKLY = 'weekly';
|
||||
case MONTHLY = 'monthly';
|
||||
case YEARLY = 'yearly';
|
||||
|
||||
public function getDateInterval(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::DAILY => 'P1D',
|
||||
self::WEEKLY => 'P1W',
|
||||
self::MONTHLY => 'P1M',
|
||||
self::YEARLY => 'P1Y',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Log\Writer;
|
||||
|
||||
use TYPO3\CMS\Core\Core\Environment;
|
||||
use TYPO3\CMS\Core\Crypto\HashService;
|
||||
use TYPO3\CMS\Core\Log\Exception\InvalidLogWriterConfigurationException;
|
||||
use TYPO3\CMS\Core\Log\LogRecord;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\PathUtility;
|
||||
|
||||
/**
|
||||
* Log writer that writes the log records into a file.
|
||||
*/
|
||||
class FileWriter extends AbstractWriter
|
||||
{
|
||||
/**
|
||||
* Log file path, relative to TYPO3's base project folder
|
||||
*/
|
||||
protected string $logFile = '';
|
||||
|
||||
protected string $logFileInfix = '';
|
||||
|
||||
/**
|
||||
* Default log file path
|
||||
*/
|
||||
protected string $defaultLogFileTemplate = '/log/typo3_%s.log';
|
||||
|
||||
/**
|
||||
* Log file handle storage
|
||||
*
|
||||
* To avoid concurrent file handles on a the same file when using several FileWriter instances,
|
||||
* we share the file handles in a static class variable
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
protected static array $logFileHandles = [];
|
||||
|
||||
/**
|
||||
* Keep track of used file handles by different fileWriter instances
|
||||
*
|
||||
* As the logger gets instantiated by class name but the resources
|
||||
* are shared via the static $logFileHandles we need to track usage
|
||||
* of file handles to avoid closing handles that are still needed
|
||||
* by different instances. Only if the count is zero may the file
|
||||
* handle be closed.
|
||||
*/
|
||||
protected static array $logFileHandlesCount = [];
|
||||
|
||||
/**
|
||||
* Constructor, opens the log file handle
|
||||
*/
|
||||
public function __construct(array $options = [])
|
||||
{
|
||||
// the parent constructor reads $options and sets them
|
||||
parent::__construct($options);
|
||||
if (empty($options['logFile'])
|
||||
// omit logging if TYPO3 has not been configured (avoid creating a guessable filename)
|
||||
&& ($GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'] ?? '') !== ''
|
||||
) {
|
||||
$this->setLogFile($this->getDefaultLogFileName());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destructor, closes the log file handle
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
if ($this->logFile === '') {
|
||||
return;
|
||||
}
|
||||
self::$logFileHandlesCount[$this->logFile]--;
|
||||
if (self::$logFileHandlesCount[$this->logFile] <= 0) {
|
||||
$this->closeLogFile();
|
||||
}
|
||||
}
|
||||
|
||||
public function setLogFileInfix(string $infix)
|
||||
{
|
||||
$this->logFileInfix = $infix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the path to the log file.
|
||||
*
|
||||
* @param string $relativeLogFile path to the log file, relative to public web dir
|
||||
* @return WriterInterface
|
||||
* @throws InvalidLogWriterConfigurationException
|
||||
*/
|
||||
public function setLogFile(string $relativeLogFile)
|
||||
{
|
||||
$logFile = $relativeLogFile;
|
||||
if (!PathUtility::isAbsolutePath($logFile)) {
|
||||
$logFile = GeneralUtility::getFileAbsFileName($logFile);
|
||||
if (empty($logFile)) {
|
||||
throw new InvalidLogWriterConfigurationException(
|
||||
'Log file path "' . $relativeLogFile . '" is not valid!',
|
||||
1444374805
|
||||
);
|
||||
}
|
||||
}
|
||||
$this->logFile = $logFile;
|
||||
$this->openLogFile();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the path to the log file.
|
||||
*/
|
||||
public function getLogFile(): string
|
||||
{
|
||||
return $this->logFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the log record
|
||||
*
|
||||
* @param LogRecord $record Log record
|
||||
* @return WriterInterface $this
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function writeLog(LogRecord $record)
|
||||
{
|
||||
if ($this->logFile === '') {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$data = '';
|
||||
$context = $record->getData();
|
||||
$message = $record->getMessage();
|
||||
if (!empty($context)) {
|
||||
// Fold an exception into the message, and string-ify it into context so it can be jsonified.
|
||||
if (isset($context['exception']) && $context['exception'] instanceof \Throwable) {
|
||||
$message .= $this->formatException($context['exception']);
|
||||
$context['exception'] = (string)$context['exception'];
|
||||
}
|
||||
$data = '- ' . json_encode($context, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
$message = sprintf(
|
||||
'%s [%s] request="%s" component="%s": %s %s',
|
||||
date('r', (int)$record->getCreated()),
|
||||
strtoupper($record->getLevel()),
|
||||
$record->getRequestId(),
|
||||
$record->getComponent(),
|
||||
$this->interpolate($message, $context),
|
||||
$data
|
||||
);
|
||||
|
||||
if (fwrite(self::$logFileHandles[$this->logFile], $message . LF) === false) {
|
||||
throw new \RuntimeException('Could not write log record to log file', 1345036335);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the log file handle
|
||||
*
|
||||
* @throws \RuntimeException if the log file can't be opened.
|
||||
*/
|
||||
protected function openLogFile()
|
||||
{
|
||||
if (isset(self::$logFileHandlesCount[$this->logFile])) {
|
||||
self::$logFileHandlesCount[$this->logFile]++;
|
||||
} else {
|
||||
self::$logFileHandlesCount[$this->logFile] = 1;
|
||||
}
|
||||
if (isset(self::$logFileHandles[$this->logFile]) && is_resource(self::$logFileHandles[$this->logFile] ?? false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->createLogFile();
|
||||
self::$logFileHandles[$this->logFile] = fopen($this->logFile, 'a');
|
||||
if (!is_resource(self::$logFileHandles[$this->logFile])) {
|
||||
throw new \RuntimeException('Could not open log file "' . $this->logFile . '"', 1321804422);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the log file handle.
|
||||
*/
|
||||
protected function closeLogFile()
|
||||
{
|
||||
if (!empty(self::$logFileHandles[$this->logFile]) && is_resource(self::$logFileHandles[$this->logFile])) {
|
||||
fclose(self::$logFileHandles[$this->logFile]);
|
||||
unset(self::$logFileHandles[$this->logFile]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the log file with correct permissions
|
||||
* and parent directories, if needed
|
||||
*/
|
||||
protected function createLogFile()
|
||||
{
|
||||
if (file_exists($this->logFile)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// skip mkdir if logFile refers to any scheme but file:// or empty
|
||||
$scheme = parse_url($this->logFile, PHP_URL_SCHEME);
|
||||
if ($scheme === null || $scheme === 'file' || PathUtility::isAbsolutePath($this->logFile)) {
|
||||
// remove file:/ before creating the directory
|
||||
$logFileDirectory = PathUtility::dirname((string)preg_replace('#^file:/#', '', $this->logFile));
|
||||
if (!@is_dir($logFileDirectory)) {
|
||||
GeneralUtility::mkdir_deep($logFileDirectory);
|
||||
// create .htaccess file if log file is within the site path
|
||||
if (PathUtility::getCommonPrefix([Environment::getPublicPath() . '/', $logFileDirectory]) === (Environment::getPublicPath() . '/')) {
|
||||
// only create .htaccess, if we created the directory on our own
|
||||
$this->createHtaccessFile($logFileDirectory . '/.htaccess');
|
||||
}
|
||||
}
|
||||
}
|
||||
// create the log file
|
||||
GeneralUtility::writeFile($this->logFile, '', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates .htaccess file inside a new directory to access protect it
|
||||
*
|
||||
* @param string $htaccessFile Path of .htaccess file
|
||||
*/
|
||||
protected function createHtaccessFile($htaccessFile)
|
||||
{
|
||||
// write .htaccess file to protect the log file
|
||||
if (!empty($GLOBALS['TYPO3_CONF_VARS']['SYS']['generateApacheHtaccess']) && !file_exists($htaccessFile)) {
|
||||
$htaccessContent = <<<END
|
||||
# Apache < 2.3
|
||||
<IfModule !mod_authz_core.c>
|
||||
Order allow,deny
|
||||
Deny from all
|
||||
Satisfy All
|
||||
</IfModule>
|
||||
|
||||
# Apache ≥ 2.3
|
||||
<IfModule mod_authz_core.c>
|
||||
Require all denied
|
||||
</IfModule>
|
||||
END;
|
||||
GeneralUtility::writeFile($htaccessFile, $htaccessContent, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the default log file.
|
||||
* Uses the defaultLogFileTemplate and replaces the %s placeholder with a short MD5 hash
|
||||
* based on a static string and the current encryption key.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getDefaultLogFileName()
|
||||
{
|
||||
$hashService = GeneralUtility::makeInstance(HashService::class);
|
||||
$namePart = substr($hashService->hmac($this->defaultLogFileTemplate, 'defaultLogFile'), 0, 10);
|
||||
if ($this->logFileInfix !== '') {
|
||||
$namePart = $this->logFileInfix . '_' . $namePart;
|
||||
}
|
||||
return Environment::getVarPath() . sprintf($this->defaultLogFileTemplate, $namePart);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Log\Writer;
|
||||
|
||||
use TYPO3\CMS\Core\Log\LogRecord;
|
||||
|
||||
/**
|
||||
* Null writer - just forgets about everything
|
||||
*/
|
||||
class NullWriter extends AbstractWriter
|
||||
{
|
||||
/**
|
||||
* Writes the log record
|
||||
*
|
||||
* @param LogRecord $record Log record
|
||||
* @return \TYPO3\CMS\Core\Log\Writer\WriterInterface $this
|
||||
*/
|
||||
public function writeLog(LogRecord $record)
|
||||
{
|
||||
// do nothing
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Log\Writer;
|
||||
|
||||
use TYPO3\CMS\Core\Log\LogRecord;
|
||||
|
||||
/**
|
||||
* Log writer that writes the log records into PHP error log.
|
||||
*/
|
||||
class PhpErrorLogWriter extends AbstractWriter
|
||||
{
|
||||
/**
|
||||
* Writes the log record
|
||||
*
|
||||
* @param LogRecord $record Log record
|
||||
* @return \TYPO3\CMS\Core\Log\Writer\WriterInterface $this
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function writeLog(LogRecord $record)
|
||||
{
|
||||
$data = '';
|
||||
$context = $record->getData();
|
||||
$message = $record->getMessage();
|
||||
if (!empty($context)) {
|
||||
// Fold an exception into the message, and string-ify it into context so it can be jsonified.
|
||||
if (isset($context['exception']) && $context['exception'] instanceof \Throwable) {
|
||||
$message .= $this->formatException($context['exception']);
|
||||
$context['exception'] = (string)$context['exception'];
|
||||
}
|
||||
$data = '- ' . json_encode($context);
|
||||
}
|
||||
|
||||
$message = sprintf(
|
||||
'TYPO3 [%s] request="%s" component="%s": %s %s',
|
||||
strtoupper($record->getLevel()),
|
||||
$record->getRequestId(),
|
||||
$record->getComponent(),
|
||||
$this->interpolate($message, $context),
|
||||
$data
|
||||
);
|
||||
if (error_log($message) === false) {
|
||||
throw new \RuntimeException('Could not write log record to PHP error log', 1345036336);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Log\Writer;
|
||||
|
||||
use TYPO3\CMS\Core\Locking\Exception\LockAcquireException;
|
||||
use TYPO3\CMS\Core\Locking\Exception\LockAcquireWouldBlockException;
|
||||
use TYPO3\CMS\Core\Locking\Exception\LockCreateException;
|
||||
use TYPO3\CMS\Core\Locking\LockFactory;
|
||||
use TYPO3\CMS\Core\Log\LogRecord;
|
||||
use TYPO3\CMS\Core\Log\Writer\Enum\Interval;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Write logs into files while providing basic rotation capabilities. This is a very basic approach, suitable for
|
||||
* environments where established tools like logrotate are not available.
|
||||
*/
|
||||
class RotatingFileWriter extends FileWriter
|
||||
{
|
||||
private const string ROTATION_DATE_FORMAT = 'YmdHis';
|
||||
|
||||
private Interval $interval = Interval::DAILY;
|
||||
private int $maxFiles = 5;
|
||||
private \DateTimeImmutable $lastRotation;
|
||||
private \DateTimeImmutable $nextRotation;
|
||||
|
||||
public function __construct(array $options = [])
|
||||
{
|
||||
parent::__construct($options);
|
||||
}
|
||||
|
||||
public function setLogFile(string $relativeLogFile): self
|
||||
{
|
||||
parent::setLogFile($relativeLogFile);
|
||||
|
||||
$this->updateRuntimeRotationState($this->getLastRotation());
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal setter called by FileWriter constructor
|
||||
*/
|
||||
protected function setInterval(string|Interval $interval): void
|
||||
{
|
||||
if (is_string($interval)) {
|
||||
// String support is required for use in system/settings.php
|
||||
$this->interval = Interval::tryFrom($interval) ?? Interval::DAILY;
|
||||
} else {
|
||||
$this->interval = $interval;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal setter called by FileWriter constructor
|
||||
*/
|
||||
protected function setMaxFiles(int $maxFiles): void
|
||||
{
|
||||
$this->maxFiles = max(0, $maxFiles);
|
||||
}
|
||||
|
||||
public function writeLog(LogRecord $record)
|
||||
{
|
||||
if ($this->needsRotation()) {
|
||||
$lockFactory = GeneralUtility::makeInstance(LockFactory::class);
|
||||
try {
|
||||
$lock = $lockFactory->createLocker('rotate-' . $this->logFile);
|
||||
if ($lock->acquire()) {
|
||||
$this->updateRuntimeRotationState($this->getLastRotation());
|
||||
// check again if rotation is still needed (could have happened in the meantime)
|
||||
if ($this->needsRotation()) {
|
||||
$this->rotate();
|
||||
}
|
||||
}
|
||||
$lock->release();
|
||||
} catch (LockCreateException|LockAcquireException|LockAcquireWouldBlockException) {
|
||||
}
|
||||
}
|
||||
return parent::writeLog($record);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method rotates all log files found by using `glob()` to take all already rotated logs into account, even
|
||||
* after a configuration change.
|
||||
*
|
||||
* Log files are rotated using the "copytruncate" approach: the current open log file is copied as-is to a new
|
||||
* location, the current log file gets flushed afterward. This way, file handles don't need to get re-created.
|
||||
*/
|
||||
protected function rotate(): void
|
||||
{
|
||||
$rotationSuffix = date(self::ROTATION_DATE_FORMAT);
|
||||
|
||||
// copytruncate: Rotate the currently used log file
|
||||
copy($this->logFile, $this->logFile . '.' . $rotationSuffix);
|
||||
ftruncate(self::$logFileHandles[$this->logFile], 0);
|
||||
|
||||
$rotatedLogFiles = glob($this->logFile . '.*');
|
||||
rsort($rotatedLogFiles, SORT_NATURAL);
|
||||
|
||||
// Remove any excess files
|
||||
if ($this->maxFiles > 0) {
|
||||
$excessFiles = array_slice($rotatedLogFiles, $this->maxFiles);
|
||||
foreach ($excessFiles as $excessFile) {
|
||||
unlink($excessFile);
|
||||
}
|
||||
}
|
||||
|
||||
$this->updateRuntimeRotationState(new \DateTimeImmutable());
|
||||
}
|
||||
|
||||
protected function getLastRotation(): \DateTimeImmutable
|
||||
{
|
||||
// Rotate already rotated files again
|
||||
$rotatedLogFiles = glob($this->logFile . '.*');
|
||||
|
||||
if ($rotatedLogFiles !== []) {
|
||||
// Sort rotated files to handle the newest one first
|
||||
rsort($rotatedLogFiles, SORT_NATURAL);
|
||||
$newestLog = current($rotatedLogFiles);
|
||||
|
||||
$rotationDelimiterPosition = strrpos($newestLog, '.');
|
||||
$timestamp = substr($newestLog, $rotationDelimiterPosition + 1);
|
||||
|
||||
$latestRotationDateTime = \DateTimeImmutable::createFromFormat(self::ROTATION_DATE_FORMAT, $timestamp);
|
||||
if ($latestRotationDateTime instanceof \DateTimeImmutable) {
|
||||
return $latestRotationDateTime;
|
||||
}
|
||||
}
|
||||
|
||||
return new \DateTimeImmutable('@0');
|
||||
}
|
||||
|
||||
protected function determineNextRotation(): \DateTimeImmutable
|
||||
{
|
||||
return $this->lastRotation->add(new \DateInterval($this->interval->getDateInterval()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if log files need to be rotated under following conditions:
|
||||
*
|
||||
* 1.
|
||||
* a) either the next rotation is due
|
||||
* b) logs were never rotated before
|
||||
* 2. the log file is not empty - FileWriter::setLogFile() creates one if missing
|
||||
*/
|
||||
protected function needsRotation(): bool
|
||||
{
|
||||
return ($this->nextRotation <= new \DateTimeImmutable() || $this->lastRotation->getTimestamp() === 0) && filesize($this->logFile) > 0;
|
||||
}
|
||||
|
||||
protected function updateRuntimeRotationState(\DateTimeImmutable $lastRotation): void
|
||||
{
|
||||
$this->lastRotation = $lastRotation;
|
||||
$this->nextRotation = $this->determineNextRotation();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Log\Writer;
|
||||
|
||||
use TYPO3\CMS\Core\Log\LogLevel;
|
||||
use TYPO3\CMS\Core\Log\LogRecord;
|
||||
|
||||
/**
|
||||
* Log writer that writes to syslog
|
||||
*/
|
||||
class SyslogWriter extends AbstractWriter
|
||||
{
|
||||
/**
|
||||
* List of valid syslog facility names.
|
||||
* private as it's not supposed to be changed.
|
||||
*
|
||||
* @var array<string, int> Facilities
|
||||
*/
|
||||
private $facilities = [
|
||||
'auth' => LOG_AUTH,
|
||||
'authpriv' => LOG_AUTHPRIV,
|
||||
'cron' => LOG_CRON,
|
||||
'daemon' => LOG_DAEMON,
|
||||
'kern' => LOG_KERN,
|
||||
'lpr' => LOG_LPR,
|
||||
'mail' => LOG_MAIL,
|
||||
'news' => LOG_NEWS,
|
||||
'syslog' => LOG_SYSLOG,
|
||||
'user' => LOG_USER,
|
||||
'uucp' => LOG_UUCP,
|
||||
];
|
||||
|
||||
/**
|
||||
* Type of program that is logging to syslog.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $facility = LOG_USER;
|
||||
|
||||
/**
|
||||
* Constructor, adds facilities on *nix environments.
|
||||
*
|
||||
* @param array $options Configuration options
|
||||
* @throws \RuntimeException if connection to syslog cannot be opened
|
||||
* @see \TYPO3\CMS\Core\Log\Writer\AbstractWriter
|
||||
*/
|
||||
public function __construct(array $options = [])
|
||||
{
|
||||
// additional facilities for *nix environments
|
||||
if (!defined('PHP_WINDOWS_VERSION_BUILD')) {
|
||||
$this->facilities['local0'] = LOG_LOCAL0;
|
||||
$this->facilities['local1'] = LOG_LOCAL1;
|
||||
$this->facilities['local2'] = LOG_LOCAL2;
|
||||
$this->facilities['local3'] = LOG_LOCAL3;
|
||||
$this->facilities['local4'] = LOG_LOCAL4;
|
||||
$this->facilities['local5'] = LOG_LOCAL5;
|
||||
$this->facilities['local6'] = LOG_LOCAL6;
|
||||
$this->facilities['local7'] = LOG_LOCAL7;
|
||||
}
|
||||
parent::__construct($options);
|
||||
openlog('TYPO3', LOG_ODELAY | LOG_PID, $this->facility);
|
||||
}
|
||||
|
||||
/**
|
||||
* Destructor, closes connection to syslog.
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
closelog();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the facility to use when logging to syslog.
|
||||
*
|
||||
* @param string $facility Facility to use when logging.
|
||||
*/
|
||||
public function setFacility($facility): void
|
||||
{
|
||||
if (array_key_exists(strtolower($facility), $this->facilities)) {
|
||||
$this->facility = $this->facilities[strtolower($facility)];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data of the record in syslog format
|
||||
*/
|
||||
public function getMessageForSyslog(LogRecord $record): string
|
||||
{
|
||||
$data = '';
|
||||
$context = $record->getData();
|
||||
$message = $record->getMessage();
|
||||
if (!empty($context)) {
|
||||
// Fold an exception into the message, and string-ify it into context so it can be jsonified.
|
||||
if (isset($context['exception']) && $context['exception'] instanceof \Throwable) {
|
||||
$message .= $this->formatException($context['exception']);
|
||||
$context['exception'] = (string)$context['exception'];
|
||||
}
|
||||
$data = '- ' . json_encode($context);
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'[request="%s" component="%s"] %s %s',
|
||||
$record->getRequestId(),
|
||||
$record->getComponent(),
|
||||
$this->interpolate($message, $context),
|
||||
$data
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the log record to syslog
|
||||
*
|
||||
* @return \TYPO3\CMS\Core\Log\Writer\WriterInterface
|
||||
*/
|
||||
public function writeLog(LogRecord $record)
|
||||
{
|
||||
syslog(LogLevel::normalizeLevel($record->getLevel()), $this->getMessageForSyslog($record));
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Log\Writer;
|
||||
|
||||
use TYPO3\CMS\Core\Log\LogRecord;
|
||||
|
||||
/**
|
||||
* Log writer interface
|
||||
*/
|
||||
interface WriterInterface
|
||||
{
|
||||
/**
|
||||
* Writes the log record
|
||||
*
|
||||
* @param \TYPO3\CMS\Core\Log\LogRecord $record Log record
|
||||
* @return \TYPO3\CMS\Core\Log\Writer\WriterInterface $this
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function writeLog(LogRecord $record);
|
||||
}
|
||||
Reference in New Issue
Block a user