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,
Comment on lines +38 to +39

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These could be moved to the end to keep it backwards compatible.

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,
Comment on lines +52 to +53

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These could be moved to the end to keep it backwards compatible.

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,
Comment on lines +98 to +99

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These could be moved to the end to keep it backwards compatible.

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