redis = new RedisClient([ 'scheme' => getenv('REDIS_SCHEME') ?: 'tcp', 'host' => getenv('REDIS_HOST') ?: '127.0.0.1', 'port' => getenv('REDIS_PORT') ?: 6379, 'password' => getenv('REDIS_PASSWORD') ?: null, 'database' => getenv('REDIS_DB') ?: 0, ]); // Verificar conexión $this->redis->ping(); $this->enabled = true; } catch (Exception $e) { error_log("RateLimitMonitor: Redis no disponible - " . $e->getMessage()); $this->enabled = false; } } /** * Captura y almacena información de rate limit desde headers HTTP * @param array $responseHeaders - Headers de la respuesta de Facebook API * @param string $endpoint - Endpoint llamado (para tracking) */ public function captureFromHeaders(array $responseHeaders, string $endpoint = 'unknown'): void { if (!$this->enabled) return; try { $headerText = is_array($responseHeaders) ? implode("\n", $responseHeaders) : $responseHeaders; // Buscar X-App-Usage if (preg_match('/x-app-usage:\s*({[^}]+})/i', $headerText, $matches)) { $usage = json_decode($matches[1], true); if ($usage) { $this->recordAppUsage($usage, $endpoint); } } // Buscar X-Business-Use-Case-Usage if (preg_match('/x-business-use-case-usage:\s*({.+})/i', $headerText, $matches)) { $usage = json_decode($matches[1], true); if ($usage) { $this->recordBusinessUsage($usage, $endpoint); } } } catch (Exception $e) { error_log("RateLimitMonitor: Error capturando headers - " . $e->getMessage()); } } /** * Registra uso a nivel de App (X-App-Usage) */ private function recordAppUsage(array $usage, string $endpoint): void { $timestamp = time(); $key = "fb:ratelimit:app:current"; $historyKey = "fb:ratelimit:app:history"; $data = [ 'call_count' => $usage['call_count'] ?? 0, 'total_time' => $usage['total_time'] ?? 0, 'total_cputime' => $usage['total_cputime'] ?? 0, 'endpoint' => $endpoint, 'timestamp' => $timestamp, 'datetime' => date('Y-m-d H:i:s') ]; // Guardar estado actual $this->redis->setex($key, 3600, json_encode($data)); // Agregar a historial (últimas 100 muestras) $this->redis->lpush($historyKey, json_encode($data)); $this->redis->ltrim($historyKey, 0, 99); // Verificar umbrales y generar alertas $this->checkThresholds('app', $data); } /** * Registra uso a nivel de Business (X-Business-Use-Case-Usage) */ private function recordBusinessUsage(array $usage, string $endpoint): void { $timestamp = time(); foreach ($usage as $businessId => $metrics) { if (!is_array($metrics)) continue; foreach ($metrics as $metric) { if (!is_array($metric)) continue; $type = $metric['type'] ?? 'unknown'; $key = "fb:ratelimit:business:{$businessId}:{$type}"; $historyKey = "fb:ratelimit:business:{$businessId}:{$type}:history"; $data = [ 'business_id' => $businessId, 'type' => $type, 'call_count' => $metric['call_count'] ?? 0, 'total_time' => $metric['total_time'] ?? 0, 'total_cputime' => $metric['total_cputime'] ?? 0, 'estimated_time_to_regain_access' => $metric['estimated_time_to_regain_access'] ?? 0, 'ads_api_access_tier' => $metric['ads_api_access_tier'] ?? null, 'endpoint' => $endpoint, 'timestamp' => $timestamp, 'datetime' => date('Y-m-d H:i:s') ]; // Guardar estado actual $this->redis->setex($key, 3600, json_encode($data)); // Agregar a historial $this->redis->lpush($historyKey, json_encode($data)); $this->redis->ltrim($historyKey, 0, 99); // Verificar umbrales $this->checkThresholds('business', $data); } } } /** * Verifica umbrales y genera alertas si es necesario */ private function checkThresholds(string $level, array $data): void { $callCount = $data['call_count'] ?? 0; $totalTime = $data['total_time'] ?? 0; $totalCputime = $data['total_cputime'] ?? 0; $maxUsage = max($callCount, $totalTime, $totalCputime); if ($maxUsage >= self::CRITICAL_THRESHOLD) { $this->triggerAlert('critical', $level, $data, $maxUsage); } elseif ($maxUsage >= self::WARNING_THRESHOLD) { $this->triggerAlert('warning', $level, $data, $maxUsage); } } /** * Dispara una alerta de rate limit */ private function triggerAlert(string $severity, string $level, array $data, float $usage): void { $alertKey = "fb:ratelimit:alerts"; $alert = [ 'severity' => $severity, 'level' => $level, 'usage' => $usage, 'data' => $data, 'timestamp' => time(), 'datetime' => date('Y-m-d H:i:s') ]; // Guardar alerta $this->redis->lpush($alertKey, json_encode($alert)); $this->redis->ltrim($alertKey, 0, 49); // Mantener últimas 50 alertas $this->redis->expire($alertKey, 86400 * 7); // 7 días // Log error_log("R 'error' => 'Redis no disponible', 'app_usage' => null, 'business_usage' => [], 'local_limits' => $this->getLocalLimits(), 'alerts' => [], 'status' => 'unavailable' rt - {$level} usage at {$usage}% for endpoint: " . ($data['endpoint'] ?? 'unknown')); } /** * Obtiene estadísticas actuales de rate limit */ public function getCurrentStats(): array { if (!$this->enabled) { return ['error' => 'Redis no disponible']; } try { $stats = [ 'app_usage' => null, 'business_usage' => [], 'local_limits' => $this->getLocalLimits(), 'alerts' => [], 'status' => 'healthy' ]; // Obtener uso a nivel de app $appData = $this->redis->get("fb:ratelimit:app:current"); if ($appData) { $stats['app_usage'] = json_decode($appData, true); } // Obtener uso a nivel de business $businessKeys = $this->redis->keys("fb:ratelimit:business:*:current") ?: []; foreach ($businessKeys as $key) { $data = $this->redis->get($key); if ($data) { $decoded = json_decode($data, true); $stats['business_usage'][] = $decoded; } } // Obtener alertas recientes $alerts = $this->redis->lrange("fb:ratelimit:alerts", 0, 9); foreach ($alerts as $alert) { $stats['alerts'][] = json_decode($alert, true); } // Determinar estado general $maxUsage = 0; if ($stats['app_usage']) { $maxUsage = max( $stats['app_usage']['call_count'] ?? 0, $stats['app_usage']['total_time'] ?? 0, $stats['app_usage']['total_cputime'] ?? 0 ); } if ($maxUsage >= self::CRITICAL_THRESHOLD) { $stats['status'] = 'critical'; } elseif ($maxUsage >= self::WARNING_THRESHOLD) { $stats['status'] = 'warning'; } return $stats; } catch (Exception $e) { error_log("RateLimitMonitor: Error obteniendo stats - " . $e->getMessage()); return ['error' => $e->getMessage()]; } } /** * Obtiene límites locales (del sistema) */ private function getLocalLimits(): array { try { // Intentar obtener de WhatsAppServiceWithRateLimit si existe if (class_exists('WhatsAppServiceWithRateLimit')) { $reflection = new ReflectionClass('WhatsAppServiceWithRateLimit'); return [ 'per_second' => $reflection->getConstant('RATE_LIMIT_PER_SECOND') ?: 80, 'per_hour' => $reflection->getConstant('RATE_LIMIT_PER_HOUR') ?: 1000, 'per_day' => $reflection->getConstant('RATE_LIMIT_PER_DAY') ?: 10000, ]; } } catch (Exception $e) { // Fallback a valores por defecto } return [ 'per_second' => 80, 'per_hour' => 1000, 'per_day' => 10000, ]; } /** * Obtiene historial de uso */ public function getUsageHistory(string $type = 'app', int $limit = 20): array { if (!$this->enabled) { return []; } try { $key = "fb:ratelimit:{$type}:history"; $items = $this->redis->lrange($key, 0, $limit - 1); $history = []; foreach ($items as $item) { $decoded = json_decode($item, true); if ($decoded) { $history[] = $decoded; } } return $history; } catch (Exception $e) { error_log("RateLimitMonitor: Error obteniendo historial - " . $e->getMessage()); return []; } } /** * Limpia datos antiguos (mantenimiento) */ public function cleanup(): void { if (!$this->enabled) return; try { // Limpiar alertas antiguas $this->redis->expire("fb:ratelimit:alerts", 86400 * 7); // Limpiar historiales antiguos $historyKeys = $this->redis->keys("fb:ratelimit:*:history") ?: []; foreach ($historyKeys as $key) { $this->redis->ltrim($key, 0, 99); } } catch (Exception $e) { error_log("RateLimitMonitor: Error en cleanup - " . $e->getMessage()); } } }