Error Logging

JA JASWANT Updated 17 Oct 2026
6 min read ·Lesson 103 of 109
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 Home 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 Slicing Strings User-defined Functions Slicing Strings 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
Ask about this post
AI Ask about this post

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

Signed-in readers only.