Logger

Overview

The Logger class provides file-based application logging with level filtering, log rotation, and PSR-3-inspired {placeholder} message interpolation. A single shared instance is used by both the framework's error handler and your application code — so PHP errors, uncaught exceptions, and your own log calls all land in the same file.

  • Class: JiFramework\Core\Logger\Logger
  • Access: $app->logger

Features at a glance:

  • 8 log levels — DEBUG, INFO, NOTICE, WARNING, ERROR, CRITICAL, ALERT, EMERGENCY
  • Level filtering — entries below the configured threshold are silently discarded
  • Automatic log rotation — keeps a configurable number of archived log files
  • {placeholder} interpolation — substitutes context values into message strings
  • Graceful failure — unwritable paths trigger E_USER_WARNING and disable the logger rather than crashing the application
  • Shared with ErrorHandler — PHP errors and uncaught exceptions are logged automatically
$app = new App();

$app->logger->info('User {name} logged in', ['name' => 'Alice']);
$app->logger->error('Payment failed for order {id}', ['id' => 1042]);
$app->logger->debug('Cache miss for key {key}', ['key' => 'user:42']);

Configuration

Logger settings are defined in your jiconfig.php file.

// jiconfig.php
return [
    'log_enabled'       => true,
    'log_level'         => 'DEBUG',   // minimum level to write
    'log_file_name'     => 'app.log',
    'log_max_file_size' => 5242880,   // 5 MB in bytes
    'log_max_files'     => 20,        // number of archived files to keep
    // 'log_file_path'  => '/custom/path/logs/', // defaults to storage/Logs/
];
  • log_enabled(bool) Enable or disable all logging. When false the Logger constructs successfully but all write calls are no-ops. Default: true.
  • log_level(string) Minimum level to write. Entries below this threshold are silently discarded. Default: DEBUG.
  • log_file_name(string) Name of the active log file. Default: app.log.
  • log_max_file_size(int) Maximum file size in bytes before rotation is triggered. Default: 5242880 (5 MB).
  • log_max_files(int) Number of archived (rotated) files to keep alongside the active log. Default: 20.
  • log_file_path(string, optional) Absolute path to the log directory including trailing separator. Defaults to storage/Logs/ inside the project root.

Log Levels

Eight levels are available in ascending order of severity. Only entries at or above the configured log_level threshold are written to disk — everything below is discarded without file I/O.

  • DEBUG — Detailed diagnostic information. Use during development to trace execution flow.
  • INFO — Normal application events: user logins, successful operations, background job completions.
  • NOTICE — Normal but noteworthy events that may warrant attention.
  • WARNING — Unexpected situations that do not stop execution but should be reviewed.
  • ERROR — Runtime errors that require attention. The operation failed but the application continues.
  • CRITICAL — Critical conditions: a component is unavailable, unexpected exceptions in core paths.
  • ALERT — Action must be taken immediately (e.g. database down, payment gateway unreachable).
  • EMERGENCY — System is unusable.
// jiconfig.php: log_level = 'WARNING'
// Only WARNING and above will be written:

$app->logger->debug('Cache miss');    // discarded
$app->logger->info('User logged in'); // discarded
$app->logger->notice('Slow query');   // discarded
$app->logger->warning('Disk low');    // written
$app->logger->error('DB timeout');   // written
$app->logger->critical('Cache down');// written

In production it is recommended to set log_level to WARNING or ERROR to avoid filling disk with debug noise. Use DEBUG during development.

log()

log(string $level, string $message, array $context = []): void

The core logging method. All convenience methods delegate to this. Use it directly when the level is determined at runtime.

  • $level(string) Log level name (case-insensitive). Unknown values fall back to DEBUG.
  • $message(string) Log message. May contain {placeholder} tokens.
  • $context(array, optional) Key-value pairs substituted into the message placeholders.
$logger = $app->logger;

$logger->log('INFO',    'Application started');
$logger->log('WARNING', 'Retrying request {n} of {max}', ['n' => 2, 'max' => 3]);
$logger->log('ERROR',   'Query failed: {error}', ['error' => $e->getMessage()]);

// Level determined at runtime
$level = $someCondition ? 'WARNING' : 'INFO';
$logger->log($level, 'Status: {status}', ['status' => $status]);

Each written entry follows the format:

[2026-03-03 12:00:00] [WARNING] Retrying request 2 of 3

Convenience Methods

Each of the eight log levels has a dedicated shorthand method. All accept the same signature: a message string and an optional context array.

debug(string $message, array $context = []): void
info(string $message, array $context = []): void
notice(string $message, array $context = []): void
warning(string $message, array $context = []): void
error(string $message, array $context = []): void
critical(string $message, array $context = []): void
alert(string $message, array $context = []): void
emergency(string $message, array $context = []): void
$log = $app->logger;

