V2 Pro: Logging rotativo, Redis cache, Health Check de LibreTranslate y Rate Limiting en botones (#7)

This commit is contained in:
2026-03-20 18:03:56 -06:00
parent 57d570ad31
commit 06da793709
13 changed files with 264 additions and 17 deletions

0
utils/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

83
utils/cache.py Normal file
View File

@@ -0,0 +1,83 @@
"""
Módulo de caché Redis - Bots de Traducción
Proporciona una interfaz unificada de caché con Redis como backend
y fallback a memoria RAM si Redis no está disponible.
"""
import os
import redis
from utils.logger import discord_logger as log
_redis_client = None
def get_redis() -> redis.Redis | None:
"""Retorna cliente Redis, intentando conectar si no existe. Devuelve None si no disponible."""
global _redis_client
if _redis_client is not None:
return _redis_client
host = os.getenv("REDIS_HOST", "localhost")
port = int(os.getenv("REDIS_PORT", "6379"))
password = os.getenv("REDIS_PASSWORD", "translation_redis_secret")
db = int(os.getenv("REDIS_DB", "0"))
try:
client = redis.Redis(
host=host,
port=port,
password=password,
db=db,
decode_responses=True,
socket_connect_timeout=2,
socket_timeout=2
)
client.ping()
_redis_client = client
log.info(f"✅ Redis conectado en {host}:{port}")
return _redis_client
except Exception as e:
log.warning(f"⚠️ Redis no disponible, usando caché en RAM: {e}")
return None
# ─── API de caché genérica (con TTL en segundos) ───────────────────────────
def cache_get(key: str) -> str | None:
"""Obtiene un valor del caché. Devuelve None si no existe o si Redis no está disponible."""
r = get_redis()
if r:
try:
return r.get(key)
except Exception:
pass
return None
def cache_set(key: str, value: str, ttl: int = 86400) -> None:
"""Guarda un valor en el caché con TTL en segundos (default 24h)."""
r = get_redis()
if r:
try:
r.setex(key, ttl, value)
except Exception:
pass
def cache_delete(key: str) -> None:
"""Elimina una clave del caché."""
r = get_redis()
if r:
try:
r.delete(key)
except Exception:
pass
def cache_increment(key: str, ttl: int = 60) -> int:
"""Incrementa un contador atómico en Redis. Ideal para Rate Limiting."""
r = get_redis()
if r:
try:
pipe = r.pipeline()
pipe.incr(key)
pipe.expire(key, ttl)
results = pipe.execute()
return results[0]
except Exception:
pass
return 0

50
utils/logger.py Normal file
View File

@@ -0,0 +1,50 @@
"""
Módulo de Logging Centralizado - Bots de Traducción
Proporciona loggers rotativos por día para Discord, Telegram y Panel Web.
"""
import logging
import os
from logging.handlers import TimedRotatingFileHandler
# Directorio base para almacenar los logs
LOG_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "logs")
os.makedirs(LOG_DIR, exist_ok=True)
def _create_logger(name: str, filename: str, level=logging.INFO) -> logging.Logger:
"""Crea y configura un logger con salida a archivo rotativo y a consola."""
logger = logging.getLogger(name)
if logger.handlers:
return logger # Ya está configurado, no duplicar handlers
logger.setLevel(level)
formatter = logging.Formatter(
"[%(asctime)s] %(levelname)s [%(name)s] %(message)s",
datefmt="%Y-%m-%d %H:%M:%S"
)
# Handler de archivo: rota a medianoche, guarda los últimos 14 días
file_handler = TimedRotatingFileHandler(
os.path.join(LOG_DIR, filename),
when="midnight",
interval=1,
backupCount=14,
encoding="utf-8"
)
file_handler.setFormatter(formatter)
file_handler.suffix = "%Y-%m-%d.log"
# Handler de consola para visualización en Docker
console_handler = logging.StreamHandler()
console_handler.setFormatter(formatter)
logger.addHandler(file_handler)
logger.addHandler(console_handler)
return logger
# Loggers disponibles para cada componente del ecosistema
discord_logger = _create_logger("discord_bot", "discord.log")
telegram_logger = _create_logger("telegram_bot", "telegram.log")
panel_logger = _create_logger("panel_web", "panel.log")