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(); ?>
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.
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