PHP Error Logging Setup Guide: From Basics to Production
Learn how to configure PHP error logging for production applications, including custom error handlers and centralized logging.
PHP's default error handling often falls short for production applications. Errors get written to server logs that are hard to search, or worse, displayed to users. This guide shows you how to build a robust error logging system.
PHP Configuration Basics
Start by configuring PHP's error handling in php.ini or at runtime:
// Production settings
ini_set('display_errors', '0'); // Never show errors to users
ini_set('log_errors', '1'); // Log errors instead
ini_set('error_log', '/var/log/php/errors.log');
error_reporting(E_ALL); // Report all errors
Custom Error Handler
PHP's default error handler is limited. Create a custom one:
<?php
class ErrorHandler
{
private string $apiUrl;
private string $apiToken;
private array $buffer = [];
public function __construct(string $apiUrl, string $apiToken)
{
$this->apiUrl = $apiUrl;
$this->apiToken = $apiToken;
set_error_handler([$this, 'handleError']);
set_exception_handler([$this, 'handleException']);
register_shutdown_function([$this, 'handleShutdown']);
}
public function handleError(int $errno, string $errstr, string $errfile, int $errline): bool
{
$level = match($errno) {
E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR => 'error',
E_WARNING, E_CORE_WARNING, E_COMPILE_WARNING, E_USER_WARNING => 'warning',
E_NOTICE, E_USER_NOTICE => 'notice',
default => 'info',
};
$this->log($level, $errstr, [
'file' => $errfile,
'line' => $errline,
'errno' => $errno,
]);
return false; // Let PHP handle it too
}
public function handleException(\Throwable $e): void
{
$this->log('error', $e->getMessage(), [
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => $e->getTraceAsString(),
'exception_class' => get_class($e),
]);
}
public function handleShutdown(): void
{
$error = error_get_last();
if ($error && in_array($error['type'], [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR])) {
$this->log('critical', $error['message'], [
'file' => $error['file'],
'line' => $error['line'],
]);
}
$this->flush();
}
private function log(string $level, string $message, array $context = []): void
{
$this->buffer[] = [
'level' => $level,
'message' => $message,
'timestamp' => date('c'),
'context' => $context,
];
if (count($this->buffer) >= 10) {
$this->flush();
}
}
private function flush(): void
{
if (empty($this->buffer)) return;
$ch = curl_init($this->apiUrl);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(['logs' => $this->buffer]),
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer ' . $this->apiToken,
],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
]);
curl_exec($ch);
curl_close($ch);
$this->buffer = [];
}
}
Initialize the Error Handler
// bootstrap.php or similar
require_once 'ErrorHandler.php';
$errorHandler = new ErrorHandler(
'https://logs.401clicks.com/api/v1/logs/batch',
getenv('CLICKS_API_TOKEN')
);
Using Monolog (Recommended)
For more sophisticated logging, use Monolog:
composer require monolog/monolog
<?php
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
class HttpHandler extends \Monolog\Handler\AbstractProcessingHandler
{
private string $url;
private string $token;
private array $buffer = [];
public function __construct(string $url, string $token, $level = Logger::DEBUG)
{
parent::__construct($level, true);
$this->url = $url;
$this->token = $token;
register_shutdown_function([$this, 'flush']);
}
protected function write(array $record): void
{
$this->buffer[] = [
'level' => strtolower($record['level_name']),
'message' => $record['message'],
'timestamp' => $record['datetime']->format('c'),
'context' => $record['context'],
];
if (count($this->buffer) >= 10) {
$this->flush();
}
}
public function flush(): void
{
if (empty($this->buffer)) return;
// Send logs via HTTP (same as above)
$this->buffer = [];
}
}
$logger = new Logger('app');
$logger->pushHandler(new StreamHandler('php://stderr', Logger::DEBUG));
$logger->pushHandler(new HttpHandler(
'https://logs.401clicks.com/api/v1/logs/batch',
getenv('CLICKS_API_TOKEN')
));
Best Practices
- Never display errors in production: Always set display_errors to 0
- Log everything: Set error_reporting to E_ALL
- Include context: Add request info, user IDs, etc.
- Use appropriate levels: Don't log everything as "error"
- Buffer and batch: Reduce HTTP overhead
- Handle fatal errors: Register a shutdown function
Conclusion
PHP error handling requires more setup than some languages, but once configured properly, you'll catch every error that occurs in your application. Combine with a central logging service for searchable, actionable error data.
Admin
Published on August 6, 2025