diff --git a/nginxmon/templates/nginxmon/dashboard.html b/nginxmon/templates/nginxmon/dashboard.html index 3763314..c9c8292 100644 --- a/nginxmon/templates/nginxmon/dashboard.html +++ b/nginxmon/templates/nginxmon/dashboard.html @@ -2,6 +2,8 @@ {% block content %} + +
@@ -111,6 +113,15 @@
+ +
+
+
Traffic by Region — {{ range_label }}
+
+
+
+
+
@@ -413,4 +424,160 @@ function _applyToggleUI(enabled) { es.onerror = function() { es.close(); setTimeout(startStatusStream, 5000); }; })(); + + {% endblock %} diff --git a/nginxmon/urls.py b/nginxmon/urls.py index 44ad6a0..5903c31 100644 --- a/nginxmon/urls.py +++ b/nginxmon/urls.py @@ -26,4 +26,5 @@ urlpatterns = [ path('api/ingest/', views.IngestLogsView.as_view(), name='nginxmon-ingest'), path('api/toggle-ingestion/', views.ToggleIngestionView.as_view(), name='nginxmon-toggle'), path('api/status-stream/', views.StatusStreamView.as_view(), name='nginxmon-status-stream'), + path('api/geo-stats/', views.GeoStatsView.as_view(), name='nginxmon-geo-stats'), ] diff --git a/nginxmon/views.py b/nginxmon/views.py index 7ae5e2c..73c023a 100644 --- a/nginxmon/views.py +++ b/nginxmon/views.py @@ -16,7 +16,7 @@ from django.views.generic import CreateView, DeleteView, TemplateView, UpdateVie from .detector import detect_threats as run_detect from .fetcher import fetch_and_store, ingest_raw from .forms import NginxSettingsForm, NginxAlertProfileForm, PasteLogsForm -from .models import NginxAccessLog, NginxAlertProfile, NginxSettings, ThreatAlert +from .models import IPGeoCache, NginxAccessLog, NginxAlertProfile, NginxSettings, ThreatAlert from .notifications import send_test_telegram from .tasks import start_fetch_job, stop_fetch_job, schedule_profile, unschedule_profile @@ -334,6 +334,42 @@ class ChartDataView(View): }) +class GeoStatsView(View): + """Returns country-level aggregates + IP bubble data for the geo map.""" + def get(self, request): + range_key, cfg, since = _get_range(request) + countries = list( + NginxAccessLog.objects + .filter(timestamp__gte=since).exclude(country_code='') + .values('country_code', 'country') + .annotate(total=Count('id'), errors=Count('id', filter=Q(status__gte=400))) + .order_by('-total') + ) + ip_agg = list( + NginxAccessLog.objects.filter(timestamp__gte=since) + .values('remote_addr') + .annotate(total=Count('id'), errors=Count('id', filter=Q(status__gte=400))) + .order_by('-total')[:300] + ) + ip_set = [r['remote_addr'] for r in ip_agg] + geo_cache = { + c.ip: (c.lat, c.lon, c.country, c.city) + for c in IPGeoCache.objects.filter(ip__in=ip_set, lat__isnull=False, is_private=False) + } + bubbles = [] + for row in ip_agg: + c = geo_cache.get(row['remote_addr']) + if c and c[0] is not None and c[1] is not None: + bubbles.append({ + 'lat': round(float(c[0]), 2), + 'lon': round(float(c[1]), 2), + 'total': row['total'], + 'errors': row['errors'], + 'label': c[3] or c[2] or row['remote_addr'], + }) + return JsonResponse({'countries': countries, 'bubbles': bubbles, 'range': range_key}) + + class IngestLogsView(View): """POST endpoint for the paste-logs form (used from SettingsView).""" def post(self, request):