Spaces:
Paused
Paused
File size: 13,476 Bytes
4223796 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 | """
TelemetrΓa de Scrapers β CrowData.
Registra el estado de salud de cada scraper tras su ejecuciΓ³n:
- Γltimo estado (ok / error / bloqueado)
- Timestamp del ΓΊltimo run
- Tasa de Γ©xito acumulada (rolling window de 24h)
- Latencia promedio
Los resultados se almacenan en memoria (TTL de 48h) y se exponen
via el endpoint /reports/scrapers/health.
"""
import time
import logging
from collections import deque, defaultdict
from datetime import datetime, timezone
from threading import Lock
from typing import Optional
logger = logging.getLogger(__name__)
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Constantes
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
MAX_HISTORY = 50 # MΓ‘ximo de registros por scraper
WINDOW_SECS = 86400 # Ventana de cΓ‘lculo de tasa de Γ©xito: 24h
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Estado global en memoria (thread-safe vΓa Lock)
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
_lock = Lock()
# scraper_name -> deque of (timestamp_float, status: "ok"|"error"|"blocked", latency_ms: float)
_history: dict[str, deque] = defaultdict(lambda: deque(maxlen=MAX_HISTORY))
# scraper_name -> dict con datos del ΓΊltimo run
_last_run: dict[str, dict] = {}
# Metadatos estΓ‘ticos de cada scraper (descripciΓ³n y fuente)
SCRAPER_META = {
"ARCA/AFIP": {"fuente": "api.afip.gov.ar", "descripcion": "SituaciΓ³n fiscal, IVA, Monotributo"},
"BCRA": {"fuente": "api.bcra.gob.ar", "descripcion": "SituaciΓ³n crediticia y cheques rechazados"},
"BoletΓn Oficial": {"fuente": "www.boletinoficial.gob.ar", "descripcion": "Publicaciones oficiales"},
"Boletines Provinciales": {"fuente": "varios", "descripcion": "Boletines oficiales de 24 provincias"},
"IGJ": {"fuente": "sistemas.jus.gob.ar", "descripcion": "Sociedades y directivos registrados"},
"Poder Judicial": {"fuente": "scw.pjn.gov.ar", "descripcion": "Causas judiciales federales"},
"JUBA (Poder Judicial Bs As)":{"fuente": "juba.scba.gov.ar", "descripcion": "Causas en Justicia Bonaerense"},
"DNRPA / Automotores": {"fuente": "dnrpa.gov.ar", "descripcion": "VehΓculos registrados"},
"SINAI / Infracciones": {"fuente": "infraccionesba.gba.gob.ar", "descripcion": "Infracciones de trΓ‘nsito ANSV"},
"INPI (Marcas y Patentes)": {"fuente": "markaronline.inpi.gob.ar", "descripcion": "Marcas y patentes comerciales"},
"ANSES": {"fuente": "api.anses.gob.ar", "descripcion": "Aportes, jubilaciones, obra social"},
"Redes Sociales OSINT": {"fuente": "osint", "descripcion": "Perfiles pΓΊblicos en redes sociales"},
"TelefonΓa": {"fuente": "osint", "descripcion": "NΓΊmeros telefΓ³nicos vinculados"},
"RENAPER": {"fuente": "argentina.gob.ar", "descripcion": "ValidaciΓ³n de identidad RENAPER"},
"RENAPER Facial": {"fuente": "argentina.gob.ar", "descripcion": "BiometrΓa y estado de DNI"},
"Colegios Profesionales": {"fuente": "datos.gob.ar", "descripcion": "MatrΓculas profesionales activas"},
"RUIDO / SSSalud": {"fuente": "sssalud.gob.ar", "descripcion": "Cobertura de salud y obra social"},
"COMPR.AR / Contrataciones": {"fuente": "comprear.gob.ar", "descripcion": "Contratos con el Estado"},
"PadrΓ³n Electoral": {"fuente": "padron.gob.ar", "descripcion": "Datos del padrΓ³n electoral"},
"SGARHU / AcadΓ©mico": {"fuente": "siu.edu.ar", "descripcion": "TΓtulos universitarios oficiales"},
"SISCOP / Registro Civil": {"fuente": "siscop.gov.ar", "descripcion": "DetecciΓ³n de defunciΓ³n"},
"ARBA Automotores": {"fuente": "arba.gob.ar", "descripcion": "Deuda de patentes vehiculares ARBA"},
"ARBA Catastro": {"fuente": "arba.gob.ar", "descripcion": "Inmuebles y deuda inmobiliaria"},
"CNV (ComisiΓ³n Nacional de Valores)": {"fuente": "cnv.gob.ar", "descripcion": "Registro de agentes financieros"},
"UIF / PEPs": {"fuente": "uif.gob.ar", "descripcion": "Personas Expuestas PolΓticamente"},
"ARBA / AGIP": {"fuente": "arba.gob.ar", "descripcion": "Deudas impositivas provinciales"},
"Name Search OSINT": {"fuente": "osint", "descripcion": "BΓΊsqueda de nombre en fuentes abiertas"},
"PadrΓ³n RUIDO": {"fuente": "sssalud.gob.ar", "descripcion": "Obra social por CUIL"},
"CONTRATAR": {"fuente": "datos.gob.ar", "descripcion": "Contrataciones pΓΊblicas de obra pΓΊblica"},
"AFIP_CONSTANCIA": {"fuente": "soa.afip.gob.ar", "descripcion": "Constancia de inscripciΓ³n fiscal AFIP"},
"DEUDORES_ALIMENTARIOS": {"fuente": "rdam.mjus.gba.gob.ar", "descripcion": "Registro de deudores alimentarios morosos PBA"},
}
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# API de registro de resultados
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def record_scraper_result(
scraper_name: str,
status: str, # "ok" | "error" | "blocked" | "empty"
latency_ms: float,
detail: Optional[str] = None,
records_found: int = 0
):
"""
Registra el resultado de una ejecuciΓ³n de un scraper.
Llamar desde el orquestador tras cada scraper en service.py.
Args:
scraper_name: Nombre canΓ³nico del scraper (source_name)
status: "ok" si obtuvo datos, "empty" si ejecutΓ³ pero sin hallazgos,
"error" si lanzΓ³ excepciΓ³n, "blocked" si fue bloqueado antibot
latency_ms: Tiempo de ejecuciΓ³n en milisegundos
detail: Mensaje de error o detalle adicional opcional
records_found: Cantidad de registros obtenidos
"""
ts = time.time()
entry = {
"ts": ts,
"status": status,
"latency_ms": round(latency_ms, 1),
"detail": detail,
"records_found": records_found,
}
with _lock:
_history[scraper_name].append(entry)
_last_run[scraper_name] = {
**entry,
"last_seen": datetime.fromtimestamp(ts, tz=timezone.utc).isoformat(),
}
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# API de consulta de estado
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def get_scraper_health() -> list[dict]:
"""
Retorna la lista de scrapers con su estado de salud actual.
Returns:
Lista de dicts con: name, status, success_rate_24h, avg_latency_ms,
last_seen, records_found, fuente, descripcion
"""
now = time.time()
cutoff = now - WINDOW_SECS
results = []
# Incluir todos los scrapers conocidos, aunque no hayan corrido aΓΊn
all_names = set(SCRAPER_META.keys()) | set(_last_run.keys())
with _lock:
for name in sorted(all_names):
meta = SCRAPER_META.get(name, {"fuente": "desconocida", "descripcion": ""})
last = _last_run.get(name)
history = list(_history.get(name, []))
# Calcular tasa de Γ©xito en las ΓΊltimas 24h
window_entries = [e for e in history if e["ts"] >= cutoff]
if window_entries:
ok_count = sum(1 for e in window_entries if e["status"] in ("ok", "empty"))
success_rate = round(ok_count / len(window_entries) * 100, 1)
avg_latency = round(
sum(e["latency_ms"] for e in window_entries) / len(window_entries), 1
)
else:
success_rate = None
avg_latency = None
results.append({
"name": name,
"fuente": meta["fuente"],
"descripcion": meta["descripcion"],
"status": last["status"] if last else "sin_datos",
"last_seen": last["last_seen"] if last else None,
"latency_ms": last["latency_ms"] if last else None,
"avg_latency_ms_24h": avg_latency,
"success_rate_24h": success_rate,
"records_found_last": last["records_found"] if last else 0,
"detail": last.get("detail") if last else None,
"runs_24h": len(window_entries),
})
return results
def get_system_health_summary() -> dict:
"""
Resumen ejecutivo del estado del sistema de scrapers.
"""
health = get_scraper_health()
total = len(health)
statuses = [s["status"] for s in health]
ok_count = statuses.count("ok") + statuses.count("empty")
error_count = statuses.count("error")
blocked_count = statuses.count("blocked")
sin_datos = statuses.count("sin_datos")
return {
"total_scrapers": total,
"operativos": ok_count,
"con_error": error_count,
"bloqueados": blocked_count,
"sin_ejecutar": sin_datos,
"health_pct": round(ok_count / max(total - sin_datos, 1) * 100, 1),
"scrapers": health,
}
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Alertas de monitoreo
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
ALERT_CONSECUTIVE_ERRORS = 3 # Errores consecutivos para activar alerta
ALERT_MIN_RUNS = 5 # MΓnimo de runs antes de evaluar
ALERT_LOW_SUCCESS_PCT = 30 # % mΓnimo de Γ©xito en 24h para no alertar
def get_scrapers_alerts() -> list[dict]:
"""
Detecta scrapers con problemas consistentes y genera alertas.
Returns:
Lista de alertas con: scraper, tipo, severidad, mensaje, detalle
"""
alerts = []
health = get_scraper_health()
for s in health:
name = s["name"]
status = s["status"]
runs = s.get("runs_24h", 0)
success_rate = s.get("success_rate_24h")
last_detail = s.get("detail")
# Alert 1: Scraper con error actual
if status == "error" and runs >= ALERT_CONSECUTIVE_ERRORS:
alerts.append({
"scraper": name,
"tipo": "error_actual",
"severidad": "alta",
"mensaje": f"{name} tiene error activo con {runs} ejecuciones recientes",
"detalle": last_detail,
})
# Alert 2: Scraper bloqueado por antibot
if status == "blocked":
alerts.append({
"scraper": name,
"tipo": "bloqueado_antibot",
"severidad": "alta",
"mensaje": f"{name} bloqueado por sistema antibot",
"detalle": last_detail,
})
# Alert 3: Tasa de Γ©xito baja en 24h
if success_rate is not None and success_rate < ALERT_LOW_SUCCESS_PCT and runs >= ALERT_MIN_RUNS:
alerts.append({
"scraper": name,
"tipo": "baja_tasa_exito",
"severidad": "media",
"mensaje": f"{name} solo tiene {success_rate}% de Γ©xito en las ΓΊltimas 24h ({runs} runs)",
"detalle": None,
})
# Alert 4: Scraper que nunca corriΓ³
if status == "sin_datos":
alerts.append({
"scraper": name,
"tipo": "nunca_ejecutado",
"severidad": "baja",
"mensaje": f"{name} nunca ha sido ejecutado",
"detalle": None,
})
# Ordenar por severidad (alta > media > baja)
severity_order = {"alta": 0, "media": 1, "baja": 2}
alerts.sort(key=lambda a: severity_order.get(a["severidad"], 3))
return alerts
|