Skip to content

marko/log

Logging contracts and formatters --- define how your application logs messages without coupling to a storage backend. This package provides the LoggerInterface, log levels as a backed enum, the LogRecord value object, a LineFormatter, and typed LogConfig for accessing log settings. It contains no storage implementation; install a driver like marko/log-file for actual log writing.

Terminal window
composer require marko/log

Note: You also need an implementation package such as marko/log-file.

Inject the logger interface and call level-specific methods:

use Marko\Log\Contracts\LoggerInterface;
class OrderService
{
public function __construct(
private LoggerInterface $logger,
) {}
public function placeOrder(
int $orderId,
): void {
$this->logger->info('Order placed', ['order_id' => $orderId]);
// On failure:
$this->logger->error('Payment failed for order {order_id}', [
'order_id' => $orderId,
]);
}
}

Context placeholders ({key}) are interpolated automatically from the context array.

Eight severity levels via the LogLevel enum (most to least severe):

  • Emergency --- System unusable
  • Alert --- Immediate action required
  • Critical --- Critical conditions
  • Error --- Runtime errors
  • Warning --- Exceptional but non-error conditions
  • Notice --- Normal but significant events
  • Info --- Interesting events
  • Debug --- Detailed debug information

Each level has a numeric severity (lower numbers are more severe). Use meetsThreshold() to check whether a level is severe enough:

use Marko\Log\LogLevel;
LogLevel::Error->meetsThreshold(LogLevel::Warning); // true (Error is more severe)
LogLevel::Debug->meetsThreshold(LogLevel::Warning); // false
use Marko\Log\LogLevel;
$this->logger->log(LogLevel::Warning, 'Disk space low', [
'free_mb' => 120,
]);

The LogConfig class provides typed access to log configuration values:

use Marko\Log\Config\LogConfig;
class MyService
{
public function __construct(
private LogConfig $logConfig,
) {}
public function setup(): void
{
$driver = $this->logConfig->driver();
$path = $this->logConfig->path();
$level = $this->logConfig->level();
$channel = $this->logConfig->channel();
$format = $this->logConfig->format();
$dateFormat = $this->logConfig->dateFormat();
$maxFiles = $this->logConfig->maxFiles();
$maxFileSize = $this->logConfig->maxFileSize();
$escapeNewlines = $this->logConfig->escapeNewlines(); // default true
}
}
CommandDescription
marko log:clearClear log files older than configured max_files days
marko log:clear --days=7Clear log files older than 7 days

Replace the default log formatter via Preference:

use Marko\Core\Attributes\Preference;
use Marko\Log\Contracts\LogFormatterInterface;
use Marko\Log\Formatter\LineFormatter;
use Marko\Log\LogRecord;
#[Preference(replaces: LineFormatter::class)]
class JsonFormatter implements LogFormatterInterface
{
public function format(
LogRecord $record,
): string {
return json_encode([
'level' => $record->level->value,
'message' => $record->interpolatedMessage(),
'channel' => $record->channel,
'datetime' => $record->datetime->format('c'),
'context' => $record->context,
]) . "\n";
}
}

The default LineFormatter uses the format [{datetime}] {channel}.{level}: {message} {context} and accepts custom format and date format strings via its constructor. It also accepts an escapeNewlines boolean (default true) that collapses any CR (\r) or LF (\n) characters in the interpolated message and serialized context to the literal escape sequences \r and \n. This prevents log line-injection attacks where a message containing embedded newlines could be mistaken for multiple log entries. Disable it only if a downstream log processor requires literal newlines.

use Marko\Log\Contracts\LoggerInterface;
use Marko\Log\LogLevel;
public function emergency(string $message, array $context = []): void;
public function alert(string $message, array $context = []): void;
public function critical(string $message, array $context = []): void;
public function error(string $message, array $context = []): void;
public function warning(string $message, array $context = []): void;
public function notice(string $message, array $context = []): void;
public function info(string $message, array $context = []): void;
public function debug(string $message, array $context = []): void;
public function log(LogLevel $level, string $message, array $context = []): void;
use Marko\Log\Contracts\LogFormatterInterface;
use Marko\Log\LogRecord;
public function format(LogRecord $record): string;
use Marko\Log\LogLevel;
public function severity(): int;
public function meetsThreshold(LogLevel $minimum): bool;
public function upperName(): string;
use Marko\Log\LogRecord;
use Marko\Log\LogLevel;
use DateTimeImmutable;
readonly class LogRecord
{
public LogLevel $level;
public string $message;
public array $context;
public DateTimeImmutable $datetime;
public string $channel;
public function interpolatedMessage(): string;
public function contextAsJson(): string;
}
use Marko\Log\Config\LogConfig;
use Marko\Log\LogLevel;
public function driver(): string;
public function path(): string;
public function level(): LogLevel;
public function channel(): string;
public function format(): string;
public function dateFormat(): string;
public function maxFiles(): int;
public function maxFileSize(): int;
public function escapeNewlines(): bool; // log.escape_newlines, default true
ExceptionDescription
LogExceptionBase exception for all log errors --- includes getContext() and getSuggestion() methods
InvalidLogLevelExceptionThrown when a log level string does not match any valid level
LogWriteExceptionThrown when writing to a log file fails (missing directory, permissions)