Operations

Logging & Errors

The logging system uses a pluggable driver registry behind a static facade — while exceptions map to HTTP statuses and your own error views.

Writing logs

System\Logs\Log is a static facade; a file driver is registered as "file" at boot and set as the default.

use System\Logs\Log;

Log::error("Something went wrong");
Log::warning("Almost failed");
Log::info("User logged in");
Log::debug("This is a message");

Logs are appended to storage/logs/YYYY-MM-DD.log:

[2026-09-23 11:32:46] [DEBUG] : This is a message

Levels

ERROR, WARNING, INFO, DEBUG — defined in the System\Logs\LogLevel enum.

Context

Pass context arrays — scalar values are interpolated into {key} placeholders and the remainder is appended as JSON:

Log::info("User {id} signed in", ["id" => 42, "ip" => "127.0.0.1"]);
// [2026-09-23 12:00:00] [INFO] : User 42 signed in {"id":42,"ip":"127.0.0.1"}

Channels

Per-driver channels via __callStatic (the driver name becomes the method):

Log::file()->debug("via the file channel");

// or explicitly
Log::channel("file")->info("also via the file channel");

Custom drivers

Implement System\Logs\LogDriver and register it:

use System\Logs\Log;
use System\Logs\LogDriver;
use System\Logs\LogLevel;

class SyslogDriver implements LogDriver {
    public function write(LogLevel $level, string $message, array $context = []): void {
        syslog(LOG_INFO, "[$level->value] $message");
    }
}

Log::extend("syslog", new SyslogDriver());
Log::setDefaultDriver("syslog");

Exceptions

The exception hierarchy is deliberately tiny:

Throwable → Exception → LunarException (System\Exceptions)
                          ├── AppException         (500 Internal Server Error)
                          ├── NotFoundException    (404 Not found)
                          └── ForbiddenException   (403 Forbidden)

LunarException carries an HTTP status via the Error enum:

NOT_FOUND = 404, INTERNAL_SERVER_ERROR = 500, FORBIDDEN = 403

Throwing helpers

notfound();                 // throw new NotFoundException()
notfound("No such page");   // custom message
forbidden();
error();                    // 500
error("Database down");

Custom error pages

Configure views for each status in config/vars.php (or via environment):

"errors" => [
    "notfound" => "errors/404",     // renders views/errors/404.php
    "forbidden" => "errors/403",    // renders views/errors/403.php
    "exception" => "errors/500",    // renders views/errors/500.php
],

The error pages receive message, trace and exception variables:

<?php $template->extends("layouts/app") ?>

<?php $template->section("content") ?>
    <h1>Something went wrong</h1>
    <p><?= htmlspecialchars($message) ?></p>
<?php $template->endSection(); ?>
Production safety

Error views receive an unescaped trace and the exception object — only enable error pages in production if you accept stack traces being served, or strip trace/exception before rendering.

Fallback behaviour

If no error page is configured, the raw exception message is printed. The HTTP status code is sent only when a configured error view is rendered; the plain-message fallback currently responds with 200.

Part of the Lunaris documentation · Source on Codeberg · MIT License