php-fpm: capture & logs exceptions
All checks were successful
php-fpm-build / build (7.4) (push) Successful in 4m53s

This commit is contained in:
Elijah Duffy
2025-12-08 20:08:07 -08:00
parent f3c65de9da
commit 072c284ba5
3 changed files with 76 additions and 18 deletions

View File

@@ -42,8 +42,9 @@ The CI workflow is configured to build with the repository root as the Docker bu
## Logging & debugging ## Logging & debugging
- PHP-FPM is configured to stream its master/process worker logs and PHP error log to stderr, so `docker compose logs php-fpm` (or your platform equivalent) will always contain fatal errors. - PHP-FPM streams master/worker logs plus PHP fatals to stderr, so `docker compose logs php-fpm` (or your platform equivalent) will always contain the messages you need for incident response.
- When you need full stack traces in the browser, set `FORCE_DEBUG_ERRORS=1` on the `php-fpm` service. The shipped `force-debug.php` bootstrap will notice the flag, turn on verbose error reporting, and emit a single log line indicating that debug mode is active. - The auto-prepend bootstrap additionally installs shutdown/exception hooks that write uncaught throwables and fatal errors to stderr even if WordPress or a plugin tampers with `ini_set()`.
- When you need full stack traces in the browser, set `FORCE_DEBUG_ERRORS=1` on the `php-fpm` service. The bootstrap enables verbose output and logs a single notice so you remember to remove it later.
- Remove or unset `FORCE_DEBUG_ERRORS` after troubleshooting so production responses stay clean. - Remove or unset `FORCE_DEBUG_ERRORS` after troubleshooting so production responses stay clean.
## Local Testing & Development ## Local Testing & Development

View File

@@ -13,7 +13,7 @@ services:
php-fpm: php-fpm:
build: build:
context: . context: .
dockerfile: docker/7.4/Dockerfile dockerfile: php-fpm/7.4/Dockerfile
image: local/auvem-php:7.4 image: local/auvem-php:7.4
restart: unless-stopped restart: unless-stopped
volumes: volumes:

View File

@@ -1,23 +1,80 @@
<?php <?php
// force-debug.php // force-debug.php
// Optional debug bootstrap that can be toggled with FORCE_DEBUG_ERRORS=1. // Automatically prepended; guarantees fatals reach container logs and optionally surfaces verbose output.
$forceDebug = getenv('FORCE_DEBUG_ERRORS'); $forceDebugRaw = getenv('FORCE_DEBUG_ERRORS');
if ($forceDebug === false) { $forceDebugEnabled = false;
return; if ($forceDebugRaw !== false) {
$normalized = strtolower(trim($forceDebugRaw));
$forceDebugEnabled = in_array($normalized, ['1', 'true', 'yes', 'on'], true);
} }
$normalized = strtolower(trim($forceDebug)); // Ensure PHP always logs to stderr even if application code tampers with settings.
$enabled = in_array($normalized, ['1', 'true', 'yes', 'on'], true);
if (!$enabled) {
return;
}
error_reporting(E_ALL);
ini_set('display_errors', '1');
ini_set('display_startup_errors', '1');
ini_set('log_errors', '1'); ini_set('log_errors', '1');
ini_set('error_log', '/proc/self/fd/2'); ini_set('error_log', '/proc/self/fd/2');
error_log('[force-debug] Verbose error reporting enabled via FORCE_DEBUG_ERRORS'); // Helper to safely write to container stderr without relying on php.ini.
$forceDebugLog = static function (string $message): void {
static $stderr = null;
if ($stderr === null) {
$stderr = @fopen('php://stderr', 'ab');
}
if ($stderr) {
@fwrite($stderr, $message . PHP_EOL);
} else {
// Fallback to PHP's error_log if stderr is unavailable for any reason.
error_log($message);
}
};
// Capture uncaught exceptions so they can never disappear silently.
set_exception_handler(static function (Throwable $throwable) use ($forceDebugLog, $forceDebugEnabled): void {
$message = sprintf(
'[force-debug] Uncaught %s: %s in %s:%d',
get_class($throwable),
$throwable->getMessage(),
$throwable->getFile(),
$throwable->getLine()
);
$forceDebugLog($message);
$forceDebugLog($throwable->getTraceAsString());
if ($forceDebugEnabled) {
http_response_code(500);
echo '<pre>' . htmlspecialchars($message . "\n" . $throwable->getTraceAsString(), ENT_QUOTES, 'UTF-8') . '</pre>';
}
});
// Capture fatal shutdown errors (E_ERROR, E_PARSE, etc.) and surface them to stderr.
register_shutdown_function(static function () use ($forceDebugLog, $forceDebugEnabled): void {
$error = error_get_last();
if ($error === null) {
return;
}
$fatalTypes = [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR];
if (!in_array($error['type'], $fatalTypes, true)) {
return;
}
$message = sprintf(
'[force-debug] Fatal error (%s): %s in %s:%d',
$error['type'],
$error['message'],
$error['file'],
$error['line']
);
$forceDebugLog($message);
if ($forceDebugEnabled) {
http_response_code(500);
echo '<pre>' . htmlspecialchars($message, ENT_QUOTES, 'UTF-8') . '</pre>';
}
});
if ($forceDebugEnabled) {
error_reporting(E_ALL);
ini_set('display_errors', '1');
ini_set('display_startup_errors', '1');
$forceDebugLog('[force-debug] Verbose error reporting enabled via FORCE_DEBUG_ERRORS');
}