Logging
Melodic includes a PSR-3-style logging system with daily file rotation, message interpolation, exception formatting, and configurable severity filtering. The logger is designed to never crash your application — if writing fails, the error is silently swallowed.
LogLevel Enum
The Melodic\Log\LogLevel backed enum defines eight severity levels. Each level has a numeric severity where lower numbers indicate higher urgency. The enum is backed by lowercase string values.
| Level | Value | Severity | Description |
|---|---|---|---|
EMERGENCY |
'emergency' |
0 | System is unusable |
ALERT |
'alert' |
1 | Immediate action required |
CRITICAL |
'critical' |
2 | Critical conditions |
ERROR |
'error' |
3 | Runtime errors |
WARNING |
'warning' |
4 | Exceptional occurrences that are not errors |
NOTICE |
'notice' |
5 | Normal but significant events |
INFO |
'info' |
6 | Informational messages |
DEBUG |
'debug' |
7 | Detailed debug information |
Lower severity number = higher urgency. EMERGENCY (0) is the most urgent; DEBUG (7) is the least. When you set a minimum level, only messages with a severity number less than or equal to that level's severity are written.
Parsing Levels
Use the static parse() method to convert a string to a LogLevel case. Parsing is case-insensitive. An invalid string throws a ValueError.
<?php
use Melodic\Log\LogLevel;
$level = LogLevel::parse('warning'); // LogLevel::WARNING
$level = LogLevel::parse('WARNING'); // LogLevel::WARNING
$level = LogLevel::parse('invalid'); // throws ValueError
LoggerInterface
The Melodic\Log\LoggerInterface provides a convenience method for each severity level plus a generic log() method that accepts a LogLevel enum value.
| Method | Signature |
|---|---|
emergency() |
emergency(string $message, array $context = []): void |
alert() |
alert(string $message, array $context = []): void |
critical() |
critical(string $message, array $context = []): void |
error() |
error(string $message, array $context = []): void |
warning() |
warning(string $message, array $context = []): void |
notice() |
notice(string $message, array $context = []): void |
info() |
info(string $message, array $context = []): void |
debug() |
debug(string $message, array $context = []): void |
log() |
log(LogLevel $level, string $message, array $context = []): void |
Every convenience method delegates to log() with the appropriate LogLevel. The $context array supports placeholder interpolation and exception formatting.
FileLogger
The Melodic\Log\FileLogger is the default production logger. It writes log entries to daily-rotated files with automatic directory creation, message interpolation, and exception stack trace formatting.
Constructor
<?php
use Melodic\Log\FileLogger;
use Melodic\Log\LogLevel;
// Log everything (DEBUG and above)
$logger = new FileLogger('/var/log/myapp');
// Only log WARNING and more severe
$logger = new FileLogger('/var/log/myapp', LogLevel::WARNING);
| Parameter | Type | Default | Description |
|---|---|---|---|
$logDirectory |
string |
— | Absolute path to the directory where log files are written |
$minLevel |
LogLevel |
LogLevel::DEBUG |
Minimum severity level; messages less severe are ignored |
Daily File Rotation
Each day's log entries are written to a file named melodic-YYYY-MM-DD.log. A new file is created automatically when the date changes. There is no maximum file count — you manage cleanup externally (e.g., with logrotate or a cron job).
logs/
melodic-2026-02-15.log
melodic-2026-02-16.log
melodic-2026-02-17.log
Message Interpolation
Context values are interpolated into the log message using {placeholder} syntax. Any scalar value or object with a __toString() method can be interpolated. The exception key is reserved and skipped during interpolation.
<?php
$logger->info('User {user} logged in from {ip}', [
'user' => 'alice',
'ip' => '192.168.1.10',
]);
// Output: [2026-02-17 14:30:00] INFO: User alice logged in from 192.168.1.10
Exception Formatting
When the context array contains an exception key with a Throwable value, the logger appends the exception class, message, file/line, and full stack trace to the log entry.
<?php
try {
$this->processPayment($order);
} catch (\Throwable $e) {
$logger->error('Payment failed for order {order_id}', [
'order_id' => $order->id,
'exception' => $e,
]);
}
This produces a log entry like:
[2026-02-17 14:30:00] ERROR: Payment failed for order 1234
Exception: App\Exceptions\PaymentException
Message: Insufficient funds
At: /var/www/app/Services/PaymentService.php:47
Trace:
#0 /var/www/app/Controllers/OrderController.php(32): ...
#1 /var/www/vendor/melodicdev/framework/src/Routing/RoutingMiddleware.php(45): ...
Minimum Level Filtering
When a minimum level is set, messages with a severity number greater than the minimum are silently discarded. Severity is compared numerically: lower number = more severe.
<?php
// Only WARNING (4), ERROR (3), CRITICAL (2), ALERT (1), and EMERGENCY (0)
$logger = new FileLogger('/var/log/myapp', LogLevel::WARNING);
$logger->warning('Disk space low'); // Written (severity 4 <= 4)
$logger->error('Disk full'); // Written (severity 3 <= 4)
$logger->info('Request served'); // Ignored (severity 6 > 4)
$logger->debug('Query took 12ms'); // Ignored (severity 7 > 4)
Silent failure. The FileLogger catches all exceptions during the write operation. If the log directory is not writable or the disk is full, the logger fails silently rather than crashing your application.
Directory Auto-Creation
If the log directory does not exist when a message is written, FileLogger creates it recursively with 0755 permissions. This happens inside the same try/catch that protects against write failures.
NullLogger
The Melodic\Log\NullLogger implements LoggerInterface with all methods as no-ops. Use it in tests or when you want to disable logging entirely without changing any consuming code.
<?php
use Melodic\Log\NullLogger;
$logger = new NullLogger();
$logger->error('This goes nowhere'); // No-op
$logger->debug('Neither does this'); // No-op
Testing tip. Bind NullLogger in your test container to suppress log output during test runs while still verifying that your services accept a LoggerInterface dependency.
LoggingServiceProvider
The Melodic\Log\LoggingServiceProvider reads configuration values and registers a FileLogger singleton. It pulls the log directory and minimum level from your JSON configuration file.
<?php
use Melodic\Core\Application;
use Melodic\Log\LoggingServiceProvider;
$app = new Application(__DIR__);
$app->loadEnvironmentConfig();
$app->services(function ($container) {
(new LoggingServiceProvider())->register($container);
});
$app->run();
Configuration Keys
| Key | Type | Default | Description |
|---|---|---|---|
logging.path |
string |
{basePath}/logs |
Absolute path to the log directory |
logging.level |
string |
'debug' |
Minimum log level (case-insensitive, e.g. 'warning', 'error') |
Example config.json:
{
"logging": {
"path": "/var/log/myapp",
"level": "warning"
}
}
If logging.path is not set, the provider defaults to a logs directory inside your application's base path. If logging.level is not set, it defaults to 'debug', meaning all messages are logged.
How the Provider Works
The service provider registers a singleton factory that reads configuration at resolve time:
<?php
// Inside LoggingServiceProvider::register()
$container->singleton(LoggerInterface::class, function (Container $c) {
$config = $c->get(Configuration::class);
$app = $c->get(Application::class);
$path = $config->get('logging.path') ?? $app->getBasePath() . '/logs';
$level = LogLevel::parse($config->get('logging.level', 'debug'));
return new FileLogger($path, $level);
});
Usage in Services and Controllers
Type-hint LoggerInterface in any constructor to receive the configured logger instance.
<?php
use Melodic\Log\LoggerInterface;
use Melodic\Service\Service;
class OrderService extends Service
{
public function __construct(
private readonly LoggerInterface $logger,
) {}
public function placeOrder(array $items): Order
{
$this->logger->info('Placing order with {count} items', [
'count' => count($items),
]);
try {
$order = $this->createOrder($items);
$this->logger->info('Order {id} placed successfully', [
'id' => $order->id,
]);
return $order;
} catch (\Throwable $e) {
$this->logger->error('Order placement failed', [
'exception' => $e,
]);
throw $e;
}
}
}
Class Reference
| Class | Namespace | Purpose |
|---|---|---|
LogLevel |
Melodic\Log |
Backed enum defining eight severity levels with severity() and parse() |
LoggerInterface |
Melodic\Log |
Contract with convenience methods for each level plus generic log() |
FileLogger |
Melodic\Log |
Daily-rotating file logger with interpolation and exception formatting |
NullLogger |
Melodic\Log |
No-op logger for testing or disabling logging |
LoggingServiceProvider |
Melodic\Log |
Reads config and registers FileLogger as a singleton |