Error Logging

JA JASWANT Updated 17 Oct 2026
6 min read ·Lesson 102 of 248
In this post

Why Logging Matters In simple terms: In production, error messages must never be shown to visitors — but developers still need to know what went wrong. Logging writes errors and important events to a

Why Logging Matters

In simple terms: In production, error messages must never be shown to visitors — but developers still need to know what went wrong. Logging writes errors and important events to a file or service where developers can read them later. Without logs, a bug reported as "the payment page sometimes fails" is almost impossible to investigate.

Technically: PHP writes its own errors to the destination configured by the error_log setting (when log_errors is on). Applications add their own entries with error_log() or, more commonly, with a logging library such as Monolog, which implements the standard PSR-3 logger interface. Good logs are structured, include context, use severity levels, and never contain secrets.


PHP's Built-in Logging

Configuration

php.ini SettingRecommendedPurpose
log_errorsOnWrite PHP errors to the log
error_logA file path, e.g. /var/log/php/app.log (or unset in containers)Where PHP's errors go; if unset, they go to the web server's error log or to stderr
display_errorsOff in productionNever show errors to visitors

In Docker and many cloud platforms, the convention is to log to stderr, where the platform collects logs from all containers.

error_log()

error_log(string $message, int $message_type = 0, ?string $destination = null): bool
TypeDestination
0 (default)PHP's configured error log (or the system log)
3Appended to the file given in $destination
4Sent directly to the server API's logging (for example, the web server)
1Sent by email — avoid: a burst of errors becomes a flood of emails
<?php
$logFile = __DIR__ . '/app.log';

error_log("Order 501 failed: gateway timeout\n", 3, $logFile);
error_log("Order 502 failed: card declined\n", 3, $logFile);

echo file_get_contents($logFile);
unlink($logFile);

Output

Order 501 failed: gateway timeout
Order 502 failed: card declined

With type 3, error_log() appends exactly the given text — add the newline yourself. With type 0, PHP adds a timestamp and newline automatically.


What Makes a Good Log Entry

A useful log entry answers: when, how serious, what happened, and in what context.

ElementExampleWhy
Timestamp with time zone2026-10-15T10:02:17+05:30Order events and correlate with other systems
LevelERRORFilter by severity
MessagePayment failedHuman-readable summary
Contextorder_id=501, gateway=razorpayThe data needed to investigate
Request IDreq-7f3aConnect all entries from one request

Log Levels (PSR-3)

LevelUse For
emergencyThe whole system is unusable
alertImmediate action needed (database down)
criticalCritical component failure
errorAn operation failed (payment error, exception)
warningSomething unusual that did not fail (slow API, retry needed)
noticeNormal but significant events
infoRoutine events (user logged in, order placed)
debugDetailed diagnostic information (only in development)

Production systems typically record info and above; debug produces too much data.


Structured Logging With Context and Redaction

Writing logs as JSON lines makes them searchable by log tools (ELK, Grafana Loki, cloud logging services). Sensitive values must be removed before writing:

<?php
function redact(array $context): array
{
    foreach ($context as $key => $value) {
        if (is_array($value)) {
            $context[$key] = redact($value);
        } elseif (in_array(strtolower((string) $key), ['password', 'token', 'secret', 'api_key'], true)) {
            $context[$key] = '[REDACTED]';
        } elseif ($key === 'card_number' && is_string($value)) {
            $context[$key] = str_repeat('*', max(0, strlen($value) - 4)) . substr($value, -4);
        }
    }

    return $context;
}

