feat: Enhance status dashboard with SSL, deploy, security and API metrics

- Add load average display (1/5/15 min)
- Add SSL certificate monitoring (expiry, issuer, days left)
- Add Git/deploy info (branch, commit, date)
- Add extended DB metrics (cache hit ratio, slow queries, deadlocks)
- Add security metrics (failed logins, GeoIP blocks, rate limits, locked accounts)
- Add external APIs status with latency (Google, Gemini, Gravatar)
- Add servers ping monitoring (R11-REVPROXY-01, NORDABIZ-01, R11-DNS-01)
- Fix pgrep path for Gunicorn worker detection

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-01-31 20:28:26 +01:00
parent edcb755588
commit fca0e9d51e
2 changed files with 410 additions and 1 deletions

View File

@ -131,12 +131,73 @@ def admin_status():
# System uptime
try:
result = subprocess.run(['uptime'], capture_output=True, text=True, timeout=5)
result = subprocess.run(['/usr/bin/uptime'], capture_output=True, text=True, timeout=5)
if result.returncode == 0:
system_metrics['uptime'] = result.stdout.strip().split('up')[1].split(',')[0].strip()
except Exception:
system_metrics['uptime'] = None
# Load average
try:
result = subprocess.run(['/usr/bin/uptime'], capture_output=True, text=True, timeout=5)
if result.returncode == 0:
# Parse: "load average: 0.52, 0.58, 0.59"
load_part = result.stdout.split('load average:')[1].strip()
loads = [float(x.strip()) for x in load_part.split(',')]
system_metrics['load_1'] = loads[0]
system_metrics['load_5'] = loads[1]
system_metrics['load_15'] = loads[2]
except Exception:
system_metrics['load_1'] = None
# ===== SSL CERTIFICATE =====
ssl_metrics = {}
try:
import ssl
import socket
context = ssl.create_default_context()
with socket.create_connection(('nordabiznes.pl', 443), timeout=5) as sock:
with context.wrap_socket(sock, server_hostname='nordabiznes.pl') as ssock:
cert = ssock.getpeercert()
not_after = datetime.strptime(cert['notAfter'], '%b %d %H:%M:%S %Y %Z')
ssl_metrics['domain'] = 'nordabiznes.pl'
ssl_metrics['expires'] = not_after.strftime('%Y-%m-%d')
ssl_metrics['days_left'] = (not_after - datetime.now()).days
ssl_metrics['issuer'] = dict(x[0] for x in cert['issuer']).get('organizationName', 'Unknown')
ssl_metrics['status'] = 'ok' if ssl_metrics['days_left'] > 14 else 'warning' if ssl_metrics['days_left'] > 0 else 'expired'
except Exception as e:
ssl_metrics['status'] = 'error'
ssl_metrics['error'] = str(e)[:50]
# ===== GIT/DEPLOY INFO =====
deploy_metrics = {}
try:
# Current commit
result = subprocess.run(['/usr/bin/git', 'rev-parse', '--short', 'HEAD'],
capture_output=True, text=True, timeout=5, cwd='/var/www/nordabiznes')
if result.returncode == 0:
deploy_metrics['commit'] = result.stdout.strip()
# Commit date
result = subprocess.run(['/usr/bin/git', 'log', '-1', '--format=%ci'],
capture_output=True, text=True, timeout=5, cwd='/var/www/nordabiznes')
if result.returncode == 0:
deploy_metrics['commit_date'] = result.stdout.strip()[:16]
# Commit message (first line)
result = subprocess.run(['/usr/bin/git', 'log', '-1', '--format=%s'],
capture_output=True, text=True, timeout=5, cwd='/var/www/nordabiznes')
if result.returncode == 0:
deploy_metrics['commit_message'] = result.stdout.strip()[:60]
# Branch
result = subprocess.run(['/usr/bin/git', 'branch', '--show-current'],
capture_output=True, text=True, timeout=5, cwd='/var/www/nordabiznes')
if result.returncode == 0:
deploy_metrics['branch'] = result.stdout.strip()
except Exception as e:
deploy_metrics['error'] = str(e)[:50]
# ===== DATABASE METRICS =====
db_metrics = {}
@ -179,6 +240,39 @@ def admin_status():
except Exception:
pass
# Cache hit ratio
try:
result = db.execute(text("""
SELECT
sum(heap_blks_hit) as hits,
sum(heap_blks_read) as reads
FROM pg_statio_user_tables
""")).fetchone()
if result and result[0] and (result[0] + result[1]) > 0:
db_metrics['cache_hit_ratio'] = round(100 * result[0] / (result[0] + result[1]), 1)
except Exception:
pass
# Slow queries (queries over 1 second in last 24h)
try:
result = db.execute(text("""
SELECT count(*) FROM pg_stat_statements
WHERE mean_exec_time > 1000
""")).scalar()
db_metrics['slow_queries'] = result or 0
except Exception:
db_metrics['slow_queries'] = 'N/A'
# Deadlocks
try:
result = db.execute(text("""
SELECT deadlocks FROM pg_stat_database
WHERE datname = current_database()
""")).scalar()
db_metrics['deadlocks'] = result or 0
except Exception:
pass
db_metrics['status'] = 'ok'
except Exception as e:
db_metrics['status'] = 'error'
@ -229,6 +323,53 @@ def admin_status():
except Exception:
app_metrics['alerts_24h'] = 0
# ===== SECURITY METRICS =====
security_metrics = {}
# Failed logins (24h)
try:
security_metrics['failed_logins_24h'] = db.query(AuditLog).filter(
AuditLog.action == 'login_failed',
AuditLog.created_at >= yesterday
).count()
except Exception:
security_metrics['failed_logins_24h'] = 0
# Blocked by GeoIP (24h)
try:
security_metrics['geoip_blocked_24h'] = db.query(SecurityAlert).filter(
SecurityAlert.alert_type == 'geoip_blocked',
SecurityAlert.created_at >= yesterday
).count()
except Exception:
security_metrics['geoip_blocked_24h'] = 0
# Rate limit hits (24h)
try:
security_metrics['rate_limit_24h'] = db.query(SecurityAlert).filter(
SecurityAlert.alert_type == 'rate_limit',
SecurityAlert.created_at >= yesterday
).count()
except Exception:
security_metrics['rate_limit_24h'] = 0
# Brute force blocked accounts
try:
security_metrics['locked_accounts'] = db.query(User).filter(
User.is_locked == True
).count()
except Exception:
security_metrics['locked_accounts'] = 0
# Unique IPs blocked (24h)
try:
result = db.query(func.count(func.distinct(SecurityAlert.ip_address))).filter(
SecurityAlert.created_at >= yesterday
).scalar()
security_metrics['unique_ips_blocked'] = result or 0
except Exception:
security_metrics['unique_ips_blocked'] = 0
# ===== GUNICORN/PROCESS METRICS =====
process_metrics = {}
try:
@ -244,6 +385,66 @@ def admin_status():
logger.error(f"pgrep gunicorn exception: {e}")
process_metrics['gunicorn_status'] = 'unknown'
# ===== EXTERNAL APIS STATUS =====
external_apis = []
import urllib.request
import time as time_module
# Gemini API
try:
start = time_module.time()
req = urllib.request.Request('https://generativelanguage.googleapis.com/', method='HEAD')
req.add_header('User-Agent', 'NordaBiz-HealthCheck/1.0')
urllib.request.urlopen(req, timeout=5)
latency = round((time_module.time() - start) * 1000)
external_apis.append({'name': 'Google Gemini', 'status': 'ok', 'latency': latency})
except Exception:
external_apis.append({'name': 'Google Gemini', 'status': 'error', 'latency': None})
# Brave Search API
try:
start = time_module.time()
req = urllib.request.Request('https://api.search.brave.com/', method='HEAD')
req.add_header('User-Agent', 'NordaBiz-HealthCheck/1.0')
urllib.request.urlopen(req, timeout=5)
latency = round((time_module.time() - start) * 1000)
external_apis.append({'name': 'Brave Search', 'status': 'ok', 'latency': latency})
except Exception:
external_apis.append({'name': 'Brave Search', 'status': 'error', 'latency': None})
# Google PageSpeed API
try:
start = time_module.time()
req = urllib.request.Request('https://pagespeedonline.googleapis.com/', method='HEAD')
req.add_header('User-Agent', 'NordaBiz-HealthCheck/1.0')
urllib.request.urlopen(req, timeout=5)
latency = round((time_module.time() - start) * 1000)
external_apis.append({'name': 'PageSpeed API', 'status': 'ok', 'latency': latency})
except Exception:
external_apis.append({'name': 'PageSpeed API', 'status': 'error', 'latency': None})
# ===== SERVERS PING =====
servers_status = []
servers_to_ping = [
('NORDABIZ-01', '10.22.68.249'),
('R11-REVPROXY-01', '10.22.68.250'),
('R11-DNS-01', '10.22.68.171'),
('R11-GIT-INPI', '10.22.68.180'),
]
for name, ip in servers_to_ping:
try:
start = time_module.time()
result = subprocess.run(['/usr/bin/ping', '-c', '1', '-W', '2', ip],
capture_output=True, text=True, timeout=5)
latency = round((time_module.time() - start) * 1000)
if result.returncode == 0:
servers_status.append({'name': name, 'ip': ip, 'status': 'online', 'latency': latency})
else:
servers_status.append({'name': name, 'ip': ip, 'status': 'offline', 'latency': None})
except Exception:
servers_status.append({'name': name, 'ip': ip, 'status': 'unknown', 'latency': None})
# ===== TECHNOLOGY STACK =====
import flask
import sqlalchemy
@ -303,6 +504,11 @@ def admin_status():
app_metrics=app_metrics,
process_metrics=process_metrics,
technology_stack=technology_stack,
ssl_metrics=ssl_metrics,
deploy_metrics=deploy_metrics,
security_metrics=security_metrics,
external_apis=external_apis,
servers_status=servers_status,
generated_at=now
)
finally:

