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