up
This commit is contained in:
@@ -0,0 +1,325 @@
|
||||
<?php
|
||||
/**
|
||||
* Servicio de Monitoreo de Rate Limiting de Facebook
|
||||
* Captura y analiza headers X-App-Usage y X-Business-Use-Case-Usage
|
||||
* Fecha: 6 de febrero de 2026
|
||||
*/
|
||||
|
||||
use Predis\Client as RedisClient;
|
||||
|
||||
class RateLimitMonitor {
|
||||
private $redis;
|
||||
private $enabled;
|
||||
|
||||
// Umbrales de alerta
|
||||
const WARNING_THRESHOLD = 75; // Advertencia al 75%
|
||||
const CRITICAL_THRESHOLD = 90; // Crítico al 90%
|
||||
|
||||
public function __construct() {
|
||||
try {
|
||||
// Usar el mismo patrón que RedisQueue
|
||||
$this->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());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user