ForgeLogger

The logging capability your app uses to write structured records of what it's doing — to a file, to the system log, or nowhere at all — with level-based filtering and a CLI command to tidy things up.

Overview

ForgeLogger gives your app a small, consistent way to record what's happening. You call a logger through one contract, and a driver decides where the line actually lands — a file on disk, the system log, or a no-op that swallows everything.

Logging lives under a single interface whose methods mirror the levels you'll most often use: debug, info, warning, error, and critical — plus a generic log() if you prefer to pass the level yourself, and an exception() helper for wrapping thrown errors with their trace.

Every line carries a timestamp, a level, your message, and any structured context you provide as JSON. That makes the same log readable by a human and easy to scan with tools.

Framing: this is a component of your app, not a framework subsystem. You pick the level of detail you want, choose a driver, and call the interface wherever you need to — the wiring is yours.

Installation

php forge.php package:install-module --module=forge-logger

Installation registers the logger under its contract and wires up a sensible default configuration, so $container->get(LoggerInterface::class) is immediately ready to use. There's no database work involved — logs are plain records, not tables.

Configuration

You control the driver, its target, the minimum level worth recording, and optional rotation through a few environment variables:

Variable Default Effect
LOG_DRIVER (or FORGE_LOGGER_DRIVER) file Which driver writes the lines (file, syslog, null)
LOG_PATH (or FORGE_LOGGER_PATH) storage/logs/forge.log Where the file driver appends records
FORGE_LOGGER_MIN_LEVEL DEBUG Lowest level recorded; anything less important is dropped
FORGE_LOGGER_MAX_FILE_SIZE 0 Bytes before the current file is rotated to .1 (0 disables rotation)

These feed the forge_logger config block. Because FORGE_LOGGER_* mirrors the shorter LOG_* keys, you can prefix things without changing behavior — useful when the same environment serves more than one purpose.

Writing Logs

Resolve the logger through its contract and call it like any object:

use Forge\Core\Contracts\LoggerInterface;

$logger = $container->get(LoggerInterface::class);

$logger->info('Order completed');
$logger->warning('Disk nearly full', ['used' => 92]);

The generic log($message, $level, $context) lets you pass any level by name, and exception() records a thrown error along with its class, file, line, and full trace:

try {
    $result = $service->doSomething();
} catch (\Throwable $e) {
    $logger->exception($e);
    // handled
}

Each line is written as [YYYY-MM-DD HH:MM:SS] [LEVEL] message {"context": ...}, with the context serialized as JSON when you supply one. Any newlines inside your message are flattened to spaces, so a single log entry always stays one line — a small guard against log-injection tricks.

Log Levels

Five levels capture the importance of a record, in ascending order:

Level Priority Use it for
DEBUG 0 Detailed, low-noise detail you don't want in production
INFO 1 Normal, useful milestones (order placed, user signed in)
WARNING 2 Something odd but not fatal (high usage, retry imminent)
ERROR 3 A failure happened but the app kept running
CRITICAL 4 Something serious that needs attention right away

The min_level setting is a floor: any record below it is silently skipped before it reaches a driver. Set it to ERROR in production and debug/info chatter never touches disk; set it to DEBUG locally if you want the full picture.

Drivers

A driver is a tiny class implementing one method, write($message). Three ship with the capability:

File

The default. Appends each line to your configured path, creating the directory as needed. When max_file_size is set and the current file grows past it, the file is rolled over to forge.log.1 (the previous backup is replaced) so a runaway log can't grow without bound.

Syslog

Sends each record to the operating system's log facility via syslog() — handy when you want your app's messages to appear alongside everything else the server reports.

Null

Discards everything. Useful as a safety net (if a configured driver can't be found, records fall back here rather than crashing) or when you want the logger's API without any output.

The driver set is extensible: register your own class (anything implementing the single-method contract) through the service's registerDriver(), then select it with LOG_DRIVER. Your app code keeps calling the same logger interface.

The CLI Command

ForgeLogger ships one command: log:clear. It removes the current log file and any rotated backups (up to .9):

php forge.php log:clear

It reports how many files it cleared, or notes that no log files were found at the configured path. Useful before a fresh run or to reclaim space on a busy app.

From Your Code

The logger registers under the core LoggerInterface, so your classes can depend on the contract and stay independent of the implementation. Inject it with constructor dependencies:

use Forge\Core\Contracts\LoggerInterface;

final class OrderService
{
    public function __construct(
        private readonly LoggerInterface $logger,
    ) {}

    public function complete(int $orderId): void
    {
        // ... place the order ...
        $this->logger->info('Order completed', ['order_id' => $orderId]);
    }
}

Pass structured context as the second argument to keep records searchable — user ids, record ids, or any values you'll want to correlate later. Whether you log to a file, the system log, or a custom driver is a configuration concern, separate from the calls you make in your code.