View File

@ -445,6 +445,12 @@
<span class="info-value">{{ system_metrics.uptime }}</span>
</div>
{% endif %}
{% if system_metrics.load_1 is defined %}
<div class="info-row">
<span class="info-label">Load Average</span>
<span class="info-value">{{ system_metrics.load_1 }} / {{ system_metrics.load_5 }} / {{ system_metrics.load_15 }}</span>
</div>
{% endif %}
</div>
<!-- Database Metrics Card -->
@ -500,6 +506,27 @@
</div>
</div>
{% endif %}
{% if db_metrics.cache_hit_ratio is defined %}
<div style="margin-top: var(--spacing-md); padding-top: var(--spacing-md); border-top: 1px solid var(--border);">
<div class="info-row">
<span class="info-label">Cache Hit Ratio</span>
<span class="info-value" style="color: {{ '#22c55e' if db_metrics.cache_hit_ratio > 95 else '#f59e0b' if db_metrics.cache_hit_ratio > 80 else '#ef4444' }};">{{ db_metrics.cache_hit_ratio }}%</span>
</div>
{% if db_metrics.slow_queries is defined %}
<div class="info-row">
<span class="info-label">Slow Queries (>1s)</span>
<span class="info-value">{{ db_metrics.slow_queries }}</span>
</div>
{% endif %}
{% if db_metrics.deadlocks is defined %}
<div class="info-row">
<span class="info-label">Deadlocks</span>
<span class="info-value" style="color: {{ '#22c55e' if db_metrics.deadlocks == 0 else '#ef4444' }};">{{ db_metrics.deadlocks }}</span>
</div>
{% endif %}
</div>
{% endif %}
{% else %}
<div style="padding: var(--spacing-xl); text-align: center; color: #991b1b;">
<p>⚠️ Błąd połączenia z bazą danych</p>
@ -613,6 +640,182 @@
</div>
</div>
<!-- New Metrics Row -->
<div class="metrics-grid" style="margin-bottom: var(--spacing-xl);">
<!-- SSL Certificate Card -->
{% if ssl_metrics is defined %}
<div class="metric-card" style="border-top: 4px solid {{ '#22c55e' if ssl_metrics.status == 'ok' else '#f59e0b' if ssl_metrics.status == 'warning' else '#ef4444' }};">
<div class="metric-card-header">
<div class="metric-card-icon" style="background: linear-gradient(135deg, #dcfce7, #d1fae5);">🔒</div>
<div>
<div class="metric-card-title">Certyfikat SSL</div>
<div class="metric-card-subtitle">{{ ssl_metrics.domain if ssl_metrics.domain else 'nordabiznes.pl' }}</div>
</div>
<span class="status-badge {{ ssl_metrics.status }}">
{% if ssl_metrics.status == 'ok' %}✓ Ważny
{% elif ssl_metrics.status == 'warning' %}⚠️ Wkrótce wygasa
{% else %}✗ Problem{% endif %}
</span>
</div>
{% if ssl_metrics.days_left is defined %}
<div class="metric-stats" style="grid-template-columns: 1fr 1fr;">
<div class="metric-stat">
<div class="metric-stat-value" style="color: {{ '#22c55e' if ssl_metrics.days_left > 30 else '#f59e0b' if ssl_metrics.days_left > 7 else '#ef4444' }};">{{ ssl_metrics.days_left }}</div>
<div class="metric-stat-label">Dni do wygaśnięcia</div>
</div>
<div class="metric-stat">
<div class="metric-stat-value" style="font-size: var(--font-size-sm);">{{ ssl_metrics.expires }}</div>
<div class="metric-stat-label">Data wygaśnięcia</div>
</div>
</div>
<div class="info-row" style="margin-top: var(--spacing-md);">
<span class="info-label">Wystawca</span>
<span class="info-value">{{ ssl_metrics.issuer }}</span>
</div>
{% else %}
<div style="padding: var(--spacing-lg); text-align: center; color: #991b1b;">
<p>⚠️ Nie można sprawdzić certyfikatu</p>
{% if ssl_metrics.error %}<p style="font-size: var(--font-size-xs);">{{ ssl_metrics.error }}</p>{% endif %}
</div>
{% endif %}
</div>
{% endif %}
<!-- Deploy Info Card -->
{% if deploy_metrics is defined %}
<div class="metric-card" style="border-top: 4px solid #8b5cf6;">
<div class="metric-card-header">
<div class="metric-card-icon" style="background: linear-gradient(135deg, #ede9fe, #ddd6fe);">🚀</div>
<div>
<div class="metric-card-title">Deploy</div>
<div class="metric-card-subtitle">Git / Wersja</div>
</div>
</div>
{% if deploy_metrics.commit %}
<div class="metric-stats" style="grid-template-columns: 1fr;">
<div class="metric-stat">
<div class="metric-stat-value" style="font-family: monospace; font-size: var(--font-size-lg);">{{ deploy_metrics.commit }}</div>
<div class="metric-stat-label">Aktualny commit</div>
</div>
</div>
<div style="margin-top: var(--spacing-md);">
<div class="info-row">
<span class="info-label">Branch</span>
<span class="info-value">{{ deploy_metrics.branch }}</span>
</div>
<div class="info-row">
<span class="info-label">Data commitu</span>
<span class="info-value">{{ deploy_metrics.commit_date }}</span>
</div>
<div class="info-row">
<span class="info-label">Opis</span>
<span class="info-value" style="font-size: var(--font-size-xs); max-width: 200px; overflow: hidden; text-overflow: ellipsis;">{{ deploy_metrics.commit_message }}</span>
</div>
</div>
{% else %}
<div style="padding: var(--spacing-lg); text-align: center; color: var(--text-secondary);">
<p>Brak danych Git</p>
</div>
{% endif %}
</div>
{% endif %}
<!-- Security Metrics Card -->
{% if security_metrics is defined %}
<div class="metric-card" style="border-top: 4px solid #ef4444;">
<div class="metric-card-header">
<div class="metric-card-icon" style="background: linear-gradient(135deg, #fee2e2, #fecaca);">🛡️</div>
<div>
<div class="metric-card-title">Bezpieczeństwo</div>
<div class="metric-card-subtitle">Ostatnie 24h</div>
</div>
</div>
<div class="metric-stats">
<div class="metric-stat">
<div class="metric-stat-value" style="color: {{ '#22c55e' if security_metrics.failed_logins_24h == 0 else '#f59e0b' if security_metrics.failed_logins_24h < 10 else '#ef4444' }};">{{ security_metrics.failed_logins_24h }}</div>
<div class="metric-stat-label">Nieudane logowania</div>
</div>
<div class="metric-stat">
<div class="metric-stat-value">{{ security_metrics.geoip_blocked_24h }}</div>
<div class="metric-stat-label">Blokady GeoIP</div>
</div>
<div class="metric-stat">
<div class="metric-stat-value">{{ security_metrics.rate_limit_24h }}</div>
<div class="metric-stat-label">Rate limit hits</div>
</div>
<div class="metric-stat">
<div class="metric-stat-value" style="color: {{ '#22c55e' if security_metrics.locked_accounts == 0 else '#ef4444' }};">{{ security_metrics.locked_accounts }}</div>
<div class="metric-stat-label">Zablokowane konta</div>
</div>
</div>
<div class="info-row" style="margin-top: var(--spacing-md);">
<span class="info-label">Unikalne IP zablokowane</span>
<span class="info-value">{{ security_metrics.unique_ips_blocked }}</span>
</div>
</div>
{% endif %}
<!-- External APIs Status Card -->
{% if external_apis is defined %}
<div class="metric-card" style="border-top: 4px solid #06b6d4;">
<div class="metric-card-header">
<div class="metric-card-icon" style="background: linear-gradient(135deg, #cffafe, #a5f3fc);">🔌</div>
<div>
<div class="metric-card-title">External APIs</div>
<div class="metric-card-subtitle">Status połączeń</div>
</div>
</div>
<div style="display: flex; flex-direction: column; gap: var(--spacing-sm);">
{% for api in external_apis %}
<div style="display: flex; align-items: center; gap: var(--spacing-md); padding: var(--spacing-sm); background: var(--background); border-radius: var(--radius);">
<div class="health-dot {{ api.status }}"></div>
<span style="flex: 1; font-weight: 500;">{{ api.name }}</span>
{% if api.latency %}
<span style="font-size: var(--font-size-xs); color: var(--text-secondary);">{{ api.latency }}ms</span>
{% else %}
<span style="font-size: var(--font-size-xs); color: #ef4444;">offline</span>
{% endif %}
</div>
{% endfor %}
</div>
</div>
{% endif %}
</div>
<!-- Servers Status Row -->
{% if servers_status is defined %}
<div class="metric-card" style="margin-bottom: var(--spacing-xl); border-top: 4px solid #6366f1;">
<div class="metric-card-header">
<div class="metric-card-icon" style="background: linear-gradient(135deg, #e0e7ff, #c7d2fe);">🖥️</div>
<div>
<div class="metric-card-title">Status serwerów</div>
<div class="metric-card-subtitle">Ping w sieci INPI</div>
</div>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: var(--spacing-md);">
{% for server in servers_status %}
<div style="display: flex; align-items: center; gap: var(--spacing-md); padding: var(--spacing-md); background: var(--background); border-radius: var(--radius); border-left: 3px solid {{ '#22c55e' if server.status == 'online' else '#ef4444' }};">
<div>
<div class="health-dot {{ 'ok' if server.status == 'online' else 'error' }}" style="width: 12px; height: 12px;"></div>
</div>
<div style="flex: 1;">
<div style="font-weight: 600;">{{ server.name }}</div>
<div style="font-size: var(--font-size-xs); color: var(--text-secondary); font-family: monospace;">{{ server.ip }}</div>
</div>
<div style="text-align: right;">
{% if server.status == 'online' %}
<div style="font-size: var(--font-size-sm); color: #22c55e;">Online</div>
<div style="font-size: var(--font-size-xs); color: var(--text-secondary);">{{ server.latency }}ms</div>
{% else %}
<div style="font-size: var(--font-size-sm); color: #ef4444;">Offline</div>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Technology Stack Section -->
<div class="tech-stack-section" style="margin-top: var(--spacing-2xl);">
<h2 style="font-size: var(--font-size-xl); color: var(--text-primary); margin-bottom: var(--spacing-lg);">