Initial commit: agent_logwatch v1.0

- Réception logs MQTT depuis machines distantes (agents/logwatch/+/logs)
- Pré-filtrage sans LLM (14 patterns: ERROR, FATAL, OOM, segfault, auth fail...)
- Analyse LLM par créneau horaire configurable (APScheduler)
- Gestion round-robin avec reprise sur interruption
- Extension de créneau (+30 min) avec confirmation admin
- Skills: machine (gestion machines) + logwatch (contrôle)
- Script send_logs.sh pour machines distantes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-02 08:59:20 +00:00
commit bdcfff9f8e
20 changed files with 1618 additions and 0 deletions
+194
View File
@@ -0,0 +1,194 @@
"""
Skill MACHINE — gestion des machines qui envoient leurs logs.
Usage LLM :
SKILL:machine ARGS:list
SKILL:machine ARGS:queue
SKILL:machine ARGS:add <hostname>
SKILL:machine ARGS:remove <hostname>
SKILL:machine ARGS:status <hostname>
SKILL:machine ARGS:reorder <hostname> <position>
SKILL:machine ARGS:activate <hostname>
SKILL:machine ARGS:deactivate <hostname>
"""
from datetime import datetime
DESCRIPTION = "Gestion des machines enregistrées : liste, file d'attente, ajout, suppression, statut"
USAGE = (
"SKILL:machine ARGS:list\n"
"SKILL:machine ARGS:queue\n"
"SKILL:machine ARGS:add <hostname>\n"
"SKILL:machine ARGS:remove <hostname>\n"
"SKILL:machine ARGS:status <hostname>\n"
"SKILL:machine ARGS:reorder <hostname> <position>\n"
"SKILL:machine ARGS:activate <hostname>\n"
"SKILL:machine ARGS:deactivate <hostname>"
)
def _db(context):
return context.agent._get_db()
def run(args: str, context) -> str:
parts = args.strip().split(None, 1)
action = parts[0].lower() if parts else 'list'
rest = parts[1].strip() if len(parts) > 1 else ''
# ── list ──────────────────────────────────────────────────────────────────
if action == 'list':
with _db(context) as conn:
rows = conn.execute(
"SELECT hostname, active, last_log_at, last_analyzed_at, queue_position "
"FROM machines ORDER BY queue_position ASC"
).fetchall()
if not rows:
return "Aucune machine enregistrée."
lines = ["── Machines enregistrées ─────────────────────"]
for r in rows:
status = "🟢 actif" if r['active'] else "🔴 inactif"
last_log = r['last_log_at'][:16] if r['last_log_at'] else "jamais"
last_ana = r['last_analyzed_at'][:16] if r['last_analyzed_at'] else "jamais"
lines.append(
f" [{r['queue_position']:2d}] {r['hostname']:<30s} {status}\n"
f" Dernier log: {last_log} | Dernière analyse: {last_ana}"
)
return "\n".join(lines)
# ── queue ─────────────────────────────────────────────────────────────────
if action == 'queue':
today = datetime.now().strftime('%Y-%m-%d')
with _db(context) as conn:
rows = conn.execute(
"SELECT m.hostname, m.queue_position, m.active, "
" COALESCE(s.status, 'pending') as session_status "
"FROM machines m "
"LEFT JOIN analysis_sessions s "
" ON s.machine_id=m.id AND s.slot_date=? "
"ORDER BY m.queue_position ASC",
(today,)
).fetchall()
if not rows:
return "Aucune machine dans la file."
icons = {'done': '', 'in_progress': '🔄', 'paused': '⏸️', 'pending': ''}
lines = [f"── File d'analyse — {today} ─────────────────"]
for r in rows:
active = "" if r['active'] else " [inactif]"
icon = icons.get(r['session_status'], '')
lines.append(
f" {r['queue_position']:2d}. {icon} {r['hostname']}{active} "
f"({r['session_status']})"
)
return "\n".join(lines)
# ── add ───────────────────────────────────────────────────────────────────
if action == 'add':
hostname = rest.strip()
if not hostname:
return "Format: machine add <hostname>"
with _db(context) as conn:
existing = conn.execute(
"SELECT id FROM machines WHERE hostname=?", (hostname,)
).fetchone()
if existing:
return f"Machine '{hostname}' déjà enregistrée."
max_pos = conn.execute(
"SELECT COALESCE(MAX(queue_position), 0) FROM machines"
).fetchone()[0]
conn.execute(
"INSERT INTO machines (hostname, registered_at, queue_position) VALUES (?,?,?)",
(hostname, datetime.now().isoformat(), max_pos + 1)
)
return f"✅ Machine '{hostname}' enregistrée (position {max_pos + 1})."
# ── remove ────────────────────────────────────────────────────────────────
if action == 'remove':
hostname = rest.strip()
if not hostname:
return "Format: machine remove <hostname>"
with _db(context) as conn:
cur = conn.execute("DELETE FROM machines WHERE hostname=?", (hostname,))
if cur.rowcount == 0:
return f"Machine '{hostname}' introuvable."
return f"🗑️ Machine '{hostname}' supprimée."
# ── status ────────────────────────────────────────────────────────────────
if action == 'status':
hostname = rest.strip()
if not hostname:
return "Format: machine status <hostname>"
with _db(context) as conn:
m = conn.execute(
"SELECT * FROM machines WHERE hostname=?", (hostname,)
).fetchone()
if not m:
return f"Machine '{hostname}' introuvable."
# Logs filtrés en attente
pending_logs = conn.execute(
"SELECT COUNT(*) as cnt FROM filtered_logs WHERE machine_id=? AND analyzed=0",
(m['id'],)
).fetchone()['cnt']
# Sessions récentes
sessions = conn.execute(
"SELECT slot_date, status, started_at, completed_at, last_log_id "
"FROM analysis_sessions WHERE machine_id=? ORDER BY slot_date DESC LIMIT 5",
(m['id'],)
).fetchall()
active = "actif" if m['active'] else "inactif"
lines = [
f"── Statut de {hostname} ──────────────────────",
f" Statut : {active}",
f" Position : {m['queue_position']}",
f" Enregistrée : {m['registered_at'][:16]}",
f" Dernier log : {m['last_log_at'][:16] if m['last_log_at'] else 'jamais'}",
f" Dernière ana: {m['last_analyzed_at'][:16] if m['last_analyzed_at'] else 'jamais'}",
f" Logs en att.: {pending_logs}",
]
if sessions:
lines.append(" Sessions récentes:")
for s in sessions:
lines.append(
f" {s['slot_date']} : {s['status']} "
f"(offset log #{s['last_log_id']})"
)
return "\n".join(lines)
# ── reorder ───────────────────────────────────────────────────────────────
if action == 'reorder':
p = rest.split(None, 1)
if len(p) < 2:
return "Format: machine reorder <hostname> <nouvelle_position>"
hostname = p[0].strip()
try:
new_pos = int(p[1].strip())
except ValueError:
return "La position doit être un entier."
with _db(context) as conn:
cur = conn.execute(
"UPDATE machines SET queue_position=? WHERE hostname=?",
(new_pos, hostname)
)
if cur.rowcount == 0:
return f"Machine '{hostname}' introuvable."
return f"{hostname} déplacée en position {new_pos}."
# ── activate / deactivate ─────────────────────────────────────────────────
if action in ('activate', 'deactivate'):
hostname = rest.strip()
if not hostname:
return f"Format: machine {action} <hostname>"
val = 1 if action == 'activate' else 0
with _db(context) as conn:
cur = conn.execute(
"UPDATE machines SET active=? WHERE hostname=?", (val, hostname)
)
if cur.rowcount == 0:
return f"Machine '{hostname}' introuvable."
verb = "activée" if val else "désactivée"
return f"✅ Machine '{hostname}' {verb}."
return (
"Action inconnue. Disponible : list, queue, add, remove, status, "
"reorder, activate, deactivate"
)