Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ Examples:
* `log/dk-{Y}-{m}-{d}.log` will write every day to a different file (eg: `log/dk-2021-01-01.log`)
* `log/dk-{Y}-{W}.log` will write every week to a different file (eg: `log/dk-2021-10.log`)

The full list of format specifiers is available [here](https://www.php.net/manual/en/datetime.format.php).
The full list of format specifiers is available in the [official documentation](https://www.php.net/manual/en/datetime.format.php).

## Filtering log messages

Expand Down
13 changes: 13 additions & 0 deletions docs/book/v5/configuring-writer.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ return [
'level' => \Dot\Log\Logger::ALERT, // this is equal to 1
'options' => [
'stream' => __DIR__ . '/../../log/dk.log',
'log_lifetime' => null, // OPTIONAL
],
],
],
Expand All @@ -41,3 +42,15 @@ It is a way to organize writers.
The `level` key is optional.

The key `stream` is required only if writing into streams/files.

The `options.log_lifetime` key is optional.

## Automatic Log File Deletion

The `options.log_lifetime` key specifies a date after which the specific `writer`'s log files will be **deleted even if not empty**.

The key can be omitted completely or set to `null` in order to skip this feature.

To make use of automatic log file deletion, set the value to the number of **days** after which a log file should be deleted.

> This feature will delete all relevant log files for the configured `writer`, take care not to misconfigure it in a production environment!
3 changes: 3 additions & 0 deletions log.global.php.dist
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ return [
'name' => 'MyFilter',
],
],
// optional key that can be omitted or set to null to disable auto-deletion of logs
// accepts an integer representing the number of days after which a log will be deleted
'log_lifetime' => null,
],
],
],
Expand Down
5 changes: 4 additions & 1 deletion src/Factory/LoggerAbstractServiceFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use function date;
use function explode;
use function is_array;
use function pathinfo;
use function preg_match_all;
use function str_replace;

Expand Down Expand Up @@ -103,7 +104,9 @@ protected function processConfig(array &$config, ContainerInterface $services):
if (isset($config['writers'])) {
foreach ($config['writers'] as $index => $writerConfig) {
if (! empty($writerConfig['options']['stream'])) {
$config['writers'][$index]['options']['stream'] = self::parseVariables(
$config['writers'][$index]['options']['stream_format'] =
pathinfo($writerConfig['options']['stream'])['basename'];
$config['writers'][$index]['options']['stream'] = self::parseVariables(
$writerConfig['options']['stream']
);
}
Expand Down
94 changes: 93 additions & 1 deletion src/Writer/Stream.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,45 @@

namespace Dot\Log\Writer;

use DateTimeImmutable;
use Dot\Log\Exception\InvalidArgumentException;
use Dot\Log\Exception\RuntimeException;
use ErrorException;
use Exception;
use Laminas\Stdlib\ErrorHandler;
use Psr\Container\ContainerExceptionInterface;
use Traversable;

use function chmod;
use function dirname;
use function error_log;
use function fclose;
use function file_exists;
use function filemtime;
use function fopen;
use function fwrite;
use function get_resource_type;
use function gettype;
use function is_array;
use function is_dir;
use function is_link;
use function is_numeric;
use function is_resource;
use function is_string;
use function is_writable;
use function iterator_to_array;
use function pathinfo;
use function preg_grep;
use function preg_match_all;
use function scandir;
use function sprintf;
use function str_replace;
use function stream_get_meta_data;
use function time;
use function touch;
use function unlink;

use const PATHINFO_DIRNAME;
use const PHP_EOL;

class Stream extends AbstractWriter
Expand All @@ -41,6 +57,10 @@ class Stream extends AbstractWriter
*/
protected mixed $stream;

protected ?int $logLifetime = null;

protected ?string $streamFormat = null;

/**
* @throws ContainerExceptionInterface
* @throws ErrorException
Expand All @@ -51,12 +71,16 @@ public function __construct(
?string $logSeparator = null,
?int $filePermissions = null
) {
$logLifetime = null;
$streamFormat = null;

if ($streamOrUrl instanceof Traversable) {
$streamOrUrl = iterator_to_array($streamOrUrl);
}

if (is_array($streamOrUrl)) {
parent::__construct($streamOrUrl);
$logLifetime = $streamOrUrl['log_lifetime'] ?? null;
$streamFormat = $streamOrUrl['stream_format'] ?? null;
$mode = $streamOrUrl['mode'] ?? null;
$logSeparator = $streamOrUrl['log_separator'] ?? null;
$filePermissions = $streamOrUrl['chmod'] ?? $filePermissions;
Expand Down Expand Up @@ -113,6 +137,25 @@ public function __construct(
if (null !== $logSeparator) {
$this->setLogSeparator($logSeparator);
}

if (is_numeric($logLifetime)) {
$logLifetime = ($logLifetime > 0 ? '-' . $logLifetime : $logLifetime) . ' days';

try {
$logLifetime = (new DateTimeImmutable($logLifetime))->getTimestamp();
if ($logLifetime >= time()) {
$logLifetime = null;
}
} catch (Exception $e) {
$logLifetime = null;
}

if ($logLifetime !== null && $streamFormat !== null) {
$this->streamFormat = $streamFormat;
}

$this->logLifetime = $logLifetime;
}
}

/**
Expand Down Expand Up @@ -146,8 +189,57 @@ public function getLogSeparator(): string
*/
public function shutdown(): void
{
if ($this->logLifetime !== null && $this->streamFormat !== null) {
$this->removeOldLogs();
}

if (is_resource($this->stream)) {
fclose($this->stream);
}
}

protected function removeOldLogs(): void
{
preg_match_all('/{([a-z])}/i', $this->streamFormat, $matches);

if (! empty($matches[1])) {
foreach ($matches[1] as $match) {
$this->streamFormat = str_replace('{' . $match . '}', '\d+', $this->streamFormat);
}

$this->streamFormat = str_replace('.', '\.', $this->streamFormat);
}
$streamData = stream_get_meta_data($this->stream);

$path = $streamData['uri'];
$directory = pathinfo($path, PATHINFO_DIRNAME);

if (! is_dir($directory)) {
error_log("{$directory} is not a directory");
return;
}

$files = scandir($directory) ?: [];
$matches = preg_grep('/^' . $this->streamFormat . '$/', $files) ?: [];

foreach ($matches as $match) {
$match = sprintf('%s/%s', $directory, $match);

if (is_link($match)) {
continue;
}

$fileTimestamp = filemtime($match);

if (
$fileTimestamp !== false
&& $fileTimestamp < $this->logLifetime
&& is_writable($directory)
) {
if (! @unlink($match)) {
error_log("{$match} could not be deleted");
}
}
}
}
}