Skip to content

Logging

Ray Fung edited this page Feb 26, 2026 · 3 revisions

Logging

Razy provides a PSR-3 compatible logging system with two main components: a standalone Logger for simple file-based logging, and a LogManager for multi-channel logging with pluggable handlers. Both implement PSR-3 interfaces without requiring psr/log as a dependency.


Table of Contents


Quick Start

use Razy\Logger;

// Simple file-based logger
$logger = new Logger('/var/log/myapp');
$logger->info('User logged in', ['user_id' => 42]);
$logger->error('Payment failed', ['order_id' => 'ORD-123', 'reason' => 'timeout']);

// Multi-channel logging
use Razy\Log\LogManager;
use Razy\Log\FileHandler;
use Razy\Log\StderrHandler;

$log = new LogManager();
$log->addHandler('file', new FileHandler('/var/log/myapp'));
$log->addHandler('stderr', new StderrHandler());
$log->info('Application started');

Logger (Standalone)

The Logger class provides straightforward file-based logging with date-based filenames:

use Razy\Logger;
use Razy\Contract\Log\LogLevel;

$logger = new Logger(
    logDirectory: '/var/log/myapp',    // null = null-logger (no-op)
    minLevel: LogLevel::DEBUG,          // minimum level to record
    filenamePattern: 'Y-m-d',          // date() format for log filenames
    bufferEnabled: false                // hold logs in memory
);

// PSR-3 level methods
$logger->emergency('System is down');
$logger->alert('Database connection lost');
$logger->critical('Uncaught exception', ['exception' => $e]);
$logger->error('Failed to process order {order_id}', ['order_id' => 'ORD-123']);
$logger->warning('Deprecated function called');
$logger->notice('User preference updated');
$logger->info('Email sent to {email}', ['email' => 'user@example.com']);
$logger->debug('Query executed in {time}ms', ['time' => 42]);

// Generic log method
$logger->log(LogLevel::INFO, 'Custom message');

// Adjust minimum level at runtime
$logger->setMinLevel(LogLevel::WARNING);
$logger->debug('This will be skipped');  // below WARNING, not logged

Log File Output

Files are named using the filenamePattern (default: Y-m-d), producing files like 2026-02-23.log:

[2026-02-23 14:30:00] INFO: User logged in {"user_id":42}
[2026-02-23 14:30:01] ERROR: Payment failed {"order_id":"ORD-123","reason":"timeout"}

Null-Logger Mode

When no directory is given, the Logger acts as a no-op (PSR-3 NullLogger equivalent):

$nullLogger = new Logger(null);
$nullLogger->info('This goes nowhere'); // silently ignored

LogManager (Multi-Channel)

The LogManager supports named channels, each with its own stack of handlers. It enables routing different log types to different destinations:

use Razy\Log\LogManager;
use Razy\Log\{FileHandler, StderrHandler, NullHandler};
use Razy\Contract\Log\LogLevel;

$log = new LogManager(defaultChannel: 'app');

// Register handlers on channels
$log->addHandler('app', new FileHandler('/var/log/app'));
$log->addHandler('app', new StderrHandler(LogLevel::ERROR));  // also stderr for errors
$log->addHandler('audit', new FileHandler('/var/log/audit', LogLevel::INFO));
$log->addHandler('debug', new StderrHandler(LogLevel::DEBUG));

// Log to default channel
$log->info('User logged in', ['user_id' => 42]);

// Log to a specific channel
$log->channel('audit')->info('Password changed', ['user_id' => 42]);

// Broadcast to multiple channels simultaneously
$log->stack(['app', 'audit'])->warning('Suspicious login attempt');

Channel Override Behavior

The channel() and stack() methods set a one-time override ??it resets after each log() call:

$log->channel('audit')->info('Audit event 1');  // goes to 'audit'
$log->info('Normal event');                      // goes to default 'app'

Channel Inspection

$log->hasChannel('audit');       // true
$log->getChannelNames();         // ['app', 'audit', 'debug']
$log->getHandlers('app');        // [FileHandler, StderrHandler]
$log->getDefaultChannel();       // 'app'
$log->setDefaultChannel('debug');

Log Handlers

Handlers implement LogHandlerInterface and decide how/where each log entry is written.

FileHandler

Writes to date-based log files with thread-safe LOCK_EX:

use Razy\Log\FileHandler;
use Razy\Contract\Log\LogLevel;

$handler = new FileHandler(
    directory: '/var/log/myapp',
    minLevel: LogLevel::WARNING,      // only WARNING and above
    filenamePattern: 'Y-m-d'          // files: 2026-02-23.log
);

// Check if handler will process a level
$handler->isHandling(LogLevel::ERROR);   // true
$handler->isHandling(LogLevel::DEBUG);   // false

// Adjust at runtime
$handler->setMinLevel(LogLevel::DEBUG);

StderrHandler

