|

PHP-FPM Logging mit Pipes und Monolog-Formatierung

Das Problem mit PHP-FPM Logging

Bei der Arbeit mit PHP-FPM kann ein unerwartetes Verhalten auftreten: Alle Logs landen in stderr, auch wenn Sie explizit nach stdout schreiben. Dies geschieht, weil PHP-FPM stdout während der Initialisierung schließt und nur stderr für die Ausgabe verfügbar bleibt.

Dies führt zu Problemen, wenn Sie verschiedene Log-Level oder Ausgabetypen trennen möchten. Anwendungslogs, Fehlerlogs und Zugriffslogs werden alle in stderr gemischt, was das Filtern und Verarbeiten erschwert.

Die Ursache verstehen

Die Architektur von PHP-FPM schließt stdout früh in seinem Lebenszyklus. Wenn Sie error_log konfigurieren oder Standard-Ausgabeströme verwenden, wird standardmäßig alles nach stderr umgeleitet. Dies ist für das FastCGI-Prozessmanagement so konzipiert, aber nicht ideal für moderne Logging-Praktiken, bei denen strukturierte, getrennte Log-Streams gewünscht sind.

Die Lösung: Pipes und Level-Konfiguration

Der richtige Ansatz besteht darin, Named Pipes (FIFOs) in Kombination mit Log-Level-Konfiguration zu verwenden, um Logs korrekt zu routen:

# Named Pipe erstellen
mkfifo /tmp/php-log.pipe
chmod 666 /tmp/php-log.pipe

PHP-FPM Pool konfigurieren, um diese Pipes zu verwenden:

[www]
catch_workers_output = yes
decorate_workers_output = no

; Logs zur Pipe routen
php_admin_value[error_log] = /tmp/php-log.pipe
php_admin_flag[log_errors] = on

; Log-Level konfigurieren
php_admin_value[error_reporting] = E_ALL

Monolog-Integration mit Format-Erkennung

Monolog kann Logs automatisch erkennen und entsprechend formatieren. Für ein einfacheres Setup verwenden Sie php://stdout und lassen Sie bash die Pipe-Umleitung handhaben:

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

$logger = new Logger('app');

// Erkennen, ob unter FPM ausgeführt wird
$isFpm = php_sapi_name() === 'fpm-fcgi';

if ($isFpm) {
    // Einzelner Handler, der nach stdout schreibt
    // Bash-Pipe übernimmt die Umleitung basierend auf Log-Level
    $handler = new StreamHandler('php://stdout', Logger::DEBUG);
    
    // JSON-Formatter für strukturiertes Logging
    $jsonFormatter = new JsonFormatter(
        JsonFormatter::BATCH_MODE_NEWLINES,
        true  // Stack-Traces einschließen
    );
    
    $handler->setFormatter($jsonFormatter);
    $logger->pushHandler($handler);
} else {
    // Standard-CLI-Logging mit Line-Format
    $handler = new StreamHandler('php://stdout', Logger::DEBUG);
    $handler->setFormatter(new LineFormatter());
    $logger->pushHandler($handler);
}

JSON-Ausgabe-Beispiel

Mit JsonFormatter sind Ihre Logs strukturiert und leicht zu parsen:

{
  "message": "Benutzer-Login erfolgreich",
  "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": {}
}

Pipes mit Bash-Umleitung verarbeiten

Ein robustes Shell-Skript kann mehrere Log-Formate verarbeiten und entsprechend routen:

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

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

# Bestimmt, ob eine Log-Zeile nach stderr gehen soll (Fehler-Level)
# Gibt 0 (true) für WARNING und höher zurück, 1 (false) sonst
is_error_level() {
    line="$1"
    
    # Erkennt Fehler-Level-Logs, die nach stderr gehen sollen:
    # - 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-Formate: [WARNING], .WARNING:, WARNING -
    # - Native PHP-Fehler: Fatal, Parse, Warning
    # - Stack-Traces und 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() {
    # Vorhandene Pipe entfernen, falls vorhanden
    rm -f "$LOG_PIPE"
    
    # Named Pipe mit schreibbaren Berechtigungen erstellen
    mkfifo "$LOG_PIPE"
    chmod 666 "$LOG_PIPE"
}

start_log_router() {
    # Hintergrundprozess, der von der Pipe liest und zu stdout/stderr routet
    (
        while true; do
            if [ -p "$LOG_PIPE" ]; then
                # Zeilenweise von der Pipe lesen
                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
            # Kleine Verzögerung vor erneutem Öffnen der Pipe (behandelt Pipe-Schließung)
            sleep 0.1
        done
    ) &
    
    # Router Zeit zum Starten geben
    sleep 0.2
}

# Pipe und Router initialisieren
setup_log_pipe
start_log_router

Docker-Implementierung

In containerisierten Umgebungen integrieren Sie den Log-Router in Ihren 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*)
        # Log-Router für PHP-FPM starten
        /usr/local/bin/log-router.sh
        ;;
esac

# Hauptprozess starten
exec "$@"

Dieser Ansatz behandelt:

  • Monolog JSON-Format mit numerischen Levels
  • Monolog JSON-Format mit Level-Namen
  • Plain-Text-Log-Formate
  • Native PHP-Fehler und Warnungen
  • Stack-Traces und Exceptions

Vorteile

Dieser Ansatz bietet:

  • Ordnungsgemäße Trennung von Log-Levels und -Typen
  • Strukturiertes Logging, das mit Log-Aggregationstools funktioniert
  • Automatische Format-Erkennung durch Monolog
  • Saubere Integration mit Container-Logging-Systemen
  • Keine gemischte stderr-Ausgabe

Schreiten Sie weiter voran und genießen Sie jeden Schritt Ihrer Programmierreise.