feat: collecte automatique des logs locaux au début de chaque créneau

- _collect_local_logs() appelle journalctl en local au démarrage du slot
- collect_local_logs(since=) accessible comme méthode publique
- Skill logwatch collect [since] pour collecte manuelle à la demande
- Config: local_log_since, local_log_units, local_hostname

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-02 09:07:27 +00:00
parent bdcfff9f8e
commit c039b92d6d
6 changed files with 81 additions and 1 deletions
+69
View File
@@ -324,6 +324,9 @@ class LogWatchAgent(BaseAgent):
if self._slot_end_time <= now:
self._slot_end_time += timedelta(days=1)
# Collecter les logs locaux avant de commencer l'analyse
self._collect_local_logs()
self._analysis_stop.clear()
self._analysis_thread = threading.Thread(
target=self._analysis_loop, daemon=True, name="logwatch-analysis"
@@ -336,6 +339,72 @@ class LogWatchAgent(BaseAgent):
logger.info("Fin de créneau signalée.")
self._analysis_stop.set()
# ─── Collecte locale ─────────────────────────────────────────────────────
def collect_local_logs(self, since: str = 'yesterday') -> str:
"""
Collecte les logs de la machine locale via journalctl et les pré-filtre.
Appelé automatiquement au début de chaque créneau, ou manuellement.
Retourne un résumé de ce qui a été collecté.
"""
import subprocess
import socket
local_hostname = self.config.get('local_hostname') or socket.getfqdn()
units = self.config.get('local_log_units', []) # [] = tous les services
since_str = since or self.config.get('local_log_since', 'yesterday')
cmd = ['journalctl', '--no-pager', '--output=short-iso', f'--since={since_str}']
for unit in units:
cmd += ['-u', unit]
try:
result = subprocess.run(
cmd, capture_output=True, text=True, timeout=60
)
raw_lines = result.stdout.splitlines()
except subprocess.TimeoutExpired:
logger.error("[local_logs] journalctl timeout")
return "Erreur: journalctl timeout (60s)"
except FileNotFoundError:
logger.warning("[local_logs] journalctl non disponible sur cette machine")
return "journalctl non disponible."
except Exception as e:
logger.error(f"[local_logs] {e}")
return f"Erreur collecte locale: {e}"
if not raw_lines:
return f"Aucun log local depuis '{since_str}'."
machine_id = self._register_machine(local_hostname)
filtered = self._prefilter(raw_lines)
if filtered:
now = datetime.now().isoformat()
with self._get_db() as conn:
conn.executemany(
"INSERT INTO filtered_logs (machine_id, log_line, severity, received_at) VALUES (?,?,?,?)",
[(machine_id, line, sev, now) for line, sev in filtered]
)
conn.execute(
"UPDATE machines SET last_log_at=? WHERE id=?",
(now, machine_id)
)
msg = (
f"[local] {local_hostname}: {len(filtered)}/{len(raw_lines)} lignes filtrées"
+ (f" ({', '.join(units)})" if units else " (tous services)")
)
logger.info(msg)
return msg
def _collect_local_logs(self):
"""Wrapper silencieux appelé au début du slot."""
try:
self.collect_local_logs()
except Exception as e:
logger.error(f"[_collect_local_logs] {e}")
# ─── Boucle d'analyse ────────────────────────────────────────────────────
def _analysis_loop(self):