@@ -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:
0 commit comments