Logger
The PSR-3 logger. Final; extends Psr\Log\AbstractLogger and implements LevelAwareLoggerInterface, so it is a Psr\Log\LoggerInterface.
use Solo\Logger\Logger;
$logger = Logger::file('/var/log/app/app.log');Constructor
public function __construct(
WriterInterface $writer,
FormatterInterface $formatter = new LineFormatter(),
string $minLevel = LogLevel::DEBUG,
ClockInterface $clock = new SystemClock(),
?string $channel = null,
)Parameters:
| Parameter | Type | Description |
|---|---|---|
$writer | WriterInterface | Where formatted records go |
$formatter | FormatterInterface | Turns a record into a line |
$minLevel | string | The least severe level written, a Psr\Log\LogLevel constant |
$clock | Psr\Clock\ClockInterface | The time source for the records |
$channel | ?string | The channel stamped on every record; none by default |
Throws:
Psr\Log\InvalidArgumentException—$minLevelis not one of the eight PSR-3 levelsInvalidArgumentException— a$channelthat is empty, not valid UTF-8, or holds a control character
The writer, the formatter, the clock and the channel are fixed for the logger's lifetime: build another logger for another destination or format, or call withChannel(). See Extending.
file()
public static function file(
string $path,
string $minLevel = LogLevel::DEBUG,
FormatterInterface $formatter = new LineFormatter(),
RotationOptions $rotation = new RotationOptions(),
string $timezone = 'UTC',
int $directoryPermissions = 0755,
?int $filePermissions = null,
): selfA logger writing to a local file. The same as:
$clock = new SystemClock(new \DateTimeZone($timezone));
$writer = new FileWriter($path, $rotation, $clock, $directoryPermissions, $filePermissions);
$logger = new Logger($writer, $formatter, $minLevel, $clock);One clock stamps the records, names the archives and draws the calendar of a schedule, so all three use $timezone. The modes are those of FileWriter.
Throws:
InvalidArgumentException— an empty path, a stream wrapper URL, a path ending in/,.or.., or a mode the writer could not work withPsr\Log\InvalidArgumentException—$minLevelis not a PSR-3 levelDateInvalidTimeZoneException— an unknown$timezone
Example:
use Psr\Log\LogLevel;
use Solo\Logger\Formatter\JsonFormatter;
use Solo\Logger\Rotation\RotationOptions;
use Solo\Logger\Rotation\Schedule;
$logger = Logger::file(
path: '/var/log/app/app.log',
minLevel: LogLevel::INFO,
formatter: new JsonFormatter(),
rotation: new RotationOptions(maxBytes: 10 * 1024 * 1024, schedule: Schedule::Daily, maxFiles: 14),
timezone: 'Europe/Kyiv',
filePermissions: 0640,
);stream()
public static function stream(
string $url = 'php://stderr',
string $minLevel = LogLevel::DEBUG,
FormatterInterface $formatter = new LineFormatter(),
string $timezone = 'UTC',
): selfA logger writing to a stream: standard error by default. The same as:
$logger = new Logger(new StreamWriter($url), $formatter, $minLevel, new SystemClock(new \DateTimeZone($timezone)));Throws:
InvalidArgumentException— an empty URL, or one with a NUL bytePsr\Log\InvalidArgumentException—$minLevelis not a PSR-3 levelDateInvalidTimeZoneException— an unknown$timezone
See Streams.
withChannel()
public function withChannel(string $channel): selfA new logger with the same writer, formatter, clock and threshold, stamping $channel on its records. The two thresholds change independently afterwards.
Throws: InvalidArgumentException — a $channel that is empty, not valid UTF-8, or holds a control character.
$payments = $logger->withChannel('payments');
$payments->info('Charged');
// [2026-11-01T09:30:00.123456+00:00] payments.INFO: ChargedsetMinLevel()
public function setMinLevel(string $level): voidChange the threshold — the one setting an existing logger can change.
Throws: Psr\Log\InvalidArgumentException — not a PSR-3 level; the previous threshold stays.
isHandling()
public function isHandling(string $level): boolWhether a record of $level would be written: true for the threshold and the levels more severe than it.
Throws: Psr\Log\InvalidArgumentException — not a PSR-3 level.
if ($logger->isHandling(LogLevel::DEBUG)) {
$logger->debug('Query plan', ['plan' => $database->explain($query)]);
}log()
public function log($level, string|\Stringable $message, array $context = []): voidWrite one record, and the same through emergency(), alert(), critical(), error(), warning(), notice(), info() and debug().
- The level is checked: anything but the eight PSR-3 levels throws
Psr\Log\InvalidArgumentException, also below the threshold. - A level below the threshold returns here: the message is not converted, the clock not read, nothing formatted or written.
- The message is converted to a string, the clock read, and a
LogRecordbuilt with the logger's channel. - The formatter formats the record, then the writer writes it.
Throws:
Psr\Log\InvalidArgumentException— an unknown levelSolo\Logger\Writer\WriteException— the writer could not write, rotate or prune- Whatever a custom formatter or writer throws, or the message's
__toString()
A failed call is never retried or dropped silently: the exception reaches the caller. See Fail-Safe Logging.
LevelAwareLoggerInterface
namespace Solo\Logger;
interface LevelAwareLoggerInterface extends Psr\Log\LoggerInterface
{
/** @throws \Psr\Log\InvalidArgumentException when the level is not one of the eight PSR-3 levels */
public function isHandling(string $level): bool;
}A PSR-3 logger that tells in advance whether it would write a level. Logger and the decorators implement it, and every one of them throws for an unknown level.
LogRecord
final readonly class LogRecord
{
public function __construct(
public string $level,
public string $message,
public array $context,
public \DateTimeImmutable $time,
public ?string $channel = null,
) {
}
}What a formatter receives. message is already a string, its placeholders not yet replaced; context is the array as it was passed, not normalized; channel is the logger's, or null. The properties are readonly, but objects inside the context are not frozen.
SystemClock
final readonly class SystemClock implements Psr\Clock\ClockInterface
{
public function __construct(\DateTimeZone $timezone = new \DateTimeZone('UTC'));
public function now(): \DateTimeImmutable;
}The current time, with microseconds, in the given timezone — whatever date.timezone says.