Good logs answer “what happened, when and to whom?” after something went wrong. You already know error_log() (Error Logging). Real applications use a logger with levels and context.
Log levels
The common set (from the PSR-3 standard) from least to most serious:
| Level | Use for |
|---|---|
debug | Detailed information for developers |
info | Normal events (“user logged in”, “order placed”) |
warning | Something odd, but handled |
error | A failure that needs attention |
critical | The application cannot continue |
In production you usually record info and above. In development, everything.
A small logger
<?php
final class Logger
{
private const LEVELS = ["debug" => 0, "info" => 1, "warning" => 2, "error" => 3, "critical" => 4];
public function __construct(private string $file, private string $minLevel = "info") {}
public function log(string $level, string $message, array $context = []): void
{
if (self::LEVELS[$level] < self::LEVELS[$this->minLevel]) {
return;
}
// replace {placeholders} in the message with context values
$replace = [];
foreach ($context as $key => $value) {
if (is_scalar($value)) {
$replace["{" . $key . "}"] = (string) $value;
}
}
$line = sprintf(
"[%s] %s: %s %s",
date("Y-m-d H:i:s"),
strtoupper($level),
str_replace(["\r", "\n"], " ", strtr($message, $replace)),
$context ? json_encode($context, JSON_UNESCAPED_SLASHES) : ""
);
file_put_contents($this->file, rtrim($line) . PHP_EOL, FILE_APPEND | LOCK_EX);
}
public function info(string $message, array $context = []): void { $this->log("info", $message, $context); }
public function warning(string $message, array $context = []): void { $this->log("warning", $message, $context); }
public function error(string $message, array $context = []): void { $this->log("error", $message, $context); }
public function debug(string $message, array $context = []): void { $this->log("debug", $message, $context); }
}
$logger = new Logger("app.log", "info");
$logger->debug("This is skipped");
$logger->info("User {id} logged in", ["id" => 42]);
$logger->error("Payment failed for order {order}", ["order" => 1007, "gateway" => "demo"]);
foreach (file("app.log", FILE_IGNORE_NEW_LINES) as $line) {
echo substr($line, 22) . "\n"; // skip the timestamp
}
INFO: User 42 logged in {"id":42}
ERROR: Payment failed for order 1007 {"order":1007,"gateway":"demo"}
- The
debugline was skipped because the minimum level isinfo. {id}placeholders are replaced from the context, and the context is also saved as JSON, so tools can search it.- New lines are removed from the message to prevent log injection.
What to put in the context
User id, order id, request id, URL path, duration, the exception class. Never passwords, tokens, full card numbers or whole request bodies.
Log exceptions properly
<?php
try {
$service->place($productId, $qty);
} catch (Throwable $e) {
$logger->error("Order failed: {message}", [
"message" => $e->getMessage(),
"exception" => get_class($e),
"file" => basename($e->getFile()),
"line" => $e->getLine(),
]);
}
One file per day
Name files by date so they stay small and are easy to delete.
<?php
function dailyLogFile(string $folder, string $prefix = "app"): string
{
return rtrim($folder, "/") . "/" . $prefix . "-" . date("Y-m-d") . ".log";
}
echo preg_match('#^logs/app-\d{4}-\d{2}-\d{2}\.log$#', dailyLogFile("logs")) === 1 ? "daily file name" : "unexpected";
daily file name
Use Monolog for real projects
Monolog is the standard logging library (installed with Composer). It supports levels, many destinations (files, syslog, email, Slack, cloud services), processors that add request data, and the PSR-3 interface that frameworks use.
composer require monolog/monolog
<?php
use Monolog\Handler\StreamHandler;
use Monolog\Level;
use Monolog\Logger;
$log = new Logger("app");
$log->pushHandler(new StreamHandler(__DIR__ . "/storage/logs/app.log", Level::Info));
$log->info("User logged in", ["id" => 42]);
$log->error("Payment failed", ["order" => 1007]);
Rules
- Log to files outside the web root (or a logging service).
- Rotate and delete old logs.
- Set the right level per environment.
- Never log secrets or unnecessary personal data.
- Make logs searchable: consistent messages and structured context.
- Read them and set up alerts for errors.
Why this matters in Laravel
Log::info("...", [...]) writes to storage/logs/laravel.log. Knowing log levels and context makes those logs useful.