Skip to content

Commit d15e79b

Browse files
authored
Merge branch '4.8' into feat-avif-support
2 parents c3df1eb + e08562d commit d15e79b

File tree

19 files changed

+585
-25
lines changed

19 files changed

+585
-25
lines changed

app/Config/Logger.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,41 @@ class Logger extends BaseConfig
6767
*/
6868
public bool $logGlobalContext = false;
6969

70+
/**
71+
* --------------------------------------------------------------------------
72+
* Whether to log per-call context data
73+
* --------------------------------------------------------------------------
74+
*
75+
* When enabled, context keys not used as placeholders in the message are
76+
* passed to handlers as structured data. Per PSR-3, any ``Throwable`` instance
77+
* in the ``exception`` key is automatically normalized to an array representation.
78+
*/
79+
public bool $logContext = false;
80+
81+
/**
82+
* --------------------------------------------------------------------------
83+
* Whether to include the stack trace for Throwables in context
84+
* --------------------------------------------------------------------------
85+
*
86+
* When enabled, the stack trace is included when normalizing a Throwable
87+
* in the ``exception`` context key. Only relevant when $logContext is true.
88+
*/
89+
public bool $logContextTrace = false;
90+
91+
/**
92+
* --------------------------------------------------------------------------
93+
* Whether to keep context keys that were used as placeholders
94+
* --------------------------------------------------------------------------
95+
*
96+
* By default, context keys that were interpolated into the message as
97+
* {placeholder} are stripped before passing context to handlers, since
98+
* their values are already present in the message text. Set to true to
99+
* keep them as structured data as well.
100+
*
101+
* Only relevant when $logContext is true.
102+
*/
103+
public bool $logContextUsedKeys = false;
104+
70105
/**
71106
* --------------------------------------------------------------------------
72107
* Log Handlers

system/Log/Handlers/BaseHandler.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313

1414
namespace CodeIgniter\Log\Handlers;
1515

16+
use JsonException;
17+
1618
/**
1719
* Base class for logging
1820
*/
@@ -58,4 +60,20 @@ public function setDateFormat(string $format): HandlerInterface
5860

5961
return $this;
6062
}
63+
64+
/**
65+
* Encodes the context array as a JSON string.
66+
* Returns the JSON string on success, or a descriptive error string if
67+
* encoding fails (e.g. context contains a resource or invalid UTF-8).
68+
*
69+
* @param array<string, mixed> $context
70+
*/
71+
protected function encodeContext(array $context): string
72+
{
73+
try {
74+
return json_encode($context, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
75+
} catch (JsonException $e) {
76+
return '[context: JSON encoding failed - ' . $e->getMessage() . ']';
77+
}
78+
}
6179
}

system/Log/Handlers/ChromeLoggerHandler.php

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
namespace CodeIgniter\Log\Handlers;
1515

1616
use CodeIgniter\HTTP\ResponseInterface;
17+
use JsonException;
1718

