File size: 15,902 Bytes
bde2f3a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
"""
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