Skip to content
Open
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
18 changes: 18 additions & 0 deletions system/Log/Handlers/BaseHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

namespace CodeIgniter\Log\Handlers;

use JsonException;

/**
* Base class for logging
*/
Expand Down Expand Up @@ -58,4 +60,20 @@ public function setDateFormat(string $format): HandlerInterface

return $this;
}

/**
* Encodes the context array as a JSON string.
* Returns the JSON string on success, or a descriptive error string if
* encoding fails (e.g. context contains a resource or invalid UTF-8).
*
* @param array<string, mixed> $context
*/
protected function encodeContext(array $context): string
{
try {
return json_encode($context, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
return '[context: JSON encoding failed - ' . $e->getMessage() . ']';
}
}
}
23 changes: 18 additions & 5 deletions system/Log/Handlers/ChromeLoggerHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
namespace CodeIgniter\Log\Handlers;

use CodeIgniter\HTTP\ResponseInterface;
use JsonException;

/**
* Allows for logging items to the Chrome console for debugging.
Expand Down Expand Up @@ -99,10 +100,11 @@ public function __construct(array $config = [])
* will stop. Any handlers that have not run, yet, will not
* be run.
*
* @param string $level
* @param string $message
* @param string $level
* @param string $message
* @param array<string, mixed> $context
*/
public function handle($level, $message): bool
public function handle($level, $message, array $context = []): bool
{
$message = $this->format($message);

Expand All @@ -121,7 +123,9 @@ public function handle($level, $message): bool
$type = $this->levels[$level];
}

$this->json['rows'][] = [[$message], $backtraceMessage, $type];
$logArgs = $context !== [] ? [$message, $context] : [$message];

$this->json['rows'][] = [$logArgs, $backtraceMessage, $type];

$this->sendLogs();

Expand Down Expand Up @@ -162,8 +166,17 @@ public function sendLogs(?ResponseInterface &$response = null)
$response = service('response', null, true);
}

try {
$encoded = json_encode($this->json, JSON_THROW_ON_ERROR);
} catch (JsonException) {
$encoded = json_encode($this->json, JSON_PARTIAL_OUTPUT_ON_ERROR);
if ($encoded === false) {
return;
}
}

$data = base64_encode(
mb_convert_encoding(json_encode($this->json), 'UTF-8', mb_list_encodings()),
mb_convert_encoding($encoded, 'UTF-8', mb_list_encodings()),
);

