laraveldebuggingproducciónmonitoreo

Debuggear Apps Laravel en Producción sin Volverse Loco

Debuggear Aplicaciones Laravel en Producción: Guía Práctica

Cuando tu aplicación Laravel está en producción, los errores no desaparecen: simplemente se vuelven más difíciles de rastrear. A diferencia del entorno local donde puedes ver stack traces completos y usar dd() sin restricciones, en producción necesitas herramientas más sofisticadas y seguras.

En este artículo exploraremos técnicas profesionales para debuggear aplicaciones Laravel en vivo sin perder la cordura ni comprometer la experiencia del usuario.

El Problema del Debugging en Producción

El debugging en producción es fundamentalmente diferente al desarrollo local. Tienes restricciones importantes:

  • No puedes pausar la ejecución con breakpoints
  • Los usuarios están usando tu aplicación en tiempo real
  • La información sensible no puede exponerse públicamente
  • El rendimiento es crítico – no puedes ralentizar la app
  • Los logs pueden crecer rápidamente y llenar el disco

Muchos desarrolladores Laravel cometen el error de desactivar completamente los logs en producción o, peor aún, dejar debug => true en el archivo .env, lo que expone información delicada en los error pages.

Estrategia 1: Logging Estructurado y Contextual

El primer paso hacia un debugging profesional es implementar un sistema de logging robusto que capture contexto útil sin ser excesivamente verbose.

Configurar Logging Avanzado

En config/logging.php, configura múltiples canales para diferentes tipos de eventos:

'channels' => [
    'stack' => [
        'driver' => 'stack',
        'channels' => ['single', 'slack'],
        'ignore_exceptions' => false,
    ],
    'single' => [
        'driver' => 'single',
        'path' => storage_path('logs/laravel.log'),
        'level' => env('LOG_LEVEL', 'debug'),
        'days' => 14,
    ],
    'performance' => [
        'driver' => 'single',
        'path' => storage_path('logs/performance.log'),
        'level' => 'warning',
    ],
    'database' => [
        'driver' => 'single',
        'path' => storage_path('logs/database.log'),
        'level' => 'debug',
    ],
    'slack' => [
        'driver' => 'slack',
        'url' => env('LOG_SLACK_WEBHOOK_URL'),
        'username' => 'Laravel Log',
        'emoji' => ':boom:',
        'level' => 'critical',
    ],
],

Logging con Contexto

En lugar de Log::info('Algo pasó'), incluye contexto que sea útil para debuggear:

<?php

namespace App\Http\Controllers;

use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Auth;

class OrderController extends Controller
{
    public function store(Request $request)
    {
        $userId = Auth::id();
        $requestId = $request->header('X-Request-ID') ?? uniqid();
        
        Log::info('Iniciando creación de orden', [
            'user_id' => $userId,
            'request_id' => $requestId,
            'items_count' => $request->items->count(),
            'total_amount' => $request->total,
            'timestamp' => now()->toIso8601String(),
        ]);

        try {
            $order = Order::create($request->validated());
            
            Log::info('Orden creada exitosamente', [
                'user_id' => $userId,
                'order_id' => $order->id,
                'request_id' => $requestId,
            ]);

            return response()->json($order);
        } catch (\Exception $e) {
            Log::error('Error al crear orden', [
                'user_id' => $userId,
                'request_id' => $requestId,
                'error' => $e->getMessage(),
                'exception' => get_class($e),
                'file' => $e->getFile(),
                'line' => $e->getLine(),
                'trace' => $e->getTraceAsString(),
            ]);

            return response()->json(
                ['message' => 'Error procesando la orden'],
                500
            );
        }
    }
}

Estrategia 2: Custom Exception Reporting

El exception handler es tu primer línea de defensa. Personalízalo para capturar información crítica y enviarla a servicios especializados.

Configurar Exception Handler

En app/Exceptions/Handler.php:

<?php

namespace App\Exceptions;

use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Database\QueryException;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Throwable;

class Handler extends ExceptionHandler
{
    protected $dontReport = [
        NotFoundHttpException::class,
        ValidationException::class,
    ];

