mini\Logger namespace

Mini Logger

PSR-3 compatible logging system for the Mini framework. Provides a simple, standardized way to log messages with different severity levels.

Features

  • PSR-3 Compatible - Implements the standard PHP logging interface
  • Built-in Logger - Logs to PHP's error_log by default
  • Configurable - Override with any PSR-3 compatible logger
  • MessageFormatter Integration - Uses ICU MessageFormatter for context interpolation
  • Application Locale - Uses app default locale, not per-user locale
  • Exception Support - Automatically formats exceptions in context

Basic Usage

use function mini\log;

// Basic logging
log()->info("User logged in");
log()->warning("Cache miss for key: users");
log()->error("Failed to connect to database");

// With context variables
log()->info("User {username} logged in from {ip}", [
    'username' => 'john',
    'ip' => '192.168.1.1'
]);

// With exceptions
try {
    // Some code that throws
} catch (\Exception $e) {
    log()->error("Operation failed: {message}", [
        'message' => $e->getMessage(),
        'exception' => $e  // Automatically formats stack trace
    ]);
}

Log Levels (PSR-3)

log()->emergency($message, $context);  // System is unusable
log()->alert($message, $context);      // Action must be taken immediately
log()->critical($message, $context);   // Critical conditions
log()->error($message, $context);      // Runtime errors
log()->warning($message, $context);    // Warning messages
log()->notice($message, $context);     // Normal but significant
log()->info($message, $context);       // Informational messages
log()->debug($message, $context);      // Debug-level messages

Context Interpolation

The logger uses ICU MessageFormatter with the application's default locale for context interpolation:

// Simple placeholders
log()->info("Processing {count} items", ['count' => 42]);
// Output: [2025-10-27 13:38:53] [INFO] Processing 42 items

// Multiple variables
log()->info("User {user} performed {action} on {resource}", [
    'user' => 'admin',
    'action' => 'delete',
    'resource' => 'database'
]);

// Arrays are JSON-encoded with backticks
log()->info("User data: {user}", [
    'user' => ['id' => 123, 'name' => 'John']
]);
// Output: [2025-10-27 13:38:53] [INFO] User data: `{"id":123,"name":"John"}`

// Special values use markdown-style backticks for easy parsing/colorization
log()->info("Active: {active}, Value: {value}", [
    'active' => true,
    'value' => null
]);
// Output: [2025-10-27 13:38:53] [INFO] Active: `true`, Value: `null`

// Non-stringable objects show class name
log()->info("Request: {req}", ['req' => new stdClass()]);
// Output: [2025-10-27 13:38:53] [INFO] Request: `stdClass`

Exception Logging

The exception key in context has special handling:

try {
    throw new \RuntimeException("Something went wrong");
} catch (\Exception $e) {
    log()->error("Error occurred", ['exception' => $e]);
}

// Output:
// [2025-10-27 13:38:53] [ERROR] Error occurred
// Exception: RuntimeException
// Message: Something went wrong
// File: /path/to/file.php:42
// Trace:
// #0 /path/to/file.php(42): functionName()
// #1 {main}

Custom Logger Configuration

Override the default logger by creating _config/Psr/Log/LoggerInterface.php:

<?php
// _config/Psr/Log/LoggerInterface.php

use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\FirePHPHandler;

// Example: Monolog logger with multiple handlers
$logger = new Logger('mini');
$logger->pushHandler(new StreamHandler(__DIR__ . '/../../../storage/logs/app.log', Logger::DEBUG));
$logger->pushHandler(new FirePHPHandler());

return $logger;

Any PSR-3 compatible logger works:

<?php
// _config/Psr/Log/LoggerInterface.php

// Example: Custom logger implementation
class DatabaseLogger implements \Psr\Log\LoggerInterface
{
    use \Psr\Log\LoggerTrait;

    public function log($level, $message, array $context = []): void
    {
        // Log to database
        db()->exec(
            "INSERT INTO logs (level, message, context, created_at) VALUES (?, ?, ?, ?)",
            [$level, $message, json_encode($context), date('Y-m-d H:i:s')]
        );
    }
}

return new DatabaseLogger();

Built-in Logger Format

Default format: [timestamp] [LEVEL] message

[2025-10-27 13:38:53] [INFO] User john logged in
[2025-10-27 13:38:53] [WARNING] Cache miss
[2025-10-27 13:38:53] [ERROR] Database connection failed

Best Practices

1. Use Appropriate Log Levels

// Emergency - System is down, immediate attention needed
log()->emergency("Database server is unreachable");

// Error - Something failed but app can continue
log()->error("Payment processing failed for order {id}", ['id' => $orderId]);

// Warning - Something unexpected but not an error
log()->warning("API rate limit approaching threshold");

// Info - Normal operational messages
log()->info("User {username} logged in", ['username' => $user]);

// Debug - Detailed information for debugging
log()->debug("SQL Query: {query}", ['query' => $sql]);

2. Include Relevant Context

// Good: Includes actionable context
log()->error("Failed to send email to {recipient}", [
    'recipient' => $email,
    'smtp_error' => $error,
    'attempt' => $retryCount
]);

// Bad: Missing context
log()->error("Failed to send email");

3. Log Exceptions Properly

// Good: Includes exception in context
try {
    // ...
} catch (\Exception $e) {
    log()->error("Payment processing failed: {message}", [
        'message' => $e->getMessage(),
        'exception' => $e,  // Includes full stack trace
        'order_id' => $orderId
    ]);
}

// Avoid: Losing stack trace
catch (\Exception $e) {
    log()->error("Payment failed: " . $e->getMessage());  // No stack trace
}

4. Don't Log Sensitive Data

// Bad: Logs password
log()->info("User login", ['username' => $user, 'password' => $pass]);

// Good: Omit sensitive data
log()->info("User {username} logged in", ['username' => $user]);

5. Use Lazy Evaluation for Debug Logs

// If expensive to compute, check log level first (if using custom logger)
if ($logger->isDebugEnabled()) {
    log()->debug("Complex data: {data}", ['data' => expensiveOperation()]);
}

Scoped Logging

Use ScopedLogger to prefix messages with a component/module identifier:

use mini\Logger\ScopedLogger;

class HttpKernel
{
    private LoggerInterface $log;

    public function __construct()
    {
        $this->log = new ScopedLogger('http', \mini\log());
    }

    public function handle(Request $request): Response
    {
        $this->log->info("Incoming {method} {path}", [
            'method' => $request->getMethod(),
            'path'   => $request->getPath(),
        ]);
        // Output: [http] Incoming GET /users
    }
}

Scoped loggers can be nested:

$appLog = new ScopedLogger('app', \mini\log());
$dbLog = new ScopedLogger('db', $appLog);

$dbLog->info("Query executed");
// Output: [app] [db] Query executed

The scope is also added to context for structured loggers:

$log->info("Message"); // context['scope'] = 'http'

Integration with Other Loggers

The mini logger is PSR-3 compatible, so it works with popular logging libraries:

  • Monolog - Full-featured logging library
  • KLogger - Simple file-based logger
  • Analog - Lightweight logger
  • Any PSR-3 logger - Just return it from config/logger.php

Architecture

mini\Logger\
├── Logger.php           # Built-in PSR-3 logger implementation
├── ScopedLogger.php     # Logger decorator for adding scope prefixes
├── functions.php        # Global log() function
└── README.md            # This file

The logger is lazily initialized on first use via the mini\log() function.

License

MIT License - see LICENSE

Classes (2)

Logger

Built-in logger implementation that logs to PHP's error_log

ScopedLogger

Logger decorator that prefixes messages with a scope identifier

final