$response->setHeader($this->header, $data);
Expand Down
11 changes: 8 additions & 3 deletions system/Log/Handlers/ErrorlogHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,16 @@ public function __construct(array $config = [])
* will stop. Any handlers that have not run, yet, will not
* be run.
*
* @param string $level
* @param string $message
* @param string $level
* @param string $message
* @param array<string, mixed> $context
*/
public function handle($level, $message): bool
public function handle($level, $message, array $context = []): bool
{
if ($context !== []) {
$message .= ' ' . $this->encodeContext($context);
}

$message = strtoupper($level) . ' --> ' . $message . "\n";

return $this->errorLog($message, $this->messageType);
Expand Down
11 changes: 8 additions & 3 deletions system/Log/Handlers/FileHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,13 @@ public function __construct(array $config = [])
* will stop. Any handlers that have not run, yet, will not
* be run.
*
* @param string $level
* @param string $message
* @param string $level
* @param string $message
* @param array<string, mixed> $context
*
* @throws Exception
*/
public function handle($level, $message): bool
public function handle($level, $message, array $context = []): bool
{
$filepath = $this->path . 'log-' . date('Y-m-d') . '.' . $this->fileExtension;

Expand Down Expand Up @@ -104,6 +105,10 @@ public function handle($level, $message): bool
$date = date($this->dateFormat);
}

if ($context !== []) {
$message .= ' ' . $this->encodeContext($context);
}

$msg .= strtoupper($level) . ' - ' . $date . ' --> ' . $message . "\n";

flock($fp, LOCK_EX);
Expand Down
15 changes: 12 additions & 3 deletions system/Log/Handlers/HandlerInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,25 @@
*/
interface HandlerInterface
{
/**
* The reserved key under which global CI context data is stored
* in the log context array. This data comes from the Context service
* and is injected by the Logger when $logGlobalContext is enabled.
*/
public const GLOBAL_CONTEXT_KEY = '_ci_context';

/**
* Handles logging the message.
* If the handler returns false, then execution of handlers
* will stop. Any handlers that have not run, yet, will not
* be run.
*
* @param string $level
* @param string $message
* @param string $level
* @param string $message
* @param array<string, mixed> $context Full context array; may contain
* GLOBAL_CONTEXT_KEY with CI global data
*/
public function handle($level, $message): bool;
public function handle($level, $message, array $context = []): bool;

/**
* Checks whether the Handler will handle logging items of this
Expand Down
4 changes: 2 additions & 2 deletions system/Log/Logger.php
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ public function log($level, string|Stringable $message, array $context = []): vo
if ($this->logGlobalContext) {
$globalContext = service('context')->getAll();
if ($globalContext !== []) {
$message .= ' ' . json_encode($globalContext);
$context[HandlerInterface::GLOBAL_CONTEXT_KEY] = $globalContext;
}
}

Expand All @@ -284,7 +284,7 @@ public function log($level, string|Stringable $message, array $context = []): vo
}

// If the handler returns false, then we don't execute any other handlers.
if (! $handler->setDateFormat($this->dateFormat)->handle($level, $message)) {
if (! $handler->setDateFormat($this->dateFormat)->handle($level, $message, $context)) {
break;
}
}
Expand Down
28 changes: 23 additions & 5 deletions tests/_support/Log/Handlers/TestHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ class TestHandler extends FileHandler
*/
protected static $logs = [];

/**
* Local storage for log contexts.
*
* @var array<int, array<string, mixed>>
*/
protected static array $contexts = [];

protected string $destination;

/**
Expand All @@ -45,7 +52,8 @@ public function __construct(array $config)
$this->handles = $config['handles'] ?? [];
$this->destination = $this->path . 'log-' . Time::now()->format('Y-m-d') . '.' . $this->fileExtension;

self::$logs = [];
self::$logs = [];
self::$contexts = [];
}

/**
Expand All @@ -54,14 +62,16 @@ public function __construct(array $config)
* will stop. Any handlers that have not run, yet, will not
* be run.
*
* @param string $level
* @param string $message
* @param string $level
* @param string $message
* @param array<string, mixed> $context
*/
public function handle($level, $message): bool
public function handle($level, $message, array $context = []): bool
{
$date = Time::now()->format($this->dateFormat);

self::$logs[] = strtoupper($level) . ' - ' . $date . ' --> ' . $message;
self::$logs[] = strtoupper($level) . ' - ' . $date . ' --> ' . $message;
self::$contexts[] = $context;

return true;
}
Expand All @@ -70,4 +80,12 @@ public static function getLogs()
{
return self::$logs;
}

/**
* @return array<int, array<string, mixed>>
*/
public static function getContexts(): array
{
return self::$contexts;
}
}
20 changes: 20 additions & 0 deletions tests/system/Log/Handlers/ChromeLoggerHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,26 @@ public function testSetDateFormat(): void
$this->assertSame('F j, Y', $this->getPrivateProperty($logger, 'dateFormat'));
}

public function testHandleIncludesContextInRow(): void
{
Services::injectMock('response', new MockResponse());
$response = service('response');

$config = new LoggerConfig();
$config->handlers['CodeIgniter\Log\Handlers\TestHandler']['handles'] = ['debug'];

$logger = new ChromeLoggerHandler($config->handlers['CodeIgniter\Log\Handlers\TestHandler']);
$logger->handle('debug', 'Test message', [HandlerInterface::GLOBAL_CONTEXT_KEY => ['foo' => 'bar']]);

$this->assertTrue($response->hasHeader('X-ChromeLogger-Data'));

$decoded = json_decode(base64_decode($response->getHeaderLine('X-ChromeLogger-Data'), true), true);
$logArgs = $decoded['rows'][0][0];

$this->assertSame('Test message', $logArgs[0]);
$this->assertSame([HandlerInterface::GLOBAL_CONTEXT_KEY => ['foo' => 'bar']], $logArgs[1]);
}

