[English](README.md) | Français # Logstream Un collecteur syslog simple : il reçoit les logs en UDP/TCP sur le port 514, les stocke dans [VictoriaLogs](https://docs.victoriametrics.com/victorialogs/) et sert une interface web soignée (thème clair/sombre, recherche instantanée, direct, tags de couleur, interface en anglais et en français). ``` devices ──514 udp/tcp──▶ logstream (Go) ──HTTP batches──▶ VictoriaLogs ▲ └── SSE (live) ──▶ browser └──── API / UI ◀─────────┘ ``` ## Architecture ![Schéma logique de Logstream](docs/architecture.png) - **Ingestion** : les messages syslog (UDP/TCP) et les logs des conteneurs Docker passent tous par `sink()` (résolution DNS inverse des hôtes donnés par leur IP, sans bloquer la réception), puis par la file du `Store`, qui les envoie par lots à VictoriaLogs. Quand VictoriaLogs est injoignable, les lots sont gardés sur disque (`/data/spool`, jusqu'à `SPOOL_MAX_MB`) et renvoyés, les plus anciens d'abord, dès son retour. - **Direct** : `sink()` publie aussi chaque message dans le `Hub`, qui le diffuse aux navigateurs en SSE. - **Recherche** : l'API HTTP traduit les filtres de l'interface en requêtes LogsQL envoyées à VictoriaLogs. - **Frise** : `/api/histogram` (`histogram.go`) compte les logs par intervalle et par sévérité, alignés sur l'heure locale ; les bornes du zoom (`from`/`to`) s'appliquent aussi à la liste et à l'export. - **État** : les tags et les réglages des sources sont dans `/data` (volume `logstream-data`) ; les logs eux-mêmes dans le volume `vlogs-data`. La source modifiable du schéma est [`docs/architecture.excalidraw`](docs/architecture.excalidraw) (à ouvrir sur [excalidraw.com](https://excalidraw.com)). ## Démarrage ```bash cp .env.example .env # optional docker compose up -d --build ./tools/send-test-logs.sh # sends 100 test messages ``` Ouvrez ensuite . ## Envoyer des logs - **rsyslog** (Linux) : ajoutez `*.* @SERVER_IP:514` (UDP) ou `*.* @@SERVER_IP:514` (TCP) dans `/etc/rsyslog.d/90-logstream.conf`, puis lancez `systemctl restart rsyslog`. - **Équipements réseau, NAS, pare-feu** : indiquez l'IP du serveur et le port 514 dans leurs réglages « syslog distant » (remote syslog). - **Test manuel** : `logger -n 127.0.0.1 -P 514 -d "hello error"` (util-linux) ou `echo "<14>test ok" | nc -u -w1 127.0.0.1 514`. **Paramètres > Sources de logs > Syslog** active ou désactive la réception syslog et choisit les protocoles (UDP, TCP) sans redémarrage ; le choix est enregistré dans `/data/syslog.json`. Cet onglet affiche aussi l'état d'écoute (et l'erreur si le port est déjà utilisé). Le port lui-même est publié par docker-compose : modifiez `SYSLOG_PORT` dans `.env`, puis lancez `docker compose up -d`. Formats pris en charge : RFC 3164 (BSD) et RFC 5424. En TCP, les deux découpages sont acceptés : « un message par ligne » et « octet counting » (RFC 6587). ## Recherche **Mode simple** (par défaut) : chaque mot est cherché comme sous-chaîne, sans tenir compte de la casse, dans le message, l'hôte et l'application. Les mots sont combinés avec ET. | Saisie | Signification | |---|---| | `error disk` | contient « error » **et** « disk » | | `"disk full"` | contient la phrase exacte | | `error -timeout` | contient « error » mais pas « timeout » | **Mode LogsQL** (bouton `Simple` / `LogsQL`) : le langage de requête complet de VictoriaLogs, par exemple `error AND host:web-01`, `app:~"ssh|nginx"` ou `* | stats by (host) count()`. Le direct est désactivé dans ce mode. Chaque ligne affiche, de gauche à droite : l'**heure de réception** (horloge du serveur), l'horodatage trouvé dans le message lui-même (`msg_time`), la sévérité, l'hôte, l'application, les codes des tags trouvés et le message. Un clic sur un hôte ou une application filtre dessus. Les logs sont indexés, recherchés et triés par **heure de réception** : les équipements dont l'horloge est fausse (par exemple des points d'accès dont le NTP échoue) apparaissent quand même dans la bonne plage de temps. Les logs stockés par les versions antérieures à ce changement sont indexés par l'horodatage de leur message ; purgez-les depuis les Paramètres pour repartir sur une base propre. Les badges de sévérité `err`/`crit` et `warning` reprennent les couleurs des tags `error` et `warning`. Chaque ligne commence par un badge de largeur fixe qui indique sa source, avec une couleur par source : **SYSLOG**, **DOCKER**, **KERNEL** (dmesg), **HOST** (fichiers de `/var/log`), **AUDIT**, **FILE** et **LS** (logs de LogStream lui-même). Raccourcis : `/` place le curseur dans la recherche, `Esc` la vide. Un clic sur une ligne affiche tous ses champs. Les en-têtes de colonnes restent visibles pendant le défilement. Faites glisser le bord d'un en-tête (réception, heure message, sévérité, hôte, app) pour redimensionner la colonne, et double-cliquez dessus pour revenir à la largeur automatique ; le message occupe l'espace restant. Les largeurs sont mémorisées par le navigateur (**Paramètres > Affichage > Réinitialiser les colonnes** les rétablit toutes). Sur téléphone, la liste garde sa présentation sur deux lignes, sans colonnes. ## Modes Flux et Période Le premier contrôle de la barre de filtres bascule entre deux modes d'affichage : - **Flux** : les derniers logs sur une durée glissante (5 min à 30 jours, ou tout), avec le direct. - **Période** : les logs entre une date de début et une date de fin, saisies dans le fuseau choisi dans les Paramètres. ◀ et ▶ passent à la période précédente ou suivante de même durée, la loupe la double autour de son milieu. Le direct se met en pause ; le mode et la période sont conservés au rechargement. ## Frise La frise au-dessus de la liste montre le volume de logs par intervalle, compté selon l'**heure de réception** sur l'horloge du serveur (elle correspond donc aux heures affichées dans les lignes). - Au survol d'un intervalle : ses bornes, son total et le détail par sévérité. - Un clic sur une barre affiche cet intervalle en mode Période ; un glisser sur plusieurs barres affiche la sélection. « × Revenir au flux » repasse en mode Flux. La liste, les compteurs et l'export CSV suivent la période. - En direct, le dernier intervalle grandit à l'arrivée des messages, et la frise se recharge à chaque nouvel intervalle. Rien n'est rafraîchi tant que l'onglet du navigateur est masqué ; la frise se met à jour dès qu'il redevient visible. **Paramètres > Affichage > Frise** (mémorisé par navigateur) : échelle (linéaire, √ par défaut, log), hauteur (S/M/L : 40/80/120 px), couleur (empilement par sévérité, intensité comparée à la médiane de la fenêtre : calme, rafale au-delà de 3×, anomalie au-delà de 10×, ou aucune), barres ou aire, division (automatique, environ 100 intervalles, ou fixe : 1 s, 10 s, 1 min, 5 min, 1 h, 1 jour) et rafraîchissement (désactivé, 5 s, 15 s, 30 s, 1 min, ou à chaque nouvel intervalle). Une division fixe qui dépasserait 300 intervalles sur la plage est élargie (signalé par « élargi »). Les intervalles sont alignés sur l'heure locale du fuseau horaire choisi dans les Paramètres (les jours commencent à minuit, heure locale). API : `curl 'localhost:8080/api/histogram?range=24h&step=auto&tz=Europe/Paris'` renvoie `step` (ms), `start` (ms), `count`, `now` (horloge du serveur) et les `buckets` non vides (`i` = numéro d'intervalle, `n` = total, `sev` = nombre par sévérité). Toutes les routes de logs acceptent aussi `from` / `to` (millisecondes Unix) à la place de `range`. ## Logs des conteneurs Docker Logstream collecte aussi les logs des conteneurs Docker qui tournent sur la machine où il est installé (`DOCKER_LOGS=on`, le défaut dans `docker-compose.yml`). Ils sont recherchés, filtrés, colorés et exportés comme les messages syslog : - **host** est le nom de l'hôte Docker, **app** le service compose (ou le nom du conteneur), et chaque log porte aussi `container`, `container_id`, `image`, `compose_project`, `compose_service` et `stream` (stdout/stderr), visibles dans le détail de la ligne. - Le filtre **Source** n'affiche que les logs syslog ou que les logs Docker ; les lignes Docker ont un petit cube devant le nom de l'application, de la couleur de son projet compose. - La sévérité vient de la ligne elle-même quand l'application l'écrit : JSON (`"level":"error"`), logfmt (`level=warn`), `[ERROR]`, ou un niveau en majuscules au début de la ligne (`ERROR`, `WARN`…). Sinon, elle vaut `info`. Les codes de couleur du terminal sont supprimés. - **Paramètres > Sources de logs** affiche une étiquette par conteneur (`project/service`) dans un seul champ : les conteneurs suivis en couleur, puis les non suivis en gris ; un clic bascule une étiquette. La couleur identifie le projet compose, et les noms d'applications Docker de la liste utilisent la même couleur. Les conteneurs arrêtés sont masqués par défaut (« Afficher les conteneurs arrêtés » les montre, en pointillés, et ils restent modifiables). Une zone de filtre et « Tout activer » / « Tout désactiver » (appliqués aux étiquettes affichées) aident quand les conteneurs sont nombreux. Les nouveaux conteneurs sont suivis automatiquement, sauf si cette option est désactivée. Les choix sont enregistrés par service compose (ou nom de conteneur) dans `/data/docker.json` : ils survivent aux recréations. - Logstream mémorise la position de la dernière ligne stockée pour chaque conteneur (`/data/docker-state.json`) : après un redémarrage, il reprend sans perdre de lignes. La position n'avance qu'une fois la ligne dans VictoriaLogs ou dans le tampon disque, et une file pleine ralentit la lecture au lieu de perdre des lignes. Un conteneur vu pour la première fois est lu à partir de `DOCKER_BACKFILL` en arrière (1 heure par défaut). - Logstream lui-même et le proxy ci-dessous ne sont jamais collectés ; ajoutez l'étiquette `logstream.exclude=true` à tout autre conteneur pour l'exclure définitivement. **Sécurité** : l'accès au socket Docker équivaut à un accès root sur la machine. Logstream passe donc par [docker-socket-proxy](https://github.com/Tecnativa/docker-socket-proxy), qui ne laisse passer que la liste des conteneurs, la lecture des logs, les événements et les informations du moteur (`GET` uniquement). ## Logs système de l'hôte Logstream peut aussi collecter les logs système de la machine qui héberge la stack, sans rien configurer sur l'hôte. La source est **désactivée par défaut** : activez-la dans **Paramètres > Sources de logs > Logs système de l'hôte** (enregistré dans `/data/hostlogs.json`). - `docker-compose.yml` monte `/var/log` et `/run/log/journal` en lecture seule sous `/host`. - Si l'hôte utilise systemd, Logstream lit directement les fichiers du **journal systemd** (pas besoin de `journalctl` dans l'image) : **host** est le nom de la machine, **app** le programme (`SYSLOG_IDENTIFIER`), la sévérité et la facility viennent du journal, et l'unité systemd est conservée dans `unit`. Sinon, il suit les fichiers texte de `/var/log` (`syslog`, `messages`, `*.log`), analysés comme des lignes syslog, avec le nom du fichier dans `log_file`. - Ces logs ont la source `host` : le filtre **Source** permet de les afficher seuls. - À l'activation, la dernière heure est lue d'abord (`HOST_LOGS_BACKFILL`) ; la position atteinte est enregistrée dans `/data/hostlogs-state.json`, un redémarrage ne perd donc ni ne duplique d'entrées. - **Droits** : le conteneur tourne avec un utilisateur sans privilège et reçoit le groupe `adm` (gid 4), qui peut lire le journal et `/var/log` sur Debian et Ubuntu. Sur d'autres systèmes, réglez `HOST_LOGS_GID` dans `.env` sur le gid de `systemd-journal` (`getent group systemd-journal | cut -d: -f3`). Paramètres > Sources de logs affiche un message clair si l'accès est refusé. - Limites : les champs du journal compressés par journald (messages de plus de 512 octets, compressés en zstd/lz4/xz) ne peuvent pas être décodés sans bibliothèque supplémentaire ; ils sont comptés dans les Paramètres et affichés comme « (compressed journal entry) ». Les fichiers texte tournés ou compressés (`*.1`, `*.gz`) ne sont pas lus. ## Messages du noyau (dmesg) Logstream peut collecter le buffer circulaire du noyau de l'hôte (ce qu'affiche `dmesg` : matériel, pilotes, OOM killer, pare-feu…) en lisant `/dev/kmsg`. La source est **désactivée par défaut** : activez-la dans **Paramètres > Sources de logs > Messages du noyau (dmesg)** (enregistré dans `/data/dmesg.json`). - **Périphérique** : le conteneur ne voit pas `/dev/kmsg` par défaut. Décommentez la section `devices` de `docker-compose.yml` (`/dev/kmsg:/dev/kmsg:r`, en lecture seule), puis `docker compose up -d`. - **Droits** : si l'hôte a `kernel.dmesg_restrict=1` (le défaut sur Debian et Ubuntu), seuls les processus qui ont la capacité `CAP_SYSLOG` peuvent lire le log du noyau, et le conteneur tourne avec un utilisateur sans privilège. Au choix : - l'autoriser sur l'hôte : `sysctl -w kernel.dmesg_restrict=0`, conservé au redémarrage avec `echo kernel.dmesg_restrict=0 | sudo tee /etc/sysctl.d/50-dmesg.conf` (tous les utilisateurs de l'hôte peuvent alors lancer `dmesg`) ; ce réglage ne peut pas être changé depuis un conteneur ; - ou lancer Logstream en root avec la capacité `SYSLOG` : décommentez `user: "0:0"` et `cap_add: [SYSLOG]` dans `docker-compose.yml`. Paramètres > Sources de logs indique si le périphérique est absent ou l'accès refusé ; le reste de Logstream continue de fonctionner et le périphérique est réessayé toutes les 10 secondes. - **Champs** : **app** vaut `kernel` (ou le nom du programme pour les messages écrits par des programmes, comme `systemd[1]: …`), la sévérité et la facility viennent de l'enregistrement, **host** vaut `localhost`, `subsystem` et `device` sont conservés quand le noyau les fournit. Ces logs ont la source `kernel` (`proto` `kmsg`) : le filtre **Source** permet de les afficher seuls. - **Heure** : les enregistrements portent le temps écoulé depuis le démarrage ; Logstream le convertit en date grâce à l'heure de démarrage (`/proc/uptime`). Comme avec `dmesg -T`, l'heure peut être trop tôt après une mise en veille de l'hôte. - À l'activation, la dernière heure est lue d'abord (`DMESG_BACKFILL`, `0` = seulement les nouveaux messages). Le numéro de séquence du dernier message et l'identifiant de démarrage sont enregistrés dans `/data/dmesg-state.json` : après un redémarrage de Logstream, la lecture reprend après le dernier message (sans doublon) ; après un redémarrage de l'hôte, le nouveau buffer est lu depuis `DMESG_BACKFILL`. Les messages écrasés dans le buffer du noyau avant d'être lus sont comptés dans Paramètres. ## Audit Linux (auditd) Logstream peut suivre le journal du démon d'audit Linux (`/var/log/audit/audit.log`) : connexions, `sudo`, fichiers surveillés, appels système visés par vos règles `auditctl`… La source est **désactivée par défaut** : activez-la dans **Réglages > Sources > Audit Linux (auditd)**, où le chemin du fichier peut aussi être changé (enregistré dans `/data/audit.json` ; `AUDIT_LOG` fixe le chemin par défaut). - **Montage** : décommentez `/var/log/audit:/var/log/audit:ro` dans `docker-compose.yml`, puis `docker compose up -d`. (`/var/log` est déjà monté sous `/host` pour les logs système de l'hôte : `/host/var/log/audit/audit.log` fonctionne donc aussi comme chemin, sans nouveau volume.) - **Droits** (côté hôte) : auditd écrit son journal en `root`, mode `0600`, dans un répertoire `0700`, et Logstream tourne avec l'UID `10001`. Le plus fiable est le groupe que le conteneur reçoit déjà pour les logs de l'hôte (`HOST_LOGS_GID`, `adm` par défaut) : ```bash sudo sed -i 's/^log_group.*/log_group = adm/' /etc/audit/auditd.conf sudo chgrp adm /var/log/audit && sudo chmod 750 /var/log/audit sudo service auditd restart # systemctl refuse de redémarrer auditd sur certaines distributions ``` auditd crée alors chaque fichier, rotations comprises, en mode `0640` avec le groupe `adm`. Une ACL fonctionne aussi (`sudo setfacl -m u:10001:rx /var/log/audit && sudo setfacl -m u:10001:r /var/log/audit/audit.log`) mais doit être réappliquée à chaque nouveau fichier après une rotation. Réglages > Sources indique si le fichier est absent ou l'accès refusé ; il est réessayé chaque seconde. - **Événements** : les enregistrements d'un même événement (`SYSCALL`, `EXECVE`, `CWD`, `PATH`, `PROCTITLE`…) partagent le même `msg=audit(heure:numéro)` et deviennent **un seul** log, dès qu'aucun enregistrement n'est arrivé depuis 2 secondes. Le message est un résumé — qui, quoi, résultat — comme `cedric (as root): exec apt install nginx [exec] → success` ou `bob: PAM:authentication via /usr/sbin/sshd from 203.0.113.9 on ssh → failed` ; les enregistrements bruts sont gardés dans `audit_raw` et affichés sous le résumé (repliés en densité compacte tant que la ligne n'est pas ouverte). Les valeurs hexadécimales (`proctitle`, `cmd`, arguments avec espaces) sont décodées. - **Champs** : `audit_type` (type du premier enregistrement, aussi utilisé comme **app**), `audit_serial`, `auid` (identifiant de l'utilisateur connecté), `user` (son nom, ou l'uid s'il ne peut pas être résolu), `exe`, `res` (`success` / `failed`, d'après `res=` ou `success=`) et `key` (clé de la règle). La sévérité est `warning` en cas d'échec, `info` sinon ; facility `authpriv` ; source `audit`. Dans la liste, les lignes d'audit ont un liseré gauche violet, un fond légèrement teinté et le badge de source **AUDIT** ; le filtre **Source** permet de les afficher seules. - **Noms** : `user` vient du format enrichi (`log_format = ENRICHED`, le défaut des versions récentes d'auditd), sinon du `/etc/passwd` de l'hôte s'il est monté en `/host/etc/passwd`. **host** est le `node=` des enregistrements (`name_format = hostname` dans `auditd.conf`), sinon `AUDIT_HOST`, sinon `/host/etc/hostname`, sinon `localhost`. - À l'activation, la dernière heure est lue d'abord (`AUDIT_BACKFILL`, `0` = seulement les nouveaux événements). L'inode et une position avant laquelle tout événement a été envoyé sont enregistrés dans `/data/audit-state.json` : après un redémarrage, la lecture reprend là ; après une rotation par auditd, la fin de `audit.log.1` est lue avant le nouveau fichier. ## Fichiers de logs (Traefik, nginx, applications…) Logstream peut suivre les fichiers de logs d'autres programmes : Traefik, nginx, Apache, ou toute application qui écrit dans un fichier. Déclarez chaque fichier dans **Paramètres > Sources de logs > Fichiers de logs** (enregistré dans `/data/files.json`, 50 au plus) : un **nom** (l'**app** de ses entrées, par défaut le nom du fichier), le **chemin** du fichier tel que monté dans le conteneur, un **format** (*Automatique* par défaut), un **hôte** facultatif (par défaut `/host/etc/hostname`, sinon `localhost`) et un interrupteur. - **Montage** du dossier en lecture seule, par ex. dans `docker-compose.yml` : `- /var/log/traefik:/logs/traefik:ro`, puis `docker compose up -d`. Les fichiers de `/var/log` sont déjà accessibles sous `/host/var/log/…`. Les fichiers doivent être lisibles par l'UID `10001` ou le groupe de `HOST_LOGS_GID` (`adm` par défaut) ; Paramètres > Sources de logs indique quand aucun fichier ne correspond ou que l'accès est refusé. - **Chemin** : un fichier, par ex. `/logs/traefik/access.log` (pas de jokers). Dès la saisie, le fichier est vérifié : trouvé ou non, lisible ou non, son format détecté et sa dernière ligne. - **Détection du format** (*Automatique*) : à partir des dernières lignes du fichier (50 au plus), le format structuré que suivent au moins 80 % d'entre elles — accès Traefik JSON, JSON lines, journal d'accès CLF, logfmt — sinon texte brut. Elle est refaite quand le format est changé ; le format trouvé s'affiche dans l'état du fichier. Choisissez un autre format pour le forcer (une expression régulière n'est jamais détectée). - **Formats** : - *Texte brut* : une entrée par ligne ; le niveau est détecté comme pour les logs Docker, un horodatage en début de ligne (ISO 8601, `2006/01/02 15:04:05`, `[02/Jan/2006:15:04:05 +0200]`, `Jan 2 15:04:05`) devient l'heure du message et est retiré du message. Les lignes indentées (stack traces) sont rattachées à l'entrée précédente. - *JSON lines* : `msg` / `message`, `time` / `ts` / `@timestamp` (texte ou heure Unix) et `level` (mot ou nombre, pino/bunyan compris) sont reconnus ; les autres clés deviennent des champs, les objets imbriqués avec des noms pointés (`req.id`). - *logfmt* : paires `clé=valeur`, comme les logs par défaut de Traefik ou des programmes Go ; mêmes clés que le JSON. - *Journal d'accès (CLF)* : Common / Combined Log Format de nginx et Apache, et le journal d'accès de Traefik dans ce format (routeur, URL du serveur et durée compris). Champs `client_ip`, `http_method`, `http_path`, `http_status`, `http_size`, `user_agent`, `http_referer`, `router`, `backend`, `duration_ms`. - *Accès Traefik (JSON)* : le journal d'accès JSON de Traefik (`accessLog.format: json`), résumé en `GET app.example.com/ 200 · 12.5 ms · app@docker · 203.0.113.9`, toutes les clés dans les champs. Les logs de Traefik lui-même dans le même fichier sont lus comme du JSON. - *Expression régulière* (syntaxe RE2 / Go) : les groupes nommés `time`, `level`, `msg`, `host` et `app` remplissent ces champs, `status` donne le niveau d'un journal d'accès, les autres groupes deviennent des champs, par ex. `^(?P