Skip to content

PHP Tutorial Practical PHP Development

Logging

Lesson 323 of 369 2 min read By Deepak Paswan Updated Oct 5, 2026

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:

LevelUse for
debugDetailed information for developers
infoNormal events (“user logged in”, “order placed”)
warningSomething odd, but handled
errorA failure that needs attention
criticalThe application cannot continue

In production you usually record info and above. In development, everything.

A small logger

index.php
PHP
<?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
}
Output
OUTPUT
INFO: User 42 logged in {"id":42}
ERROR: Payment failed for order 1007 {"order":1007,"gateway":"demo"}
  • The debug line was skipped because the minimum level is info.
  • {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

index.php
PHP
<?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.

index.php
PHP
<?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";
Output
OUTPUT
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.

terminal
BASH
composer require monolog/monolog
index.php
PHP
<?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

  1. Log to files outside the web root (or a logging service).
  2. Rotate and delete old logs.
  3. Set the right level per environment.
  4. Never log secrets or unnecessary personal data.
  5. Make logs searchable: consistent messages and structured context.
  6. Read them and set up alerts for errors.
info
Why this matters in Laravel
Laravel logs with Monolog. Log::info("...", [...]) writes to storage/logs/laravel.log. Knowing log levels and context makes those logs useful.