Guía completa de manejo de errores y logging en PHP para entornos profesionales
Esta guía práctica explica cómo diseñar un sistema de manejo de errores y logging robusto en PHP: desde la configuración de PHP hasta la integración con Monolog y Sentry, pasando por patrones para convertir errores en excepciones, filtrar datos sensibles y optimizar rendimiento. Incluye estructura de proyecto, código completo y el porqué de cada decisión.
Por qué importa
- Detectar fallos temprano y reproducirlos.
- Mantener la seguridad: no exponer trazas en producción.
- Operaciones y monitorización: métricas, alertas y tiempo de recuperación.
Conceptos rápidos
- Error vs Excepción vs Throwable: desde PHP 7, Throwable engloba Error y Exception.
- Tipos comunes: E_NOTICE, E_WARNING, E_ERROR (fatales), Exceptions lanzadas con throw.
- Handlers globales: set_error_handler, set_exception_handler, register_shutdown_function.
Configuración PHP básica
En desarrollo mostrar errores; en producción sólo loguear:
; php.ini (producción)
display_errors = Off
log_errors = On
error_log = /var/log/php_errors.log
error_reporting = E_ALL & ~E_NOTICE
En runtime preferimos controlar esto desde bootstrap para ambientes y tests.
Estrategia recomendada
- Convertir errores (warnings/notices) a excepciones para tener flujo uniforme.
- Tener un ExceptionHandler central que: loguee, capture en monitor (Sentry), devuelva respuesta amigable y no filtre datos sensibles.
- Manejo de errores fatales con register_shutdown_function + error_get_last.
- Logs estructurados (JSON) con metadatos: request_id, user_id anónimo, ruta, env.
Estructura mínima del proyecto (ejemplo)
project/
composer.json
public/
index.php
src/
bootstrap.php
LoggerFactory.php
ExceptionHandler.php
utils.php
logs/
app.log
Dependencias clave
Usaremos Monolog y opcionalmente Sentry:
composer require monolog/monolog
composer require sentry/sentry-php # opcional para captura centralizada
Bootstrap: dónde inicializarlo todo
// src/bootstrap.php
use Monolog\Handler\RotatingFileHandler;
use Monolog\Logger;
use Sentry\SentrySdk;
require_once __DIR__ . '/../vendor/autoload.php';
// Cargar env (ejemplo simplificado)
$env = getenv('APP_ENV') ?: 'production';
// Config PHP básico para entorno
if ($env === 'development') {
ini_set('display_errors', '1');
ini_set('display_startup_errors', '1');
error_reporting(E_ALL);
} else {
ini_set('display_errors', '0');
error_reporting(E_ALL & ~E_NOTICE);
}
// Crear logger
$logger = new Logger('app');
$handler = new RotatingFileHandler(__DIR__ . '/../logs/app.log', 7, Logger::DEBUG);
$logger->pushHandler($handler);
// Integración con Sentry si está configurado
if ($dsn = getenv('SENTRY_DSN')) {
\Sentry\init(['dsn' => $dsn, 'environment' => $env]);
}
// Hacer disponible el logger globalmente (ejemplo simple)
$GLOBALS['logger'] = $logger;
// Convertir errores a excepciones
set_error_handler(function ($severity, $message, $file, $line) {
// Respect error_reporting level
if (!(error_reporting() & $severity)) {
return; // Silenciado
}
throw new ErrorException($message, 0, $severity, $file, $line);
});
// Excepción global
set_exception_handler(function ($e) use ($logger) {
// Sanitizar antes de loguear
$context = [
'exception' => get_class($e),
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
];
$logger->error('Uncaught exception', $context);
if (class_exists('\Sentry\SentrySdk')) {
\Sentry\captureException($e);
}
// Respuesta mínima al cliente
if (php_sapi_name() === 'cli') {
fwrite(STDERR, "Fatal: " . $e->getMessage() . "\n");
exit(1);
}
http_response_code(500);
header('Content-Type: application/json');
echo json_encode(['error' => 'Internal Server Error']);
});
// Capturar errores fatales al shutdown
register_shutdown_function(function () use ($logger) {
$err = error_get_last();
if ($err && in_array($err['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
$logger->critical('Fatal error', $err);
if (class_exists('\Sentry\SentrySdk')) {
\Sentry\captureMessage('Fatal error: ' . ($err['message'] ?? ''), \Sentry\Severity::fatal());
}
}
});
Ejemplo de handler central (ExceptionHandler)
// src/ExceptionHandler.php
class ExceptionHandler
{
private $logger;
public function __construct(Psr\Log\LoggerInterface $logger)
{
$this->logger = $logger;
}
public function handle(\Throwable $e)
{
$meta = [
'type' => get_class($e),
'message' => $this->sanitize($e->getMessage()),
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => $e->getTraceAsString(), // en producción quizá limitar
];
$this->logger->error('Unhandled throwable', $meta);
if (function_exists('sentry_capture_exception')) {
sentry_capture_exception($e);
}
// Respuesta para el cliente
if (php_sapi_name() !== 'cli') {
http_response_code(500);
header('Content-Type: application/json');
echo json_encode(['error' => 'Internal Server Error']);
}
}
private function sanitize($value)
{
// Implementa reglas para borrar datos sensibles (emails, tokens)
$value = preg_replace('/[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}/i', '[email]', $value);
$value = preg_replace('/\b(?:\d[ -]*?){13,16}\b/', '[card]', $value);
return $value;
}
}
Ejemplo endpoint mínimo
// public/index.php
require_once __DIR__ . '/../src/bootstrap.php';
try {
// Simular request id
$requestId = bin2hex(random_bytes(8));
$GLOBALS['logger']->info('Request start', ['request_id' => $requestId, 'uri' => $_SERVER['REQUEST_URI'] ?? '/']);
// Código de la aplicación
if (($_GET['fail'] ?? '') === '1') {
throw new RuntimeException('Simulated failure with user email test@example.com');
}
echo json_encode(['ok' => true, 'request_id' => $requestId]);
} catch (\Throwable $e) {
// Delegar a manejador global
(new ExceptionHandler($GLOBALS['logger']))->handle($e);
}
Buenas prácticas y seguridad
- No exponer stack traces en producción: devolver mensajes genéricos.
- Sanitizar antes de loguear: emails, tokens, contraseñas y números de tarjetas.
- Separar logs por propósito: access logs vs application logs vs security logs.
- Privilegios: logs y directorios con permisos mínimos. En contenedores, enviar logs a stdout/stderr para que el orquestador los capture.
Optimización y rendimiento
- No blockear la respuesta del usuario con operaciones de I/O lentas: usar handlers asíncronos o colas para envío a terceros (Sentry, Elastic).
- Batching y buffering: Monolog tiene BufferHandler y FingersCrossedHandler para agrupar eventos.
- Sampling y rate-limiting para evitar inundar el backend de logs con errores repetitivos.
- Logs estructurados (JSON) facilitan indexado y búsquedas en ELK/Datadog.
Integración con Monolog: consejos prácticos
- Usa RotatingFileHandler para no llenar disco.
- Prefiere StreamHandler a stdout en contenedores.
- Usa processors para añadir request_id, user_id anónimo y contexto.
// Ejemplo de processor
$logger->pushProcessor(function ($record) {
$record['extra']['request_id'] = $_SERVER['HTTP_X_REQUEST_ID'] ?? (bin2hex(random_bytes(6)));
return $record;
});
Monitoreo y alertas
Define alertas en base a:
- Aumento de tasas de errores 5xx (alerta crítica).
- Errores únicos en high severity (critical / emergency).
- Reglas de correlación: misma excepción repetida en X minutos.
Pruebas
Escribe tests que simulen errores y verifiquen que:
- El handler los captura y no falla el proceso principal.
- No se logea información sensible (assert contra sanitizer).
Checklist para desplegar a producción
- display_errors = Off
- Logs dirigidos a ubicación con rotación o stdout
- Monitoreo (Sentry/Datadog) configurado
- Permisos y ownership de carpetas de logs correctos
- Sampling/rate-limits aplicados
Errores comunes y cómo evitarlos
- Loguear datos sensibles sin sanitizar: implementar sanitizador central.
- Depender sólo de display_errors para debug en producción.
- No capturar errores fatales: asegurar register_shutdown_function.
- Hacer logging síncrono en código crítico: usar colas para operaciones de red/3rd party.
Próximos pasos recomendados
Implementa logging estructurado (JSON), añade correlación de request_id en toda la stack y habilita tracing distribuido (OpenTelemetry) para unir logs, métricas y traces. Una buena práctica es empezar con Monolog + RotatingFileHandler y luego integrar Sentry o Datadog con sampling para alertas.
Consejo avanzado: añade un middleware que genere y propague un request_id y que capture y envíe solo los metadatos esenciales al backend de errores; deja el volcado completo del stack sólo para incidencias con alta prioridad o entornos de staging.
¿Quieres comentar?
Inicia sesión con Telegram para participar en la conversación