Why Application Logging Matters for PHP Projects
Every PHP application generates events worth recording. When something breaks in production, good logs are often the difference between finding the root cause in minutes and spending hours guessing. Logging is not just about capturing errors. It covers security events, business transactions, performance warnings, and system state changes that help you understand what your application is actually doing.
A well-structured logging approach supports debugging, security auditing, compliance requirements, and long-term system maintenance. Without it, troubleshooting becomes reactive and frustrating. With it, you can trace issues across requests, identify patterns, and make informed decisions about fixes and improvements.
What to Log in a PHP Application
Not every event needs to be logged. Logging too much creates noise that makes finding useful information harder. Logging too little leaves gaps when you need to investigate a problem. The key is identifying which events provide genuine value for debugging, monitoring, and security purposes.
Errors and Exceptions
Unhandled exceptions and PHP errors should always be logged. This includes fatal errors, warnings, notices, and caught exceptions. Each log entry should capture the error type, message, the file and line number where it occurred, and a stack trace when available.
Distinguish between different severity levels. A missing optional configuration file might warrant a warning, while a failed database connection is an error that needs immediate attention.
Security-Relevant Events
Authentication events belong in your logs. Record successful logins, failed login attempts, password reset requests, and account lockouts. These entries help you detect brute force attacks and unusual access patterns.
Authorisation failures matter too. When a user tries to access a resource they do not have permission for, log the user ID, the resource they tried to access, the timestamp, and the IP address.
Any unexpected input that fails validation, especially attempts to submit forms with manipulated data, should be logged. This helps identify injection attempts and other abuse patterns. A related topic worth reviewing is the OWASP Top 10 for business web applications, which covers common security vulnerabilities that logging can help detect.
Business Transactions and Key Operations
For booking systems and similar applications, log significant business events. This includes bookings created, bookings cancelled, appointments rescheduled, payments processed, and refunds issued. These logs support audit trails and help you understand usage patterns.
When building booking systems, decisions about what events to track affect both debugging and analytics. The architecture of multi-tenant booking systems often requires careful consideration of how logs are isolated and aggregated across tenants.
Performance Warnings
Slow database queries, high memory usage, and long response times are worth recording. These entries help identify performance degradation before it becomes a user-facing problem. Set thresholds that make sense for your application and log a warning when those thresholds are exceeded.
Where to Store PHP Application Logs
Logs must be stored outside the web root. If log files are accessible via a browser URL, anyone who discovers the path can read sensitive information about your application, its users, and its internal structure. This is a security risk that is easy to avoid with correct storage configuration.
Filesystem Storage
For most PHP applications, writing logs to files on the server filesystem works well. Choose a directory that the web server user can write to but that is not served publicly. A common pattern is storing logs in /var/log/yourapp/ or a dedicated logs directory within a non-public parent folder.
// Example: configuring a log file path outside web root
$logPath = '/var/log/yourapp/error.log';
if (!is_writable(dirname($logPath))) {
// Fall back to a temporary location or alert administrators
$logPath = sys_get_temp_dir() . '/yourapp_error.log';
}
When using shared hosting where you do not have control over directory structures, consider storing logs above the public_html or www directory, or in a directory protected by htaccess rules that deny web access.
Database Logging for High-Volume Applications
For high-volume applications, filesystem logging can create I/O bottlenecks. Writing logs to a database table allows for easier querying and aggregation. Use a separate database connection for logging to avoid interfering with your main application queries, and consider asynchronous writing using queues to prevent logging from slowing down request processing.
Centralised Logging for Distributed Systems
If your application runs across multiple servers, centralised logging tools help aggregate logs from all instances. Tools like the ELK stack (Elasticsearch, Logstash, Kibana) or cloud-based solutions allow you to search and analyse logs across your entire infrastructure from a single interface. This is particularly useful when debugging issues that span multiple services.
Implementing Logging in PHP
PHP offers several ways to implement logging. You can use built-in functions, third-party libraries, or build a custom solution that fits your needs.
Using PSR-3 Compatible Libraries
PSR-3 is a PHP standard recommendation that defines a common interface for logging libraries. Using a PSR-3 compatible library means you can swap the underlying logging implementation without changing your application code.
Popular PSR-3 libraries include Monolog, which is widely used and supports many handlers including files, databases, email, and external services. Setting up Monolog for a basic PHP application is straightforward.
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
// Create the logger
$logger = new Logger('app');
// Configure a file handler for errors
$logger->pushHandler(new StreamHandler(
'/var/log/yourapp/error.log',
Logger::ERROR
));
// Configure a file handler for all debug messages
$logger->pushHandler(new StreamHandler(
'/var/log/yourapp/debug.log',
Logger::DEBUG
));
// Log messages at different levels
$logger->debug('Debug information', ['user_id' => 42]);
$logger->info('User performed action', ['action' => 'booking_create']);
$logger->warning('Slow query detected', ['query_time' => 2.5]);
$logger->error('Database connection failed', ['exception' => $e->getMessage()]);
Creating a Simple Custom Logger
For smaller projects, a simple custom logger may be sufficient. The key is consistency in format and structure so that log entries are easy to parse and search.
class SimpleLogger {
private string $logFile;
public function __construct(string $logFile) {
$this->logFile = $logFile;
$dir = dirname($logFile);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
}
public function log(string $level, string $message, array $context = []): void {
$timestamp = date('Y-m-d H:i:s');
$contextJson = !empty($context) ? ' ' . json_encode($context) : '';
$entry = "[{$timestamp}] {$level}: {$message}{$contextJson}" . PHP_EOL;
file_put_contents($this->logFile, $entry, FILE_APPEND);
}
public function error(string $message, array $context = []): void {
$this->log('ERROR', $message, $context);
}
public function info(string $message, array $context = []): void {
$this->log('INFO', $message, $context);
}
}
Capturing PHP Errors and Exceptions
To log PHP errors and unhandled exceptions, set custom error and exception handlers at the start of your application.
set_error_handler(function (int $errno, string $errstr, string $errfile, int $errline): bool {
$logger = new SimpleLogger('/var/log/yourapp/error.log');
$logger->error("PHP Error [$errno]: $errstr in $errfile:$errline");
return false; // Continue to standard error handler
});
set_exception_handler(function (Throwable $e): void {
$logger = new SimpleLogger('/var/log/yourapp/error.log');
$logger->error('Uncaught Exception: ' . $e->getMessage(), [
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => $e->getTraceAsString()
]);
// Show user-friendly error page
include '500.html';
});
Note: Always test error handlers in a development environment before deploying them to production. Incorrect handler implementations can cause infinite loops or suppress errors that should be visible during development.
Log Rotation and File Size Management
Without rotation, log files grow indefinitely. Eventually they consume all available disk space, which can crash your application or prevent new logs from being written. Log rotation addresses this by archiving old logs and starting fresh files on a schedule.
Using logrotate on Linux Servers
On Linux servers, the logrotate utility handles rotation automatically. Create a configuration file for your application logs.
/var/log/yourapp/*.log {
daily
missingok
rotate 14
compress
delaycompress
notifempty
create 0640 www-data www-data
sharedscripts
postrotate
/bin/kill -SIGUSR1 $(cat /var/run/php-fpm.pid 2>/dev/null) 2>/dev/null || true
endscript
}
This configuration rotates logs daily, keeps 14 days of history, compresses old logs, and safely signals PHP-FPM to reopen log files after rotation.
Built-in Rotation for Custom Solutions
If you are building a custom logging solution, implement size-based rotation. Check the file size before writing and rotate when it exceeds your threshold.
class RotatingFileLogger {
private string $basePath;
private int $maxSizeBytes;
private int $maxFiles;
public function __construct(string $basePath, int $maxSizeMB = 10, int $maxFiles = 5) {
$this->basePath = $basePath;
$this->maxSizeBytes = $maxSizeMB * 1024 * 1024;
$this->maxFiles = $maxFiles;
}
private function rotateIfNeeded(): void {
$currentFile = $this->basePath;
if (!file_exists($currentFile) || filesize($currentFile) < $this->maxSizeBytes) {
return;
}
// Rename existing files
for ($i = $this->maxFiles - 1; $i >= 1; $i--) {
$oldName = "{$this->basePath}.{$i}";
$newName = "{$this->basePath}." . ($i + 1);
if (file_exists($oldName)) {
rename($oldName, $newName);
}
}
rename($currentFile, "{$this->basePath}.1");
// Delete the oldest file if it exists
$oldest = "{$this->basePath}." . ($this->maxFiles + 1);
if (file_exists($oldest)) {
unlink($oldest);
}
}
public function write(string $level, string $message): void {
$this->rotateIfNeeded();
$timestamp = date('Y-m-d H:i:s');
$entry = "[{$timestamp}] {$level}: {$message}" . PHP_EOL;
file_put_contents($this->basePath, $entry, FILE_APPEND);
}
}
Log Retention: How Long to Keep Records
How long you retain logs depends on several factors including regulatory requirements, storage costs, and your debugging needs. There is no single correct answer, but having a clear policy prevents either over-retaining (wasting storage) or under-retaining (losing useful evidence).
General Guidelines by Log Type
- Security logs: Keep authentication failures, authorisation denials, and suspicious activity for at least 90 days. If your application handles sensitive data or operates in regulated industries, you may need to retain these for a year or longer.
- Error logs: Keep detailed error logs for 30 to 90 days. After that, archive compressed versions if you need historical debugging capability.
- Business transaction logs: Treat these as part of your business records. Keep them long enough to support audit requirements and dispute resolution, typically a minimum of one year.
- Debug and verbose logs: These are valuable for recent troubleshooting but can be deleted after 7 to 14 days to save space.
Storage and Cost Considerations
Retaining logs indefinitely is not practical. Rotate them to compressed archives, move older archives to cheaper storage, and delete logs that are no longer needed. Cloud storage solutions offer lifecycle policies that automate this process based on age or size thresholds.
Security Considerations for PHP Application Logs
Logs are valuable for debugging, but they also contain sensitive information that needs protection. A compromised log file can reveal details about your application structure, your users, and potential vulnerabilities.
Protecting Log Data
Restrict file permissions so that only the web server user and administrators can read log files. Store logs on a separate partition when possible, so that a log-filling attack cannot crash the entire server.
Be careful about what you include in logs. Never log full credit card numbers, complete passwords, or API secrets. If you need to log request parameters for debugging, sanitise sensitive fields first.
function sanitizeForLogging(array $data): array {
$sensitiveFields = ['password', 'credit_card', 'cvv', 'api_key', 'token'];
foreach ($data as $key => $value) {
if (in_array(strtolower($key), $sensitiveFields, true)) {
$data[$key] = '[REDACTED]';
} elseif (is_array($value)) {
$data[$key] = sanitizeForLogging($value);
}
}
return $data;
}
Log Injection Attacks
User input that is written directly to log files without sanitisation can enable log injection attacks. An attacker might inject fake log entries that, when viewed in a monitoring tool, could mislead administrators or exploit vulnerabilities in log viewing software. Always sanitise and escape input before writing it to log files.
Using Logs for Application Monitoring
Beyond debugging, logs feed into broader monitoring strategies. Set up alerts that trigger when error rates exceed normal thresholds, when specific security events occur, or when performance metrics degrade.
For booking systems and similar applications, monitoring booking success rates, payment failures, and availability check times provides early warning of problems. Log entries formatted consistently make it straightforward to aggregate metrics and build dashboards that show application health at a glance.