    public function register(): void
    {
        $this->reportable(function (Throwable $e) {
            // Enviar a Sentry, Rollbar, etc.
            if ($this->shouldReport($e)) {
                $this->reportToErrorService($e);
            }
        });

        // Handler específico para query exceptions
        $this->reportable(function (QueryException $e) {
            $this->logDatabaseError($e);
        });
    }

    private function reportToErrorService(Throwable $e): void
    {
        if (!app()->environment('production')) {
            return;
        }

        try {
            \Sentry\captureException($e);
        } catch (\Exception $ex) {
            // Fallback si Sentry falla
            \Log::critical('Error enviando a Sentry', [
                'original_exception' => $e->getMessage(),
                'sentry_error' => $ex->getMessage(),
            ]);
        }
    }

    private function logDatabaseError(QueryException $e): void
    {
        \Log::channel('database')->error('Database Error', [
            'message' => $e->getMessage(),
            'query' => $e->getSql(),
            'bindings' => $e->getBindings(),
            'file' => $e->getFile(),
            'line' => $e->getLine(),
        ]);
    }
}

Estrategia 3: Monitoring de Queries Lentas

Las queries lenta son una causa común de problemas en producción. Implementa detección automática.

Registrar Queries Lentas

En app/Providers/AppServiceProvider.php:

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        // En producción, solo grabar queries muy lentas
        $slowQueryThreshold = app()->environment('production') ? 1000 : 100; // ms

        DB::listen(function (QueryExecuted $query) use ($slowQueryThreshold) {
            if ($query->time >= $slowQueryThreshold) {
                Log::channel('performance')->warning(
                    'Slow Query Detected',
                    [
                        'query' => $query->sql,
                        'bindings' => $query->bindings,
                        'time_ms' => $query->time,
                        'connection' => $query->connectionName,
                        'threshold_ms' => $slowQueryThreshold,
                    ]
                );
            }
        });
    }
}

Estrategia 4: Debugging Remoto con Xdebug en Producción

Para depuraciones más profundas, puedes usar Xdebug remotamente, pero con precaución extrema.

Configuración Segura de Xdebug

// config/xdebug.php
return [
    'enabled' => env('XDEBUG_ENABLED', false),
    'remote_host' => env('XDEBUG_REMOTE_HOST', '127.0.0.1'),
    'remote_port' => env('XDEBUG_REMOTE_PORT', 9003),
    // Solo activar para IPs específicas
    'allowed_ips' => explode(',', env('XDEBUG_ALLOWED_IPS', '127.0.0.1')),
];

En php.ini (producción):

; SOLO si realmente lo necesitas
[xdebug]
xdebug.mode = debug
xdebug.discover_client_host = off
xdebug.client_host = your-dev-machine-ip
xdebug.client_port = 9003
xdebug.start_with_request = trigger
; Muy importante: solo activar por request
xdebug.trigger_value = "debug-token-secreto"

Luego desde el cliente:

# Solo si tienes el trigger token
curl -H "XDEBUG_TRIGGER: debug-token-secreto" https://tu-app.com/endpoint

Estrategia 5: Monitoreo con Herramientas Profesionales

Para aplicaciones serias, invierte en herramientas especializadas:

Integración con Sentry

// config/sentry.php
return [
    'dsn' => env('SENTRY_LARAVEL_DSN'),
    'environment' => env('APP_ENV'),
    'release' => env('APP_VERSION'),
    'traces_sample_rate' => env('SENTRY_TRACES_SAMPLE_RATE', 0.1),
    'profiles_sample_rate' => env('SENTRY_PROFILES_SAMPLE_RATE', 0.1),
];

En tu .env:

SENTRY_LARAVEL_DSN=https://your-key@sentry.io/project-id
SENTRY_TRACES_SAMPLE_RATE=0.1
SENTRY_PROFILES_SAMPLE_RATE=0.05

Sentry automáticamente capturará:

  • Excepciones no manejadas
  • Errores de performance
  • Transacciones HTTP
  • Interacciones del usuario

