Skip to content

Commit

Permalink
Moved to a custom PSR logger.
Browse files Browse the repository at this point in the history
  • Loading branch information
parpalak committed Feb 11, 2024
1 parent 9fa0290 commit 71cef36
Show file tree
Hide file tree
Showing 4 changed files with 284 additions and 84 deletions.
8 changes: 4 additions & 4 deletions _include/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@
/**
* Simple DI container to be used in legacy code.
*
* @copyright (C) 2023 Roman Parpalak
* @copyright (C) 2023-2024 Roman Parpalak
* @license http://www.gnu.org/licenses/gpl.html GPL version 2 or higher
* @package S2
*/

use johnykvsky\Utils\JKLogger;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
use S2\Cms\Image\ThumbnailGenerator;
use S2\Cms\Layout\LayoutMatcherFactory;
use S2\Cms\Logger\Logger;
use S2\Cms\Pdo\DbLayer;
use S2\Cms\Pdo\DbLayerPostgres;
use S2\Cms\Pdo\DbLayerSqlite;
Expand Down Expand Up @@ -95,10 +95,10 @@ private static function instantiate(string $className): object
);

case LoggerInterface::class:
return new JKLogger(defined('S2_LOG_DIR') ? S2_LOG_DIR : S2_CACHE_DIR, LogLevel::INFO, ['prefix' => 'log_', 'extension' => 'log']);
return new Logger((defined('S2_LOG_DIR') ? S2_LOG_DIR : S2_CACHE_DIR) . 'app.log', 'app', LogLevel::INFO);

case 'recommendations_logger':
return new JKLogger(defined('S2_LOG_DIR') ? S2_LOG_DIR : S2_CACHE_DIR, LogLevel::DEBUG, ['prefix' => 'recommendations_', 'extension' => 'log']);
return new Logger((defined('S2_LOG_DIR') ? S2_LOG_DIR : S2_CACHE_DIR) . 'recommendations.log', 'recommendations', LogLevel::INFO);

case 'recommendations_cache':
return new FilesystemAdapter('recommendations', 0, S2_CACHE_DIR);
Expand Down
269 changes: 269 additions & 0 deletions _include/src/Logger/Logger.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
<?php
/**
* PSR logger.
* Forked to include previous exceptions and to make exception traces more pretty.
*
* @copyright (C) 2024 Roman Parpalak, partially based on code (C) 2017 Mark Rogoyski
* @license MIT
* @see https://github.com/markrogoyski/simplelog-php
* @package S2
*/

declare(strict_types=1);

namespace S2\Cms\Logger;

use Psr\Log\LoggerInterface;
use Psr\Log\LoggerTrait;
use Psr\Log\LogLevel;

