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.
