|

PHP-FPM Logging with Pipes and Monolog Formatting

The Problem with PHP-FPM Logging

When working with PHP-FPM, you might encounter an unexpected behavior: all logs end up in stderr, even when you explicitly write to stdout. This happens because PHP-FPM closes stdout during initialization, leaving only stderr available for output.

This creates issues when you want to separate different log levels or types of output. Application logs, error logs, and access logs all get mixed together in stderr, making it difficult to filter and process them effectively.

Understanding the Root Cause

PHP-FPM’s architecture closes stdout early in its lifecycle. When you configure error_log or use standard output streams, everything gets redirected to stderr by default. This is by design for FastCGI process management, but it’s not ideal for modern logging practices where you want structured, separated log streams.

The Solution: Pipes and Level Configuration

The proper approach is to use named pipes (FIFOs) combined with log level configuration to route logs correctly:

# Create named pipe
mkfifo /tmp/php-log.pipe
chmod 666 /tmp/php-log.pipe

Configure PHP-FPM pool to use these pipes:

[www]
catch_workers_output = yes
decorate_workers_output = no

; Route logs to pipe
php_admin_value[error_log] = /tmp/php-log.pipe
php_admin_flag[log_errors] = on

; Configure log levels
php_admin_value[error_reporting] = E_ALL

Monolog Integration with Format Detection

Monolog can automatically detect and format logs appropriately. For a simpler setup, use php://stdout and let bash handle the pipe redirection:

use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Formatter\JsonFormatter;
use Monolog\Formatter\LineFormatter;

$logger = new Logger('app');

// Detect if running under FPM
$isFpm = php_sapi_name() === 'fpm-fcgi';

if ($isFpm) {
    // Single handler writing to stdout
    // Bash pipe will handle redirection based on log level
    $handler = new StreamHandler('php://stdout', Logger::DEBUG);
    
    // JSON formatter for structured logging
    $jsonFormatter = new JsonFormatter(
        JsonFormatter::BATCH_MODE_NEWLINES,
        true  // Include stack traces
    );
    
    $handler->setFormatter($jsonFormatter);
    $logger->pushHandler($handler);
} else {
    // Standard CLI logging with line format
    $handler = new StreamHandler('php://stdout', Logger::DEBUG);
    $handler->setFormatter(new LineFormatter());
    $logger->pushHandler($handler);
}

JSON Output Example

With JsonFormatter, your logs will be structured and easily parseable:

{
  "message": "User login successful",
  "context": {
    "user_id": 12345,
    "ip": "192.168.1.1"
  },
  "level": 200,
  "level_name": "INFO",
  "channel": "app",
  "datetime": "2025-11-15T10:30:45.123456+00:00",
  "extra": {}
}

Processing Pipes with Bash Redirection

A robust shell script can handle multiple log formats and route them appropriately:

#!/bin/sh
# log-router.sh

LOG_PIPE="/tmp/php-log.pipe"

# Determines if a log line should go to stderr (error level)
# Returns 0 (true) for WARNING and above, 1 (false) otherwise
is_error_level() {
    line="$1"
    
    # Matches error-level logs that should go to stderr:
    # - Monolog JSON: "level":300+ (WARNING=300, ERROR=400, CRITICAL=500, ALERT=550, EMERGENCY=600)
    # - Monolog JSON: "level_name":"WARNING|ERROR|CRITICAL|ALERT|EMERGENCY"
    # - Plain text formats: [WARNING], .WARNING:, WARNING -
    # - PHP native errors: Fatal, Parse, Warning
    # - Stack traces and exceptions
    case "$line" in
        *'"level":'[3-9][0-9][0-9]* | *'"level":'[1-9][0-9][0-9][0-9]* | \
        *'"level_name":"WARNING"'* | *'"level_name":"ERROR"'* | \
        *'"level_name":"CRITICAL"'* | *'"level_name":"ALERT"'* | \
        *'"level_name":"EMERGENCY"'* | \
        *'[WARNING]'* | *'[ERROR]'* | *'[CRITICAL]'* | *'[ALERT]'* | *'[EMERGENCY]'* | \
        *'.WARNING:'* | *'.ERROR:'* | *'.CRITICAL:'* | *'.ALERT:'* | *'.EMERGENCY:'* | \
        *'WARNING -'* | *'ERROR -'* | *'CRITICAL -'* | *'ALERT -'* | *'EMERGENCY -'* | \
        *'PHP Fatal'* | *'PHP Parse'* | *'PHP Warning'* | *'Fatal error'* | \
        *'Parse error'* | *'Catchable fatal'* | \
        '#'[0-9]* | *'Stack trace:'* | *'Uncaught '* | *'Exception:'*)
            return 0
            ;;
    esac
    return 1
}

setup_log_pipe() {
    # Remove existing pipe if present
    rm -f "$LOG_PIPE"
    
    # Create named pipe with writable permissions
    mkfifo "$LOG_PIPE"
    chmod 666 "$LOG_PIPE"
}

start_log_router() {
    # Background process that reads from pipe and routes to stdout/stderr
    (
        while true; do
            if [ -p "$LOG_PIPE" ]; then
                # Read line by line from the pipe
                while IFS= read -r line; do
                    if [ -n "$line" ]; then
                        if is_error_level "$line"; then
                            printf '%s\n' "$line" >&2
                        else
                            printf '%s\n' "$line"
                        fi
                    fi
                done < "$LOG_PIPE"
            fi
            # Small delay before reopening pipe (handles pipe closure)
            sleep 0.1
        done
    ) &
    
    # Give router time to start
    sleep 0.2
}

# Initialize pipe and router
setup_log_pipe
start_log_router

Docker Implementation

In containerized environments, integrate the log router into your entrypoint:

FROM php:8.2-fpm

COPY log-router.sh /usr/local/bin/
COPY docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/*.sh

ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["php-fpm"]
#!/bin/bash
# docker-entrypoint.sh

DOCKER_CMD="$1"

case "$DOCKER_CMD" in
    php-fpm | */php-fpm | *php-fpm*)
        # Start log router for PHP-FPM
        /usr/local/bin/log-router.sh
        ;;
esac

# Start the main process
exec "$@"

This approach handles:

  • Monolog JSON format with numeric levels
  • Monolog JSON format with level names
  • Plain text log formats
  • PHP native errors and warnings
  • Stack traces and exceptions

Benefits

This approach provides:

  • Proper separation of log levels and types
  • Structured logging that works with log aggregation tools
  • Automatic format detection by Monolog
  • Clean integration with container logging systems
  • No mixed stderr output

Keep pushing forward and savor every step of your coding journey.