Files
2026-06-09 21:18:13 -03:00

320 lines
10 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Stats.py
Contiene la lógica de los endpoints utilizados para crear y consultar datos estadísticos.
"""
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from sqlalchemy import func, cast, Integer, extract
from database import get_db
from typing import Optional
from datetime import datetime, timedelta
import models
from auth import get_current_user
router = APIRouter()
@router.get("/stats/", tags=["Stats"])
def get_stats(
date_from: Optional[datetime] = None,
date_to: Optional[datetime] = None,
db: Session = Depends(get_db),
current_user: str = Depends(get_current_user),
):
"""
Devuelve todas las estadísticas agregadas en una sola llamada.
Si no se pasan fechas, usa los últimos 30 días.
"""
if date_to is None:
date_to = datetime.utcnow()
if date_from is None:
date_from = date_to - timedelta(days=30)
# ------------------------------------------------------------------
# 1. Resumen ejecutivo
# ------------------------------------------------------------------
total_alertas = db.query(models.Alert).filter(
models.Alert.created_at >= date_from,
models.Alert.created_at <= date_to
).count()
alertas_abiertas = db.query(models.Alert).filter(
models.Alert.created_at >= date_from,
models.Alert.created_at <= date_to,
models.Alert.status == "open"
).count()
alertas_en_progreso = db.query(models.Alert).filter(
models.Alert.created_at >= date_from,
models.Alert.created_at <= date_to,
models.Alert.status == "in_progress"
).count()
alertas_cerradas = total_alertas - alertas_abiertas - alertas_en_progreso
total_mensajes = db.query(models.Message).filter(
models.Message.date >= date_from,
models.Message.date <= date_to
).count()
grupos_activos = db.query(models.Group).filter(
models.Group.type != "P"
).count()
reglas_activas = db.query(models.Rule).filter(
models.Rule.is_active == True
).count()
# ------------------------------------------------------------------
# 2. Alertas por día (para gráfico de línea)
# ------------------------------------------------------------------
alertas_por_dia_raw = (
db.query(
func.date(models.Alert.created_at).label("dia"),
models.Alert.status,
func.count(models.Alert.id).label("total")
)
.filter(
models.Alert.created_at >= date_from,
models.Alert.created_at <= date_to
)
.group_by(
func.date(models.Alert.created_at),
models.Alert.status
)
.order_by(func.date(models.Alert.created_at))
.all()
)
alertas_por_dia = {}
for row in alertas_por_dia_raw:
dia = str(row.dia)
if dia not in alertas_por_dia:
alertas_por_dia[dia] = {"abiertas": 0, "cerradas": 0}
if row.status == "open":
alertas_por_dia[dia]["abiertas"] = row.total
else:
alertas_por_dia[dia]["cerradas"] = row.total
# ------------------------------------------------------------------
# 3. Distribución por severidad
# ------------------------------------------------------------------
sev_raw = (
db.query(
models.Rule.severity,
func.count(models.Alert.id).label("total")
)
.join(models.Alert, models.Alert.rule_id == models.Rule.id)
.filter(
models.Alert.created_at >= date_from,
models.Alert.created_at <= date_to
)
.group_by(models.Rule.severity)
.all()
)
distribucion_severidad = {row.severity: row.total for row in sev_raw}
# ------------------------------------------------------------------
# 4. Alertas por grupo
# ------------------------------------------------------------------
alertas_por_grupo_raw = (
db.query(
models.Alert.group_id,
func.count(models.Alert.id).label("total")
)
.filter(
models.Alert.created_at >= date_from,
models.Alert.created_at <= date_to
)
.group_by(models.Alert.group_id)
.order_by(func.count(models.Alert.id).desc())
.limit(10)
.all()
)
# Resolver nombres de grupos
group_ids = [r.group_id for r in alertas_por_grupo_raw]
grupos_map = {
g.id_telegram: g.name
for g in db.query(models.Group).filter(
models.Group.id_telegram.in_(group_ids)
).all()
}
alertas_por_grupo = [
{
"grupo": grupos_map.get(r.group_id, str(r.group_id)),
"total": r.total
}
for r in alertas_por_grupo_raw
]
# ------------------------------------------------------------------
# 5. Reglas más disparadas (top 10)
# ------------------------------------------------------------------
reglas_top_raw = (
db.query(
models.Rule.id,
models.Rule.description,
models.Rule.severity,
func.count(models.Alert.id).label("total")
)
.join(models.Alert, models.Alert.rule_id == models.Rule.id)
.filter(
models.Alert.created_at >= date_from,
models.Alert.created_at <= date_to
)
.group_by(models.Rule.id, models.Rule.description, models.Rule.severity)
.order_by(func.count(models.Alert.id).desc())
.limit(10)
.all()
)
reglas_top = [
{
"id": row.id,
"description": row.description,
"severity": row.severity,
"total": row.total
}
for row in reglas_top_raw
]
# ------------------------------------------------------------------
# 6. Volumen de mensajes por día
# ------------------------------------------------------------------
mensajes_por_dia_raw = (
db.query(
func.date(models.Message.date).label("dia"),
func.count(models.Message.id_mess_g).label("total")
)
.filter(
models.Message.date >= date_from,
models.Message.date <= date_to
)
.group_by(func.date(models.Message.date))
.order_by(func.date(models.Message.date))
.all()
)
mensajes_por_dia = {str(row.dia): row.total for row in mensajes_por_dia_raw}
# ------------------------------------------------------------------
# 7. Heatmap: mensajes por hora × día de semana
# ------------------------------------------------------------------
# MariaDB usa DAYOFWEEK() (1=Dom..7=Sab) y HOUR() en lugar de extract("dow"/"hour")
from sqlalchemy import text
heatmap_raw = (
db.query(
func.dayofweek(models.Message.date).label("dia_semana"),
func.hour(models.Message.date).label("hora"),
func.count(models.Message.id_mess_g).label("total")
)
.filter(
models.Message.date >= date_from,
models.Message.date <= date_to
)
.group_by(
func.dayofweek(models.Message.date),
func.hour(models.Message.date)
)
.all()
)
# MariaDB DAYOFWEEK: 1=Dom, 2=Lun, ..., 7=Sáb
DIAS = {1: "Dom", 2: "Lun", 3: "Mar", 4: "Mié", 5: "Jue", 6: "Vie", 7: "Sáb"}
heatmap = [
{
"dia": DIAS.get(int(row.dia_semana), str(row.dia_semana)),
"hora": int(row.hora),
"total": row.total
}
for row in heatmap_raw
]
# ------------------------------------------------------------------
# 8. Top senders por actividad
# ------------------------------------------------------------------
top_senders_raw = (
db.query(
models.Message.sender_id,
func.count(models.Message.id_mess_g).label("mensajes")
)
.filter(
models.Message.date >= date_from,
models.Message.date <= date_to
)
.group_by(models.Message.sender_id)
.order_by(func.count(models.Message.id_mess_g).desc())
.limit(10)
.all()
)
sender_ids = [r.sender_id for r in top_senders_raw]
senders_map = {
s.id_telegram: s
for s in db.query(models.Sender).filter(
models.Sender.id_telegram.in_(sender_ids)
).all()
}
# Contar alertas por sender
alertas_sender_raw = (
db.query(
models.Message.sender_id,
func.count(models.Alert.id).label("alertas")
)
.join(models.Alert, (
models.Alert.message_id == models.Message.id_mess_g) & (
models.Alert.group_id == models.Message.group_id)
)
.filter(
models.Message.date >= date_from,
models.Message.date <= date_to,
models.Message.sender_id.in_(sender_ids)
)
.group_by(models.Message.sender_id)
.all()
)
alertas_por_sender = {r.sender_id: r.alertas for r in alertas_sender_raw}
top_senders = []
for row in top_senders_raw:
s = senders_map.get(row.sender_id)
nombre = ""
if s:
nombre = f"{s.first_name or ''} {s.last_name or ''}".strip() or s.username or str(row.sender_id)
else:
nombre = str(row.sender_id)
top_senders.append({
"id_telegram": row.sender_id,
"nombre": nombre,
"username": s.username if s else None,
"mensajes": row.mensajes,
"alertas": alertas_por_sender.get(row.sender_id, 0),
})
# ------------------------------------------------------------------
# Respuesta final
# ------------------------------------------------------------------
return {
"periodo": {
"desde": date_from.isoformat(),
"hasta": date_to.isoformat(),
},
"resumen": {
"total_alertas": total_alertas,
"alertas_abiertas": alertas_abiertas,
"alertas_cerradas": alertas_cerradas,
"total_mensajes": total_mensajes,
"grupos_activos": grupos_activos,
"reglas_activas": reglas_activas,
},
"alertas_por_dia": alertas_por_dia,
"distribucion_severidad": distribucion_severidad,
"alertas_por_grupo": alertas_por_grupo,
"reglas_top": reglas_top,
"mensajes_por_dia": mensajes_por_dia,
"heatmap": heatmap,
"top_senders": top_senders,
}