Skip to content

Commit

Permalink
add context management for Yii logs
Browse files Browse the repository at this point in the history
  • Loading branch information
klimov-paul committed Jul 18, 2023
1 parent 5e563a5 commit 74b6480
Show file tree
Hide file tree
Showing 2 changed files with 245 additions and 5 deletions.
191 changes: 186 additions & 5 deletions src/Logger.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace yii1tech\psr\log;

use CLogger;
use InvalidArgumentException;
use Psr\Log\LoggerInterface;
use Yii;

Expand Down Expand Up @@ -43,11 +44,21 @@ class Logger extends CLogger
*/
public $yiiLogEnabled = true;

/**
* @var int max nested level for the log context to be written into Yii log message.
*/
public $logContextMaxNestedLevel = 3;

/**
* @var \Psr\Log\LoggerInterface|null related PSR logger.
*/
private $_psrLogger;

/**
* @var \Closure|array
*/
private $_globalLogContext;

/**
* @return \Psr\Log\LoggerInterface|null related PSR logger instance.
*/
Expand Down Expand Up @@ -80,6 +91,34 @@ public function setPsrLogger($psrLogger): self
return $this;
}

/**
* Sets the log context, which should be applied to each log message.
* You can use a `\Closure` to specify calculated expression for it.
* For example:
*
* ```php
* $logger = \yii1tech\psr\log\Logger::new()
* ->withContext(function () {
* return [
* 'ip' => $_SERVER['REMOTE_ADDR'] ?? null,
* ];
* });
* ```
*
* @param \Closure|array|null $globalLogContext global log context.
* @return static self reference.
*/
public function withContext($globalLogContext): self
{
if ($globalLogContext !== null && !is_array($globalLogContext) && !$globalLogContext instanceof \Closure) {
throw new InvalidArgumentException('"' . get_class($this) . '::$globalLogContext" should be either an array or a `\\Closure`');
}

$this->_globalLogContext = $globalLogContext;

return $this;
}

/**
* @see $yiiLogEnabled
*
Expand All @@ -93,13 +132,30 @@ public function enableYiiLog(bool $enable = true): self
return $this;
}

/**
* @see $logContextMaxNestedLevel
*
* @param int $logContextMaxNestedLevel max nested level for the log context to be written into Yii log message.
* @return static self reference.
*/
public function setLogContextMaxNestedLevel(int $logContextMaxNestedLevel): self
{
$this->logContextMaxNestedLevel = $logContextMaxNestedLevel;

return $this;
}

