๐Ÿ“ Volume II: Laravel Performance Tuning Kit

๐Ÿ“ Topic 25: Logging I/O Trap

Every Log::info() opens a file. 10,000 logs = 10,000 I/O operations.

"You add logging to debug your code.
You log every step of the process.
Your code runs fine in development.
In production, 10,000 logs per minute kill your disk I/O.
Your server slows to a crawl. And you have no idea why.
The culprit? Logging."
โš ๏ธ THE HIDDEN I/O KILLER

Every call to Log::info(), Log::error(), or any logging method triggers a file write operation. For the 'daily' and 'single' log drivers, this means opening, writing, and closing a file. In a loop with 10,000 iterations, that's 10,000 file I/O operations. Your disk bandwidth is exhausted. Your application slows down. Your users suffer.

๐Ÿ”ด The Problem: Every Log Entry = One I/O Operation

WHAT HAPPENS WHEN YOU CALL Log::info() โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• Log::info("Processing user {$user->id}"); โ”‚ โ–ผ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ 1. fopen(storage/logs/laravel.log, 'a') โ† Open file โ”‚ โ”‚ 2. flock() (file lock) โ† Wait for lock โ”‚ โ”‚ 3. fwrite( formatted log line ) โ† Write to disk โ”‚ โ”‚ 4. fflush() โ† Flush buffer โ”‚ โ”‚ 5. flock() unlock โ† Release lock โ”‚ โ”‚ 6. fclose() โ† Close file โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ Each operation: ~0.1-1ms of I/O time FOR 10,000 LOGS (BATCH PROCESSING): โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• Without optimization: โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ for ($i = 0; $i < 10000; $i++) { โ”‚ โ”‚ Log::info("Processing record {$i}"); // 10,000 I/O ops โ”‚ โ”‚ } โ”‚ โ”‚ โ”‚ โ”‚ Time: 1-10 seconds (100% I/O bound) โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ With batching: โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ $logs = []; โ”‚ โ”‚ for ($i = 0; $i < 10000; $i++) { โ”‚ โ”‚ $logs[] = "Processing record {$i}"; โ”‚ โ”‚ } โ”‚ โ”‚ Log::info(implode("\n", $logs)); // 1 I/O operation โ”‚ โ”‚ โ”‚ โ”‚ Time: 0.01 seconds (1 I/O operation) โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ Result: 1,000x faster!
THE LOGGING SINS

๐Ÿ“Š The Real Cost of Logging I/O

Number of Logs Without Batching With Batching Improvement
100 logs ~10-20ms ~0.5ms 20-40x faster
1,000 logs ~100-200ms ~1ms 100-200x faster
10,000 logs ~1-2 seconds ~5-10ms 100-200x faster
100,000 logs ~10-20 seconds ~50-100ms 100-200x faster
REAL-WORLD EXAMPLE

A batch job processing 50,000 records:

โœ… The Solution: Batch Your Logs

THE GOLDEN RULE OF LOGGING

Never call Log::*() inside a loop. Collect logs in an array and write them once.

๐Ÿ‘Ž BAD (10,000 I/O operations)

// Process 10,000 users
foreach ($users as $user) {
    try {
        $user->process();
        Log::info("User {$user->id} processed successfully");
    } catch (\Exception $e) {
        Log::error("User {$user->id} failed: " . $e->getMessage());
    }
}
// 10,000 logs = 10,000 I/O operations
// Time: 1-2 seconds just for logging

๐Ÿ‘ GOOD (1 I/O operation)

$infoLogs = [];
$errorLogs = [];

foreach ($users as $user) {
    try {
        $user->process();
        $infoLogs[] = "User {$user->id} processed successfully";
    } catch (\Exception $e) {
        $errorLogs[] = "User {$user->id} failed: " . $e->getMessage();
    }
}

if ($infoLogs) {
    Log::info(implode("\n", $infoLogs));  // One write
}
if ($errorLogs) {
    Log::error(implode("\n", $errorLogs));  // One write
}
// 2 I/O operations total
// Time: 0.002 seconds

โš™๏ธ Log Drivers and Their I/O Characteristics

Driver I/O per Log Notes
single (default) Open, write, close (3-5 I/O calls) Worst for high-volume logging
daily Open, write, close + date check Better than single, still bad for loops
stack Multiple drivers = multiple I/O Even worse for performance
syslog Pass through to syslog daemon Better, but still overhead
errorlog PHP error log (usually less I/O) Good for simple logging
redis (custom) Network I/O to Redis Fast, but requires Redis
BEST PRACTICE

For high-volume applications, use redis or syslog drivers, or batch your logs. Never use single or daily for high-frequency logging.

๐Ÿ“ค Logging in Queues (The Hidden Trap)

QUEUE WORKER LOGGING

Queue workers can process thousands of jobs per minute. Each job that logs = I/O operations. If 100 jobs log 100 times each = 10,000 log writes.

SOLUTION: Batch logs in queue jobs
class ProcessUserJob implements ShouldQueue
{
    private static array $batchLogs = [];
    
    public function handle(): void
    {
        try {
            $this->user->process();
            self::$batchLogs[] = "User {$this->user->id} processed";
        } catch (\Exception $e) {
            self::$batchLogs[] = "User {$this->user->id} failed: {$e->getMessage()}";
        }
    }
    
    public function __destruct()
    {
        // Write all logs when the job finishes (or batch across jobs)
        if (!empty(self::$batchLogs)) {
            Log::info(implode("\n", self::$batchLogs));
            self::$batchLogs = [];
        }
    }
}

๐Ÿ”ง Contextual Logging (Laravel 8+)

LOG WITH CONTEXT (No Performance Gain, but Better Organization)
// Laravel 8+ allows adding context to logs
Log::withContext(['user_id' => $userId, 'batch' => $batchId]);

// Now every log in this request/queue job includes this context
Log::info("Processing started");  // Includes user_id and batch
Log::info("Processing completed"); // Includes user_id and batch

// Still batching needed! This doesn't reduce I/O, just adds metadata

๐ŸŽฏ Log Levels: What to Log in Production

่กจ Level Use in Production? When to Use emergency โœ… Yes System is unusable alert โœ… Yes Action must be taken immediately critical โœ… Yes Critical conditions error โœ… Yes Runtime errors warning โœ… Yes Exceptional occurrences notice โš ๏ธ Limited Normal but significant events info โŒ Avoid in high volume Interesting events (batch or remove) debug โŒ NO Development only
PRODUCTION LOG LEVEL
# .env - Production
LOG_LEVEL=error  # Only log errors and above

# .env - Development
LOG_LEVEL=debug  # Log everything

# In config/logging.php
'level' => env('LOG_LEVEL', 'error'),

๐Ÿš€ Alternatives to File Logging for High Volume

๐ŸŸข Redis/Memory Logging

// Custom Redis logger
class RedisLogger
{
    public function log($level, $message)
    {
        Redis::lpush('logs', json_encode([
            'level' => $level,
            'message' => $message,
            'time' => microtime(true),
        ]));
    }
}

// Consumer process writes to file in batches
// Write once per second instead of per log

๐Ÿ”ต Database Logging (with buffer)

// Batch insert logs to database
class DbLogger
{
    private static array $buffer = [];
    
    public function log($level, $message)
    {
        self::$buffer[] = compact('level', 'message');
        
        if (count(self::$buffer) >= 100) {
            DB::table('logs')->insert(self::$buffer);
            self::$buffer = [];
        }
    }
}

๐Ÿ“ Topic 25 Summary: Logging I/O Trap

Pattern I/O Operations Time for 10k Logs Recommendation
Log inside loop 10,000 1-2 seconds โŒ NEVER
Batch logs in array 1-2 0.001-0.002 seconds โœ… For loops
Log::info() in production Per request Variable โš ๏ธ Limit usage
Redis logging + consumer Minimal Very fast โœ… For high volume
๐Ÿ“Œ THE RULE: Never call Log::*() inside a loop. Collect logs in arrays and write them once. Set LOG_LEVEL=error in production to avoid debug/info logs. For high-volume logging, use Redis or database with batching.
NEXT TOPIC PREVIEW

Topic 26: Blade View Cache โ€” Every Blade template is compiled to PHP. Without caching, this happens on every request. How view:cache saves 10-30ms per request.