// Development — verbose tracing
$log->debug('Rendering template {tpl}', ['tpl' => 'home.php']);

// Normal business events
$log->info('User {id} registered', ['id' => $user['id']]);
$log->notice('Config key {key} is deprecated', ['key' => 'old_key']);

// Problems
$log->warning('Rate limit approaching for IP {ip}', ['ip' => $ip]);
$log->error('Email delivery failed for {to}', ['to' => $email]);

// Severe
$log->critical('Primary DB unreachable, switching to replica');
$log->alert('Payment gateway down — all transactions failing');
$log->emergency('Disk full, application cannot write files');

Context Interpolation

Place {key} tokens anywhere in the message string. The second argument is an associative array of replacements. This keeps log messages readable without string concatenation.

$app->logger->info(
    'User {name} (id={id}) logged in from {ip}',
    ['name' => 'Alice', 'id' => 42, 'ip' => '192.168.1.1']
);
// [2026-03-03 12:00:00] [INFO] User Alice (id=42) logged in from 192.168.1.1

Type handling:

  • string / int / float — substituted as-is.
  • booltrue becomes "true", false becomes "false".
  • null — becomes "null".
  • array / object — JSON-encoded.
$app->logger->debug('Flags: active={active}, deleted={deleted}, meta={meta}', [
    'active'  => true,              // "true"
    'deleted' => false,             // "false"
    'meta'    => ['role' => 'admin'], // {"role":"admin"}
]);
// Flags: active=true, deleted=false, meta={"role":"admin"}

Unmatched tokens are left in the string unchanged. Context keys with no corresponding {token} in the message are silently ignored.

Log Rotation

When the active log file reaches log_max_file_size bytes, it is automatically rotated before the next write. No external cron or tool is required.

How rotation works:

  • The current app.log is renamed to app.log.0.
  • Existing archives are shifted up: .0 becomes .1, .1 becomes .2, and so on.
  • Archives beyond the log_max_files limit are deleted.
  • A fresh empty app.log is opened for subsequent writes.
// With log_max_files = 3, after several rotations:
storage/Logs/
  app.log       ← active (current request writes here)
  app.log.0     ← most recent archive
  app.log.1
  app.log.2     ← oldest archive (deleted on next rotation)

Configuration:

  • log_max_file_size(int) Size threshold in bytes. Default: 5242880 (5 MB).
  • log_max_files(int) Number of archived files to retain. Default: 20. Set to 1 to keep only the most recent archive.

setLogFile()

setLogFile(string $logFilePath): void

Closes the current log file handle and opens a new one at the given path. Useful for switching log files mid-request, such as writing request-specific or module-specific logs.

  • $logFilePath(string) Absolute path to the new log file. The directory is created automatically if it does not exist.
$log = $app->logger;

$log->info('Written to app.log');

// Switch to a module-specific file
$log->setLogFile(STORAGE_PATH . 'Logs/payments.log');
$log->info('Payment processed for order {id}', ['id' => $orderId]);

// Switch back
$log->setLogFile(STORAGE_PATH . 'Logs/app.log');
$log->info('Back to main log');

If logging is disabled (log_enabled = false), setLogFile() is a no-op. Log rotation and the log_max_file_size / log_max_files settings apply to the new file just as they do to the default one.

Integration with ErrorHandler

The Logger instance is created eagerly inside new App() and shared with ErrorHandler. This means all PHP errors, uncaught exceptions, and fatal shutdown errors are automatically written to the same log file as your application-level calls — with no extra setup.

$app = new App();
// From this point:
// - All PHP E_WARNING, E_NOTICE, etc. → logged as ERROR
// - All uncaught exceptions          → logged as ERROR with full stack trace
// - Fatal E_ERROR / E_PARSE          → logged via shutdown handler
// - Your own calls                   → $app->logger->info(...) etc.
// All in the same storage/Logs/app.log

A typical log file in development will look like:

[2026-03-03 12:00:01] [INFO] User 42 logged in
[2026-03-03 12:00:05] [WARNING] Rate limit approaching for IP 203.0.113.5
[2026-03-03 12:00:09] [ERROR] Unhandled Exception: Division by zero
Exception: DivisionByZeroError
Message: Division by zero
File: /app/pages/calc.php (Line 18)
Stack trace:
#0 ...

Graceful failure — if the log directory cannot be created or the log file cannot be opened, the Logger emits an E_USER_WARNING and disables itself. The application continues running; log calls become silent no-ops. This prevents a missing log directory from taking down the entire application.