Estrategia 6: Request Debugging Seguro

Crea un middleware que capture información de request solo cuando sea necesario.

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;

class DebugRequest
{
    private $debugTokens = [];

    public function __construct()
    {
        $this->debugTokens = explode(',', env('DEBUG_TOKENS', ''));
    }

    public function handle(Request $request, Closure $next)
    {
        $debugToken = $request->header('X-Debug-Token');
        
        if (
            in_array($debugToken, $this->debugTokens) &&
            app()->environment('production')
        ) {
            Log::info('Debug Request', [
                'method' => $request->getMethod(),
                'path' => $request->getPath(),
                'query' => $request->query(),
                'headers' => $request->headers->all(),
                'user_agent' => $request->userAgent(),
                'ip' => $request->ip(),
            ]);
        }

        return $next($request);
    }
}

Estrategia 7: Health Checks y Monitoring

Implementa endpoints específicos para monitorear la salud de tu aplicación:

<?php

namespace App\Http\Controllers;

use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Cache;

class HealthController extends Controller
{
    public function check()
    {
        $checks = [
            'database' => $this->checkDatabase(),
            'cache' => $this->checkCache(),
            'storage' => $this->checkStorage(),
            'queue' => $this->checkQueue(),
        ];

        $healthy = collect($checks)->every(fn($check) => $check['status'] === 'ok');

        return response()->json(
            [
                'status' => $healthy ? 'healthy' : 'unhealthy',
                'checks' => $checks,
                'timestamp' => now()->toIso8601String(),
            ],
            $healthy ? 200 : 503
        );
    }

    private function checkDatabase(): array
    {
        try {
            DB::connection()->getPdo();
            return ['status' => 'ok'];
        } catch (\Exception $e) {
            return [
                'status' => 'error',
                'message' => $e->getMessage(),
            ];
        }
    }

    private function checkCache(): array
    {
        try {
            Cache::put('health_check', true, 1);
            return ['status' => 'ok'];
        } catch (\Exception $e) {
            return [
                'status' => 'error',
                'message' => $e->getMessage(),
            ];
        }
    }

    private function checkStorage(): array
    {
        try {
            $disk = \Storage::disk('local');
            $disk->put('health_check.txt', 'ok');
            $disk->delete('health_check.txt');
            return ['status' => 'ok'];
        } catch (\Exception $e) {
            return [
                'status' => 'error',
                'message' => $e->getMessage(),
            ];
        }
    }

    private function checkQueue(): array
    {
        try {
            $failed = \DB::table('failed_jobs')->count();
            return [
                'status' => $failed < 10 ? 'ok' : 'warning',
                'failed_jobs' => $failed,
            ];
        } catch (\Exception $e) {
            return [
                'status' => 'error',
                'message' => $e->getMessage(),
            ];
        }
    }
}

Registra en routes/web.php:

Route::get('/health', [HealthController::class, 'check']);

Mejores Prácticas Esenciales

1. Nunca Expones Información Sensible

// ❌ Malo - expone rutas internas
Log::error('Error', ['exception' => $e->getTraceAsString()]);

// ✅ Bueno - información genérica para el usuario
return response()->json(
    ['message' => 'Algo salió mal. ID: ' . $requestId],
    500
);

2. Usa Request IDs Únicos

// Middleware
class AddRequestId
{
    public function handle(Request $request, Closure $next)
    {
        $requestId = $request->header('X-Request-ID') ?? uniqid();
        
        \Log::shareContext([
            'request_id' => $requestId,
        ]);

        return $next($request);
    }
}

3. Implementa Rate Limiting en Endpoints de Debug

Route::get('/debug/info', function () {
    // ...
})->middleware('throttle:10,1'); // 10 requests por minuto

Conclusión

El debugging en producción requiere una estrategia diferente a la del desarrollo local. La clave está en implementar logging contextual, exception handling robusto, monitoring continuo y usar herramientas profesionales que te permitan investigar problemas sin exponiendo información sensible ni ralentizar la