public function testChromeLoggerHeaderSent(): void
{
Services::injectMock('response', new MockResponse());
Expand Down
9 changes: 9 additions & 0 deletions tests/system/Log/Handlers/ErrorlogHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@ public function testErrorLoggingWithErrorLog(): void
$this->assertTrue($logger->handle('error', 'Test message.'));
}

public function testErrorLoggingAppendsContextAsJson(): void
{
$logger = $this->getMockedHandler(['handles' => ['critical', 'error']]);
$logger->method('errorLog')->willReturn(true);
$logger->expects($this->once())->method('errorLog')
->with("ERROR --> Test message. {\"_ci_context\":{\"foo\":\"bar\"}}\n", 0);
$this->assertTrue($logger->handle('error', 'Test message.', [HandlerInterface::GLOBAL_CONTEXT_KEY => ['foo' => 'bar']]));
}

/**
* @param array{handles?: list<string>, messageType?: int} $config
*
Expand Down
18 changes: 18 additions & 0 deletions tests/system/Log/Handlers/FileHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,24 @@ public function testHandleCreateFile(): void
$this->assertStringContainsString($expectedResult, (string) $line);
}

public function testHandleAppendsContextAsJson(): void
{
$config = new LoggerConfig();
$config->handlers[TestHandler::class]['path'] = $this->start;
$logger = new MockFileLogger($config->handlers[TestHandler::class]);

$logger->setDateFormat('Y-m-d');
$expected = 'log-' . date('Y-m-d') . '.log';
vfsStream::newFile($expected)->at(vfsStream::setup('root'))->withContent('');
$logger->handle('debug', 'Test message', [HandlerInterface::GLOBAL_CONTEXT_KEY => ['foo' => 'bar']]);

$fp = fopen($config->handlers[TestHandler::class]['path'] . $expected, 'rb');
$line = fgets($fp);
fclose($fp);

$this->assertStringContainsString('Test message {"_ci_context":{"foo":"bar"}}', (string) $line);
}

public function testHandleDateTimeCorrectly(): void
{
$config = new LoggerConfig();
Expand Down
8 changes: 5 additions & 3 deletions tests/system/Log/LoggerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -452,14 +452,16 @@ public function testLogsGlobalContext(): void

service('context')->set('foo', 'bar');

$expected = 'DEBUG - ' . Time::now()->format('Y-m-d') . ' --> Test message {"foo":"bar"}';
$expectedMessage = 'DEBUG - ' . Time::now()->format('Y-m-d') . ' --> Test message';

$logger->log('debug', 'Test message');

$logs = TestHandler::getLogs();
$logs = TestHandler::getLogs();
$contexts = TestHandler::getContexts();

$this->assertCount(1, $logs);
$this->assertSame($expected, $logs[0]);
$this->assertSame($expectedMessage, $logs[0]);
$this->assertSame(['_ci_context' => ['foo' => 'bar']], $contexts[0]);
}

public function testDoesNotLogGlobalContext(): void
Expand Down
2 changes: 2 additions & 0 deletions user_guide_src/source/changelogs/v4.8.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Interface Changes
**NOTE:** If you've implemented your own classes that implement these interfaces from scratch, you will need to
update your implementations to include the new methods or method changes to ensure compatibility.

- **Logging:** ``CodeIgniter\Log\Handlers\HandlerInterface::handle()`` now requires a third parameter ``array $context = []``. Any custom log handler that overrides ``handle()`` - whether implementing ``HandlerInterface`` directly or extending a built-in handler class - must add the parameter to its ``handle()`` method signature.
- **Security:** The ``SecurityInterface``'s ``verify()`` method now has a native return type of ``static``.

Method Signature Changes
Expand Down Expand Up @@ -174,6 +175,7 @@ Libraries
=========

- **Context**: This new feature allows you to easily set and retrieve normal or hidden contextual data for the current request. See :ref:`Context <context>` for details.
- **Logging:** Log handlers now receive the full context array as a third argument to ``handle()``. When ``$logGlobalContext`` is enabled, the CI global context is available under the ``HandlerInterface::GLOBAL_CONTEXT_KEY`` key. Built-in handlers append it to the log output; custom handlers can use it for structured logging.

Helpers and Functions
=====================
Expand Down
Loading
Loading