Files
links/nginxmon/models.py
T
2026-04-01 15:51:20 +11:00

170 lines
6.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
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.
from django.db import models
from netscan.fields import EncryptedCharField
class NginxSettings(models.Model):
"""
Singleton (pk=1) — the one-off cluster connection config.
Use NginxSettings.get() everywhere instead of pk lookups.
"""
namespace = models.CharField(max_length=200, default='ingress-nginx')
pod_label = models.CharField(
max_length=200,
default='app.kubernetes.io/name=ingress-nginx',
help_text='kubectl -l selector for the ingress-nginx pods',
)
container = models.CharField(max_length=100, default='controller')
fetch_interval_seconds = models.IntegerField(
default=30,
help_text='How often to pull new logs (seconds)',
)
# Local dev / testing: read from a file instead of kubectl
log_file_path = models.CharField(
max_length=500,
blank=True,
help_text=(
'Absolute path to a local nginx log file for testing. '
'Leave empty to use kubectl in production.'
),
)
enabled = models.BooleanField(default=True)
last_fetch_at = models.DateTimeField(null=True, blank=True)
class Meta:
verbose_name = 'Nginx Settings'
verbose_name_plural = 'Nginx Settings'
def __str__(self):
return f'Nginx Settings ({self.namespace})'
@classmethod
def get(cls):
obj, _ = cls.objects.get_or_create(pk=1)
return obj
@property
def source_display(self):
if self.log_file_path:
return f'File: {self.log_file_path}'
return f'kubectl -n {self.namespace} -l {self.pod_label}'
class NginxAlertProfile(models.Model):
"""
One or more alert profiles — each with its own thresholds and Telegram config.
"""
name = models.CharField(max_length=200)
alert_window_seconds = models.IntegerField(
default=60,
help_text='Rolling window in seconds for rate analysis',
)
alert_max_requests = models.IntegerField(
default=200,
help_text='Max requests per IP per window before triggering a rate alert',
)
alert_max_error_rate = models.FloatField(
default=0.6,
help_text='Error rate threshold (01) to flag an IP',
)
alert_min_requests = models.IntegerField(
default=15,
help_text='Minimum requests before running error-rate check',
)
telegram_bot_token = EncryptedCharField(blank=True)
telegram_chat_id = EncryptedCharField(blank=True)
enabled = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.name
class NginxAccessLog(models.Model):
"""Stores parsed nginx access log entries (cluster-global, no profile FK)."""
timestamp = models.DateTimeField(db_index=True)
remote_addr = models.GenericIPAddressField(db_index=True)
method = models.CharField(max_length=20)
request_uri = models.TextField()
protocol = models.CharField(max_length=20)
status = models.IntegerField(db_index=True)
body_bytes_sent = models.IntegerField()
http_referer = models.TextField(blank=True)
http_user_agent = models.TextField(blank=True)
request_length = models.IntegerField(default=0)
request_time = models.FloatField()
service = models.CharField(max_length=200, blank=True, db_index=True)
upstream_addr = models.CharField(max_length=200, blank=True)
upstream_response_time = models.FloatField(null=True, blank=True)
upstream_status = models.IntegerField(null=True, blank=True)
request_id = models.CharField(max_length=100, blank=True, db_index=True)
# Geo (populated after insert)
country = models.CharField(max_length=100, blank=True)
country_code = models.CharField(max_length=10, blank=True)
region = models.CharField(max_length=100, blank=True)
city = models.CharField(max_length=100, blank=True)
class Meta:
ordering = ['-timestamp']
indexes = [
models.Index(fields=['remote_addr', 'timestamp']),
models.Index(fields=['timestamp', 'status']), # covers chart aggregation queries
]
@property
def geo_display(self):
parts = [p for p in [self.city, self.country] if p]
return ', '.join(parts) if parts else '—'
class IPGeoCache(models.Model):
ip = models.GenericIPAddressField(unique=True, db_index=True)
country = models.CharField(max_length=100, blank=True)
country_code = models.CharField(max_length=10, blank=True)
region = models.CharField(max_length=100, blank=True)
city = models.CharField(max_length=100, blank=True)
lat = models.FloatField(null=True, blank=True)
lon = models.FloatField(null=True, blank=True)
isp = models.CharField(max_length=300, blank=True)
is_private = models.BooleanField(default=False)
fetched_at = models.DateTimeField(auto_now=True)
def __str__(self):
if self.is_private:
return f'{self.ip} (private)'
parts = [p for p in [self.city, self.country] if p]
return f'{self.ip}{", ".join(parts)}'
class ThreatAlert(models.Model):
ALERT_TYPES = [
('rate_limit', 'High Request Rate'),
('error_rate', 'High Error Rate'),
]
profile = models.ForeignKey(
NginxAlertProfile, on_delete=models.CASCADE, related_name='alerts'
)
alert_type = models.CharField(max_length=50, choices=ALERT_TYPES)
remote_addr = models.GenericIPAddressField(db_index=True)
detected_at = models.DateTimeField(auto_now_add=True)
window_start = models.DateTimeField()
window_end = models.DateTimeField()
request_count = models.IntegerField()
error_count = models.IntegerField(default=0)
detail = models.TextField()
notified = models.BooleanField(default=False)
dismissed = models.BooleanField(default=False)
country = models.CharField(max_length=100, blank=True)
city = models.CharField(max_length=100, blank=True)
class Meta:
ordering = ['-detected_at']
def __str__(self):
return f'[{self.get_alert_type_display()}] {self.remote_addr} @ {self.detected_at:%Y-%m-%d %H:%M}'
@property
def error_rate(self):
return self.error_count / self.request_count if self.request_count else 0