Writes to php://stderr ??useful for containerized environments (Docker, Kubernetes):

use Razy\Log\StderrHandler;
use Razy\Contract\Log\LogLevel;

$handler = new StderrHandler(minLevel: LogLevel::ERROR);

NullHandler

No-op handler ??absorbs all logs. Useful for testing:

use Razy\Log\NullHandler;

$handler = new NullHandler();
$handler->isHandling(LogLevel::EMERGENCY); // always true

Custom Handler

Implement LogHandlerInterface for custom destinations:

use Razy\Contract\Log\LogHandlerInterface;
use Razy\Contract\Log\LogLevel;

class SlackHandler implements LogHandlerInterface
{
    public function __construct(
        private readonly string $webhookUrl,
        private string $minLevel = LogLevel::CRITICAL
    ) {}

    public function handle(
        string $level,
        string $message,
        array $context,
        string $timestamp,
        string $channel
    ): void {
        // POST to Slack webhook
        $payload = json_encode([
            'text' => "[{$timestamp}] [{$channel}] {$level}: {$message}",
        ]);
        // ... HTTP request
    }

    public function isHandling(string $level): bool
    {
        $priorities = [
            LogLevel::DEBUG => 0, LogLevel::INFO => 1,
            LogLevel::NOTICE => 2, LogLevel::WARNING => 3,
            LogLevel::ERROR => 4, LogLevel::CRITICAL => 5,
            LogLevel::ALERT => 6, LogLevel::EMERGENCY => 7,
        ];
        return ($priorities[$level] ?? 0) >= ($priorities[$this->minLevel] ?? 0);
    }
}

Log Levels

PSR-3 log levels in order of severity (lowest to highest):

Level Constant Priority Use Case
debug LogLevel::DEBUG 0 Detailed debug information
info LogLevel::INFO 1 Informational events
notice LogLevel::NOTICE 2 Normal but significant events
warning LogLevel::WARNING 3 Exceptional but non-error
error LogLevel::ERROR 4 Runtime errors
critical LogLevel::CRITICAL 5 Critical conditions
alert LogLevel::ALERT 6 Requires immediate action
emergency LogLevel::EMERGENCY 7 System is unusable

Message Interpolation

PSR-3 {placeholder} interpolation is supported in log messages:

$logger->info('User {username} logged in from {ip}', [
    'username' => 'john',
    'ip' => '192.168.1.1',
]);
// Output: "User john logged in from 192.168.1.1"

// Exception context (PSR-3 convention)
$logger->error('Operation failed', [
    'exception' => $exception,
    'order_id' => 'ORD-123',
]);
// Exception stack trace is appended

Buffer Mode

Both Logger and LogManager support in-memory buffering for programmatic access:

// Logger with buffer
$logger = new Logger('/var/log/app', bufferEnabled: true);
$logger->info('Event 1');
$logger->error('Event 2');

// Read buffer
$entries = $logger->getBuffer();
// [
//   ['level' => 'info', 'message' => 'Event 1', 'context' => [], 'timestamp' => '...'],
//   ['level' => 'error', 'message' => 'Event 2', 'context' => [], 'timestamp' => '...'],
// ]

// Clear buffer
$logger->clearBuffer();

// LogManager with buffer
$log = new LogManager(bufferEnabled: true);
$log->addHandler('app', new FileHandler('/var/log/app'));
$log->info('Buffered too');
$entries = $log->getBuffer();

API Reference

Logger

Method Signature Description
__construct (?string $logDirectory, string $minLevel, string $filenamePattern, bool $bufferEnabled) Create logger
log (mixed $level, string|Stringable $message, array $context = []): void Log a message
emergency/alert/critical/error/warning/notice/info/debug (string|Stringable $message, array $context = []): void PSR-3 level methods
getMinLevel (): string Current minimum level
setMinLevel (string $level): static Change minimum level
getLogDirectory (): ?string Log directory path
getBuffer (): array Buffered entries
clearBuffer (): static Clear buffer

LogManager

Method Signature Description
__construct (string $defaultChannel, bool $bufferEnabled) Create manager
addHandler (string $channel, LogHandlerInterface $handler): static Add handler to channel
getHandlers (string $channel): array Handlers for channel
channel (string $channel): static Set next-log channel
stack (array $channels): static Broadcast to channels
getDefaultChannel (): string Default channel name
setDefaultChannel (string $channel): static Change default
getChannelNames (): array All channel names
hasChannel (string $channel): bool Check channel exists
log (mixed $level, string|Stringable $message, array $context = []): void Log a message
getBuffer (): array Buffered entries
clearBuffer (): static Clear buffer

LogHandlerInterface

Method Signature Description
handle (string $level, string $message, array $context, string $timestamp, string $channel): void Process a log entry
isHandling (string $level): bool Can handle this level?

??Previous: Validation Notification

Clone this wiki locally