rmi-backend/app/_archive/legacy_2026_07/alert_pipeline.py
cryptorugmunch 628c1d2a10
Some checks failed
CI / build (push) Failing after 3s
refactor(rmi-backend,audit): mount Wave 3 + archive 136 dead-code files (P2.3)
PHASE 2.3 (AUDIT-2026-Q3.md):

Task 1 — Wire-in Wave 3 (1 router mounted, 2 deferred):
  - app.routers.unified_scanner_router mounted at /api/v2/scanner/* (2 routes:
    POST /api/v2/scanner/token/scan, POST /api/v2/scanner/wallet/scan).
    Refactored prefix from /api/v2 -> /api/v2/scanner to avoid future conflicts
    with the v1 /api/v1/scanner/ stub.
  - app.routers.unified_wallet_scanner DEFERRED (no router APIRouter attribute;
    library module consumed by unified_scanner_router via get_wallet_scanner()).
  - app.routers.admin_extensions DEFERRED (DORMANT per audit; 25 routes at
    /api/v1/admin/* would shadow /api/v1/admin/alerts_webhook).

Task 2 — Archive 136 dead-code files to app/_archive/legacy_2026_07/:
  - 73 routers in app/routers/ (reach graph showed zero reach into mount.py).
  - 63 flat app/*.py (domain modules never imported by live code).
  - 1 file RESTORED post-archive: app/routers/x402_bridge_health.py (caught by
    tests/unit/test_bridge_health.py which directly imports it; reach graph
    considered tests/ only as transitive reach — to be patched in next cycle).

Forced-LIVE (NOT archived per user directive):
  - app/ai_pipeline_v3.py  (3 importers in audit window, importers themselves DEAD)
  - app/splade_bm25.py       (LIVE via app.rag_service)
  - app/wallet_manager_v2.py (LIVE via x402_enforcement, x402_tools, sweep_all, sweep_now)
  - app/crypto_embeddings.py (NOT in audit ARCHIVE list; heavy import graph)

Verification (forward-import closure from mount.py + main.py + factory.py + lifespan.py):
  - imports = 348 app.* modules
  - reached = 194 files reachable from roots
  - archive set = audit_dead (186) - reached - forced_live (4) - test_live (1) = 136
  - Net delta: 136 files moved, 44,932 LOC reduction, 293->295 active routes (+2 from Wave 3)

pyproject.toml updates:
  - setuptools.packages.find: added exclude for app._archive*
  - ruff.extend-exclude: added "app/_archive/"
  - mypy.exclude: added "app/_archive/"

Smoke test: pytest tests/ — 817 passed, 3 pre-existing failures unchanged
(0 new failures; 0 routes lost; all 4 forced-LIVE files still importable).

Restoration: git mv app/_archive/legacy_2026_07/<name>.py <original-path>
and add the import to app/mount.py ROUTER_MODULES.

Refs: AUDIT-2026-Q3.md /home/dev/pry/rmi-final-deadcode-2026-07-06.md
2026-07-06 20:52:31 +02:00

356 lines
11 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.

#!/usr/bin/env python3
"""
Auto-Alerting Pipeline for RMI Platform
Send real-time alerts from detections to Telegram + webhooks
"""
import asyncio
import json
import os
from dataclasses import asdict, dataclass
from datetime import datetime
from enum import Enum
from typing import Any
import httpx
import redis
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from app.core.logging import get_logger
logger = get_logger(__name__)
class AlertLevel(Enum):
CRITICAL = "CRITICAL"
WARNING = "WARNING"
INFO = "INFO"
DEBUG = "DEBUG"
@dataclass
class Alert:
source: str
alert_type: str
level: str # "CRITICAL", "WARNING", "INFO", "DEBUG"
wallet_address: str
token_symbol: str
message: str
timestamp: str
confidence: float = 0.0
metadata: dict[str, Any] = None
def to_dict(self) -> dict:
return asdict(self)
def to_telegram_message(self) -> str:
"""Format alert as Telegram message"""
level_emoji = {"CRITICAL": "🚨", "WARNING": "⚠️", "INFO": "", "DEBUG": "🔍"} # noqa: RUF001
return (
f"{level_emoji.get(self.level, '')} *{self.alert_type.upper()}* [{self.level}]\n\n" # noqa: RUF001
f" Wallet: `{self.wallet_address[:10]}...{self.wallet_address[-6:]}`\n"
f" Token: `{self.token_symbol}`\n"
f" Confidence: {self.confidence:.0%}\n\n"
f"{self.message}\n\n"
f"📅 {self.timestamp}"
)
# Configuration
REDIS_HOST = os.getenv("REDIS_HOST", "172.20.0.3")
REDIS_PORT = int(os.getenv("REDIS_PORT", "6379"))
REDIS_PASSWORD = os.getenv("REDIS_PASSWORD", "")
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "")
TELEGRAM_ALERTS_CHANNEL = os.getenv("TELEGRAM_ALERTS_CHANNEL", "") # -100xxx format
WEBHOOK_URLS = [
os.getenv("ALERT_WEBHOOK_URL", ""),
os.getenv("SLACK_ALERT_WEBHOOK", ""),
]
# Rate limiting (max alerts per minute)
RATE_LIMIT_MAX = 5
RATE_LIMIT_WINDOW = 60 # seconds
# API Router
router = APIRouter(prefix="/api/v1/alerts", tags=["alerting"])
# Rate limit tracking via Redis
def get_rate_limit_key() -> str:
"""Get Redis key for rate limiting"""
return "alerting:rate_limit:counts"
def check_and_update_rate_limit() -> bool:
"""Check rate limit and increment counter. Returns True if under limit."""
try:
r = redis.Redis(host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD or None, decode_responses=True)
key = get_rate_limit_key()
current_time = int(datetime.now().timestamp())
window_key = f"{key}:{current_time // RATE_LIMIT_WINDOW}"
count = r.incr(window_key)
r.expire(window_key, RATE_LIMIT_WINDOW * 2)
return not count > RATE_LIMIT_MAX
except Exception as e:
logger.warning(f"Rate limit check failed: {e}")
return True # Allow on error
# Send to Telegram
async def send_to_telegram(alert: Alert) -> bool:
"""Send alert to Telegram channel"""
if not TELEGRAM_BOT_TOKEN or not TELEGRAM_ALERTS_CHANNEL:
logger.info("Telegram config missing, skipping")
return False
if not check_and_update_rate_limit():
logger.info("Rate limit exceeded, alert queued")
return False
try:
import telegram
bot = telegram.Bot(token=TELEGRAM_BOT_TOKEN)
message = alert.to_telegram_message()
await bot.send_message(chat_id=TELEGRAM_ALERTS_CHANNEL, text=message, parse_mode="MarkdownV2")
return True
except Exception as e:
logger.warning(f"Telegram send failed: {e}")
return False
# Send to webhooks
async def send_to_webhooks(alert: Alert) -> list[dict]:
"""Send alert to configured webhooks"""
results = []
for url in WEBHOOK_URLS:
if not url:
continue
try:
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.post(
url,
json={"alert": alert.to_dict(), "timestamp": datetime.now().isoformat()},
headers={"Content-Type": "application/json"},
)
results.append(
{
"url": url,
"status": response.status_code,
"success": response.status_code == 200,
}
)
except Exception as e:
results.append({"url": url, "status": "error", "error": str(e), "success": False})
return results
# Alert storage in Redis
def cache_alert(alert: Alert, ttl: int = 3600) -> bool:
"""Cache alert in Redis for downstream processing"""
try:
r = redis.Redis(host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD or None, decode_responses=True)
key = f"alerts:{alert.alert_type}:{alert.wallet_address}"
alert_json = json.dumps(alert.to_dict())
r.hset(
key,
mapping={
"alert_json": alert_json,
"created_at": datetime.now().isoformat(),
"level": alert.level,
},
)
r.expire(key, ttl)
return True
except Exception as e:
logger.warning(f"Cache alert failed: {e}")
return False
# Process and dispatch alert
async def process_alert(alert: Alert) -> dict[str, Any]:
"""Process alert: send to Telegram, webhooks, cache"""
results = {
"telegram": False,
"webhooks": [],
"cached": False,
"timestamp": datetime.now().isoformat(),
}
# Send to Telegram
if alert.level == "CRITICAL":
results["telegram"] = await send_to_telegram(alert)
# Send to webhooks (always for all levels)
if alert.level in ["CRITICAL", "WARNING"]:
results["webhooks"] = await send_to_webhooks(alert)
# Cache alert
results["cached"] = cache_alert(alert)
return results
# API Endpoints
class AlertCreateRequest(BaseModel):
source: str
alert_type: str
level: str = "INFO"
wallet_address: str
token_symbol: str
message: str
confidence: float = 0.0
metadata: dict[str, Any] = None
@router.post("/create")
async def create_alert(request: AlertCreateRequest):
"""Create and dispatch a new alert"""
alert = Alert(
source=request.source,
alert_type=request.alert_type,
level=request.level,
wallet_address=request.wallet_address,
token_symbol=request.token_symbol,
message=request.message,
timestamp=datetime.now().isoformat(),
confidence=request.confidence,
metadata=request.metadata or {},
)
results = await process_alert(alert)
return {
"success": True,
"alert_id": f"{alert.alert_type}:{alert.wallet_address}",
"dispatch": results,
}
@router.post("/webhook")
async def webhook_alert(notification: dict[str, Any]):
"""Receive external webhook and convert to internal alert"""
source = notification.get("source", "external")
alert_type = notification.get("alert_type", "unknown")
level = notification.get("level", "INFO")
alert = Alert(
source=source,
alert_type=alert_type,
level=level,
wallet_address=notification.get("wallet_address", "unknown"),
token_symbol=notification.get("token_symbol", "unknown"),
message=notification.get("message", ""),
timestamp=datetime.now().isoformat(),
confidence=notification.get("confidence", 0.0),
metadata=notification.get("metadata", {}),
)
results = await process_alert(alert)
return {
"success": True,
"alert_id": f"{alert.alert_type}:{alert.wallet_address}",
"dispatch": results,
}
@router.get("/status")
async def get_alert_status():
"""Get current alerting system status"""
try:
r = redis.Redis(host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD or None, decode_responses=True)
total_alerts = r.dbsize()
# Count alerts by type
alert_counts = {}
for key in r.scan_iter("alerts:*"):
if ":" in key:
alert_type = key.split(":")[1]
alert_counts[alert_type] = alert_counts.get(alert_type, 0) + 1
return {
"status": "ok",
"redis_connected": True,
"total_alerts_cached": total_alerts,
"alerts_by_type": alert_counts,
"rate_limit": {"max_per_minute": RATE_LIMIT_MAX, "window_seconds": RATE_LIMIT_WINDOW},
"webhooks_configured": len([w for w in WEBHOOK_URLS if w]),
"telegram_channel": TELEGRAM_ALERTS_CHANNEL if TELEGRAM_ALERTS_CHANNEL else "not_configured",
}
except Exception as e:
return {"status": "error", "error": str(e)}
@router.get("/recent")
async def get_recent_alerts(limit: int = 10):
"""Get recent alerts from cache"""
try:
r = redis.Redis(host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD or None, decode_responses=True)
alerts = []
for key in r.scan_iter("alerts:*", count=limit):
alert_data = r.hgetall(key)
if alert_data and "alert_json" in alert_data:
alert = json.loads(alert_data["alert_json"])
alerts.append(alert)
# Sort by timestamp (newest first)
alerts.sort(key=lambda x: x.get("timestamp", ""), reverse=True)
return {"success": True, "count": len(alerts), "alerts": alerts[:limit]}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) from e
# Background task: monitor alert queue
async def alert_pipeline_monitor():
"""Monitor Redis for new alerts from agents"""
logger.info(f"[{datetime.now().isoformat()}] Alert pipeline monitor started")
while True:
try:
r = redis.Redis(
host=REDIS_HOST,
port=REDIS_PORT,
password=REDIS_PASSWORD or None,
decode_responses=True,
)
# Check for alerts in queue
alert_json = r.rpop("social_monitor_queue")
if alert_json:
alert = json.loads(alert_json)
logger.info(f"Processing alert from queue: {alert.get('alert_type', 'unknown')}")
# Process alert through pipeline
result = await process_alert(Alert(**alert))
logger.info(f"Dispatch result: {result}")
alert_json = r.rpop("chain_walker_queue")
if alert_json:
alert = json.loads(alert_json)
logger.info(f"Processing chain-walker alert: {alert.get('alert_type', 'unknown')}")
result = await process_alert(Alert(**alert))
logger.info(f"Dispatch result: {result}")
# Check syndicate detection alerts
alert_json = r.rpop("syndicate_alerts")
if alert_json:
alert = json.loads(alert_json)
logger.info(f"Processing syndicate alert: {alert.get('wallet_address', 'unknown')}")
result = await process_alert(Alert(**alert))
logger.info(f"Dispatch result: {result}")
await asyncio.sleep(5) # Poll every 5 seconds
except Exception as e:
logger.warning(f"Alert pipeline monitor error: {e}")
await asyncio.sleep(10)
def get_alert_pipeline_router():
"""Get the alerting router"""
return router