Skip to content
Open
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## 2.2.2 under development

- New #140: Add `$contextFormat` and `$stringConverter` constructor parameters to `Target` for customizing context output (@WarLikeLaux)
- Enh #146: Move `Target` configuration to constructor parameters and deprecate the corresponding setters (@WarLikeLaux)

## 2.2.1 March 22, 2026
Expand Down
61 changes: 61 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,67 @@ $logger = new \Yiisoft\Log\Logger(
);
```

### Customizing log format

Each target formats log messages using a built-in `Formatter`. You can customize various aspects of formatting.

To replace the entire message format, use `setFormat()`:

```php
$target->setFormat(static function (\Yiisoft\Log\Message $message, array $commonContext): string {
return "[{$message->level()}][{$message->context('category')}] {$message->message()}";
});
```

To add a prefix to every message:

```php
$target->setPrefix(static fn() => 'MyApp: ');
```

To change the timestamp format:

```php
$target->setTimestampFormat('Y-m-d H:i:s');
```

The context output is customized via constructor parameters.

To replace how context values are converted to strings (default uses VarDumper), pass `stringConverter`:

```php
$target = new \Yiisoft\Log\StreamTarget(
stringConverter: static fn(mixed $value): string => json_encode($value, JSON_THROW_ON_ERROR),
);
```

To reorder context sections (trace, message context, common context), pass a `contextFormat` template string
with `{trace}`, `{message}`, and `{common}` placeholders. Each placeholder expands to its section with
a header when non-empty, or an empty string when the section has no data:

```php
$target = new \Yiisoft\Log\StreamTarget(contextFormat: "{common}{message}{trace}\n");
```

For full control over context rendering, pass a `contextFormat` callable instead:

```php
$target = new \Yiisoft\Log\StreamTarget(
contextFormat: static function (string $trace, string $messageContext, string $commonContext): string {
$result = '';
if ($commonContext !== '') {
$result .= "\n\nCommon:\n" . $commonContext;
}
if ($messageContext !== '') {
$result .= "\n\nMessage:\n" . $messageContext;
}
return $result;
},
);
```

`contextFormat` accepts either a template string or a callable.

### Configuring `LoggerInterface` in Yii3

In a Yii3 application, `Psr\Log\LoggerInterface` is resolved through the DI container.
Expand Down
92 changes: 79 additions & 13 deletions src/Message/Formatter.php
Comment thread
WarLikeLaux marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,11 @@

use RuntimeException;
use Yiisoft\Log\Message;
use Yiisoft\VarDumper\VarDumper;

use function implode;
use function is_string;
use function is_object;
use function method_exists;
use function sprintf;
use function str_replace;
use function is_int;

/**
Expand All @@ -24,6 +22,20 @@ final class Formatter
{
private const DEFAULT_TIMESTAMP_FORMAT = 'Y-m-d H:i:s.u';

/**
* @var string|callable|null
*
* @see Formatter::__construct()
*/
private $contextFormat = null;

/**
* @var callable
*
* @see Formatter::__construct()
*/
private $stringConverter;

/**
* @param callable|null $format A PHP callable that returns a string representation of the log message.
* If not set, {@see Formatter::defaultFormat()} is used.
Expand All @@ -32,12 +44,24 @@ final class Formatter
* If not set, {@see Formatter::getPrefix()} is used.
* Its signature should be `function (Message $message, array $commonContext): string;`.
* @param string|null $timestampFormat The date format for the log timestamp. Defaults to `Y-m-d H:i:s.u`.
* @param string|callable|null $contextFormat A context format. A template string supports `{trace}`,
* `{message}` and `{common}` placeholders, each replaced with its formatted section (including header) if
* non-empty, or an empty string otherwise. For example, `"{common}{message}{trace}\n"` outputs common
* context first, then message context, then trace. A PHP callable gives full control over context rendering;
* its signature should be `function (string $trace, string $messageContext, string $commonContext): string;`.
* @param callable|null $stringConverter A PHP callable that converts a context value to a string. Its
* signature should be `function (mixed $value): string;`. Defaults to {@see VarDumperValueConverter}.
*/
public function __construct(
private $format = null,
private $prefix = null,
private ?string $timestampFormat = null,
) {}
string|callable|null $contextFormat = null,
?callable $stringConverter = null,
) {
$this->contextFormat = $contextFormat;
$this->stringConverter = $stringConverter ?? new VarDumperValueConverter();
}

/**
* Sets the format for the string representation of the log message.
Expand Down Expand Up @@ -165,10 +189,6 @@ private function getContext(Message $message, array $commonContext): string
$context = [];
$common = [];

if ($trace !== '') {
$context[] = $trace;
}

/**
* @var array-key $name
* @var mixed $value
Expand All @@ -186,8 +206,49 @@ private function getContext(Message $message, array $commonContext): string
$common[] = "{$name}: " . $this->convertToString($value);
}

return (empty($context) ? '' : "\n\nMessage context:\n\n" . implode("\n", $context))
. (empty($common) ? '' : "\n\nCommon context:\n\n" . implode("\n", $common)) . "\n";
$messageContext = implode("\n", $context);
$commonContextString = implode("\n", $common);
$contextFormat = $this->contextFormat;

if (is_string($contextFormat)) {
return str_replace(
['{trace}', '{message}', '{common}'],
[
$trace === '' ? '' : "\n\nTrace:\n\n" . $trace,
$messageContext === '' ? '' : "\n\nMessage context:\n\n" . $messageContext,
$commonContextString === '' ? '' : "\n\nCommon context:\n\n" . $commonContextString,
],
$contextFormat,
);
}

if ($contextFormat !== null) {
$result = $contextFormat($trace, $messageContext, $commonContextString);

if (!is_string($result)) {
throw new RuntimeException(sprintf(
'The PHP callable "contextFormat" must return a string, %s received.',
get_debug_type($result),
));
}

return $result;
}

$messageItems = [];

if ($trace !== '') {
$messageItems[] = $trace;
}

if ($messageContext !== '') {
$messageItems[] = $messageContext;
}

$messageSection = implode("\n", $messageItems);

return ($messageSection === '' ? '' : "\n\nMessage context:\n\n" . $messageSection)
. ($commonContextString === '' ? '' : "\n\nCommon context:\n\n" . $commonContextString) . "\n";
Comment thread
WarLikeLaux marked this conversation as resolved.
}

/**
Expand Down Expand Up @@ -237,10 +298,15 @@ static function (mixed $trace): string {
*/
private function convertToString(mixed $value): string
{
if (is_object($value) && method_exists($value, '__toString')) {
return (string) $value;
$result = ($this->stringConverter)($value);

if (!is_string($result)) {
throw new RuntimeException(sprintf(
'The PHP callable "convertToString" must return a string, %s received.',
get_debug_type($result),
));
}

return VarDumper::create($value)->asString();
return $result;
}
}
29 changes: 29 additions & 0 deletions src/Message/VarDumperValueConverter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Log\Message;

use Yiisoft\VarDumper\VarDumper;

use function is_object;
use function method_exists;

/**
* Default converter of a context value to its string representation.
*
* Returns the result of `__toString()` for stringable objects and falls back to {@see VarDumper} otherwise.
*
* @internal
*/
final class VarDumperValueConverter
{
public function __invoke(mixed $value): string
{
if (is_object($value) && method_exists($value, '__toString')) {
return (string) $value;
}

return VarDumper::create($value)->asString();
}
}
6 changes: 5 additions & 1 deletion src/PsrTarget.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
* @param callable|null $format A PHP callable that returns a string representation of the log message.
* @param callable|null $prefix A PHP callable that returns a string to be prefixed to every exported message.
* @param string|null $timestampFormat The date format for the log timestamp.
* @param string|callable|null $contextFormat A context format for the log context output. See {@see Target::__construct()}.
* @param callable|null $stringConverter A PHP callable that converts a context value to a string. See {@see Target::__construct()}.
* @param int $exportInterval How many messages should be accumulated before they are exported.
* @param bool|callable $enabled Whether this target is enabled, or a PHP callable that returns a boolean.
*/
Expand All @@ -33,10 +35,12 @@
?callable $format = null,
?callable $prefix = null,
?string $timestampFormat = null,
string|callable|null $contextFormat = null,
?callable $stringConverter = null,
int $exportInterval = self::DEFAULT_EXPORT_INTERVAL,
bool|callable $enabled = true,

Check warning on line 41 in src/PsrTarget.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.4-ubuntu-latest

Escaped Mutant for Mutator "TrueValue": @@ @@ string|callable|null $contextFormat = null, ?callable $stringConverter = null, int $exportInterval = self::DEFAULT_EXPORT_INTERVAL, - bool|callable $enabled = true, + bool|callable $enabled = false, ) { parent::__construct($levels, $categories, $exceptCategories, $format, $prefix, $timestampFormat, $contextFormat, $stringConverter, $exportInterval, $enabled); }
) {
parent::__construct($levels, $categories, $exceptCategories, $format, $prefix, $timestampFormat, $exportInterval, $enabled);
parent::__construct($levels, $categories, $exceptCategories, $format, $prefix, $timestampFormat, $contextFormat, $stringConverter, $exportInterval, $enabled);
}

/**
Expand Down
6 changes: 5 additions & 1 deletion src/StreamTarget.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
* @param callable|null $format A PHP callable that returns a string representation of the log message.
* @param callable|null $prefix A PHP callable that returns a string to be prefixed to every exported message.
* @param string|null $timestampFormat The date format for the log timestamp.
* @param string|callable|null $contextFormat A context format for the log context output. See {@see Target::__construct()}.
* @param callable|null $stringConverter A PHP callable that converts a context value to a string. See {@see Target::__construct()}.
* @param int $exportInterval How many messages should be accumulated before they are exported.
* @param bool|callable $enabled Whether this target is enabled, or a PHP callable that returns a boolean.
*/
Expand All @@ -47,19 +49,21 @@
?callable $format = null,
?callable $prefix = null,
?string $timestampFormat = null,
string|callable|null $contextFormat = null,
?callable $stringConverter = null,
int $exportInterval = self::DEFAULT_EXPORT_INTERVAL,
bool|callable $enabled = true,

Check warning on line 55 in src/StreamTarget.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.4-ubuntu-latest

Escaped Mutant for Mutator "TrueValue": @@ @@ string|callable|null $contextFormat = null, ?callable $stringConverter = null, int $exportInterval = self::DEFAULT_EXPORT_INTERVAL, - bool|callable $enabled = true, + bool|callable $enabled = false, ) { parent::__construct($levels, $categories, $exceptCategories, $format, $prefix, $timestampFormat, $contextFormat, $stringConverter, $exportInterval, $enabled); }
) {
parent::__construct($levels, $categories, $exceptCategories, $format, $prefix, $timestampFormat, $exportInterval, $enabled);
parent::__construct($levels, $categories, $exceptCategories, $format, $prefix, $timestampFormat, $contextFormat, $stringConverter, $exportInterval, $enabled);
}

protected function export(): void
{
$stream = $this->createStream();
flock($stream, LOCK_EX);

Check warning on line 63 in src/StreamTarget.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.4-ubuntu-latest

Escaped Mutant for Mutator "FunctionCallRemoval": @@ @@ protected function export(): void { $stream = $this->createStream(); - flock($stream, LOCK_EX); + if (fwrite($stream, $this->formatMessages("\n")) === false) { flock($stream, LOCK_UN);

if (fwrite($stream, $this->formatMessages("\n")) === false) {
flock($stream, LOCK_UN);

Check warning on line 66 in src/StreamTarget.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.4-ubuntu-latest

Escaped Mutant for Mutator "FunctionCallRemoval": @@ @@ flock($stream, LOCK_EX); if (fwrite($stream, $this->formatMessages("\n")) === false) { - flock($stream, LOCK_UN); + fclose($stream); throw new RuntimeException(sprintf( 'Unable to export the log because of an error writing to the stream: %s',
fclose($stream);
throw new RuntimeException(sprintf(
'Unable to export the log because of an error writing to the stream: %s',
Expand Down
9 changes: 8 additions & 1 deletion src/Target.php
Comment thread
WarLikeLaux marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ abstract class Target
* @param callable|null $format A PHP callable that returns a string representation of the log message.
* @param callable|null $prefix A PHP callable that returns a string to be prefixed to every exported message.
* @param string|null $timestampFormat The date format for the log timestamp.
* @param string|callable|null $contextFormat A context format for the log context output. A template string
* supports `{trace}`, `{message}` and `{common}` placeholders, or a PHP callable for full control. See
* {@see Formatter::__construct()}.
* @param callable|null $stringConverter A PHP callable that converts a context value to a string.
* See {@see Formatter::__construct()}.
* @param int $exportInterval How many messages should be accumulated before they are exported.
* @param bool|callable $enabled Whether this target is enabled, or a PHP callable that returns a boolean.
*/
Expand All @@ -90,11 +95,13 @@ public function __construct(
?callable $format = null,
?callable $prefix = null,
?string $timestampFormat = null,
string|callable|null $contextFormat = null,
?callable $stringConverter = null,
private int $exportInterval = self::DEFAULT_EXPORT_INTERVAL,
bool|callable $enabled = true,
) {
$this->categories = new CategoryFilter($categories, $exceptCategories);
$this->formatter = new Formatter($format, $prefix, $timestampFormat);
$this->formatter = new Formatter($format, $prefix, $timestampFormat, $contextFormat, $stringConverter);
/** @psalm-suppress DeprecatedMethod */
$this->setLevels($levels);
$this->enabled = $enabled;
Expand Down
Loading
Loading