345 lines
11 KiB
Python
345 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
import sys
|
|
import os
|
|
import time
|
|
import subprocess
|
|
import json
|
|
import syslog
|
|
import socket
|
|
from datetime import datetime, timedelta
|
|
|
|
CONFIG_PATH = "/etc/ssh/ssh_alerts_config.json"
|
|
STATE_FILE = "/tmp/system_resources_state.json"
|
|
ALERT_THRESHOLD = 85.0
|
|
COOLDOWN_SECONDS = 3600 # Remind every 1 hour if resource stays above threshold
|
|
|
|
def log_to_syslog(message, is_error=False):
|
|
level = syslog.LOG_ERR if is_error else syslog.LOG_INFO
|
|
syslog.openlog(ident="system_resources_monitor", logoption=syslog.LOG_PID, facility=syslog.LOG_DAEMON)
|
|
syslog.syslog(level, message)
|
|
syslog.closelog()
|
|
|
|
def get_vps_identity():
|
|
# Attempt to auto-detect WireGuard IP
|
|
try:
|
|
output = subprocess.check_output(
|
|
['ip', '-o', '-4', 'addr', 'show', 'dev', 'wg0'],
|
|
stderr=subprocess.DEVNULL
|
|
).decode('utf-8')
|
|
for line in output.split('\n'):
|
|
parts = line.split()
|
|
if len(parts) >= 4:
|
|
return f"VPS 3 ({parts[3].split('/')[0]})"
|
|
except Exception:
|
|
pass
|
|
|
|
# Fallback to general IP/hostname
|
|
try:
|
|
return f"VPS ({socket.gethostname()})"
|
|
except Exception:
|
|
return "VPS (10.99.0.3)"
|
|
|
|
def get_cpu_usage():
|
|
def read_cpu_times():
|
|
with open('/proc/stat', 'r') as f:
|
|
first_line = f.readline()
|
|
parts = first_line.split()
|
|
times = [float(x) for x in parts[1:9]]
|
|
idle = times[3] + times[4] # idle + iowait
|
|
total = sum(times)
|
|
return idle, total
|
|
|
|
try:
|
|
idle1, total1 = read_cpu_times()
|
|
time.sleep(1.0)
|
|
idle2, total2 = read_cpu_times()
|
|
|
|
idle_delta = idle2 - idle1
|
|
total_delta = total2 - total1
|
|
|
|
if total_delta > 0:
|
|
return (1.0 - idle_delta / total_delta) * 100.0
|
|
except Exception as e:
|
|
log_to_syslog(f"Erro ao obter uso de CPU: {e}", is_error=True)
|
|
return 0.0
|
|
|
|
def get_memory_usage():
|
|
try:
|
|
mem_info = {}
|
|
with open('/proc/meminfo', 'r') as f:
|
|
for line in f:
|
|
parts = line.split(':')
|
|
if len(parts) == 2:
|
|
key = parts[0].strip()
|
|
val = parts[1].strip().split()[0]
|
|
mem_info[key] = int(val)
|
|
|
|
total = mem_info.get('MemTotal', 0)
|
|
available = mem_info.get('MemAvailable', 0)
|
|
if total > 0:
|
|
used = total - available
|
|
return (used / total) * 100.0
|
|
except Exception as e:
|
|
log_to_syslog(f"Erro ao obter uso de memoria: {e}", is_error=True)
|
|
return 0.0
|
|
|
|
def get_disk_usage():
|
|
try:
|
|
output = subprocess.check_output(['df', '-Pl', '/']).decode('utf-8')
|
|
lines = output.strip().split('\n')
|
|
if len(lines) >= 2:
|
|
parts = lines[-1].split()
|
|
if len(parts) >= 5:
|
|
pct_str = parts[4]
|
|
if pct_str.endswith('%'):
|
|
return float(pct_str[:-1])
|
|
except Exception as e:
|
|
log_to_syslog(f"Erro ao obter uso de disco: {e}", is_error=True)
|
|
return 0.0
|
|
|
|
def get_top_processes():
|
|
try:
|
|
ps_output = subprocess.check_output(
|
|
['ps', '-eo', 'pid,%mem,%cpu,cmd', '--sort=-%mem'],
|
|
stderr=subprocess.DEVNULL
|
|
).decode('utf-8', errors='ignore').strip().split('\n')
|
|
return ps_output[:6] # Header + top 5 processes
|
|
except Exception as e:
|
|
log_to_syslog(f"Erro ao obter processos ofensores: {e}", is_error=True)
|
|
return []
|
|
|
|
def load_state():
|
|
if os.path.exists(STATE_FILE):
|
|
try:
|
|
with open(STATE_FILE, "r") as f:
|
|
return json.load(f)
|
|
except Exception:
|
|
pass
|
|
return {}
|
|
|
|
def save_state(state):
|
|
try:
|
|
with open(STATE_FILE, "w") as f:
|
|
json.dump(state, f)
|
|
except Exception as e:
|
|
log_to_syslog(f"Erro ao salvar estado de alertas: {e}", is_error=True)
|
|
|
|
def send_telegram_alert(token, chat_id, vps_identity, alerts_active, top_processes):
|
|
try:
|
|
url = f"https://api.telegram.org/bot{token}/sendMessage"
|
|
|
|
alerts_text = ""
|
|
for resource, val in alerts_active.items():
|
|
alerts_text += f"• *{resource}:* `{val:.1f}%` (Limite: {ALERT_THRESHOLD:.1f}%)\n"
|
|
|
|
proc_text = ""
|
|
if top_processes:
|
|
proc_text = "\n🔥 *Top 5 Processos Consumindo mais RAM:*\n```\n"
|
|
proc_text += "\n".join(top_processes)
|
|
proc_text += "\n```"
|
|
|
|
text = (
|
|
f"🚨 *ALERTA DE USO DE RECURSOS CRÍTICOS*\n\n"
|
|
f"• *Servidor:* `{vps_identity}`\n"
|
|
f"{alerts_text}"
|
|
f"{proc_text}\n"
|
|
f"📅 _Registrado automaticamente pelo monitor do sistema._"
|
|
)
|
|
|
|
payload = json.dumps({
|
|
"chat_id": chat_id,
|
|
"text": text,
|
|
"parse_mode": "Markdown"
|
|
}).encode("utf-8")
|
|
|
|
req = urllib.request.Request(
|
|
url,
|
|
data=payload,
|
|
headers={
|
|
'Content-Type': 'application/json',
|
|
'User-Agent': 'Mozilla/5.0'
|
|
}
|
|
)
|
|
import urllib.request
|
|
with urllib.request.urlopen(req, timeout=5) as r:
|
|
res = json.loads(r.read().decode())
|
|
return res.get("ok", False)
|
|
except Exception as e:
|
|
log_to_syslog(f"Erro ao enviar alerta para o Telegram: {e}", is_error=True)
|
|
return False
|
|
|
|
def send_telegram_resolved(token, chat_id, vps_identity, resolved_resources):
|
|
try:
|
|
url = f"https://api.telegram.org/bot{token}/sendMessage"
|
|
|
|
resolved_text = ""
|
|
for resource, val in resolved_resources.items():
|
|
resolved_text += f"• *{resource}:* `{val:.1f}%` (Normalizado)\n"
|
|
|
|
text = (
|
|
f"✅ *RECURSOS NORMALIZADOS*\n\n"
|
|
f"• *Servidor:* `{vps_identity}`\n"
|
|
f"{resolved_text}\n"
|
|
f"📅 _O servidor voltou a operar dentro dos limites de segurança._"
|
|
)
|
|
|
|
payload = json.dumps({
|
|
"chat_id": chat_id,
|
|
"text": text,
|
|
"parse_mode": "Markdown"
|
|
}).encode("utf-8")
|
|
|
|
req = urllib.request.Request(
|
|
url,
|
|
data=payload,
|
|
headers={
|
|
'Content-Type': 'application/json',
|
|
'User-Agent': 'Mozilla/5.0'
|
|
}
|
|
)
|
|
import urllib.request
|
|
with urllib.request.urlopen(req, timeout=5) as r:
|
|
res = json.loads(r.read().decode())
|
|
return res.get("ok", False)
|
|
except Exception as e:
|
|
log_to_syslog(f"Erro ao enviar resolucao para o Telegram: {e}", is_error=True)
|
|
return False
|
|
|
|
def create_calendar_event(vps_identity, alerts_active, top_processes, calendar_id):
|
|
try:
|
|
from google.oauth2 import service_account
|
|
from googleapiclient.discovery import build
|
|
|
|
creds = service_account.Credentials.from_service_account_file(
|
|
'/etc/ssh/gcal_key.json',
|
|
scopes=['https://www.googleapis.com/auth/calendar']
|
|
)
|
|
|
|
service = build('calendar', 'v3', credentials=creds)
|
|
|
|
now = datetime.utcnow()
|
|
end_time = now + timedelta(minutes=30)
|
|
|
|
start_iso = now.isoformat() + 'Z'
|
|
end_iso = end_time.isoformat() + 'Z'
|
|
|
|
resources_str = ", ".join(alerts_active.keys())
|
|
summary = f"{vps_identity}: 🚨 Estresse de Sistema - {resources_str}"
|
|
|
|
description = (
|
|
f"=== 🖥️ ALERTA DE RECURSOS DO SISTEMA ===\n\n"
|
|
f"• Servidor: {vps_identity}\n"
|
|
f"• Recursos acima do limite (>={ALERT_THRESHOLD}%):\n"
|
|
)
|
|
for res, val in alerts_active.items():
|
|
description += f" - {res}: {val:.1f}%\n"
|
|
|
|
if top_processes:
|
|
description += f"\n--- TOP PROCESSOS POR MEMÓRIA ---\n"
|
|
description += "\n".join(top_processes)
|
|
|
|
description += f"\n\nRegistrado automaticamente em {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}."
|
|
|
|
event_body = {
|
|
'summary': summary,
|
|
'description': description,
|
|
'start': {'dateTime': start_iso, 'timeZone': 'UTC'},
|
|
'end': {'dateTime': end_iso, 'timeZone': 'UTC'},
|
|
'colorId': "11", # Vermelho Bold
|
|
'reminders': {
|
|
'useDefault': False,
|
|
'overrides': [{'method': 'popup', 'minutes': 0}]
|
|
}
|
|
}
|
|
|
|
event = service.events().insert(
|
|
calendarId=calendar_id,
|
|
body=event_body
|
|
).execute()
|
|
return True
|
|
except Exception as e:
|
|
log_to_syslog(f"Erro ao criar evento de estresse no Google Calendar: {e}", is_error=True)
|
|
return False
|
|
|
|
def main():
|
|
# Load config
|
|
if not os.path.exists(CONFIG_PATH):
|
|
log_to_syslog(f"Arquivo de configuracao nao encontrado: {CONFIG_PATH}", is_error=True)
|
|
return
|
|
|
|
try:
|
|
with open(CONFIG_PATH, "r") as f:
|
|
config = json.load(f)
|
|
except Exception as e:
|
|
log_to_syslog(f"Erro ao carregar configuracoes: {e}", is_error=True)
|
|
return
|
|
|
|
telegram_token = config.get("telegram_token")
|
|
telegram_chat_id = config.get("telegram_chat_id")
|
|
calendar_id = config.get("calendar_id")
|
|
|
|
if not telegram_token or not telegram_chat_id:
|
|
log_to_syslog("Telegram credentials missing in config.", is_error=True)
|
|
return
|
|
|
|
# Check metrics
|
|
cpu = get_cpu_usage()
|
|
mem = get_memory_usage()
|
|
disk = get_disk_usage()
|
|
|
|
metrics = {
|
|
"CPU": cpu,
|
|
"Memoria": mem,
|
|
"Disco (/)": disk
|
|
}
|
|
|
|
vps_identity = get_vps_identity()
|
|
state = load_state()
|
|
now = time.time()
|
|
|
|
alerts_to_send = {}
|
|
resolved_to_send = {}
|
|
|
|
state_changed = False
|
|
|
|
for resource, val in metrics.items():
|
|
is_above = val >= ALERT_THRESHOLD
|
|
prev_alert_time = state.get(resource, 0)
|
|
|
|
if is_above:
|
|
# Check if we should notify: either first time or after cooldown
|
|
if prev_alert_time == 0 or (now - prev_alert_time >= COOLDOWN_SECONDS):
|
|
alerts_to_send[resource] = val
|
|
state[resource] = now
|
|
state_changed = True
|
|
else:
|
|
# If it was previously in alert state, send resolution message
|
|
if prev_alert_time > 0:
|
|
resolved_to_send[resource] = val
|
|
state[resource] = 0
|
|
state_changed = True
|
|
|
|
if state_changed:
|
|
save_state(state)
|
|
|
|
# Process resolutions
|
|
if resolved_to_send:
|
|
log_to_syslog(f"Métricas normalizadas para: {', '.join(resolved_to_send.keys())}")
|
|
send_telegram_resolved(telegram_token, telegram_chat_id, vps_identity, resolved_to_send)
|
|
|
|
# Process alerts
|
|
if alerts_to_send:
|
|
log_to_syslog(f"ALERTA: Métricas acima de {ALERT_THRESHOLD}%: " + ", ".join([f"{k} ({v:.1f}%)" for k, v in alerts_to_send.items()]))
|
|
|
|
top_processes = get_top_processes()
|
|
|
|
# Send to Telegram
|
|
send_telegram_alert(telegram_token, telegram_chat_id, vps_identity, alerts_to_send, top_processes)
|
|
|
|
# Log to Calendar
|
|
if calendar_id:
|
|
create_calendar_event(vps_identity, alerts_to_send, top_processes, calendar_id)
|
|
|
|
if __name__ == "__main__":
|
|
main()
|