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