function logEvent(string $file, string $level, string $message, array $context, DateTimeInterface $time, string $requestId): void
{
    $entry = [
        'time' => $time->format(DATE_ATOM),
        'level' => strtoupper($level),
        'message' => $message,
        'context' => redact($context),
        'request_id' => $requestId,
    ];

    file_put_contents($file, json_encode($entry, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n", FILE_APPEND | LOCK_EX);
}

$file = __DIR__ . '/app.jsonl';

logEvent($file, 'error', 'Payment failed', [
    'order_id' => 501,
    'card_number' => '4111111111111111',
    'password' => 'secret123',
], new DateTimeImmutable('2026-10-15 10:02:17', new DateTimeZone('Asia/Kolkata')), 'req-7f3a');

echo file_get_contents($file);
unlink($file);

Output

{"time":"2026-10-15T10:02:17+05:30","level":"ERROR","message":"Payment failed","context":{"order_id":501,"card_number":"************1111","password":"[REDACTED]"},"request_id":"req-7f3a"}

How It Works

  1. Each entry is one JSON object on one line ("JSON Lines"), easy to append and to parse.
  2. redact() replaces passwords, tokens, and secrets (recursively, including nested arrays) and masks card numbers to their last four digits.
  3. The request ID connects this entry with all other entries from the same request. Generate one per request (for example, bin2hex(random_bytes(8))), or use the ID from a load balancer header.
  4. Time and request ID are parameters, which makes the function testable.

Never Log

  • Passwords, password hashes, and one-time codes
  • Full card numbers, CVV, bank account numbers
  • API keys, access tokens, session IDs
  • Unnecessary personal data (log a user ID instead of name, email, and phone)

Logs are often kept for a long time and accessed by many people and tools; leaked logs are a common source of data breaches.


Message Placeholders

PSR-3 messages may contain {placeholders} that are filled from the context, keeping the message text constant (which helps grouping in log tools):

<?php
function interpolate(string $message, array $context): string
{
    $replacements = [];

    foreach ($context as $key => $value) {
        if (is_scalar($value) || $value instanceof Stringable) {
            $replacements['{' . $key . '}'] = (string) $value;
        }
    }

    return strtr($message, $replacements);
}

echo interpolate('Order {order_id} failed at gateway {gateway}', ['order_id' => 501, 'gateway' => 'razorpay']) . "\n";

Output

Order 501 failed at gateway razorpay

Using Monolog

In real projects, use Monolog, the standard PHP logging library (used by Laravel and Symfony):

composer require monolog/monolog
<?php
require __DIR__ . '/vendor/autoload.php';

use Monolog\Formatter\JsonFormatter;
use Monolog\Handler\RotatingFileHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Level;
use Monolog\Logger;
use Monolog\Processor\PsrLogMessageProcessor;

$logger = new Logger('shop');

$fileHandler = new RotatingFileHandler(__DIR__ . '/storage/logs/app.log', 14, Level::Info);
$fileHandler->setFormatter(new JsonFormatter());
$logger->pushHandler($fileHandler);

$logger->pushHandler(new StreamHandler('php://stderr', Level::Error));
$logger->pushProcessor(new PsrLogMessageProcessor());

$logger->info('User {user_id} logged in', ['user_id' => 7]);
$logger->error('Payment failed for order {order_id}', ['order_id' => 501, 'gateway' => 'razorpay']);

try {
    throw new RuntimeException('Gateway timeout');
} catch (RuntimeException $e) {
    $logger->error('Payment exception', ['exception' => $e]);
}

What This Configures

  • Handlers decide where entries go: a rotating daily file that keeps 14 days, and stderr for errors.
  • Each handler has a minimum level: the file receives info and above, stderr only error and above.
  • Formatters decide the format (JSON here).
  • Processors enrich entries (here, filling {placeholders}); others add memory usage, request IDs, or user information.
  • Passing an exception in the exception context key records its class, message, file, line, and stack trace.

Because Monolog implements PSR-3's Psr\Log\LoggerInterface, application classes can depend on the interface and receive any compatible logger.


Log Rotation and Retention

Log files grow continuously. Configure rotation (new file per day or per size) and retention (delete files after a number of days), either with Monolog's RotatingFileHandler, the operating system's logrotate, or your logging platform. Keep logs long enough for investigations, and no longer than your privacy obligations allow.


Common Mistakes

Displaying Instead of Logging

Effect: Errors are visible to users and lost to developers.

Fix: display_errors = Off, log_errors = On.

Logging Sensitive Data

Effect: Passwords, tokens, and card numbers end up in log files.

Fix: Redact before writing; log identifiers, not personal data.

Unstructured, Context-Free Messages

Effect: "Error occurred" entries that cannot be investigated.

Fix: Include context (IDs, operation, relevant values) and use levels.

Logging Everything at One Level

Effect: Important errors are buried among routine messages.

Fix: Use appropriate PSR-3 levels and minimum levels per handler.

Never Reading the Logs

Effect: Problems are noticed only when users complain.

Fix: Set up alerts for error and above, and review logs regularly.


Best Practices

  • Log every error with a timestamp, level, message, context, and request ID.
  • Use JSON lines (or a logging platform) so logs are searchable.
  • Redact secrets and personal data before writing.
  • Use Monolog (or your framework's logger) via the PSR-3 interface.
  • Rotate logs, limit retention, and alert on serious errors.

Official reference: https://www.php.net/manual/en/function.error-log.php

0 Comments

Reviewed before they appear

No comments yet.

PHP
PHP Home PHP Switch PHP Match Alternative Syntax PHP Loops While Loop Do While Loop For Loop Foreach Loop Nested Loops Break Statement PHP Intro PHP Install PHP Built-in Web Server PHP Syntax PHP Tags PHP and HTML PHP Comments PHP Multiline Comments PHP Variables Variables Scope PHP Casting Logical Operators Null Coalescing Operator Spaceship Operator Operator Precedence Continue Statement Modify Strings PHP If...Else...Elseif PHP If Modify Strings PHP If Operators PHP Functions Concatenate Strings PHP If...Else User-defined Functions Slicing Strings Function Arguments PHP Shorthand if Escape Characters Default Argument Values PHP Nested if Pass by Reference String Interpolation Named Arguments PHP File Upload Variadic Functions Return Values Search in Strings Multibyte Strings Type Declarations in Functions PHP Numbers PHP Math PHP Floats PHP Random Numbers PHP Constants PHP Properties PHP Methods PHP Constructor Constructor Property Promotion PHP Cookies PHP Destructor PHP Access Modifiers Readonly Properties PHP Sessions PHP Inheritance Method Overriding PHP Filters The final Keyword CSRF Protection PHP Filters Advanced Session Security Secure Cookies PHP Magic Constants File Upload Security PHP Callback Functions PHP Operators Filesystem Security PHP JSON Arithmetic Operators Error Reporting in Production Hiding PHP Assignment Operators PHP Configuration (php.ini) Comparison Operators PHP Command Line (CLI) PHP Exceptions Increment / Decrement Operators Migrating to PHP 8 PHP Form Validation PHP Form Required PHP Form URL/E-mail Error Handling Form Input Sanitizing Escaping Output (htmlspecialchars) Custom Error Handler Checkbox, Radio and Select Inputs Sticky Forms PHP Form Complete PHP Date and Time Date Formatting Error Logging HTTP Headers and Redirects Output Buffering Sending Email PHP cURL Consuming REST APIs PHP Generators AJAX PHP AJAX Database AJAX XML AJAX Live Search AJAX Poll AJAX with JSON Security Introduction Password Hashing SQL Injection Prevention XSS Prevention Garbage Collection Project: Simple Blog PHP What is OOP PHP Classes/Objects The $this Keyword Access Array Items Update Array Items Add Array Items Remove Array Items Sorting Arrays Multidimensional Arrays Array Destructuring Array Spread Operator Search in Arrays Merge Arrays PHP Abstract Classes PHP Interfaces Polymorphism PHP Traits PHP Static Methods PHP Static Properties Late Static Binding Magic Methods Object Cloning MySQL Database MySQL Connect MySQL Create DB MySQL Create Table MySQL Insert Data MySQL Get Last ID MySQL Insert Multiple MySQL Prepared MySQL Select Data MySQL Where Debugging PHP Project: Image Upload Gallery $_SESSION $_ENV PHP RegEx RegEx Patterns Heredoc and Nowdoc RegEx Modifiers preg_match preg_replace preg_split PHP RegEx Functions Nullable Types Mixed and Never Types Nullsafe Operator PHP Attributes PHP Fibers Readonly Classes Property Hooks (PHP 8.4) Asymmetric Visibility (PHP 8.4) Reflection API SPL Data Structures Composer PHP Form Handling PDO Transactions PDO Error Handling PHP XML Parsers PHP SimpleXML Parser PHP SimpleXML - Get PHP XML Expat Parser PHP DOM Parser PHP XMLReader PHP XMLWriter AJAX Intro PHP Class Constants Encryption with Sodium Recursive Functions Variable Functions Anonymous Functions Closures Arrow Functions First-class Callable Syntax PHP Arrays Indexed Arrays Associative Arrays Create Arrays Variables Scope Static Variables Variable Variables PHP Echo / Print PHP printf / sprintf PHP Data Types PHP Booleans PHP NULL PHP Strings String Functions array_map / array_filter / array_reduce Array Functions PHP Superglobals $GLOBALS What Is `$_SERVER` ? $_REQUEST $_POST $_GET $_FILES $_COOKIE PHP Type Juggling MySQL Order By MySQL Delete Data MySQL Update Data MySQL Limit Data MySQL Joins with PHP Pagination with PHP PDO Introduction PDO Connect PDO Prepared Statements PDO Fetch Modes PSR-4 Autoloading Installing Extensions (PECL / PIE) OPcache & Performance PHP-FPM Project: Contact Form with Email Project: Login & Registration System Project: CRUD App with MySQL Project: Shopping Cart with Sessions Project: REST API with PHP DateTime Class Timezones PHP Include Include vs Require PHP File Handling PHP File Open/Read PHP File Create/Write File Delete / Rename / Copy PHP Directories PHP CSV Files
Ask about this post
AI Ask about this post

Ask questions about Error Logging and get answers drawn from it.

Signed-in readers only.