From c039b92d6da4787d54a492cef5f41788b80cfaa0 Mon Sep 17 00:00:00 2001 From: sylvain Date: Thu, 2 Apr 2026 09:07:27 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20collecte=20automatique=20des=20logs=20l?= =?UTF-8?q?ocaux=20au=20d=C3=A9but=20de=20chaque=20cr=C3=A9neau?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - _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 --- __pycache__/agent_logwatch.cpython-313.pyc | Bin 37035 -> 40488 bytes agent_logwatch.py | 69 ++++++++++++++++++++ config/config.json | 3 + config/system_prompt.txt | 1 + skills/__pycache__/logwatch.cpython-313.pyc | Bin 12594 -> 12932 bytes skills/logwatch.py | 9 ++- 6 files changed, 81 insertions(+), 1 deletion(-) diff --git a/__pycache__/agent_logwatch.cpython-313.pyc b/__pycache__/agent_logwatch.cpython-313.pyc index 98829860f9b1673e1164bba931b9d02a0e97cf55..ed5b8ec5b3690adf3ec0c1538466640d9bbd031d 100644 GIT binary patch delta 4848 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 delta 1744 zcmZ9MeNa?Y6u{qk`vEQNnl7#ixGWzFEZ~ZY;!GMc2+9N+zQBZy71jr=?T6jn6~Ux& z3?0luITM;XHD+TDm`yw;rgSVvs~MWiJ|jhY|b^H6JgfBfcr-h0lu z_rAl&#lZujX3lDNfIq?hcPWjfdF+@GdtX_CWZlWd+iDcxh|9!F8Ul%aXjA9{FZre?{I z=4iGo9_{2S8l`NquLYJV<#3x}wI_}T=W4-Ol8bAil&5J3Q_jq408o^? zImh%cq+BdMp))FcOl(*gcp}G;GRkF_PSv-CV@x#${h?S?W~V6NOT4^j6$~g9r6rZ^VZl0KiJIkk{waSV=22A)_c}?oqJer*)d_xd9Tzd8$$=~DbifwKy>mcO^!WdzI zpd)B4y+e|Rpy$9l70Vw#Pufp}p9uDVBz#z6aqYjSXc5jY&Q$WR=4VzK* zuIIz9S=}g38KM1XkgmA@lz0_CZ*~IU$iWd=G~b{N{-SV^jVPyEt6`Bg6m3S&k+YF-ncyYd;!ySN zP5!9M$V$ng)wqi!J8+n=#&3svyuZoj$n#!#LsSk${Nd06rO#iIzz_Y~Xa^rlRcsgz zDX@M35+2piS%Hl)C!TAU!GWb4_QN4OzrmUEE^Qef2)B9z{*cVFx#|*C(Ot+UpuM9S z%m)J*mMjc*$75M{-T1@1if?sK&eul2 zm=@Qzd9;1R$ZK$DAIqhp?+bXL+X;)&u(cgZlucVN@qwYgrv^&!SWm9Kn6IHusS3BQ zY>N8YN?YYn)a~^(1p?kcAoz>|+g}u61$OPQiMIso-;oAw_~DL#LDl(!Ym}T 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): diff --git a/config/config.json b/config/config.json index cd9cca4..071d829 100644 --- a/config/config.json +++ b/config/config.json @@ -24,6 +24,9 @@ "db_path": "/opt/agent_logwatch/data/logwatch.db", "system_prompt": "/opt/agent_logwatch/config/system_prompt.txt", "use_llm_coordinator": true, + "local_log_since": "yesterday", + "local_log_units": [], + "local_hostname": "", "llm_profiles": { "cloud": "gpt-oss:120b-cloud" } diff --git a/config/system_prompt.txt b/config/system_prompt.txt index 5eaa59c..a82015e 100644 --- a/config/system_prompt.txt +++ b/config/system_prompt.txt @@ -23,6 +23,7 @@ Tu reçois des instructions via MQTT (depuis Nexus) ou XMPP (directement). - `retention ` : durée de conservation des logs filtrés - `analyze ` : lancer l'analyse d'une machine spécifique maintenant - `analyze_all` : lancer l'analyse complète de toutes les machines + - `collect [since]` : collecter maintenant les logs locaux (ex: collect "1 hour ago") - `logs [N]` : voir les N derniers logs filtrés d'une machine - `reset ` : réinitialiser l'analyse d'une machine diff --git a/skills/__pycache__/logwatch.cpython-313.pyc b/skills/__pycache__/logwatch.cpython-313.pyc index ee2b927844280f2ccd643c12c04d3d08d579a749..feada4a4996e736e3fe481104e2906da90f015f4 100644 GIT binary patch delta 1025 zcmY+CZD?Cn7{||ZPw&0iZJPErZ*6AVn=UWSlHBIfYF}uV)+y{$kR{O%YLeSDcGWbU zH!awzd-0757OBr*4i)u_IFT_I7@H_4iWS7sZo;}aN9va$_#spU*@xk|SrI)Se$R8B z`|v;ibDwiz>Xm}+kCqmRu!Y){*=6-J+j|^j@Kf#)dqm047mJ1bLP06c=k;Pif2VSn zyARrS%gW>B*;2mnq=W#Dil<2c@S&H#0Dbrqe+eSEDm-jeWI{5KiDuMHKi(2{!-;rJqY)h*oY&>pWFcDt^=!2NjPxgV-YXcB(d#Ob$>V zNqdtio>`vjPeWQw_v0a1V|~kVl<{LZ%tuq~0Q^GQ%PWMW%oE~Fe7OB<_We%#B;%O< zp7IFGKXGb!+n%zU`Ug$@9kbgeWBWTq%s_MA!bq zBlpVxWjdT(8i7(IxtxyJ$NfH4LFXByS7>+e~ z90y(g))Ohy+ey<~H<@_}e|PPrLShwPbDg9#F@TcWOS!}ls%}4A#h6=>5^s*v9`Y1~ zUalN-KP$q;O1;~|!Fr|H8{{U$-4RyzEEUQN1!G!Y3PF``%yY4aUF6qyUsVqNfBFjR zo0~0tbG9%lRkVHc0A2W@?>ASqeOD~ocqNQ89_h78eYW zZ494@I72zcpyY5f;KN!#?3|dGFwDnTrN&~(IK?uvp-lEvY{4~gxbvFm*=lo~mewS+ zUT+J3(7eKL$sMP=*1E3C+NP{s;x}Y%Lq4z~Y{}aB!1<1i*3k{iD4tQjhsSWL|H$!& zfY>~%PuwZL&xF7AOa^iS{Xu)MDc3=Me4v%_iqN3TX6Ali+>FS%K615J&JB}mqrJL? zZrOWu54pjsdX(N6_cQ)7rs{EeYjBcdp(aQX@>27h`W7YT5?GTuZO8S_%9hatB#1I&r%x0*$DrU3O^Z9Jncm`jOX~NU2YMjEa zVjehyKgX1z(uwAJlf;MXlz_0)4Y2P|3qWLtAy~KMDF`e1Ix*wh<-Z!yuriH*03`Mp AJOBUy delta 799 zcmXAnUr19?9LLY^9=E&Q&HcOTbkp3-IpyBzR+N^R&FCR2liYer?@UdLmN<$2nn?vg zgvl=0+ z&6NmS2ZM7bpQ)3!2Lc?z$3lzHaZb0>V$cug7y&lpQNt7j(QJGU3eFn$*(V%?v_ren zuC|B9NF06E65OAgk6&F@jENI$>aRFYl_M_vZk2J(l$Y&??Xqq)uK~MuTJo^eWj2Hf zi7C3%^2R{bn2Jk|Mo@854q}tDtVp$y*4C}GoW%XDF$0U448!N*c9tb9^rKvwC?N}m>O`0>Gz1twHCA^Bbh$Pvu#z@QTA`TsN?IjN2Z2+O zv^tuu1#J&2^-{t_S47~nNC}U)QesVn=!y!QVJXolt_o6zL#%>S5sO)6Srj#`tFKej zQoXnoiTHciX^Ma3Lz1%co\n" "SKILL:logwatch ARGS:analyze \n" "SKILL:logwatch ARGS:analyze_all\n" + "SKILL:logwatch ARGS:collect [since]\n" "SKILL:logwatch ARGS:retention \n" "SKILL:logwatch ARGS:logs [N]\n" "SKILL:logwatch ARGS:reset " @@ -239,6 +240,12 @@ def run(args: str, context) -> str: ) return "\n".join(lines) + # ── collect [since] ─────────────────────────────────────────────────────── + if action == 'collect': + since = rest.strip() or 'yesterday' + result = context.agent.collect_local_logs(since=since) + return f"✅ Collecte locale terminée:\n{result}" + # ── reset ────────────────────────────────────────────────────── if action == 'reset': hostname = rest.strip()