1819
/**
1920
* Allows for logging items to the Chrome console for debugging.
@@ -99,10 +100,11 @@ public function __construct(array $config = [])
99100
* will stop. Any handlers that have not run, yet, will not
100101
* be run.
101102
*
102-
* @param string $level
103-
* @param string $message
103+
* @param string $level
104+
* @param string $message
105+
* @param array<string, mixed> $context
104106
*/
105-
public function handle($level, $message): bool
107+
public function handle($level, $message, array $context = []): bool
106108
{
107109
$message = $this->format($message);
108110

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

124-
$this->json['rows'][] = [[$message], $backtraceMessage, $type];
126+
$logArgs = $context !== [] ? [$message, $context] : [$message];
127+
128+
$this->json['rows'][] = [$logArgs, $backtraceMessage, $type];
125129

126130
$this->sendLogs();
127131

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

169+
try {
170+
$encoded = json_encode($this->json, JSON_THROW_ON_ERROR);
171+
} catch (JsonException) {
172+
$encoded = json_encode($this->json, JSON_PARTIAL_OUTPUT_ON_ERROR);
173+
if ($encoded === false) {
174+
return;
175+
}
176+
}
177+
165178
$data = base64_encode(
166-
mb_convert_encoding(json_encode($this->json), 'UTF-8', mb_list_encodings()),
179+
mb_convert_encoding($encoded, 'UTF-8', mb_list_encodings()),
167180
);
168181

169182
$response->setHeader($this->header, $data);

system/Log/Handlers/ErrorlogHandler.php

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,16 @@ public function __construct(array $config = [])
6666
* will stop. Any handlers that have not run, yet, will not
6767
* be run.
6868
*
69-
* @param string $level
70-
* @param string $message
69+
* @param string $level
70+
* @param string $message
71+
* @param array<string, mixed> $context
7172
*/
72-
public function handle($level, $message): bool
73+
public function handle($level, $message, array $context = []): bool
7374
{
75+
if ($context !== []) {
76+
$message .= ' ' . $this->encodeContext($context);
77+
}
78+
7479
$message = strtoupper($level) . ' --> ' . $message . "\n";
7580

7681
return $this->errorLog($message, $this->messageType);

system/Log/Handlers/FileHandler.php

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,13 @@ public function __construct(array $config = [])
6969
* will stop. Any handlers that have not run, yet, will not
7070
* be run.
7171
*
72-
* @param string $level
73-
* @param string $message
72+
* @param string $level
73+
* @param string $message
74+
* @param array<string, mixed> $context
7475
*
7576
* @throws Exception
7677
*/
77-
public function handle($level, $message): bool
78+
public function handle($level, $message, array $context = []): bool
7879
{
7980
$filepath = $this->path . 'log-' . date('Y-m-d') . '.' . $this->fileExtension;
8081

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

108+
if ($context !== []) {
109+
$message .= ' ' . $this->encodeContext($context);
110+
}
111+
107112
$msg .= strtoupper($level) . ' - ' . $date . ' --> ' . $message . "\n";
108113

109114
flock($fp, LOCK_EX);

system/Log/Handlers/HandlerInterface.php

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,25 @@
1818
*/
1919
interface HandlerInterface
2020
{
21+
/**
22+
* The reserved key under which global CI context data is stored
23+
* in the log context array. This data comes from the Context service
24+
* and is injected by the Logger when $logGlobalContext is enabled.
25+
*/
26+
public const GLOBAL_CONTEXT_KEY = '_ci_context';
27+
2128
/**
2229
* Handles logging the message.
2330
* If the handler returns false, then execution of handlers
2431
* will stop. Any handlers that have not run, yet, will not
2532
* be run.
2633
*
27-
* @param string $level
28-
* @param string $message
34+
* @param string $level
35+
* @param string $message
36+
* @param array<string, mixed> $context Full context array; may contain
37+
* GLOBAL_CONTEXT_KEY with CI global data
2938
*/
30-
public function handle($level, $message): bool;
39+
public function handle($level, $message, array $context = []): bool;
3140

3241
/**
3342
* Checks whether the Handler will handle logging items of this

system/Log/Logger.php

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,27 @@ class Logger implements LoggerInterface
121121
*/
122122
protected bool $logGlobalContext = false;
123123

124+
/**
125+
* Whether to log per-call context data passed to log methods.
126+
*
127+
* Set in app/Config/Logger.php
128+
*/
129+
protected bool $logContext = false;
130+
131+
/**
132+
* Whether to include the stack trace when a Throwable is in the context.
133+
*
134+
* Set in app/Config/Logger.php
135+
*/
136+
protected bool $logContextTrace = false;
137+
138+
/**
139+
* Whether to keep context keys that were already used as placeholders.
140+
*
141+
* Set in app/Config/Logger.php
142+
*/
143+
protected bool $logContextUsedKeys = false;
144+
124145
/**
125146
* Constructor.
126147
*
@@ -162,7 +183,10 @@ public function __construct($config, bool $debug = CI_DEBUG)
162183
$this->logCache = [];
163184
}
164185

165-
$this->logGlobalContext = $config->logGlobalContext ?? $this->logGlobalContext;
186+
$this->logGlobalContext = $config->logGlobalContext ?? $this->logGlobalContext;
187+
$this->logContext = $config->logContext ?? $this->logContext;
188+
$this->logContextTrace = $config->logContextTrace ?? $this->logContextTrace;
189+
$this->logContextUsedKeys = $config->logContextUsedKeys ?? $this->logContextUsedKeys;
166190
}
167191

168192
/**
@@ -259,12 +283,30 @@ public function log($level, string|Stringable $message, array $context = []): vo
259283
return;
260284
}
261285

286+
$interpolatedKeys = array_keys(array_filter(
287+
$context,
288+
static fn ($key): bool => str_contains((string) $message, '{' . $key . '}'),
289+
ARRAY_FILTER_USE_KEY,
290+
));
291+
262292
$message = $this->interpolate($message, $context);
263293

294+
if ($this->logContext) {
295+
if (! $this->logContextUsedKeys) {
296+
foreach ($interpolatedKeys as $key) {
297+
unset($context[$key]);
298+
}
299+
}
300+
301+
$context = $this->normalizeContext($context);
302+
} else {
303+
$context = [];
304+
}
305+
264306
if ($this->logGlobalContext) {
265307
$globalContext = service('context')->getAll();
266308
if ($globalContext !== []) {
267-
$message .= ' ' . json_encode($globalContext);
309+
$context[HandlerInterface::GLOBAL_CONTEXT_KEY] = $globalContext;
268310
}
269311
}
270312

@@ -284,12 +326,45 @@ public function log($level, string|Stringable $message, array $context = []): vo
284326
}
285327

286328
// If the handler returns false, then we don't execute any other handlers.
287-
if (! $handler->setDateFormat($this->dateFormat)->handle($level, $message)) {
329+
if (! $handler->setDateFormat($this->dateFormat)->handle($level, $message, $context)) {
288330
break;
289331
}
290332
}
291333
}
292334

335+
/**
336+
* Normalizes context values for structured logging.
337+
* Per PSR-3, if an Exception is given to produce a stack trace, it MUST be
338+
* in a key named "exception". Only that key is converted into an array
339+
* representation.
340+
*
341+
* @param array<string, mixed> $context
342+
*
343+
* @return array<string, mixed>
344+
*/
345+
protected function normalizeContext(array $context): array
346+
{
347+
if (isset($context['exception']) && $context['exception'] instanceof Throwable) {
348+
$value = $context['exception'];
349+
350+
$normalized = [
351+
'class' => $value::class,
352+
'message' => $value->getMessage(),
353+
'code' => $value->getCode(),
354+
'file' => clean_path($value->getFile()),
355+
'line' => $value->getLine(),
356+
];
357+
358+
if ($this->logContextTrace) {
359+
$normalized['trace'] = $value->getTraceAsString();
360+
}
361+
362+
$context['exception'] = $normalized;
363+
}
364+
365+
return $context;
366+
}
367+
293368
/**
294369
* Replaces any placeholders in the message with variables
295370
* from the context, as well as a few special items like:

tests/_support/Log/Handlers/TestHandler.php

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ class TestHandler extends FileHandler
3131
*/
3232
protected static $logs = [];
3333

34+
/**
35+
* Local storage for log contexts.
36+
*
37+
* @var array<int, array<string, mixed>>
38+
*/
39+
protected static array $contexts = [];
40+
3441
protected string $destination;
3542

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

48-
self::$logs = [];
55+
self::$logs = [];
56+
self::$contexts = [];
4957
}
5058

5159
/**
@@ -54,14 +62,16 @@ public function __construct(array $config)
5462
* will stop. Any handlers that have not run, yet, will not
5563
* be run.
5664
*
57-
* @param string $level
58-
* @param string $message
65+
* @param string $level
66+
* @param string $message
67+
* @param array<string, mixed> $context
5968
*/
60-
public function handle($level, $message): bool
69+
public function handle($level, $message, array $context = []): bool
6170
{
6271
$date = Time::now()->format($this->dateFormat);
6372

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

6676
return true;
6777
}
@@ -70,4 +80,12 @@ public static function getLogs()
7080
{
7181
return self::$logs;
7282
}
83+
84+
/**
85+
* @return array<int, array<string, mixed>>
86+
*/
87+
public static function getContexts(): array
88+
{
89+
return self::$contexts;
90+
}
7391
}

0 commit comments

Comments
 (0)