mirror of
https://github.com/wahyd4/links.git
synced 2026-08-09 05:06:16 +10:00
308 lines
12 KiB
Python
308 lines
12 KiB
Python
import json
|
|
import logging
|
|
import time
|
|
from datetime import timedelta
|
|
|
|
from django.contrib import messages
|
|
from django.db.models import Count, Q
|
|
from django.db.models.functions import TruncDay, TruncHour, TruncMinute
|
|
from django.http import JsonResponse, StreamingHttpResponse
|
|
from django.shortcuts import redirect, render
|
|
from django.utils import timezone
|
|
from django.views.generic import TemplateView, View
|
|
|
|
from .forms import RouterMonSettingsForm
|
|
from .models import DhcpLease, DnsQuery, RouterMonSettings
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# ── Time range config (mirrors nginxmon) ─────────────────────────────────────
|
|
|
|
_RANGES = {
|
|
'5m': {'delta': timedelta(minutes=5), 'trunc': TruncMinute, 'group_n': 1, 'label': 'Last 5 min', 'fmt': '%H:%M'},
|
|
'30m': {'delta': timedelta(minutes=30), 'trunc': TruncMinute, 'group_n': 2, 'label': 'Last 30 min', 'fmt': '%H:%M'},
|
|
'6h': {'delta': timedelta(hours=6), 'trunc': TruncMinute, 'group_n': 15, 'label': 'Last 6 hours', 'fmt': '%H:%M'},
|
|
'1d': {'delta': timedelta(hours=24), 'trunc': TruncHour, 'group_n': 1, 'label': 'Last 24 hours', 'fmt': '%H:%M'},
|
|
'7d': {'delta': timedelta(days=7), 'trunc': TruncHour, 'group_n': 4, 'label': 'Last 7 days', 'fmt': '%m/%d %H:%M'},
|
|
'30d': {'delta': timedelta(days=30), 'trunc': TruncDay, 'group_n': 1, 'label': 'Last 30 days', 'fmt': '%m/%d'},
|
|
}
|
|
_DEFAULT_RANGE = '30m'
|
|
|
|
|
|
def _get_range(request):
|
|
key = request.GET.get('range', _DEFAULT_RANGE)
|
|
if key not in _RANGES:
|
|
key = _DEFAULT_RANGE
|
|
cfg = _RANGES[key]
|
|
return key, cfg, timezone.now() - cfg['delta']
|
|
|
|
|
|
def _floor_bucket_key(dt, trunc_fn, group_n, fmt):
|
|
if trunc_fn is TruncMinute:
|
|
m = (dt.minute // group_n) * group_n
|
|
aligned = dt.replace(minute=m, second=0, microsecond=0)
|
|
elif trunc_fn is TruncHour:
|
|
h = (dt.hour // group_n) * group_n
|
|
aligned = dt.replace(hour=h, minute=0, second=0, microsecond=0)
|
|
else:
|
|
from datetime import date
|
|
epoch = date(2020, 1, 1)
|
|
d = dt.date() if hasattr(dt, 'date') else dt
|
|
days = (d - epoch).days
|
|
floored = epoch + timedelta(days=(days // group_n) * group_n)
|
|
aligned = dt.replace(
|
|
year=floored.year, month=floored.month, day=floored.day,
|
|
hour=0, minute=0, second=0, microsecond=0,
|
|
)
|
|
return aligned.strftime(fmt)
|
|
|
|
|
|
# ── Dashboard ─────────────────────────────────────────────────────────────────
|
|
|
|
class DashboardView(TemplateView):
|
|
template_name = 'routermon/dashboard.html'
|
|
|
|
def get_context_data(self, **kwargs):
|
|
ctx = super().get_context_data(**kwargs)
|
|
range_key, cfg, since = _get_range(self.request)
|
|
settings = RouterMonSettings.get()
|
|
|
|
qs = DnsQuery.objects.filter(timestamp__gte=since)
|
|
total = qs.count()
|
|
nxdomain_count = qs.filter(is_nxdomain=True).count()
|
|
|
|
stats = {
|
|
'total': total,
|
|
'unique_domains': qs.values('domain').distinct().count(),
|
|
'unique_clients': qs.values('client_ip').distinct().count(),
|
|
'nxdomain_count': nxdomain_count,
|
|
'nxdomain_pct': round(nxdomain_count / total * 100, 1) if total else 0,
|
|
}
|
|
|
|
top_domains = list(
|
|
qs.values('domain')
|
|
.annotate(
|
|
count=Count('id'),
|
|
nxcount=Count('id', filter=Q(is_nxdomain=True)),
|
|
)
|
|
.order_by('-count')[:20]
|
|
)
|
|
|
|
# Attach geo (country) to top domains via resolved_ip
|
|
domain_geo = {}
|
|
for row in qs.filter(resolved_ip__isnull=False, country__gt='').values('domain', 'country_code', 'country').distinct()[:100]:
|
|
if row['domain'] not in domain_geo:
|
|
domain_geo[row['domain']] = {'country': row['country'], 'country_code': row['country_code']}
|
|
for d in top_domains:
|
|
geo = domain_geo.get(d['domain'], {})
|
|
d['country'] = geo.get('country', '')
|
|
d['country_code'] = geo.get('country_code', '')
|
|
|
|
# Build client hostname map from DhcpLease
|
|
client_ips = list(qs.values_list('client_ip', flat=True).distinct()[:50])
|
|
hostname_map = {
|
|
lease.ip: lease.hostname
|
|
for lease in DhcpLease.objects.filter(ip__in=client_ips)
|
|
if lease.hostname
|
|
}
|
|
|
|
top_clients = list(
|
|
qs.values('client_ip', 'client_name')
|
|
.annotate(count=Count('id'), nxcount=Count('id', filter=Q(is_nxdomain=True)))
|
|
.order_by('-count')[:15]
|
|
)
|
|
for c in top_clients:
|
|
c['hostname'] = hostname_map.get(c['client_ip'], c['client_name'] or '')
|
|
|
|
ctx.update({
|
|
'settings': settings,
|
|
'stats': stats,
|
|
'top_domains': top_domains,
|
|
'top_clients': top_clients,
|
|
'current_range': range_key,
|
|
'range_label': cfg['label'],
|
|
'all_ranges': [(k, v['label']) for k, v in _RANGES.items()],
|
|
'top_domains_json': json.dumps(top_domains, default=str),
|
|
})
|
|
return ctx
|
|
|
|
|
|
# ── Settings ──────────────────────────────────────────────────────────────────
|
|
|
|
class SettingsView(View):
|
|
template_name = 'routermon/settings.html'
|
|
|
|
def get(self, request):
|
|
obj = RouterMonSettings.get()
|
|
return render(request, self.template_name, {
|
|
'form': RouterMonSettingsForm(instance=obj),
|
|
'settings': obj,
|
|
})
|
|
|
|
def post(self, request):
|
|
obj = RouterMonSettings.get()
|
|
form = RouterMonSettingsForm(request.POST, instance=obj)
|
|
if form.is_valid():
|
|
form.save()
|
|
messages.success(request, 'Router Monitor settings saved.')
|
|
return redirect('routermon-settings')
|
|
return render(request, self.template_name, {'form': form, 'settings': obj})
|
|
|
|
|
|
# ── HTMX partials ─────────────────────────────────────────────────────────────
|
|
|
|
class LiveQueriesPartialView(View):
|
|
_SORT_MAP = {
|
|
'timestamp': 'timestamp',
|
|
'client': 'client_ip',
|
|
'domain': 'domain',
|
|
'type': 'query_type',
|
|
}
|
|
|
|
def get(self, request):
|
|
qs = DnsQuery.objects.all()
|
|
|
|
client_f = request.GET.get('client', '').strip()
|
|
domain_f = request.GET.get('domain', '').strip()
|
|
type_f = request.GET.get('type', '').strip()
|
|
nx_f = request.GET.get('nxdomain', '').strip()
|
|
sort_col = request.GET.get('sort', 'timestamp')
|
|
sort_ord = request.GET.get('order', 'desc')
|
|
|
|
if client_f:
|
|
qs = qs.filter(Q(client_ip=client_f) | Q(client_name__icontains=client_f))
|
|
if domain_f:
|
|
qs = qs.filter(domain__icontains=domain_f)
|
|
if type_f:
|
|
qs = qs.filter(query_type__iexact=type_f)
|
|
if nx_f == '1':
|
|
qs = qs.filter(is_nxdomain=True)
|
|
|
|
db_col = self._SORT_MAP.get(sort_col, 'timestamp')
|
|
qs = qs.order_by(f'{"" if sort_ord == "asc" else "-"}{db_col}')
|
|
|
|
return render(request, 'routermon/_live_queries.html', {'queries': qs[:60]})
|
|
|
|
|
|
class GeoStatsView(View):
|
|
"""Country-level aggregates + bubble data for the geo map."""
|
|
|
|
def get(self, request):
|
|
range_key, cfg, since = _get_range(request)
|
|
qs = DnsQuery.objects.filter(timestamp__gte=since)
|
|
|
|
countries = list(
|
|
qs.exclude(country_code='')
|
|
.values('country_code', 'country')
|
|
.annotate(total=Count('id'), nxcount=Count('id', filter=Q(is_nxdomain=True)))
|
|
.order_by('-total')
|
|
)
|
|
|
|
# Bubble data from IPGeoCache for resolved IPs
|
|
ip_agg = list(
|
|
qs.filter(resolved_ip__isnull=False)
|
|
.values('resolved_ip')
|
|
.annotate(total=Count('id'))
|
|
.order_by('-total')[:300]
|
|
)
|
|
ip_set = [r['resolved_ip'] for r in ip_agg]
|
|
|
|
try:
|
|
from nginxmon.models import IPGeoCache
|
|
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)
|
|
}
|
|
except Exception:
|
|
geo_cache = {}
|
|
|
|
bubbles = []
|
|
for row in ip_agg:
|
|
c = geo_cache.get(row['resolved_ip'])
|
|
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'],
|
|
'label': c[3] or c[2] or row['resolved_ip'],
|
|
})
|
|
|
|
return JsonResponse({'countries': countries, 'bubbles': bubbles, 'range': range_key})
|
|
|
|
|
|
# ── Chart data ────────────────────────────────────────────────────────────────
|
|
|
|
class ChartDataView(View):
|
|
def get(self, request):
|
|
range_key, cfg, since = _get_range(request)
|
|
trunc_fn = cfg['trunc']
|
|
group_n = cfg['group_n']
|
|
fmt = cfg['fmt']
|
|
|
|
rows = list(
|
|
DnsQuery.objects
|
|
.filter(timestamp__gte=since)
|
|
.annotate(bucket=trunc_fn('timestamp'))
|
|
.values('bucket')
|
|
.annotate(
|
|
total=Count('id'),
|
|
nxcount=Count('id', filter=Q(is_nxdomain=True)),
|
|
)
|
|
.order_by('bucket')
|
|
)
|
|
|
|
buckets: dict = {}
|
|
for row in rows:
|
|
bkt = row['bucket']
|
|
if bkt is None:
|
|
continue
|
|
key = _floor_bucket_key(bkt, trunc_fn, group_n, fmt)
|
|
if key not in buckets:
|
|
buckets[key] = {'total': 0, 'nxcount': 0}
|
|
buckets[key]['total'] += row['total']
|
|
buckets[key]['nxcount'] += row['nxcount']
|
|
|
|
return JsonResponse({
|
|
'labels': list(buckets),
|
|
'total': [v['total'] for v in buckets.values()],
|
|
'nxcount': [v['nxcount'] for v in buckets.values()],
|
|
'range': range_key,
|
|
'label': cfg['label'],
|
|
})
|
|
|
|
|
|
# ── Toggle + SSE status stream ─────────────────────────────────────────────────
|
|
|
|
class ToggleView(View):
|
|
def post(self, request):
|
|
settings = RouterMonSettings.get()
|
|
settings.enabled = not settings.enabled
|
|
settings.save(update_fields=['enabled'])
|
|
return JsonResponse({'enabled': settings.enabled})
|
|
|
|
|
|
class StatusStreamView(View):
|
|
"""SSE — pushes receiver status JSON every 3 seconds."""
|
|
|
|
def get(self, request):
|
|
def event_stream():
|
|
while True:
|
|
try:
|
|
settings = RouterMonSettings.get()
|
|
total = DnsQuery.objects.count()
|
|
last = settings.last_received_at.isoformat() if settings.last_received_at else None
|
|
payload = json.dumps({
|
|
'enabled': settings.enabled,
|
|
'last_received_at': last,
|
|
'total_queries': total,
|
|
})
|
|
yield f'data: {payload}\n\n'
|
|
except Exception:
|
|
pass
|
|
time.sleep(3)
|
|
|
|
resp = StreamingHttpResponse(event_stream(), content_type='text/event-stream')
|
|
resp['Cache-Control'] = 'no-cache'
|
|
resp['X-Accel-Buffering'] = 'no'
|
|
return resp
|