/**
* {@inheritdoc}
*/
public function log($message, $level = 'info', $category = 'application'): void
{
if (is_array($category)) {
$context = $category;
$rawContext = array_merge(
$this->getGlobalLogContext(),
$category
);
$context = $rawContext;

if (isset($context['category'])) {
$category = $context['category'];
Expand All @@ -108,9 +164,13 @@ public function log($message, $level = 'info', $category = 'application'): void
$context['category'] = $category;
}
} else {
$context = [
'category' => $category,
];
$rawContext = $this->getGlobalLogContext();
$context = array_merge(
$rawContext,
[
'category' => $category,
]
);
}

if (($psrLogger = $this->getPsrLogger()) !== null) {
Expand All @@ -123,13 +183,134 @@ public function log($message, $level = 'info', $category = 'application'): void

if ($this->yiiLogEnabled) {
parent::log(
$message,
$message . $this->createMessageSuffixFromContext($rawContext),
LogLevelConverter::toYii($level),
$category
);
}
}

/**
* Returns global log context.
*
* @return array log context.
*/
protected function getGlobalLogContext(): array
{
if ($this->_globalLogContext === null) {
return [];
}

if ($this->_globalLogContext instanceof \Closure) {
try {
return call_user_func($this->_globalLogContext);
} catch (\Throwable $exception) {
return [];
}
}

return $this->_globalLogContext;
}

/**
* Creates a trailing suffix for the log message from the log context.
*
* @param array $logContext log context.
* @return string log message suffix.
*/
protected function createMessageSuffixFromContext(array $logContext): string
{
if (empty($logContext)) {
return '';
}

$logContext = $this->formatLogContext($logContext);

return "\n\n" . $this->serializeLogContext($logContext);
}

/**
* Serializes log context into a string.
*
* @param array $logContext raw log context.
* @return string serialized log context.
*/
protected function serializeLogContext(array $logContext): string
{
if (YII_DEBUG) {
return json_encode($logContext, JSON_PRETTY_PRINT);
}

return json_encode($logContext);
}

/**
* Formats log context to be suitable for string serialization.
*
* @param array $logContext raw log context.
* @param int $nestedLevel current nested level.
* @return array formatted log context.
*/
protected function formatLogContext(array $logContext, int $nestedLevel = 0): array
{
if ($nestedLevel > $this->logContextMaxNestedLevel) {
return [];
}

foreach ($logContext as $key => $value) {
if (is_object($value)) {
if ($value instanceof \Throwable) {
$logContext[$key] = [
'class' => get_class($value),
'code' => $value->getCode(),
'message' => $value->getMessage(),
'file' => $value->getFile(),
'line' => $value->getLine(),
];

continue;
}

if ($value instanceof \Traversable) {
$logContext[$key] = $this->formatLogContext(iterator_to_array($value), $nestedLevel + 1);

continue;
}

if ($value instanceof \JsonSerializable) {
$value = $value->jsonSerialize();
if (is_array($value)) {
$logContext[$key] = $this->formatLogContext($value, $nestedLevel + 1);

continue;
}

if (is_object($value)) {
$logContext[$key] = get_class($value);

continue;
}

$logContext[$key] = $value;

continue;
}

$logContext[$key] = get_class($value);

continue;
}

if (is_array($value)) {
$logContext[$key] = $this->formatLogContext($value, $nestedLevel + 1);

continue;
}
}

return $logContext;
}

/**
* Creates new self instance.
* This method can be useful when writing chain methods calls.
Expand Down
59 changes: 59 additions & 0 deletions tests/LoggerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ public function testWriteYiiLog(): void
$this->assertSame(LogLevel::INFO, $logs[0][1]);
}

/**
* @depends testWriteYiiLog
*/
public function testDisableYiiLog(): void
{
$logger = (new Logger())
Expand All @@ -126,4 +129,60 @@ public function testDisableYiiLog(): void

$this->assertEmpty($logger->getLogs());
}

/**
* @depends testWriteYiiLog
*/
public function testWriteYiiLogContext(): void
{
$logger = (new Logger())
->enableYiiLog(true);

$logger->log('test message', CLogger::LEVEL_INFO, ['foo' => 'bar']);

$logs = $logger->getLogs();
$logger->flush();

$this->assertFalse(empty($logs[0]));
$this->assertStringContainsString('"foo"', $logs[0][0]);
$this->assertStringContainsString('"bar"', $logs[0][0]);

try {
throw new \RuntimeException('test-exception-message');
} catch (\Throwable $exception) {
// exception prepared
}

$logger->log('test message', CLogger::LEVEL_INFO, ['exception' => $exception]);

$logs = $logger->getLogs();
$logger->flush();

$this->assertFalse(empty($logs[0]));
$this->assertStringContainsString(\RuntimeException::class, $logs[0][0]);
$this->assertStringContainsString('test-exception-message', $logs[0][0]);
}

/**
* @depends testWritePsrLog
*/
public function testGlobalLogContext(): void
{
$psrLogger = new ArrayLogger();

$logger = (new Logger())
->setPsrLogger($psrLogger)
->withContext(function () {
return [
'global' => 'global-context',
];
});

$logger->log('test message', CLogger::LEVEL_INFO, 'test-category');

$logs = $psrLogger->flush();
$this->assertFalse(empty($logs[0]));
$this->assertArrayHasKey('global', $logs[0]['context']);
$this->assertSame('global-context', $logs[0]['context']['global']);
}
}

0 comments on commit 74b6480

Please sign in to comment.