/**
* Simple Logger
* Powerful PSR-3 logging so easy it's simple!
*
* Implements PHP Standard Recommendation interface: PSR-3 \Psr\Log\LoggerInterface
*
* Log the following severities: debug, info, notice, warning, error, critical, alert, emergency.
* Log format: YYYY-mm-dd HH:ii:ss.uuuuuu [loglevel] [channel] Log message content {"Optional":"JSON Contextual Support Data"} {"Optional":"Exception Data"}
*
* Standard usage - default options:
* $logger = new SimpleLog\Logger('logfile.log', 'channelname');
* $logger->info('Normal informational event happened.');
* $logger->error('Something bad happened.', ['key1' => 'value that gives context', 'key2' => 'some more context', 'exception' => $e]);
*
* Optional constructor option: Set default lowest log level (Example error and above):
* $logger = new SimpleLog\Logger('logfile.log', 'channelname', \Psr\Log\LogLevel::ERROR);
* $logger->error('This will get logged');
* $logger->info('This is below the minimum log level and will not get logged');
*
* To log an exception, set as data context array key 'exception'
* $logger->error('Something exceptional happened.', ['exception' => $e]);
*
* To set output to standard out (STDOUT) as well as a log file:
* $logger->setOutput(true);
*
* To change the channel after construction:
* $logger->setChannel('newname')
*/
class Logger implements LoggerInterface
{
use LoggerTrait;

/**
* Lowest log level to log.
*/
private int $logLevel;

/**
* Whether to log to standard out.
*/
private bool $stdout = false;

/**
* Log fields separated by tabs to form a TSV (CSV with tabs).
*/
private const TAB = "\t";

/**
* Special minimum log level which will not log any log levels.
*/
public const LOG_LEVEL_NONE = 'none';

/**
* Log level hierarchy
*/
public const LEVELS = [
self::LOG_LEVEL_NONE => -1,
LogLevel::DEBUG => 0,
LogLevel::INFO => 1,
LogLevel::NOTICE => 2,
LogLevel::WARNING => 3,
LogLevel::ERROR => 4,
LogLevel::CRITICAL => 5,
LogLevel::ALERT => 6,
LogLevel::EMERGENCY => 7,
];

/**
* @param string $log_file File name and path of log file.
* @param string $channel Logger channel ("namespace") associated with this logger.
* @param string $logLevel (optional) Lowest log level to log.
*/
public function __construct(
private readonly string $log_file,
private string $channel,
string $logLevel = LogLevel::DEBUG
) {
$this->setLogLevel($logLevel);
}

/**
* Set the lowest log level to log.
*/
public function setLogLevel(string $logLevel): void
{
if (!isset(self::LEVELS[$logLevel])) {
throw new \DomainException("Log level $logLevel is not a valid log level. Must be one of (" . implode(', ', array_keys(self::LEVELS)) . ')');
}

$this->logLevel = self::LEVELS[$logLevel];
}

/**
* Set the log channel which identifies the log line.
*/
public function setChannel(string $channel): void
{
$this->channel = $channel;
}

/**
* Set the standard out option on or off.
* If set to true, log lines will also be printed to standard out.
*/
public function setOutput(bool $stdout): void
{
$this->stdout = $stdout;
}

/**
* Log a message.
* Generic log routine that all severity levels use to log an event.
*
* @throws \RuntimeException when log file cannot be opened for writing.
*/
public function log($level, string|\Stringable $message, array $context = []): void
{
if (!$this->logAtThisLevel($level)) {
return;
}

// Build log line
/** @var string $formattedException */
[$formattedException, $contextData] = $this->handleException($context);
try {
$formattedContext = json_encode($contextData, JSON_THROW_ON_ERROR | \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE);
} catch (\JsonException $e) {
$formattedContext = '{}';
}
$logLine = $this->formatLogLine($level, $message, $formattedContext, $formattedException);

// Log to file
try {
$fh = fopen($this->log_file, 'ab');
if ($fh === false) {
throw new \RuntimeException('fopen failed');
}
fwrite($fh, $logLine);
fclose($fh);
} catch (\Throwable $e) {
throw new \RuntimeException("Could not open log file {$this->log_file} for writing to SimpleLog channel {$this->channel}!", 0, $e);
}

// Log to stdout if option set to do so.
if ($this->stdout) {
print($logLine);
}
}

/**
* Determine if the logger should log at a certain log level.
*
* @return bool True if we log at this level; false otherwise.
*/
private function logAtThisLevel(string $level): bool
{
return self::LEVELS[$level] >= $this->logLevel;
}

/**
* Handle an exception in the data context array.
* If an exception is included in the data context array, extract it.
*
* @param mixed[] $context
*
* @return mixed[] [exception, data (without exception)]
*/
private function handleException(array $context): array
{
if (isset($context['exception']) && $context['exception'] instanceof \Throwable) {
$exception = $context['exception'];
$exception_data = self::buildExceptionData($exception);
unset($context['exception']);
} else {
$exception_data = '';
}

return [$exception_data, $context];
}

/**
* Build the exception log data.
*
* @param \Throwable $e
*
* @return string JSON {message, code, file, line, trace}
*/
private static function buildExceptionData(\Throwable $e): string
{
try {
$str = json_encode([
'message' => $e->getMessage(),
'code' => $e->getCode(),
], JSON_THROW_ON_ERROR | \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE) . \PHP_EOL .
'#0 ' . $e->getFile() . ':' . $e->getLine() . \PHP_EOL .
self::formatTrace($e->getTrace());

if ($e->getPrevious() !== null) {
$str .= \PHP_EOL . 'Previous Exception: ' . self::buildExceptionData($e->getPrevious());
}
return $str;
} catch (\JsonException $e) {
return '{"message":"' . $e->getMessage() . '"}';
}
}

private static function formatTrace(array $trace): string
{
$stack = '';
$i = 1;
foreach ($trace as $node) {
$stack .= "#$i " . $node['file'] . ":" . $node['line'] . " ";
if (isset($node['class'])) {
$stack .= $node['class'] . "->";
}
$stack .= $node['function'] . "()" . PHP_EOL;
$i++;
}
return $stack;
}

/**
* Format the log line.
* ```
* YYYY-mm-dd HH:ii:ss.uuuuuu [loglevel] [channel] Log message content {"Optional":"JSON Contextual Support Data"} {"Optional":"Exception Data"}
* Exception Trace if any
* ```
*/
private function formatLogLine(string $level, string $message, string $data, string $formattedException): string
{
return
$this->getTime() . self::TAB .
"[$level]" . self::TAB .
"[{$this->channel}]" . self::TAB .
str_replace(\PHP_EOL, ' ', trim($message)) . self::TAB .
str_replace(\PHP_EOL, ' ', $data) . self::TAB .
$formattedException . \PHP_EOL;
}

/**
* Get current date time, with microsecond precision.
* Format: YYYY-mm-dd HH:ii:ss.uuuuuu
*
* date('...') does not support microseconds (u)
*/
private function getTime(): string
{
return (new \DateTimeImmutable('now'))->format('Y-m-d H:i:s.u');
}
}
3 changes: 1 addition & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@
"ext-dom": "*",
"ext-gd": "*",
"altorouter/altorouter": "dev-master",
"psr/log": "^1.1",
"johnykvsky/jklogger": "^0.1.2",
"psr/log": "^3.0",
"s2/rose": "dev-master",
"symfony/error-handler": "^7.0",
"symfony/cache": "^7.0",
Expand Down
Loading

0 comments on commit 71cef36

Please sign in to comment.