PHP Logging and monitoring basics
Hey, fellow developers. Picture this: it's 2 AM, your phone buzzes with an alert, and you're staring at a production outage that nobody saw coming. The server logs? A wall of unreadable text, timestamps mashed together, errors buried under noise. We've all been there—that sinking feeling when a bug slips through and takes down the site right when traffic spikes.
I've lost count of the nights I spent grepping through bloated log files, cursing plain-text dumps that hid the real problem. But here's the thing: PHP logging doesn't have to be a nightmare. Done right, it becomes your silent guardian, whispering exactly what went wrong before it escalates. And monitoring? That's the part that turns logs from passive records into active defenders.
In this piece, we'll cut through the basics—no fluff, just the stuff that saves your sanity. We'll talk built-in tools, structured logging with Monolog, security traps to dodge, and monitoring setups that actually alert you before coffee goes cold. Whether you're wiring up a fresh Laravel app or debugging a legacy beast, these are the foundations that stick.
Why logging feels personal
Logging isn't just code; it's memory. Every entry captures a moment— a user's failed login at midnight, a slow query tanking checkout, or that one rogue API call flooding your database. Get it wrong, and you're blind. Get it right, and you're steps ahead of chaos.
Think back to your last fire drill. Did you trace the issue in minutes or hours? Structured logging changes that game. Instead of "User error at login," you get JSON with user_id, IP (anonymized), request_id, and stack trace. Tools like Elasticsearch chew through it effortlessly.
And monitoring? It's the emotional payoff. Alerts hit Slack or your phone for CRITICAL only—no alert fatigue from every notice. Real-time detection means you fix security blips or perf hiccups before users notice.
Have you ever wondered why some teams sleep soundly while others chase ghosts? It boils down to logging discipline.
Starting simple: PHP's built-in logging
PHP ships with solid basics. No composer installs needed. Kick off with error_log()—your Swiss Army knife.
error_log('Database connection failed: ' . $errorMessage);
By default, it dumps to the web server's error log (Apache's error_log, Nginx via PHP-FPM). Tweak it in php.ini:
log_errors = On
error_log = /var/log/php/app_errors.log
error_reporting = E_ALL & ~E_NOTICE
That ~E_NOTICE bit? Gold. It skips trivia, logs warnings and errors only. Clean, focused files.
But plain text? It's 2026—time to level up. Rotate logs with logrotate to avoid disk hogs:
sudo cat > /etc/logrotate.d/php-app << EOF
/var/log/php/*.log {
daily
rotate 14
compress
missingok
}
EOF
Permissions matter too. chown www-data:www-data /var/log/php and chmod 750. No world-readable secrets.
Question for you: How often do you check your php.ini? Set display_errors = Off in prod—never expose stacks to users.
Enter Monolog: The structured logging powerhouse
If built-ins are a bicycle, Monolog is your Ferrari. PSR-3 compliant, it's the de facto standard. Laravel and Symfony bake it in.
Install: composer require monolog/monolog
Basic setup:
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
$log = new Logger('app');
$log->pushHandler(new StreamHandler('/var/log/php/app.log', Logger::WARNING));
$log->warning('Slow query detected', ['query' => $sql, 'duration' => 5.2]);
$log->error('Payment gateway down', ['user_id' => 123, 'request_id' => 'abc-xyz']);
See that context array? JSON-structured logging. Output:
[2026-02-20 08:00:00] app.WARNING: Slow query detected {"query":"SELECT * FROM users","duration":5.2}
Parseable. Searchable. Beautiful.
Log levels? DEBUG, INFO, NOTICE, WARNING, ERROR, CRITICAL, ALERT, EMERGENCY. Filter ruthlessly—WARNING+ in prod.
For Heroku or containers, pipe to stderr:
$log->pushHandler(new StreamHandler('php://stderr', Logger::Warning));
Remote? Handlers for Slack, Sentry, Graylog, Elasticsearch. Scalable heaven.
Pro tip: Unique request IDs. Generate once per request:
$requestId = bin2hex(random_bytes(16));
$log->withName($requestId)->info('User action', ['user_id' => $id]);
Trace flows end-to-end. Game-changer.
The security minefield: Logging without leaks
Logs are double-edged. They reveal truths but can spill secrets. Ever logged a password by accident? I have. Heart-stopping.
Never log PII raw. Passwords, tokens, SSNs, cards—mask 'em.
Build a SecureLogger class. Here's one I've battle-tested:
class SecureLogger {
protected $sensitiveKeys = ['password', 'token', 'secret', 'credit_card', 'cvv'];
public function log($message, $context = []) {
$safeContext = $this->sanitize($context);
error_log(json_encode([
'timestamp' => date('Y-m-d H:i:s'),
'level' => 'INFO',
'message' => $message,
'context' => $safeContext
]));
}
protected function sanitize($data, $parentKey = '') {
// Recursively mask sensitive keys—logic as in search results
// Returns masked values like '411***111'
}
}
Whitelist approved fields:
$logger->info('User login', [
'user_id' => $user['id'],
'name' => $user['name'] // Safe
// No 'password' here
]);
Store securely: Encrypt high-risk logs. Use AES-256:
// Simplified encrypt/decrypt wrapper
$encrypted = openssl_encrypt($json, 'AES-256-CBC', $key, 0, $iv);
For finance/healthcare, add compliance: ISO timestamps, anonymized IPs (zero last octet), process IDs.
'ip_address' => preg_replace('/\.\d+$/', '.0', $_SERVER['REMOTE_ADDR']),
Scan code with static tools. Dynamic filters block slips. Rotate, compress, restrict access.
Miss this, and you're one breach away from headlines. Nail it, and auditors smile.
Monitoring: From logs to alerts
Logging alone is passive. Monitoring wakes you up.
Benefits hit hard:
- Real-time detection: Spot errors as they drop.
- Security watch: Failed logins, odd patterns.
- Perf insights: Slow queries, crashes.
- Compliance: Audit trails.
Tools: ELK (Elasticsearch, Logstash, Kibana), Graylog, Splunk. Free? Better Stack or self-hosted.
Ingest structured JSON—filters fly. Dashboards show error rates, top warnings.
Alerts? Thresholds rule. "CRITICAL > 5/min? PagerDuty." Test weekly.
Organize files: /var/log/php/{app}/errors/{date}.log. Rotate religiously.
Best practices:
- Consistent fields: timestamp, level, message, request_id.
- No alert fatigue: Tune for signal.
- Prioritize security: High alert on auth fails.
Integrate with frameworks. Laravel? Log::channel('slack')->error(...).
Custom handlers and real-world tweaks
Extend Monolog for flair. Database handler? Slack for CRIT?
use Monolog\Handler\AbstractHandler;
class SlackHandler extends AbstractHandler {
public function write(array $record): void {
// POST to Slack webhook
}
}
Prod tweak: Conditional logging.
if (APP_ENV === 'production') {
$log->pushHandler(new StreamHandler('php://stderr', Logger::ERROR));
} else {
$log->pushHandler(new StreamHandler('dev.log', Logger::DEBUG));
}
Performance? Async handlers (e.g., via queue) for high-traffic.
Legacy code? Wrap error_log in your SecureLogger. Global exception handler:
set_exception_handler(function ($e) use ($logger) {
$logger->error('Uncaught exception', ['trace' => $e->getTraceAsString()]);
});
Lessons from the trenches
I remember a Black Friday deploy. Unmonitored logs filled disks silently. Site down, 10k users furious. Now? Alerts fire at 80% disk, logs rotate daily.
Structured formats saved a client's PCI audit—JSON proved no card data leaked.
Friends, start small: Monolog + rotation + one alert channel. Scale from there.
What if your next outage becomes a five-minute fix? That's the quiet power of solid logging and monitoring.
Implement today. Sleep better tonight. Your future self—and your users—will thank you.