mirror of
https://github.com/Alkatrazz24/Funk-lab.git
synced 2026-07-09 01:24:42 +02:00
350 lines
16 KiB
Markdown
350 lines
16 KiB
Markdown
# n8n — Automatisation de workflows
|
|
|
|
n8n est le moteur d'automatisation du lab. Il orchestre les tâches récurrentes, connecte les services entre eux et envoie des rapports/alertes par email via LiteLLM + Postfix.
|
|
|
|
---
|
|
|
|
## Accès
|
|
|
|
| URL | `http://n8n.lab.local` |
|
|
|---|---|
|
|
| Namespace k8s | `ai` |
|
|
| Port interne | `5678` |
|
|
| API key | générée dans Settings → n8n API — stockée dans le vault Ansible (`vault_n8n_api_key`) |
|
|
|
|
---
|
|
|
|
## Architecture
|
|
|
|
```
|
|
Déclencheurs Pipeline Sortie
|
|
──────────── ──────── ──────
|
|
AlertManager webhook → Formatage JSON Email Gmail
|
|
Cron (toutes les heures) → Requêtes Prometheus → (via Postfix :25)
|
|
Webhook manuel → LiteLLM hermes-default
|
|
(résumé/analyse)
|
|
```
|
|
|
|
**SMTP** : n8n envoie via Postfix sur `192.168.10.1:25` (pas d'auth, réseau interne). Postfix relaie vers `smtp.gmail.com:587` avec App Password.
|
|
|
|
**LLM** : n8n appelle LiteLLM sur `http://192.168.10.1:4000/v1/chat/completions` avec `model: hermes-default`.
|
|
|
|
---
|
|
|
|
## Déploiement (ArgoCD)
|
|
|
|
Manifests dans `k8s/apps/n8n/` :
|
|
- `deployment.yaml` — pod n8n
|
|
- `service.yaml` — ClusterIP :5678
|
|
- `ingress.yaml` — IngressRoute Traefik
|
|
- `pvc.yaml` — données applicatives (NFS)
|
|
- `secret.yaml` — secrets (non commité, à créer manuellement)
|
|
|
|
Application ArgoCD : `k8s/apps-of-apps/apps/n8n.yaml`
|
|
|
|
### Créer le secret (première installation)
|
|
|
|
```bash
|
|
kubectl create secret generic n8n-secret -n ai \
|
|
--from-literal=db-password="PASSWORD" \
|
|
--from-literal=encryption-key="$(openssl rand -hex 32)"
|
|
```
|
|
|
|
Le mot de passe est dans le vault Ansible (`vault_pg_n8n_password`).
|
|
|
|
---
|
|
|
|
## Credentials configurées
|
|
|
|
| Nom | Type | Détail |
|
|
|---|---|---|
|
|
| `Postfix local` | SMTP | `192.168.10.1:25`, sans auth, STARTTLS désactivé |
|
|
|
|
> Pour créer une credential OpenAI/LiteLLM manuellement :
|
|
> Settings → Credentials → New → OpenAI → Base URL: `http://192.168.10.1:4000/v1`, API Key: `lm-studio`
|
|
|
|
---
|
|
|
|
## Workflows actifs
|
|
|
|
### 1. Alertes Prometheus → Résumé LLM → Gmail
|
|
|
|
**ID** : `4lfZ0sHZqQ5HYEph`
|
|
**Déclencheur** : Webhook POST sur `http://n8n.lab.local/webhook/alerts`
|
|
**Configuré dans** : `k8s/infra/monitoring/values.yaml` (receiver AlertManager)
|
|
|
|
**Pipeline** :
|
|
```
|
|
AlertManager POST
|
|
│
|
|
├── Respond to Webhook (répond OK immédiatement)
|
|
│
|
|
▼
|
|
Format Alertes (Code)
|
|
→ extrait alertname, instance, severity, summary, description
|
|
│
|
|
▼
|
|
Résumé LLM (HTTP Request → LiteLLM)
|
|
→ system: "assistant ops, résumé concis en français"
|
|
│
|
|
▼
|
|
Envoyer Email (SMTP)
|
|
→ Subject: [Funk] N alerte(s) — firing/resolved
|
|
→ Body: résumé LLM
|
|
```
|
|
|
|
**Test manuel** :
|
|
```bash
|
|
curl -X POST http://n8n.lab.local/webhook/alerts \
|
|
-H "Content-Type: application/json" \
|
|
-d '{
|
|
"version":"4","status":"firing","receiver":"n8n-webhook",
|
|
"alerts":[{
|
|
"status":"firing",
|
|
"labels":{"alertname":"HighMemory","instance":"192.168.10.12:9100","severity":"warning"},
|
|
"annotations":{"summary":"RAM élevée","description":"RAM > 85%"}
|
|
}]
|
|
}'
|
|
```
|
|
|
|
---
|
|
|
|
### 2. Rapport Horaire Système — Funk
|
|
|
|
**ID** : `i2KAGxBFDps7IjIP`
|
|
**Déclencheur** : Cron toutes les heures (`:00`)
|
|
|
|
**Pipeline** :
|
|
```
|
|
Schedule (toutes les heures)
|
|
│
|
|
├── CPU (Prometheus) avg 1h par nœud
|
|
├── Mémoire (Prometheus) avg_over_time 1h
|
|
├── Mémoire Max (Prometheus) max_over_time 1h
|
|
├── Disque (Prometheus) snapshot actuel
|
|
├── Services (Prometheus) up/down
|
|
└── Services Changes changements d'état sur 1h
|
|
│
|
|
▼
|
|
Format Rapport (Code)
|
|
→ tableau métriques par nœud (avg/max mémoire, CPU moyen 1h)
|
|
→ liste services stables/instables
|
|
│
|
|
▼
|
|
Analyse LLM (HTTP Request → LiteLLM)
|
|
→ system: "résumé état général, points d'attention, recommandation"
|
|
│
|
|
▼
|
|
Préparer Email (Code)
|
|
→ assemble : résumé LLM + séparateur + tableau métriques
|
|
│
|
|
▼
|
|
Envoyer Rapport (SMTP)
|
|
→ Subject: [Funk] Rapport systeme - DD/MM/YYYY HH:MM:SS
|
|
→ Body HTML (pre tag) : résumé + métriques
|
|
```
|
|
|
|
**Queries Prometheus utilisées** :
|
|
|
|
| Nœud | Query |
|
|
|---|---|
|
|
| CPU | `100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[1h])) * 100)` |
|
|
| Mémoire | `avg_over_time((1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)[1h:5m]) * 100` |
|
|
| Mémoire Max | `max_over_time((1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)[1h:5m]) * 100` |
|
|
| Disque | `(filesystem_size - filesystem_free) / filesystem_size * 100` (mountpoint=/) |
|
|
| Services | `up` |
|
|
| Services Changes | `changes(up[1h]) > 0` |
|
|
| GPU Power 1h | `avg_over_time(rocm_gpu_power_watts{instance="gpu-01"}[1h:5m])` → Wh cette heure |
|
|
| GPU Power 24h | `avg_over_time(rocm_gpu_power_watts{instance="gpu-01"}[24h:5m])` → kWh 24h glissants |
|
|
|
|
**Section énergie GPU** : affichée dans chaque rapport entre l'analyse Hermes et les métriques brutes. À minuit (exécution 00:00), un encart **BILAN JOURNALIER** met en avant la consommation totale en kWh des 24h écoulées.
|
|
|
|
---
|
|
|
|
### 3. Hermes Doc Worker — Funk-lab
|
|
|
|
**ID** : `CX72UUOempE1YtNL`
|
|
**Déclencheurs** : Schedule quotidien **22h00** · webhook manuel `POST /webhook/hermes-doc-run` · (un ancien Schedule 30min existe mais est **désactivé**)
|
|
|
|
**Pipeline** :
|
|
```
|
|
Schedule 22h (ou webhook manuel)
|
|
│
|
|
▼
|
|
Déclencher run (HTTP POST storage-01:9095/trigger/all)
|
|
→ lance hermes-auto-improve --all (28 docs, ~60-90 min)
|
|
│
|
|
▼
|
|
┌── Attendre run (5 min) ◄────────────┐
|
|
│ │ │
|
|
│ ▼ │
|
|
│ Status run (GET :9095/status) │
|
|
│ │ │
|
|
│ ▼ │
|
|
│ Run terminé ? (IF running=false) ─┘ false → reboucle
|
|
│ │ true
|
|
└───────▼
|
|
Format Run → LLM Run (résumé) → Email Run → Envoyer Run
|
|
│
|
|
▼
|
|
Est 22h ? (IF heure courante = 22)
|
|
│ true
|
|
▼
|
|
Déclencher PR (POST :9095/trigger/daily-pr)
|
|
→ crée ou met à jour la PR hermes/daily-work → main (squash-only)
|
|
│
|
|
▼
|
|
Attendre PR (5 min) → Status PR → Format PR → LLM PR → Email PR → Envoyer PR
|
|
```
|
|
|
|
**Backend** : trigger server `hermes-auto-improve` sur storage-01 (`:9095`), déployé par le rôle Ansible `hermes_auto_improve`. Voir `admin/ia/hermes-auto-improve.md`.
|
|
|
|
**Gestion via API** : la clé API n8n est dans le vault Ansible (`vault_n8n_api_key`) :
|
|
```bash
|
|
make vault-view | grep n8n_api_key
|
|
curl -s -H "X-N8N-API-KEY: $KEY" http://n8n.lab.local/api/v1/workflows
|
|
```
|
|
|
|
---
|
|
|
|
## Workflows à ajouter
|
|
|
|
### Court terme
|
|
|
|
**Alerte disque > 80%**
|
|
Cron toutes les 30 min → query Prometheus `disk > 80` → si résultat non vide → email d'alerte.
|
|
Utile car AlertManager ne couvre pas toujours les seuils fins par nœud.
|
|
|
|
**Redémarrage automatique pod crashé**
|
|
Webhook AlertManager sur `KubePodCrashLooping` → `kubectl rollout restart` via HTTP Request vers l'API k8s (ou script SSH sur storage-01) → notification email.
|
|
|
|
**Rapport hebdomadaire étendu**
|
|
Cron dimanche 09h → même pipeline que le rapport horaire mais avec fenêtre 7 jours (`[7d]`) + statistiques min/max de la semaine.
|
|
|
|
### Moyen terme
|
|
|
|
**Budget Anthropic API**
|
|
Cron quotidien → requête API Anthropic usage → si > seuil configuré → email d'alerte.
|
|
Évite les surprises de facturation Claude.
|
|
|
|
**Notification nouveaux commits Funk-lab**
|
|
GitHub webhook → n8n → email/notification résumé du commit.
|
|
Utile pour tracker les changements sur d'autres postes.
|
|
|
|
**Monitor llama-server performance**
|
|
Cron horaire → query Prometheus `llamacpp_tokens_per_second` → si tokens/s < seuil (GPU dégradé) → alerte email.
|
|
|
|
**Rotation/nettoyage logs**
|
|
Cron quotidien → SSH storage-01 → `journalctl --vacuum-time=30d` → rapport taille libérée.
|
|
|
|
### Long terme
|
|
|
|
**Auto-scaling modèle Hermes**
|
|
Monitor charge LiteLLM → si charge élevée, switcher automatiquement vers `claude-sonnet-4-6` via `hermes-switch claude` → revenir à `qwen` après N minutes.
|
|
|
|
**RAG re-indexation automatique**
|
|
Webhook Git (push sur admin/) → déclencher `rag-ingest` via SSH sur storage-01 → email confirmation nombre de chunks indexés.
|
|
|
|
**Résumé quotidien Hermes**
|
|
Cron 08h00 → interroger Hermes sur l'état du lab → recevoir un briefing matinal par email avec état services, alertes de la nuit, métriques clés.
|
|
|
|
---
|
|
|
|
## Opérations courantes
|
|
|
|
```bash
|
|
# Logs n8n
|
|
kubectl logs deploy/n8n -n ai -f
|
|
|
|
# Redémarrer n8n
|
|
kubectl rollout restart deploy/n8n -n ai
|
|
|
|
# Lister les workflows via API
|
|
curl -s http://n8n.lab.local/api/v1/workflows \
|
|
-H "X-N8N-API-KEY: <clé>" | python3 -m json.tool
|
|
|
|
# Activer/désactiver un workflow
|
|
curl -s -X POST http://n8n.lab.local/api/v1/workflows/<id>/activate \
|
|
-H "X-N8N-API-KEY: <clé>"
|
|
curl -s -X POST http://n8n.lab.local/api/v1/workflows/<id>/deactivate \
|
|
-H "X-N8N-API-KEY: <clé>"
|
|
|
|
# Dernières exécutions
|
|
curl -s "http://n8n.lab.local/api/v1/executions?limit=5" \
|
|
-H "X-N8N-API-KEY: <clé>" | python3 -c \
|
|
"import sys,json; [print(e['id'], e['status'], e.get('stoppedAt','')) for e in json.load(sys.stdin)['data']]"
|
|
```
|
|
|
|
---
|
|
|
|
## Dépannage
|
|
|
|
| Symptôme | Cause | Action |
|
|
|---|---|---|
|
|
| Email vide | Expression `$json.xxx` évaluée à vide dans emailSend | Utiliser un nœud Code intermédiaire qui assemble le corps, puis `emailType: "html"` + `html: "={{ $json.htmlBody }}"` |
|
|
| SMTP ETIMEDOUT | Port 25 non ouvert pour les pods | Vérifier nftables : `tcp dport 25 ip saddr { 192.168.10.0/24, 10.42.0.0/16 }` |
|
|
| LLM timeout | hermes-default trop lent (> 2 min) | Passer en mode async : ajouter "Respond to Webhook" en branche parallèle avant le LLM |
|
|
| Prometheus query vide | Service DNS interne non résolu | Utiliser l'URL service k8s : `http://kube-prometheus-stack-prometheus.monitoring.svc.cluster.local:9090` |
|
|
| Workflow ne se déclenche pas | Webhook AlertManager mal configuré | Tester : `curl -X POST http://n8n.lab.local/webhook/alerts -d '{...}'` |
|
|
|
|
---
|
|
|
|
## Workflows « Rapport Portefeuille » — Ghostfolio + Qwen
|
|
|
|
Deux workflows (rapport commenté du portefeuille : données exactes via API, narration + actus par le LLM local) :
|
|
- **Hebdo** (`Rapport Portefeuille Hebdo — Ghostfolio`) : cron `0 18 * * 0` (dimanche 18h), `range=1w`, analyse approfondie (max_tokens 1500).
|
|
- **Quotidien** (`Rapport Portefeuille Quotidien — Ghostfolio`) : cron `0 8 * * *` (tous les jours 8h), `range=1d`, format digest concis (max_tokens 900).
|
|
|
|
**Pipeline** (identique, paramétré) : `Trigger` → `Auth Ghostfolio` → `Get Performance` → `Get Holdings` → `Préparer Données` → `Analyse LLM` → `Mise en forme` → `Envoyer`.
|
|
|
|
- **Auth durable** : nœud `Auth Ghostfolio` = `POST /api/v1/auth/anonymous` avec le **jeton de sécurité** (compte anonyme) → renvoie un JWT frais à chaque run (les nœuds GET utilisent `=Bearer {{ $('Auth Ghostfolio').first().json.authToken }}`). **Plus d'expiration à gérer.** Le jeton de sécurité est stocké dans le body du nœud Auth (base n8n, non versionné).
|
|
- **Préparer Données** (Code) : exclut `assetClass=LIQUIDITY` (cash/Livret A) ; calcule valeur, perf, top/flop, concentration (poids **relatifs au portefeuille actions**) ; **récupère les actus par action** (top 6 par poids) via Yahoo RSS `feeds.finance.yahoo.com/rss/2.0/headline?s=<sym>` avec `this.helpers.httpRequest` (wrap try/catch → si échec, le rapport part sans news).
|
|
- **Analyse LLM** : LiteLLM `:4000`, modèle `hermes-default` (Qwen). Le prompt autorise des **recommandations/pistes** + signale bonne/mauvaise nouvelle par action, en s'appuyant UNIQUEMENT sur données + news fournies.
|
|
- **News en français** : les titres Yahoo RSS arrivent **en anglais**. Le prompt système force la traduction : *« Les titres d'actualité fournis sont en anglais : traduis ou résume chaque news en français (ne recopie aucun titre en anglais). »* (sans ça, Qwen recopie les titres en anglais).
|
|
- ⚠️ **Caveat précision** : Qwen 8B **peut se tromper sur les chiffres** dans sa prose. L'email inclut donc TOUJOURS le **tableau de positions exact** (calculé par n8n) : se fier au tableau pour les nombres, à la prose pour le contexte. Le LLM ne navigue pas — c'est n8n qui apporte les news.
|
|
- ⚠️ **Caveat tokens** : Qwen3 « réfléchit » avant de répondre, et la réflexion consomme le budget `max_tokens`. Si un digest paraît tronqué (`finish_reason: length`, `content` vide), augmenter `max_tokens` du nœud `Analyse LLM`.
|
|
|
|
> ⚠️ **Piège apostrophe JS — corrigé le 2026-06-16.** Dans le nœud `Préparer Données`, `aujourd'hui` était écrit dans une chaîne entre **apostrophes simples** (`'...aujourd'hui...'`) → l'apostrophe fermait la chaîne → `SyntaxError: Unexpected identifier 'hui'`. Règle : tout texte FR avec apostrophe (`l'`, `d'`, `aujourd'hui`) dans un Code node doit être **échappé** (`\'`) ou placé entre guillemets doubles `"..."` / backticks. À noter : `node --check` brut signale un faux positif `await is only valid in async functions` car n8n enveloppe le code d'un Code node dans une fonction async — vérifier en l'enveloppant soi-même.
|
|
|
|
### Édition des workflows via l'API
|
|
|
|
PUT `/api/v1/workflows/:id` n'accepte que `{name, nodes, connections, settings}`, et `settings` **rejette les clés non whitelistées** (`binaryMode` → HTTP 400 `must NOT have additional properties`). Filtrer `settings` sur : `saveExecutionProgress`, `saveManualExecutions`, `saveDataErrorExecution`, `saveDataSuccessExecution`, `executionTimeout`, `errorWorkflow`, `timezone`, `executionOrder`. Le PUT **désactive** le workflow → réactiver via `POST /api/v1/workflows/:id/activate` (avec `Content-Type: application/json`).
|
|
|
|
---
|
|
|
|
## Workflow « Alertes Prometheus → Résumé LLM → Gmail » — parsing du webhook
|
|
|
|
> ⚠️ **Piège corrigé le 2026-06-15.** Le nœud Webhook n8n imbrique le payload HTTP sous
|
|
> `.body` (`$input.first().json` = `{ headers, params, query, body }`). Le nœud **« Format Alertes »**
|
|
> lisait `body.alerts` / `body.status` **au mauvais niveau** → toujours `[]` et `'unknown'`.
|
|
> Résultat : **tous** les mails affichaient « 0 alerte(s) — unknown » sans détail, **même lors d'une vraie alerte** (pipeline d'alerte aveugle).
|
|
|
|
Correctif appliqué dans le code du nœud « Format Alertes » :
|
|
|
|
```js
|
|
const root = $input.first().json;
|
|
const payload = root.body || root; // <-- lit le payload imbriqué (repli si format plat)
|
|
const groupStatus = payload.status || 'unknown';
|
|
const allAlerts = payload.alerts || [];
|
|
// Anti-bruit : exclure Watchdog / InfoInhibitor et severity none/info
|
|
const alerts = allAlerts.filter(a => {
|
|
const sev = (a.labels?.severity || '').toLowerCase();
|
|
const name = a.labels?.alertname || '';
|
|
return !['none','info',''].includes(sev) && !['Watchdog','InfoInhibitor'].includes(name);
|
|
});
|
|
if (alerts.length === 0) { return []; } // aucune vraie alerte -> pas d'email (stoppe le workflow)
|
|
```
|
|
|
|
- AlertManager reçoit toujours son `HTTP 200` (nœud « Respond 200 » en branche parallèle, indépendant).
|
|
- Test sans email : `curl -XPOST http://n8n.lab.local/webhook/alerts -d '{"status":"firing","alerts":[{"labels":{"alertname":"Watchdog","severity":"none"}}]}'` → doit produire **0 item** au nœud « Format Alertes », **pas d'email**.
|
|
- Workflow non versionné (vit dans la base n8n) ; édition via l'API : `PUT /api/v1/workflows/<id>` avec `{name,nodes,connections,settings}`.
|
|
|
|
---
|
|
|
|
## Configuration Ansible / k8s
|
|
|
|
- **Secrets k8s** : `kubectl get secret n8n-secret -n ai`
|
|
- **Base de données** : PostgreSQL `n8n` sur storage-01 (`vault_pg_n8n_password`)
|
|
- **PVC data** : NFS, 5 Gi, claimName `n8n-data`
|
|
- **nftables** : port 25 ouvert `10.42.0.0/16` (pods) dans le rôle `gateway`
|
|
- **AlertManager** : receiver `hermes-monitor` dans `k8s/infra/monitoring/values.yaml`
|