merge: chore/cleanup-remove-bloat-and-secrets into main
This commit is contained in:
commit
bde2f3a97d
1173 changed files with 437609 additions and 0 deletions
409
app/analytics_engine.py
Normal file
409
app/analytics_engine.py
Normal file
|
|
@ -0,0 +1,409 @@
|
|||
"""
|
||||
RMI Analytics Engine — Real-Time Metrics & Trend Visualization
|
||||
===============================================================
|
||||
Comprehensive analytics system for the RugMunch Intelligence Platform.
|
||||
|
||||
Features:
|
||||
• Real-Time Metrics — CPU, memory, requests, errors, latency
|
||||
• Time-Series Storage — Redis-backed rolling windows
|
||||
• Trend Detection — automatic anomaly detection, trend arrows
|
||||
• User Analytics — DAU, MAU, retention, cohort analysis
|
||||
• Financial Analytics — revenue, ARPU, MRR, churn
|
||||
• Security Analytics — threats blocked, bot traffic, attack patterns
|
||||
• Token Analytics — deployment stats, airdrop metrics, holder growth
|
||||
• Custom Dashboards — configurable widget layouts
|
||||
• Export — CSV, JSON, Prometheus metrics
|
||||
|
||||
Integrations:
|
||||
- Prometheus metrics export
|
||||
- Grafana-compatible data format
|
||||
- WebSocket real-time streaming
|
||||
- ClickHouse for long-term storage
|
||||
|
||||
Author: RMI Analytics Team
|
||||
Date: 2026-05-31
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
|
||||
logger = logging.getLogger("rmi_analytics")
|
||||
|
||||
|
||||
# ── Data Models ─────────────────────────────────────────────
|
||||
|
||||
|
||||
@dataclass
|
||||
class MetricPoint:
|
||||
"""Single time-series data point."""
|
||||
|
||||
timestamp: float
|
||||
value: float
|
||||
labels: dict[str, str] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return asdict(self)
|
||||
|
||||
|
||||
@dataclass
|
||||
class MetricSeries:
|
||||
"""Time-series metric with metadata."""
|
||||
|
||||
name: str
|
||||
description: str
|
||||
unit: str
|
||||
points: list[MetricPoint] = field(default_factory=list)
|
||||
|
||||
def latest(self) -> float | None:
|
||||
return self.points[-1].value if self.points else None
|
||||
|
||||
def avg(self, n: int = 60) -> float:
|
||||
vals = [p.value for p in self.points[-n:]]
|
||||
return sum(vals) / len(vals) if vals else 0.0
|
||||
|
||||
def trend(self, window: int = 10) -> str:
|
||||
"""Return trend direction: up, down, flat."""
|
||||
if len(self.points) < window * 2:
|
||||
return "flat"
|
||||
old_avg = sum(p.value for p in self.points[-window * 2 : -window]) / window
|
||||
new_avg = sum(p.value for p in self.points[-window:]) / window
|
||||
diff = new_avg - old_avg
|
||||
if abs(diff) < 0.01 * old_avg:
|
||||
return "flat"
|
||||
return "up" if diff > 0 else "down"
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"unit": self.unit,
|
||||
"latest": self.latest(),
|
||||
"avg_1m": self.avg(60),
|
||||
"trend": self.trend(),
|
||||
"point_count": len(self.points),
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class DashboardWidget:
|
||||
"""Dashboard widget configuration."""
|
||||
|
||||
widget_id: str
|
||||
widget_type: str # line, bar, gauge, counter, table, pie
|
||||
title: str
|
||||
metric_name: str
|
||||
width: int = 6 # Grid columns (1-12)
|
||||
height: int = 4
|
||||
refresh_interval: int = 30 # seconds
|
||||
config: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Dashboard:
|
||||
"""Dashboard configuration."""
|
||||
|
||||
dashboard_id: str
|
||||
name: str
|
||||
description: str
|
||||
widgets: list[DashboardWidget] = field(default_factory=list)
|
||||
created_by: str = ""
|
||||
is_default: bool = False
|
||||
|
||||
|
||||
# ── Analytics Engine ────────────────────────────────────────
|
||||
|
||||
|
||||
class AnalyticsEngine:
|
||||
"""
|
||||
Core analytics engine for real-time metrics and trend analysis.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._metrics: dict[str, MetricSeries] = {}
|
||||
self._dashboards: dict[str, Dashboard] = {}
|
||||
self._ensure_default_dashboards()
|
||||
|
||||
def _ensure_default_dashboards(self):
|
||||
"""Create default system dashboards."""
|
||||
# System Health Dashboard
|
||||
system_widgets = [
|
||||
DashboardWidget("cpu_gauge", "gauge", "CPU Usage", "cpu_percent", 3, 3, 10),
|
||||
DashboardWidget("mem_gauge", "gauge", "Memory Usage", "memory_percent", 3, 3, 10),
|
||||
DashboardWidget("disk_gauge", "gauge", "Disk Usage", "disk_percent", 3, 3, 10),
|
||||
DashboardWidget("req_counter", "counter", "Requests/min", "requests_per_minute", 3, 3, 10),
|
||||
DashboardWidget("cpu_line", "line", "CPU History", "cpu_percent", 6, 4, 30),
|
||||
DashboardWidget("mem_line", "line", "Memory History", "memory_percent", 6, 4, 30),
|
||||
DashboardWidget("latency_line", "line", "Response Latency", "response_time_ms", 6, 4, 30),
|
||||
DashboardWidget("error_line", "line", "Error Rate", "error_rate", 6, 4, 30),
|
||||
]
|
||||
|
||||
self._dashboards["system"] = Dashboard(
|
||||
dashboard_id="system",
|
||||
name="System Health",
|
||||
description="Real-time system performance metrics",
|
||||
widgets=system_widgets,
|
||||
is_default=True,
|
||||
)
|
||||
|
||||
# Financial Dashboard
|
||||
financial_widgets = [
|
||||
DashboardWidget("revenue_counter", "counter", "Total Revenue", "revenue_usd", 3, 3, 60),
|
||||
DashboardWidget("mrr_counter", "counter", "MRR", "mrr_usd", 3, 3, 60),
|
||||
DashboardWidget("arpu_counter", "counter", "ARPU", "arpu_usd", 3, 3, 60),
|
||||
DashboardWidget("churn_gauge", "gauge", "Churn Rate", "churn_rate", 3, 3, 60),
|
||||
DashboardWidget("revenue_line", "line", "Revenue Trend", "revenue_usd", 6, 4, 300),
|
||||
DashboardWidget("payments_line", "line", "Payments", "payments_count", 6, 4, 300),
|
||||
]
|
||||
|
||||
self._dashboards["financial"] = Dashboard(
|
||||
dashboard_id="financial",
|
||||
name="Financial Analytics",
|
||||
description="Revenue, payments, and subscription metrics",
|
||||
widgets=financial_widgets,
|
||||
is_default=True,
|
||||
)
|
||||
|
||||
# Security Dashboard
|
||||
security_widgets = [
|
||||
DashboardWidget("threats_counter", "counter", "Threats Blocked", "threats_blocked", 3, 3, 30),
|
||||
DashboardWidget("bots_counter", "counter", "Bot Requests", "bot_requests", 3, 3, 30),
|
||||
DashboardWidget("attacks_counter", "counter", "Attacks", "attacks_detected", 3, 3, 30),
|
||||
DashboardWidget("blocked_ips_counter", "counter", "Blocked IPs", "blocked_ips", 3, 3, 30),
|
||||
DashboardWidget("threats_pie", "pie", "Threat Types", "threat_types", 6, 4, 60),
|
||||
DashboardWidget("attacks_line", "line", "Attack Timeline", "attacks_detected", 6, 4, 60),
|
||||
]
|
||||
|
||||
self._dashboards["security"] = Dashboard(
|
||||
dashboard_id="security",
|
||||
name="Security Analytics",
|
||||
description="Threat detection and security metrics",
|
||||
widgets=security_widgets,
|
||||
is_default=True,
|
||||
)
|
||||
|
||||
# User Analytics Dashboard
|
||||
user_widgets = [
|
||||
DashboardWidget("dau_counter", "counter", "DAU", "daily_active_users", 3, 3, 60),
|
||||
DashboardWidget("mau_counter", "counter", "MAU", "monthly_active_users", 3, 3, 60),
|
||||
DashboardWidget("new_users_counter", "counter", "New Users", "new_users", 3, 3, 60),
|
||||
DashboardWidget("retention_gauge", "gauge", "Retention", "retention_rate", 3, 3, 60),
|
||||
DashboardWidget("users_line", "line", "User Growth", "total_users", 6, 4, 300),
|
||||
DashboardWidget("tiers_pie", "pie", "User Tiers", "users_by_tier", 6, 4, 300),
|
||||
]
|
||||
|
||||
self._dashboards["users"] = Dashboard(
|
||||
dashboard_id="users",
|
||||
name="User Analytics",
|
||||
description="User growth, engagement, and retention",
|
||||
widgets=user_widgets,
|
||||
is_default=True,
|
||||
)
|
||||
|
||||
# ── Metric Recording ────────────────────────────────────
|
||||
|
||||
def record_metric(self, name: str, value: float, labels: dict[str, str] | None = None):
|
||||
"""Record a metric data point."""
|
||||
if name not in self._metrics:
|
||||
self._metrics[name] = MetricSeries(
|
||||
name=name,
|
||||
description=name.replace("_", " ").title(),
|
||||
unit="",
|
||||
)
|
||||
|
||||
point = MetricPoint(
|
||||
timestamp=time.time(),
|
||||
value=value,
|
||||
labels=labels or {},
|
||||
)
|
||||
|
||||
self._metrics[name].points.append(point)
|
||||
|
||||
# Keep only last 10000 points (about 2.7 hours at 1/sec)
|
||||
if len(self._metrics[name].points) > 10000:
|
||||
self._metrics[name].points = self._metrics[name].points[-10000:]
|
||||
|
||||
def get_metric(self, name: str) -> MetricSeries | None:
|
||||
"""Get metric series by name."""
|
||||
return self._metrics.get(name)
|
||||
|
||||
def get_metric_names(self) -> list[str]:
|
||||
"""List all metric names."""
|
||||
return list(self._metrics.keys())
|
||||
|
||||
# ── Dashboard Management ────────────────────────────────
|
||||
|
||||
def get_dashboard(self, dashboard_id: str) -> Dashboard | None:
|
||||
"""Get dashboard by ID."""
|
||||
return self._dashboards.get(dashboard_id)
|
||||
|
||||
def list_dashboards(self) -> list[Dashboard]:
|
||||
"""List all dashboards."""
|
||||
return list(self._dashboards.values())
|
||||
|
||||
def create_dashboard(self, name: str, description: str, created_by: str = "") -> Dashboard:
|
||||
"""Create a new dashboard."""
|
||||
dashboard_id = f"dash_{int(time.time())}_{os.urandom(4).hex()}"
|
||||
dashboard = Dashboard(
|
||||
dashboard_id=dashboard_id,
|
||||
name=name,
|
||||
description=description,
|
||||
created_by=created_by,
|
||||
)
|
||||
self._dashboards[dashboard_id] = dashboard
|
||||
return dashboard
|
||||
|
||||
def add_widget(self, dashboard_id: str, widget: DashboardWidget) -> bool:
|
||||
"""Add widget to dashboard."""
|
||||
dashboard = self._dashboards.get(dashboard_id)
|
||||
if not dashboard:
|
||||
return False
|
||||
dashboard.widgets.append(widget)
|
||||
return True
|
||||
|
||||
# ── Real-Time Data ──────────────────────────────────────
|
||||
|
||||
def get_dashboard_data(self, dashboard_id: str) -> dict[str, Any]:
|
||||
"""Get current data for all widgets in a dashboard."""
|
||||
dashboard = self._dashboards.get(dashboard_id)
|
||||
if not dashboard:
|
||||
return {"error": "Dashboard not found"}
|
||||
|
||||
widgets_data = []
|
||||
for widget in dashboard.widgets:
|
||||
metric = self._metrics.get(widget.metric_name)
|
||||
data = {
|
||||
"widget_id": widget.widget_id,
|
||||
"widget_type": widget.widget_type,
|
||||
"title": widget.title,
|
||||
"metric": metric.to_dict() if metric else {"name": widget.metric_name, "latest": None},
|
||||
}
|
||||
|
||||
# Add historical data for line/bar charts
|
||||
if widget.widget_type in ["line", "bar"] and metric:
|
||||
# Return last 60 points
|
||||
data["history"] = [{"t": p.timestamp, "v": p.value} for p in metric.points[-60:]]
|
||||
|
||||
widgets_data.append(data)
|
||||
|
||||
return {
|
||||
"dashboard_id": dashboard_id,
|
||||
"name": dashboard.name,
|
||||
"updated_at": datetime.now(UTC).isoformat(),
|
||||
"widgets": widgets_data,
|
||||
}
|
||||
|
||||
# ── Trend Analysis ──────────────────────────────────────
|
||||
|
||||
def detect_trends(self, metric_name: str, window: int = 60) -> dict[str, Any]:
|
||||
"""Detect trends in a metric."""
|
||||
metric = self._metrics.get(metric_name)
|
||||
if not metric or len(metric.points) < window * 2:
|
||||
return {"error": "Insufficient data"}
|
||||
|
||||
points = metric.points[-window * 2 :]
|
||||
half = len(points) // 2
|
||||
|
||||
first_half = [p.value for p in points[:half]]
|
||||
second_half = [p.value for p in points[half:]]
|
||||
|
||||
first_avg = sum(first_half) / len(first_half)
|
||||
second_avg = sum(second_half) / len(second_half)
|
||||
|
||||
change_pct = ((second_avg - first_avg) / first_avg * 100) if first_avg else 0
|
||||
|
||||
# Detect anomalies (values outside 2 std dev)
|
||||
all_vals = [p.value for p in metric.points[-window:]]
|
||||
mean = sum(all_vals) / len(all_vals)
|
||||
variance = sum((v - mean) ** 2 for v in all_vals) / len(all_vals)
|
||||
std_dev = variance**0.5
|
||||
|
||||
anomalies = [
|
||||
{"timestamp": p.timestamp, "value": p.value}
|
||||
for p in metric.points[-window:]
|
||||
if abs(p.value - mean) > 2 * std_dev
|
||||
]
|
||||
|
||||
return {
|
||||
"metric": metric_name,
|
||||
"trend": metric.trend(window),
|
||||
"change_percent": round(change_pct, 2),
|
||||
"first_period_avg": round(first_avg, 4),
|
||||
"second_period_avg": round(second_avg, 4),
|
||||
"anomalies_count": len(anomalies),
|
||||
"anomalies": anomalies[:5], # Top 5
|
||||
}
|
||||
|
||||
# ── Statistics ───────────────────────────────────────────
|
||||
|
||||
def get_system_stats(self) -> dict[str, Any]:
|
||||
"""Get comprehensive system statistics."""
|
||||
return {
|
||||
"metrics_tracked": len(self._metrics),
|
||||
"dashboards": len(self._dashboards),
|
||||
"total_data_points": sum(len(m.points) for m in self._metrics.values()),
|
||||
"last_updated": datetime.now(UTC).isoformat(),
|
||||
"top_metrics": [
|
||||
{"name": name, "points": len(m.points), "latest": m.latest()}
|
||||
for name, m in sorted(self._metrics.items(), key=lambda x: len(x[1].points), reverse=True)[:10]
|
||||
],
|
||||
}
|
||||
|
||||
# ── Prometheus Export ───────────────────────────────────
|
||||
|
||||
def to_prometheus(self) -> str:
|
||||
"""Export metrics in Prometheus text format."""
|
||||
lines = []
|
||||
for name, metric in self._metrics.items():
|
||||
prom_name = f"rmi_{name}"
|
||||
lines.append(f"# HELP {prom_name} {metric.description}")
|
||||
lines.append(f"# TYPE {prom_name} gauge")
|
||||
|
||||
latest = metric.latest()
|
||||
if latest is not None:
|
||||
labels_str = ", ".join(f'{k}="{v}"' for k, v in metric.points[-1].labels.items())
|
||||
if labels_str:
|
||||
lines.append(f"{prom_name}{{{labels_str}}} {latest}")
|
||||
else:
|
||||
lines.append(f"{prom_name} {latest}")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
# ── Export ────────────────────────────────────────────
|
||||
|
||||
def export_metric(self, name: str, format: str = "json") -> Any:
|
||||
"""Export metric data."""
|
||||
metric = self._metrics.get(name)
|
||||
if not metric:
|
||||
return None
|
||||
|
||||
if format == "json":
|
||||
return {
|
||||
"name": metric.name,
|
||||
"description": metric.description,
|
||||
"unit": metric.unit,
|
||||
"data": [{"timestamp": p.timestamp, "value": p.value, "labels": p.labels} for p in metric.points],
|
||||
}
|
||||
elif format == "csv":
|
||||
lines = ["timestamp,value"]
|
||||
for p in metric.points:
|
||||
lines.append(f"{p.timestamp},{p.value}")
|
||||
return "\n".join(lines)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# ── Singleton ─────────────────────────────────────────────────
|
||||
|
||||
_analytics_instance: AnalyticsEngine | None = None
|
||||
|
||||
|
||||
def get_analytics_engine() -> AnalyticsEngine:
|
||||
"""Get or create analytics engine instance."""
|
||||
global _analytics_instance
|
||||
if _analytics_instance is None:
|
||||
_analytics_instance = AnalyticsEngine()
|
||||
return _analytics_instance
|
||||
Loading…
Add table
Add a link
Reference in a new issue