diff --git a/DIAGNOSTICO_RENDIMIENTO.md b/DIAGNOSTICO_RENDIMIENTO.md new file mode 100644 index 0000000..bd7c108 --- /dev/null +++ b/DIAGNOSTICO_RENDIMIENTO.md @@ -0,0 +1,30 @@ +# Plan de Diagnóstico de Rendimiento del Bot de Telegram + +Este archivo rastrea los pasos para diagnosticar la latencia en las respuestas del bot cuando se ejecuta en Docker. + +## Análisis Final + +- **Diagnóstico Definitivo:** El problema no era la base de datos, sino 100% la capa de red. Cada petición (`cURL`) desde el contenedor Docker a la API de Telegram (`api.telegram.org`) sufría una demora de ~5.6 segundos. Esto se debía probablemente a un intento fallido de conexión por IPv6 antes de recurrir a IPv4. +- **Solución Aplicada:** Se modificó la función `private function request` en `bot/TelegramBot.php` para forzar el uso de IPv4 en todas las peticiones `cURL`, además de añadir timeouts de seguridad. Se refactorizó `public/admin/webhook.php` para usar los métodos centralizados en `TelegramBot.php`, asegurando que la solución se aplique de manera consistente en toda la aplicación. + +## Pasos de Diagnóstico (Historial) + +- [x] **Paso 1: Medir Tiempos dentro del Script (Instrumentación)** + - **Estado:** Completado. + - **Análisis:** Los logs revelaron que cada llamada a la API de Telegram tardaba ~5.6s, y las acciones con dos llamadas (como `ver_turnos`) tardaban ~11.2s. + +- [x] **Paso 2: Diagnóstico de Red y DNS desde DENTRO del Contenedor** + - **Estado:** Revisado. + - **Análisis:** La prueba inicial con `curl` ya apuntaba a una conexión lenta (~0.8s), lo que fue el primer indicio del problema de red. + +- [ ] **Paso 3: Análisis de Recursos del Contenedor** + - **Estado:** Cancelado. + - **Análisis:** Se determinó que el problema era de red, no de consumo de CPU/Memoria, por lo que este paso ya no es necesario. + +- [x] **Paso 4: Revisión y Corrección de la Capa de Red (TelegramBot.php y admin/webhook.php)** + - **Estado:** Completado. + - **Acción:** Se analizó y mejoró `bot/TelegramBot.php` y se refactorizó `public/admin/webhook.php` para usar la lógica de comunicación centralizada y corregida. + +- [ ] **Paso 5: Verificación de la Solución** + - **Estado:** En progreso. + - **Acción:** Esperando los resultados de `logs/webhook_timing.log` y la confirmación visual de la página `admin/webhook.php` después de reiniciar el contenedor y probar el bot con el parche aplicado. \ No newline at end of file diff --git a/bot/TelegramBot.php b/bot/TelegramBot.php index 6be3610..1037b90 100755 --- a/bot/TelegramBot.php +++ b/bot/TelegramBot.php @@ -14,14 +14,39 @@ class TelegramBot { $this->apiUrl = "https://api.telegram.org/bot{$this->token}"; } - private function request($method, $data = []) { + private function request($method, $data = [], $httpMethod = 'POST') { $url = "{$this->apiUrl}/{$method}"; - $ch = curl_init($url); - curl_setopt($ch, CURLOPT_POST, true); - curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); - curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + $ch = curl_init(); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + + // --- MEJORAS --- + // 1. Forzar el uso de IPv4. Un problema común en entornos Docker + // es un timeout al intentar resolver AAAA (IPv6) antes de usar A (IPv4). + curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); + + // 2. Añadir timeouts para evitar que el script se cuelgue indefinidamente. + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); // 5 segundos para conectar + curl_setopt($ch, CURLOPT_TIMEOUT, 10); // 10 segundos para la transferencia total + + if ($httpMethod === 'POST') { + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); + curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); + curl_setopt($ch, CURLOPT_URL, $url); + } else { // GET request + $url .= empty($data) ? '' : '?' . http_build_query($data); + curl_setopt($ch, CURLOPT_URL, $url); + } + $response = curl_exec($ch); + + // Añadir manejo de errores de cURL + if (curl_errno($ch)) { + error_log("cURL Error en {$method}: " . curl_error($ch)); + curl_close($ch); + return null; + } + curl_close($ch); return json_decode($response, true); } @@ -39,13 +64,30 @@ class TelegramBot { } public function getMe() { - return $this->request('getMe'); + return $this->request('getMe', [], 'GET'); } public function getUpdates($offset = 0) { - return $this->request('getUpdates', ['offset' => $offset, 'timeout' => 60]); + return $this->request('getUpdates', ['offset' => $offset, 'timeout' => 60], 'GET'); } + // --- NUEVOS MÉTODOS PARA CENTRALIZAR COMUNICACIONES --- + public function getWebhookInfo() { + return $this->request('getWebhookInfo', [], 'GET'); + } + + public function deleteWebhook() { + return $this->request('deleteWebhook', []); + } + + public function setWebhook($webhookUrl, $allowedUpdates = ['message', 'callback_query']) { + return $this->request('setWebhook', [ + 'url' => $webhookUrl, + 'allowed_updates' => $allowedUpdates + ]); + } + + public function sendKeyboard($chatId, $text) { $keyboard = [ 'inline_keyboard' => [ diff --git a/bot/webhook.php b/bot/webhook.php index 9f7f5fc..a10dad1 100755 --- a/bot/webhook.php +++ b/bot/webhook.php @@ -1,5 +1,35 @@ config = require __DIR__ . '/../config/config.php'; $this->bot = new TelegramBot(); + log_timing("TurnoBot: __construct end"); } public function handleUpdate($update) { + log_timing("handleUpdate: start"); try { // Manejar callback de botones inline if (isset($update['callback_query'])) { + log_timing("handleUpdate: detected callback_query"); $this->handleCallback($update['callback_query']); return; } // Manejar mensajes normales if (!isset($update['message'])) { + log_timing("handleUpdate: no message found, exiting"); return; } @@ -35,64 +70,87 @@ class TurnoBot { $text = trim($message['text'] ?? ''); if (empty($text)) { + log_timing("handleUpdate: empty text, exiting"); return; } $textLower = mb_strtolower($text, 'UTF-8'); + log_timing("handleUpdate: processing command '{$textLower}'"); // Comandos if ($textLower === '/start' || $textLower === '/menu' || $textLower === 'menu') { $this->sendMenu($chatId); } elseif ($textLower === '/turnos' || $textLower === 'turnos') { + log_timing("handleUpdate: /turnos command start"); $this->bot->sendMessage($chatId, $this->bot->getTablaTurnos(8)); + log_timing("handleUpdate: /turnos command end"); } elseif ($textLower === '/semana' || $textLower === 'semana' || $textLower === 'hoy') { + log_timing("handleUpdate: /semana command start"); $this->bot->sendMessage($chatId, $this->bot->getSemanaActual()); + log_timing("handleUpdate: /semana command end"); } elseif ($textLower === '/ayudantes' || $textLower === 'ayudantes') { + log_timing("handleUpdate: /ayudantes command start"); $ayudantes = $this->bot->getListaAyudantesParaBusqueda(); $this->bot->sendMessage($chatId, "AYUDANTES DISPONIBLES:\n\n" . implode("\n", $ayudantes)); + log_timing("handleUpdate: /ayudantes command end"); } elseif ($textLower === '/pdf' || $textLower === 'pdf' || $textLower === 'mi pdf') { + log_timing("handleUpdate: /pdf command start"); $this->bot->sendPDFGeneral($chatId); + log_timing("handleUpdate: /pdf command end"); } else { - // Buscar por nombre - verificar si existe el usuario + log_timing("handleUpdate: searching by name '{$text}'"); $config = require __DIR__ . '/../config/config.php'; try { + log_timing("handleUpdate: DB connection start"); $pdo = new PDO( "mysql:host={$config['db']['host']};port={$config['db']['port']};dbname={$config['db']['database']};charset=utf8mb4", $config['db']['username'], $config['db']['password'], [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION] ); + log_timing("handleUpdate: DB connection end"); } catch (Exception $e) { + log_timing("handleUpdate: DB connection FAILED"); $this->bot->sendMessage($chatId, "Error de conexion."); return; } + log_timing("handleUpdate: DB query start"); $stmt = $pdo->prepare("SELECT * FROM users WHERE (nombre LIKE ? OR username LIKE ?) AND rol = 'ayudante' AND activo = 1 LIMIT 1"); $stmt->execute(["%$text%", "%$text%"]); $user = $stmt->fetch(); + log_timing("handleUpdate: DB query end"); if ($user) { + log_timing("handleUpdate: user found, sending PDF"); $this->bot->sendMessage($chatId, "Generando PDF de turnos..."); $this->bot->sendPDF($chatId, $user['id']); + log_timing("handleUpdate: PDF sent"); } else { + log_timing("handleUpdate: user not found, getting plain text turnos"); $this->bot->sendMessage($chatId, $this->bot->getTurnosAyudante($text)); + log_timing("handleUpdate: plain text turnos sent"); } } } catch (Exception $e) { error_log("Error en handleUpdate: " . $e->getMessage()); + log_timing("handleUpdate: EXCEPTION: " . $e->getMessage()); if (isset($update['message']['chat']['id'])) { $this->bot->sendMessage($update['message']['chat']['id'], "Error: " . $e->getMessage()); } } + log_timing("handleUpdate: end"); } private function handleCallback($callback) { + log_timing("handleCallback: start"); try { $callbackId = $callback['id']; $data = $callback['data']; $message = $callback['message']; $chatId = $message['chat']['id']; $messageId = $message['message_id']; + log_timing("handleCallback: processing data '{$data}'"); switch ($data) { case 'ver_turnos': @@ -124,12 +182,16 @@ class TurnoBot { default: $this->bot->answerCallback($callbackId, 'Opcion no reconocida'); } + log_timing("handleCallback: data '{$data}' processed"); } catch (Exception $e) { error_log("Error en handleCallback: " . $e->getMessage()); + log_timing("handleCallback: EXCEPTION: " . $e->getMessage()); } + log_timing("handleCallback: end"); } private function sendMenu($chatId) { + log_timing("sendMenu: start"); $mensaje = "BOT DE TURNOS - CONTENEDOR IBIZA\n\n"; $mensaje .= "Selecciona una opcion del menu:\n\n"; $mensaje .= "Ver Turnos - Tabla completa de asignaciones\n"; @@ -139,19 +201,25 @@ class TurnoBot { $mensaje .= "Mi Turno - Ver tu proximo turno"; $this->bot->sendKeyboard($chatId, $mensaje); + log_timing("sendMenu: end"); } } // Recibir actualización $update = json_decode(file_get_contents('php://input'), true); +log_timing("Webhook invoked"); + // Log para debugging -error_log("Webhook recibido: " . json_encode($update)); +// error_log("Webhook recibido: " . json_encode($update)); // Se puede comentar para no llenar el log de errores if ($update) { + log_timing("Update received, initializing bot"); $bot = new TurnoBot(); $bot->handleUpdate($update); + log_timing("Script finished"); } else { http_response_code(200); - echo "Webhook activo. Usa /start para ver el menu."; + log_timing("Webhook checked (no update provided)"); + // echo "Webhook activo. Usa /start para ver el menu."; // No es necesario en producción } diff --git a/public/admin/webhook.php b/public/admin/webhook.php index 66b38da..60fdb13 100755 --- a/public/admin/webhook.php +++ b/public/admin/webhook.php @@ -17,7 +17,7 @@ $botInfo = null; // Obtener información del bot $botMe = $bot->getMe(); -if ($botMe && isset($botMe['ok']) && $botMe['ok']) { +if ($botMe && $botMe['ok']) { // Simplificado $botInfo = $botMe['result']; } @@ -26,14 +26,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $action = $_POST['action'] ?? ''; if ($action === 'verificar') { - $url = "https://api.telegram.org/bot{$config['telegram_bot_token']}/getWebhookInfo"; - $ch = curl_init($url); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - $response = curl_exec($ch); - curl_close($ch); - $result = json_decode($response, true); + $result = $bot->getWebhookInfo(); // Usar el método centralizado - if ($result && isset($result['ok'])) { + if ($result && $result['ok']) { // Simplificado $webhookInfo = $result; $message = 'Información del webhook obtenida'; $messageType = 'success'; @@ -42,17 +37,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $messageType = 'danger'; } } elseif ($action === 'borrar') { - $url = "https://api.telegram.org/bot{$config['telegram_bot_token']}/deleteWebhook"; - $ch = curl_init($url); - curl_setopt($ch, CURLOPT_POST, true); - curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([])); - curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - $response = curl_exec($ch); - curl_close($ch); - $result = json_decode($response, true); + $result = $bot->deleteWebhook(); // Usar el método centralizado - if ($result && isset($result['ok']) && $result['ok']) { + if ($result && $result['ok']) { // Simplificado $message = 'Webhook eliminado correctamente'; $messageType = 'success'; $webhookInfo = null; @@ -70,20 +57,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $message = 'La URL ingresada no es válida'; $messageType = 'danger'; } else { - $url = "https://api.telegram.org/bot{$config['telegram_bot_token']}/setWebhook"; - $ch = curl_init($url); - curl_setopt($ch, CURLOPT_POST, true); - curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([ - 'url' => $webhookUrl, - 'allowed_updates' => ['message', 'callback_query'] - ])); - curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - $response = curl_exec($ch); - curl_close($ch); - $result = json_decode($response, true); + $result = $bot->setWebhook($webhookUrl); // Usar el método centralizado - if ($result && isset($result['ok']) && $result['ok']) { + if ($result && $result['ok']) { // Simplificado $message = "Webhook configurado correctamente en:\n" . htmlspecialchars($webhookUrl); $messageType = 'success'; } else { @@ -94,13 +70,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { } } -// Obtener estado actual del webhook -$url = "https://api.telegram.org/bot{$config['telegram_bot_token']}/getWebhookInfo"; -$ch = curl_init($url); -curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); -$response = curl_exec($ch); -curl_close($ch); -$webhookInfo = json_decode($response, true); +// Obtener estado actual del webhook al cargar la página +$webhookInfo = $bot->getWebhookInfo(); // Usar el método centralizado $currentPage = 'webhook'; $pageTitle = 'Administración del Bot de Telegram';