Guía completa de manejo de errores y logging en PHP para entornos profesionales

php Guía completa de manejo de errores y logging en PHP para entornos profesionales

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

  1. Convertir errores (warnings/notices) a excepciones para tener flujo uniforme.
  2. Tener un ExceptionHandler central que: loguee, capture en monitor (Sentry), devuelva respuesta amigable y no filtre datos sensibles.
  3. Manejo de errores fatales con register_shutdown_function + error_get_last.
  4. 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.

Comentarios
¿Quieres comentar?

Inicia sesión con Telegram para participar en la conversación


Comentarios (0)

Aún no hay comentarios. ¡Sé el primero en comentar!

Iniciar Sesión