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.
Installation
Section titled “Installation”composer require marko/logNote: You also need an implementation package such as marko/log-file.
Logging Messages
Section titled “Logging Messages”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.
Log Levels
Section titled “Log Levels”Eight severity levels via the LogLevel enum (most to least severe):
Emergency--- System unusableAlert--- Immediate action requiredCritical--- Critical conditionsError--- Runtime errorsWarning--- Exceptional but non-error conditionsNotice--- Normal but significant eventsInfo--- Interesting eventsDebug--- 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); // falseUsing a Specific Level
Section titled “Using a Specific Level”use Marko\Log\LogLevel;
$this->logger->log(LogLevel::Warning, 'Disk space low', [ 'free_mb' => 120,]);Configuration
Section titled “Configuration”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 }}CLI Commands
Section titled “CLI Commands”| Command | Description |
|---|---|
marko log:clear | Clear log files older than configured max_files days |
marko log:clear --days=7 | Clear log files older than 7 days |
Customization
Section titled “Customization”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.
API Reference
Section titled “API Reference”LoggerInterface
Section titled “LoggerInterface”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;LogFormatterInterface
Section titled “LogFormatterInterface”use Marko\Log\Contracts\LogFormatterInterface;use Marko\Log\LogRecord;
public function format(LogRecord $record): string;LogLevel
Section titled “LogLevel”use Marko\Log\LogLevel;
public function severity(): int;public function meetsThreshold(LogLevel $minimum): bool;public function upperName(): string;LogRecord
Section titled “LogRecord”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;}LogConfig
Section titled “LogConfig”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 trueExceptions
Section titled “Exceptions”| Exception | Description |
|---|---|
LogException | Base exception for all log errors --- includes getContext() and getSuggestion() methods |
InvalidLogLevelException | Thrown when a log level string does not match any valid level |
LogWriteException | Thrown when writing to a log file fails (missing directory, permissions) |