Load color tag presets from an editable presets.json file
The presets move from app.js to presets.json, built into the binary and served by /api/presets. PRESETS_FILE (default /data/presets.json) replaces the list when present; it is read again each time Settings opens and the built-in list is used if it is invalid. docs/presets.md (EN/FR) explains each preset and the file format. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
4225a2c870
commit
688a7dc2e6
14 files changed
+680
-126
No files matched your search
@@ -12,6 +12,11 @@ AUTH_PASS=
|
||||
# local mode: PNG logo shown on the login page, path inside the container (empty = no logo).
|
||||
# Mount the file in docker-compose.yml, e.g. ./logo.png:/config/logo.png:ro, then LOGIN_LOGO=/config/logo.png
|
||||
LOGIN_LOGO=
|
||||
|
||||
# Ready-made color tags offered in Settings > Filters (see docs/presets.md).
|
||||
# Empty: /data/presets.json if present, else the built-in list. To use your own file,
|
||||
# mount it in docker-compose.yml, e.g. ./presets.json:/config/presets.json:ro
|
||||
PRESETS_FILE=
|
||||
# Session lifetime, both modes (e.g. 8h, 24h)
|
||||
SESSION_TTL=12h
|
||||
# oidc mode: issuer URL exactly as the provider announces it
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
FROM golang:1.27.1-alpine3.24 AS build
|
||||
WORKDIR /src
|
||||
COPY go.mod ./
|
||||
COPY *.go ./
|
||||
COPY *.go presets.json ./
|
||||
COPY web ./web
|
||||
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/logstream .
|
||||
|
||||
|
||||
+9
-8
@@ -228,14 +228,13 @@ couleur, est mémorisé par navigateur.
|
||||
Tags par défaut (pastel) : `warning` (orange), `error` (rouge), `ok` (vert). Les tags par
|
||||
défaut qui utilisent encore les couleurs des versions précédentes passent automatiquement aux
|
||||
couleurs pastel.
|
||||
Le menu *+ Préréglage…* ajoute des tags tout faits pour les logs d'accès HTTP/HTTPS (nginx et
|
||||
Apache common/combined, Traefik CLF et JSON, Caddy JSON, HAProxy httplog) : codes de statut
|
||||
(2xx vert, 3xx bleu, 4xx orange, 5xx rouge), méthodes, sondes et attaques (`wp-login.php`,
|
||||
`/.env`, `../`…), robots et scripts, erreurs TLS et proxy. D'autres préréglages couvrent les
|
||||
logs système (échecs et succès de connexion SSH, commandes sudo, OOM et erreurs du noyau,
|
||||
services systemd, UFW/iptables et fail2ban), les applications (conteneurs Docker, erreurs
|
||||
PostgreSQL et MySQL/MariaDB) et des motifs généraux (niveaux de log, adresses IPv4). Les tags déjà présents ne sont pas
|
||||
ajoutés en double, et les tags ajoutés se modifient comme les autres. Dans une expression
|
||||
Le menu *+ Préréglage…* ajoute des tags tout faits : logs d'accès HTTP/HTTPS (codes de statut,
|
||||
méthodes, sondes, robots, erreurs TLS et proxy), logs système (SSH, sudo, noyau, systemd,
|
||||
pare-feu), applications (Docker, bases de données) et motifs généraux (niveaux de log,
|
||||
adresses IPv4). Les tags déjà présents ne sont pas ajoutés en double, et les tags ajoutés se
|
||||
modifient comme les autres. La liste vient d'un fichier texte modifiable (`PRESETS_FILE`) :
|
||||
voir [docs/presets.fr.md](docs/presets.fr.md) pour le détail de chaque préréglage et le
|
||||
format du fichier. Dans une expression
|
||||
régulière, un groupe nommé `hl` (`(?<hl>…)`) ne colore que cette partie de la correspondance :
|
||||
les préréglages s'en servent pour colorer le code de statut ou la méthode, pas le texte autour.
|
||||
Chaque tag reçoit un code à deux chiffres (`01`, `02`…) attribué par le serveur : il reste
|
||||
@@ -352,6 +351,7 @@ résolutions.
|
||||
| `DNS_SERVER` | vide | serveur DNS pour les résolutions inverses (`ip` ou `ip:port`) |
|
||||
| `ALLOW_PURGE` | `true` | autoriser « Supprimer tous les logs » dans les Paramètres |
|
||||
| `EXPORT_MAX` | `100000` | nombre maximal de lignes dans un export CSV |
|
||||
| `PRESETS_FILE` | `/data/presets.json` | fichier des préréglages de tags ; liste intégrée s'il est absent (voir [docs/presets.fr.md](docs/presets.fr.md)) |
|
||||
| `DOCKER_LOGS` | `on` dans compose | collecter les logs des conteneurs Docker locaux |
|
||||
| `DOCKER_HOST` | `tcp://docker-proxy:2375` dans compose | adresse de l'API Docker (`unix:///var/run/docker.sock` hors compose) |
|
||||
| `DOCKER_BACKFILL` | `1h` | historique lu pour un conteneur vu pour la première fois |
|
||||
@@ -418,6 +418,7 @@ Pour mettre à jour l'une d'elles :
|
||||
| `syslogserver.go` | écoutes syslog ouvertes et fermées depuis Paramètres > Sources |
|
||||
| `hostlogs.go`, `journal.go` | logs système de l'hôte : lecteur du journal systemd (sans `journalctl`) et suivi de `/var/log` |
|
||||
| `tags.go` | stockage des tags de couleur |
|
||||
| `presets.go`, `presets.json` | préréglages de tags (`/api/presets`), liste intégrée |
|
||||
| `api.go` | routes HTTP `/api/*` |
|
||||
| `web/` | interface (HTML, CSS, JavaScript simple, sans étape de build), embarquée dans le binaire ; les traductions sont dans `web/app.js` (`I18N`), et dans `web/login.html` pour la page de connexion |
|
||||
|
||||
|
||||
@@ -207,14 +207,12 @@ remembered per browser.
|
||||
(`logstream-data` volume), so they are shared by every browser. Default tags (pastel):
|
||||
`warning` (orange), `error` (red), `ok` (green). Default tags still using the colors of
|
||||
earlier versions are switched to the pastel ones automatically.
|
||||
The *+ Preset…* menu adds ready-made tags for HTTP/HTTPS access logs (nginx and Apache
|
||||
common/combined, Traefik CLF and JSON, Caddy JSON, HAProxy httplog): status codes (2xx green,
|
||||
3xx blue, 4xx orange, 5xx red), methods, probes and attacks (`wp-login.php`, `/.env`,
|
||||
`../`…), bots and scripts, TLS and proxy errors. Other presets cover system logs (SSH and
|
||||
login failures/successes, sudo commands, kernel OOM and errors, systemd services, UFW/iptables
|
||||
and fail2ban), applications (Docker containers, PostgreSQL and MySQL/MariaDB errors) and
|
||||
general patterns (log levels, IPv4 addresses). Tags already in the list are skipped, and the
|
||||
added tags can be edited like any other. In a regular expression, a group named `hl`
|
||||
The *+ Preset…* menu adds ready-made tags: HTTP/HTTPS access logs (status codes, methods,
|
||||
probes, bots, TLS and proxy errors), system logs (SSH, sudo, kernel, systemd, firewall),
|
||||
applications (Docker, databases) and general patterns (log levels, IPv4 addresses). Tags
|
||||
already in the list are skipped, and the added tags can be edited like any other. The list
|
||||
comes from a text file you can edit (`PRESETS_FILE`): see [docs/presets.md](docs/presets.md)
|
||||
for each preset and the file format. In a regular expression, a group named `hl`
|
||||
(`(?<hl>…)`) colors only that part of the match: the presets use it to color the status code
|
||||
or the method, not the text around it.
|
||||
Each tag gets a two-digit code (`01`, `02`…) assigned by the server: it stays with the tag
|
||||
@@ -322,6 +320,7 @@ are only known by your router or a local DNS (Pi-hole, AdGuard, Unbound…), set
|
||||
| `DNS_SERVER` | empty | DNS server for reverse lookups (`ip` or `ip:port`) |
|
||||
| `ALLOW_PURGE` | `true` | allow "Delete all logs" in Settings |
|
||||
| `EXPORT_MAX` | `100000` | maximum number of rows in a CSV export |
|
||||
| `PRESETS_FILE` | `/data/presets.json` | color tag presets file; the built-in list when missing (see [docs/presets.md](docs/presets.md)) |
|
||||
| `DOCKER_LOGS` | `on` in compose | collect the logs of the local Docker containers |
|
||||
| `DOCKER_HOST` | `tcp://docker-proxy:2375` in compose | Docker API address (`unix:///var/run/docker.sock` outside compose) |
|
||||
| `DOCKER_BACKFILL` | `1h` | history read from a container seen for the first time |
|
||||
@@ -385,6 +384,7 @@ To update one of them:
|
||||
| `syslogserver.go` | syslog listeners opened and closed from Settings > Sources |
|
||||
| `hostlogs.go`, `journal.go` | host system logs: systemd journal reader (no `journalctl`) and `/var/log` follower |
|
||||
| `tags.go` | color tag storage |
|
||||
| `presets.go`, `presets.json` | color tag presets (`/api/presets`), built-in list |
|
||||
| `api.go` | `/api/*` HTTP routes |
|
||||
| `web/` | UI (HTML, CSS, plain JavaScript, no build step), embedded in the binary; translations live in `web/app.js` (`I18N`), and in `web/login.html` for the login page |
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ type API struct {
|
||||
store *Store
|
||||
hub *Hub
|
||||
tags *TagStore
|
||||
presets string // presets file, built-in presets when missing
|
||||
rdns *ReverseDNS
|
||||
allowPurge bool
|
||||
exportMax int
|
||||
@@ -34,6 +35,7 @@ func (a *API) Routes(mux *http.ServeMux) {
|
||||
mux.HandleFunc("GET /api/tags", a.listTags)
|
||||
mux.HandleFunc("POST /api/tags", a.createTag)
|
||||
mux.HandleFunc("POST /api/tags/reset", a.resetTags)
|
||||
mux.HandleFunc("GET /api/presets", a.listPresets)
|
||||
mux.HandleFunc("PUT /api/tags/{id}", a.updateTag)
|
||||
mux.HandleFunc("DELETE /api/tags/{id}", a.deleteTag)
|
||||
mux.HandleFunc("GET /api/purge", a.purgeStatus)
|
||||
@@ -316,6 +318,10 @@ func (a *API) listTags(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, a.tags.List())
|
||||
}
|
||||
|
||||
func (a *API) listPresets(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, loadPresets(a.presets))
|
||||
}
|
||||
|
||||
func decodeTag(w http.ResponseWriter, r *http.Request) (Tag, error) {
|
||||
var t Tag
|
||||
err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64*1024)).Decode(&t)
|
||||
|
||||
@@ -18,6 +18,7 @@ services:
|
||||
AUTH_USER: ${AUTH_USER:-} # vide = pas d'authentification, on delegue ca au reverse proxy traefik
|
||||
AUTH_PASS: ${AUTH_PASS:-}
|
||||
LOGIN_LOGO: ${LOGIN_LOGO:-} # PNG affiche sur la page de connexion (chemin dans le conteneur, voir volumes)
|
||||
PRESETS_FILE: ${PRESETS_FILE:-} # prereglages de tags (defaut /data/presets.json, voir docs/presets.fr.md)
|
||||
OIDC_ISSUER: ${OIDC_ISSUER:-}
|
||||
OIDC_CLIENT_ID: ${OIDC_CLIENT_ID:-}
|
||||
OIDC_CLIENT_SECRET: ${OIDC_CLIENT_SECRET:-}
|
||||
@@ -41,6 +42,8 @@ services:
|
||||
- /run/log/journal:/host/run/log/journal:ro # journal systemd volatile
|
||||
# logo de la page de connexion, avec LOGIN_LOGO=/config/logo.png dans .env
|
||||
# - ./logo.png:/config/logo.png:ro
|
||||
# prereglages de tags personnalises, avec PRESETS_FILE=/config/presets.json dans .env
|
||||
# - ./presets.json:/config/presets.json:ro
|
||||
labels:
|
||||
logstream.exclude: "true" # pas de collect des logs logstream
|
||||
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
[English](presets.md) · **Français**
|
||||
|
||||
# Préréglages de tags de couleur
|
||||
|
||||
Dans **Paramètres › Filtres**, le menu **+ Préréglage…** ajoute d'un clic un groupe de tags de
|
||||
couleur tout faits. Les tags ajoutés sont des tags ordinaires : vous pouvez changer leur couleur,
|
||||
leur motif ou leurs options, ou les supprimer. Un tag dont le motif est déjà dans la liste n'est
|
||||
pas ajouté en double.
|
||||
|
||||
La liste vient d'un fichier texte, [`presets.json`](../presets.json), intégré à LogStream. Vous
|
||||
pouvez le remplacer par votre propre fichier (voir [Utiliser votre propre fichier](#utiliser-votre-propre-fichier)).
|
||||
|
||||
## Préréglages intégrés
|
||||
|
||||
### HTTP/HTTPS
|
||||
|
||||
Ces préréglages lisent les logs d'accès de nginx et Apache (formats common et combined), Traefik
|
||||
(CLF et JSON), Caddy (JSON) et HAProxy (`option httplog`).
|
||||
|
||||
| Préréglage | Tags | Ce qui est coloré |
|
||||
| --- | --- | --- |
|
||||
| Codes HTTP | `HTTP 2xx` vert, `HTTP 3xx` bleu, `HTTP 4xx` orange, `HTTP 5xx` rouge | seulement le code de statut, par exemple `404` dans `"GET /x HTTP/1.1" 404 153`, `"status":404` ou `"DownstreamStatus":404`. Les autres nombres de la ligne (taille, chemin) ne sont pas touchés. |
|
||||
| Méthodes HTTP | `GET/HEAD/OPTIONS` gris, `POST/PUT/PATCH` violet, `DELETE` rose | seulement la méthode dans `"GET /chemin` ou `"method":"GET"` (en majuscules uniquement) |
|
||||
| Sondes et attaques | `sondes / attaques` | `wp-login.php`, `xmlrpc.php`, `wp-admin`, `phpmyadmin`, `/.env`, `/.git`, `/.aws`, `/cgi-bin/`, `../`, `%2e%2e`, `/etc/passwd`, `<script`, `union select` |
|
||||
| Robots et scripts | `robots / scripts` | les mots finissant par `bot` (`Googlebot`, `bingbot`…), `crawler`, `spider`, `curl`, `wget`, `python-requests`, `Go-http-client`, `zgrab`, `masscan`, `nmap`, `sqlmap`, `nikto` |
|
||||
| Erreurs TLS/HTTPS et proxy | `erreurs TLS`, `erreurs proxy` | échecs de handshake TLS, certificats expirés ou refusés, `x509:` ; `upstream timed out`, `no live upstreams`, `connect() failed`, `connection refused`, `bad gateway`, `gateway timeout`, `service unavailable` |
|
||||
|
||||
### Système
|
||||
|
||||
| Préréglage | Tags | Ce qui est coloré |
|
||||
| --- | --- | --- |
|
||||
| SSH et connexions | `échecs de connexion` rouge, `connexions` vert | sshd/PAM : `Failed password`, `Invalid user`, `authentication failure`, `incorrect password attempts`, `NOT in sudoers`… ; `Accepted publickey`, `session opened for user`, `New session … of user` |
|
||||
| Commandes sudo | `commandes sudo` | la commande lancée, par exemple `COMMAND=/usr/bin/apt` |
|
||||
| Noyau : OOM, plantages, disques | `mémoire épuisée`, `erreurs noyau` | `Out of memory`, `oom-killer`, `Killed process 4242` ; `Kernel panic`, `BUG:`, `Oops`, `Call Trace`, `segfault at`, `I/O error`, `EXT4-fs error`, `blocked for more than 120 seconds`, `soft lockup` |
|
||||
| Services systemd | `services en échec` rouge, `démarrage/arrêt de service` vert | `Failed to start`, `Failed with result`, `Main process exited, code=killed`, `Start request repeated too quickly` ; `Started`, `Stopping`, `Reloaded`, `Reached target` |
|
||||
| Pare-feu et fail2ban | `pare-feu` | `[UFW BLOCK]`, `[UFW ALLOW]`, `DROP`, `REJECT`, `Ban 203.0.113.9`, `Unban …`, `Found …` |
|
||||
|
||||
### Applications
|
||||
|
||||
| Préréglage | Tags | Ce qui est coloré |
|
||||
| --- | --- | --- |
|
||||
| Docker et conteneurs | `problèmes de conteneur` | `exited with code 137` (codes non nuls seulement), `OOMKilled`, `unhealthy`, `Back-off restarting`, `CrashLoopBackOff`, `container die/kill/oom` |
|
||||
| Bases de données | `erreurs base de données` | PostgreSQL et MySQL/MariaDB : `deadlock detected`, `duplicate key`, `too many connections`, `lock wait timeout`, `slow query`, `server has gone away`, `Access denied for user`, `password authentication failed`… |
|
||||
|
||||
### Général
|
||||
|
||||
| Préréglage | Tags | Ce qui est coloré |
|
||||
| --- | --- | --- |
|
||||
| Niveaux de log | `fatal / critique` rouge, `info / notice` bleu, `debug / trace` gris | ces mots en mots entiers, quelle que soit la casse (les tags par défaut `warning` et `error` couvrent le reste) |
|
||||
| Adresses IPv4 | `adresses IPv4` | `192.168.1.20`, `203.0.113.9`… Les numéros de version à quatre parties comme `1.2.3.4` sont aussi colorés. |
|
||||
|
||||
Quand des tags se chevauchent, celui placé le plus haut dans la liste l'emporte : les
|
||||
préréglages ajoutés après les tags par défaut ne les masquent donc jamais.
|
||||
|
||||
## Utiliser votre propre fichier
|
||||
|
||||
LogStream lit le fichier indiqué par `PRESETS_FILE`, `/data/presets.json` par défaut (dans le
|
||||
volume `logstream-data`). S'il n'existe pas, la liste intégrée est utilisée. Le fichier est relu
|
||||
à chaque ouverture des Paramètres : pas besoin de redémarrer après une modification.
|
||||
|
||||
Avec docker-compose, le plus simple est de garder le fichier à côté de `docker-compose.yml` :
|
||||
|
||||
1. Copiez [`presets.json`](../presets.json) depuis ce dépôt et modifiez-le.
|
||||
2. Dans `docker-compose.yml`, décommentez la ligne `- ./presets.json:/config/presets.json:ro`.
|
||||
3. Dans `.env`, mettez `PRESETS_FILE=/config/presets.json`, puis lancez `docker compose up -d`.
|
||||
|
||||
Si le fichier est invalide (erreur JSON, expression régulière ou couleur incorrecte, id en
|
||||
double), les Paramètres affichent l'erreur et la liste intégrée est utilisée jusqu'à correction.
|
||||
|
||||
## Format du fichier
|
||||
|
||||
Le fichier est une liste JSON de groupes. Chaque groupe a un nom et une liste de préréglages ;
|
||||
chaque préréglage a un `id`, un nom et ses tags.
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"group": { "en": "My apps", "fr": "Mes applis" },
|
||||
"presets": [
|
||||
{
|
||||
"id": "monappli",
|
||||
"name": "Mon appli",
|
||||
"tags": [
|
||||
{ "label": "paiement refusé", "color": "#fca5a5", "pattern": "payment (?:failed|refused)" },
|
||||
{ "label": "commande", "color": "#86efac", "pattern": "order #\\d+" },
|
||||
{ "label": "lent", "color": "#fde68a", "pattern": "SLOW", "regex": false, "caseSensitive": true }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
| Champ | Obligatoire | Signification |
|
||||
| --- | --- | --- |
|
||||
| `group` | oui | nom du groupe dans le menu |
|
||||
| `id` | oui | identifiant unique du préréglage |
|
||||
| `name` | oui | nom du préréglage dans le menu |
|
||||
| `tags[].pattern` | oui | ce qu'il faut colorer : une expression régulière, ou du texte simple avec `"regex": false` |
|
||||
| `tags[].color` | oui | couleur de fond, `#rrggbb` |
|
||||
| `tags[].label` | non | nom affiché dans la liste des tags à la place du motif |
|
||||
| `tags[].regex` | non | `true` par défaut |
|
||||
| `tags[].wholeWord` | non | mots entiers seulement, `false` par défaut |
|
||||
| `tags[].caseSensitive` | non | respecter la casse, `false` par défaut |
|
||||
|
||||
Les noms et libellés sont soit un seul texte pour toutes les langues (`"Mon appli"`), soit un
|
||||
texte par langue (`{ "en": "My app", "fr": "Mon appli" }`) ; une langue absente se rabat sur
|
||||
l'anglais.
|
||||
|
||||
Les expressions régulières doivent fonctionner à la fois dans le navigateur (JavaScript) et en
|
||||
Go, qui les vérifie : évitez les assertions arrière `(?<=…)`, avant `(?=…)` et les références
|
||||
arrière `\1`. En JSON, chaque barre oblique inverse s'écrit deux fois : `\d` devient `"\\d"`. Un
|
||||
groupe nommé `hl`, `(?<hl>…)`, ne colore que cette partie de la correspondance, comme le font
|
||||
les préréglages HTTP avec `(?<hl>5\\d\\d)`.
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
**English** · [Français](presets.fr.md)
|
||||
|
||||
# Color tag presets
|
||||
|
||||
In **Settings › Filters**, the **+ Preset…** menu adds a group of ready-made color tags in one
|
||||
click. Added tags are ordinary tags: you can change their color, pattern or options, or delete
|
||||
them. A tag whose pattern is already in the list is not added twice.
|
||||
|
||||
The list comes from a text file, [`presets.json`](../presets.json), built into LogStream. You
|
||||
can replace it with your own file (see [Using your own file](#using-your-own-file)).
|
||||
|
||||
## Built-in presets
|
||||
|
||||
### HTTP/HTTPS
|
||||
|
||||
These presets read access logs from nginx and Apache (common and combined formats), Traefik
|
||||
(CLF and JSON), Caddy (JSON) and HAProxy (`option httplog`).
|
||||
|
||||
| Preset | Tags | What gets colored |
|
||||
| --- | --- | --- |
|
||||
| HTTP status codes | `HTTP 2xx` green, `HTTP 3xx` blue, `HTTP 4xx` orange, `HTTP 5xx` red | only the status code, e.g. `404` in `"GET /x HTTP/1.1" 404 153`, `"status":404` or `"DownstreamStatus":404`. Other numbers on the line (size, path) are left alone. |
|
||||
| HTTP methods | `GET/HEAD/OPTIONS` grey, `POST/PUT/PATCH` purple, `DELETE` pink | only the method in `"GET /path` or `"method":"GET"` (upper case only) |
|
||||
| Probes and attacks | `probes / attacks` | `wp-login.php`, `xmlrpc.php`, `wp-admin`, `phpmyadmin`, `/.env`, `/.git`, `/.aws`, `/cgi-bin/`, `../`, `%2e%2e`, `/etc/passwd`, `<script`, `union select` |
|
||||
| Bots and scripts | `bots / scripts` | words ending in `bot` (`Googlebot`, `bingbot`…), `crawler`, `spider`, `curl`, `wget`, `python-requests`, `Go-http-client`, `zgrab`, `masscan`, `nmap`, `sqlmap`, `nikto` |
|
||||
| TLS/HTTPS and proxy errors | `TLS errors`, `proxy errors` | TLS handshake failures, expired or rejected certificates, `x509:`; `upstream timed out`, `no live upstreams`, `connect() failed`, `connection refused`, `bad gateway`, `gateway timeout`, `service unavailable` |
|
||||
|
||||
### System
|
||||
|
||||
| Preset | Tags | What gets colored |
|
||||
| --- | --- | --- |
|
||||
| SSH and logins | `login failures` red, `logins` green | sshd/PAM: `Failed password`, `Invalid user`, `authentication failure`, `incorrect password attempts`, `NOT in sudoers`…; `Accepted publickey`, `session opened for user`, `New session … of user` |
|
||||
| sudo commands | `sudo commands` | the command run, e.g. `COMMAND=/usr/bin/apt` |
|
||||
| Kernel: OOM, crashes, disks | `out of memory`, `kernel errors` | `Out of memory`, `oom-killer`, `Killed process 4242`; `Kernel panic`, `BUG:`, `Oops`, `Call Trace`, `segfault at`, `I/O error`, `EXT4-fs error`, `blocked for more than 120 seconds`, `soft lockup` |
|
||||
| systemd services | `failed services` red, `service start/stop` green | `Failed to start`, `Failed with result`, `Main process exited, code=killed`, `Start request repeated too quickly`; `Started`, `Stopping`, `Reloaded`, `Reached target` |
|
||||
| Firewall and fail2ban | `firewall` | `[UFW BLOCK]`, `[UFW ALLOW]`, `DROP`, `REJECT`, `Ban 203.0.113.9`, `Unban …`, `Found …` |
|
||||
|
||||
### Applications
|
||||
|
||||
| Preset | Tags | What gets colored |
|
||||
| --- | --- | --- |
|
||||
| Docker and containers | `container problems` | `exited with code 137` (non-zero codes only), `OOMKilled`, `unhealthy`, `Back-off restarting`, `CrashLoopBackOff`, `container die/kill/oom` |
|
||||
| Databases | `database errors` | PostgreSQL and MySQL/MariaDB: `deadlock detected`, `duplicate key`, `too many connections`, `lock wait timeout`, `slow query`, `server has gone away`, `Access denied for user`, `password authentication failed`… |
|
||||
|
||||
### General
|
||||
|
||||
| Preset | Tags | What gets colored |
|
||||
| --- | --- | --- |
|
||||
| Log levels | `fatal / critical` red, `info / notice` blue, `debug / trace` grey | these words as whole words, any case (the default `warning` and `error` tags cover the rest) |
|
||||
| IPv4 addresses | `IPv4 addresses` | `192.168.1.20`, `203.0.113.9`… Four-part version numbers such as `1.2.3.4` are colored too. |
|
||||
|
||||
When tags overlap, the one highest in the tag list wins, so presets added after the default
|
||||
tags never hide them.
|
||||
|
||||
## Using your own file
|
||||
|
||||
LogStream reads the file named by `PRESETS_FILE`, `/data/presets.json` by default (in the
|
||||
`logstream-data` volume). When the file does not exist, the built-in list is used. The file is
|
||||
read again each time Settings is opened: no restart is needed after an edit.
|
||||
|
||||
With docker-compose, the simplest is to keep the file next to `docker-compose.yml`:
|
||||
|
||||
1. Copy [`presets.json`](../presets.json) from this repository and edit it.
|
||||
2. In `docker-compose.yml`, uncomment the line `- ./presets.json:/config/presets.json:ro`.
|
||||
3. In `.env`, set `PRESETS_FILE=/config/presets.json`, then run `docker compose up -d`.
|
||||
|
||||
If the file is invalid (JSON error, bad regular expression or color, duplicate id), Settings
|
||||
shows the error and the built-in list is used until the file is fixed.
|
||||
|
||||
## File format
|
||||
|
||||
The file is a JSON list of groups. Each group has a name and a list of presets; each preset has
|
||||
an `id`, a name and its tags.
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"group": { "en": "My apps", "fr": "Mes applis" },
|
||||
"presets": [
|
||||
{
|
||||
"id": "myapp",
|
||||
"name": "My app",
|
||||
"tags": [
|
||||
{ "label": "payment failed", "color": "#fca5a5", "pattern": "payment (?:failed|refused)" },
|
||||
{ "label": "order", "color": "#86efac", "pattern": "order #\\d+" },
|
||||
{ "label": "slow", "color": "#fde68a", "pattern": "SLOW", "regex": false, "caseSensitive": true }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
| Field | Required | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `group` | yes | name of the group in the menu |
|
||||
| `id` | yes | unique identifier of the preset |
|
||||
| `name` | yes | name of the preset in the menu |
|
||||
| `tags[].pattern` | yes | what to color: a regular expression, or plain text with `"regex": false` |
|
||||
| `tags[].color` | yes | background color, `#rrggbb` |
|
||||
| `tags[].label` | no | name shown in the tag list instead of the pattern |
|
||||
| `tags[].regex` | no | `true` by default |
|
||||
| `tags[].wholeWord` | no | only match whole words, `false` by default |
|
||||
| `tags[].caseSensitive` | no | match case, `false` by default |
|
||||
|
||||
Names and labels are either one text for every language (`"My app"`) or one text per language
|
||||
(`{ "en": "My app", "fr": "Mon appli" }`); a missing language falls back to English.
|
||||
|
||||
Regular expressions must work both in the browser (JavaScript) and in Go, which checks them:
|
||||
avoid look-behind `(?<=…)`, look-ahead `(?=…)` and back-references `\1`. In JSON, every
|
||||
backslash is written twice: `\d` becomes `"\\d"`. A group named `hl`, `(?<hl>…)`, colors only
|
||||
that part of the match, as the HTTP presets do with `(?<hl>5\\d\\d)`.
|
||||
@@ -143,7 +143,8 @@ func main() {
|
||||
log.Fatal(err)
|
||||
}
|
||||
mux := http.NewServeMux()
|
||||
api := &API{store: store, hub: hub, tags: tags, rdns: rdns, allowPurge: cfg.allowPurge, exportMax: cfg.exportMax, syslog: syslogSrv}
|
||||
presets := getenv("PRESETS_FILE", filepath.Join(cfg.dataDir, "presets.json"))
|
||||
api := &API{store: store, hub: hub, tags: tags, presets: presets, rdns: rdns, allowPurge: cfg.allowPurge, exportMax: cfg.exportMax, syslog: syslogSrv}
|
||||
if cfg.dockerLogs {
|
||||
dm, err := NewDockerManager(cfg.dockerHost, cfg.dataDir, cfg.backfill, sink)
|
||||
if err != nil {
|
||||
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Built-in tag presets, used when no presets file is found (PRESETS_FILE,
|
||||
// /data/presets.json by default). See docs/presets.md.
|
||||
//
|
||||
//go:embed presets.json
|
||||
var builtinPresets []byte
|
||||
|
||||
// i18nText is a text per language ({"en": "...", "fr": "..."}) or one plain
|
||||
// string for every language.
|
||||
type i18nText map[string]string
|
||||
|
||||
func (t *i18nText) UnmarshalJSON(b []byte) error {
|
||||
var s string
|
||||
if json.Unmarshal(b, &s) == nil {
|
||||
*t = i18nText{"en": s}
|
||||
return nil
|
||||
}
|
||||
var m map[string]string
|
||||
if err := json.Unmarshal(b, &m); err != nil {
|
||||
return errors.New("expected a string or an object of strings per language")
|
||||
}
|
||||
*t = m
|
||||
return nil
|
||||
}
|
||||
|
||||
type PresetTag struct {
|
||||
Label i18nText `json:"label,omitempty"`
|
||||
Pattern string `json:"pattern"`
|
||||
Color string `json:"color"`
|
||||
Regex *bool `json:"regex,omitempty"` // regular expression unless false
|
||||
WholeWord bool `json:"wholeWord,omitempty"`
|
||||
CaseSensitive bool `json:"caseSensitive,omitempty"`
|
||||
}
|
||||
|
||||
type Preset struct {
|
||||
ID string `json:"id"`
|
||||
Name i18nText `json:"name"`
|
||||
Tags []PresetTag `json:"tags"`
|
||||
}
|
||||
|
||||
type PresetGroup struct {
|
||||
Group i18nText `json:"group"`
|
||||
Presets []Preset `json:"presets"`
|
||||
}
|
||||
|
||||
// parsePresets decodes and checks a presets file with the same rules as the
|
||||
// tags, so that every preset can be added as is.
|
||||
func parsePresets(b []byte) ([]PresetGroup, error) {
|
||||
var groups []PresetGroup
|
||||
if err := json.Unmarshal(b, &groups); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, g := range groups {
|
||||
for _, p := range g.Presets {
|
||||
if p.ID == "" || seen[p.ID] {
|
||||
return nil, fmt.Errorf("preset %q: missing or duplicate id", p.ID)
|
||||
}
|
||||
seen[p.ID] = true
|
||||
if len(p.Tags) == 0 {
|
||||
return nil, fmt.Errorf("preset %q: no tags", p.ID)
|
||||
}
|
||||
for i, pt := range p.Tags {
|
||||
t := Tag{Pattern: pt.Pattern, Color: pt.Color, Regex: pt.Regex == nil || *pt.Regex}
|
||||
if err := t.validate(); err != nil {
|
||||
return nil, fmt.Errorf("preset %q, tag %d: %w", p.ID, i+1, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
// presetsResponse is what GET /api/presets returns.
|
||||
type presetsResponse struct {
|
||||
Source string `json:"source"` // "file" or "builtin"
|
||||
File string `json:"file"`
|
||||
Error string `json:"error,omitempty"` // the file is invalid: built-in presets are used
|
||||
Groups []PresetGroup `json:"groups"`
|
||||
}
|
||||
|
||||
// loadPresets reads the presets file on every call, so that edits apply
|
||||
// without a restart, and falls back to the built-in presets.
|
||||
func loadPresets(path string) presetsResponse {
|
||||
res := presetsResponse{Source: "builtin", File: path}
|
||||
if path != "" {
|
||||
b, err := os.ReadFile(path)
|
||||
switch {
|
||||
case err == nil:
|
||||
if res.Groups, err = parsePresets(b); err == nil {
|
||||
res.Source = "file"
|
||||
return res
|
||||
}
|
||||
res.Error = err.Error()
|
||||
case !errors.Is(err, os.ErrNotExist):
|
||||
res.Error = err.Error()
|
||||
}
|
||||
}
|
||||
res.Groups, _ = parsePresets(builtinPresets) // checked by the tests
|
||||
return res
|
||||
}
|
||||
+237
@@ -0,0 +1,237 @@
|
||||
[
|
||||
{
|
||||
"group": "HTTP/HTTPS",
|
||||
"presets": [
|
||||
{
|
||||
"id": "http_status",
|
||||
"name": { "en": "HTTP status codes", "fr": "Codes HTTP" },
|
||||
"tags": [
|
||||
{
|
||||
"label": "HTTP 2xx",
|
||||
"color": "#86efac",
|
||||
"pattern": "(?:\" |\"(?:status|DownstreamStatus|OriginStatus|status_code)\": ?|(?:-1|\\d+)/(?:-1|\\d+)/(?:-1|\\d+)/(?:-1|\\d+)/\\+?\\d+ )(?<hl>2\\d\\d)\\b"
|
||||
},
|
||||
{
|
||||
"label": "HTTP 3xx",
|
||||
"color": "#93c5fd",
|
||||
"pattern": "(?:\" |\"(?:status|DownstreamStatus|OriginStatus|status_code)\": ?|(?:-1|\\d+)/(?:-1|\\d+)/(?:-1|\\d+)/(?:-1|\\d+)/\\+?\\d+ )(?<hl>3\\d\\d)\\b"
|
||||
},
|
||||
{
|
||||
"label": "HTTP 4xx",
|
||||
"color": "#fdba74",
|
||||
"pattern": "(?:\" |\"(?:status|DownstreamStatus|OriginStatus|status_code)\": ?|(?:-1|\\d+)/(?:-1|\\d+)/(?:-1|\\d+)/(?:-1|\\d+)/\\+?\\d+ )(?<hl>4\\d\\d)\\b"
|
||||
},
|
||||
{
|
||||
"label": "HTTP 5xx",
|
||||
"color": "#f87171",
|
||||
"pattern": "(?:\" |\"(?:status|DownstreamStatus|OriginStatus|status_code)\": ?|(?:-1|\\d+)/(?:-1|\\d+)/(?:-1|\\d+)/(?:-1|\\d+)/\\+?\\d+ )(?<hl>5\\d\\d)\\b"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "http_methods",
|
||||
"name": { "en": "HTTP methods", "fr": "Méthodes HTTP" },
|
||||
"tags": [
|
||||
{
|
||||
"label": "GET/HEAD/OPTIONS",
|
||||
"color": "#cbd5e1",
|
||||
"caseSensitive": true,
|
||||
"pattern": "\"(?<hl>GET|HEAD|OPTIONS)[ \"]"
|
||||
},
|
||||
{
|
||||
"label": "POST/PUT/PATCH",
|
||||
"color": "#c4b5fd",
|
||||
"caseSensitive": true,
|
||||
"pattern": "\"(?<hl>POST|PUT|PATCH)[ \"]"
|
||||
},
|
||||
{
|
||||
"label": "DELETE",
|
||||
"color": "#f9a8d4",
|
||||
"caseSensitive": true,
|
||||
"pattern": "\"(?<hl>DELETE)[ \"]"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "http_probes",
|
||||
"name": { "en": "Probes and attacks", "fr": "Sondes et attaques" },
|
||||
"tags": [
|
||||
{
|
||||
"label": { "en": "probes / attacks", "fr": "sondes / attaques" },
|
||||
"color": "#fda4af",
|
||||
"pattern": "(?:wp-login\\.php|xmlrpc\\.php|wp-admin|phpmyadmin|/\\.env|/\\.git|/\\.aws|/cgi-bin/|\\.\\./|%2e%2e|/etc/passwd|<script|union(?:\\s|%20|\\+)+select)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "http_bots",
|
||||
"name": { "en": "Bots and scripts", "fr": "Robots et scripts" },
|
||||
"tags": [
|
||||
{
|
||||
"label": { "en": "bots / scripts", "fr": "robots / scripts" },
|
||||
"color": "#fde68a",
|
||||
"pattern": "\\b(?:[a-z]*bot|crawler|spider|curl|wget|python-requests|Go-http-client|zgrab|masscan|nmap|sqlmap|nikto)\\b"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "http_errors",
|
||||
"name": { "en": "TLS/HTTPS and proxy errors", "fr": "Erreurs TLS/HTTPS et proxy" },
|
||||
"tags": [
|
||||
{
|
||||
"label": { "en": "TLS errors", "fr": "erreurs TLS" },
|
||||
"color": "#f0abfc",
|
||||
"pattern": "(?:TLS handshake error|SSL_do_handshake\\(\\) failed|SSL handshake|SSL routines|certificate (?:has )?expired|certificate verify failed|bad certificate|unknown certificate|x509:)"
|
||||
},
|
||||
{
|
||||
"label": { "en": "proxy errors", "fr": "erreurs proxy" },
|
||||
"color": "#fdba74",
|
||||
"pattern": "(?:upstream timed out|upstream prematurely closed|no live upstreams|connect\\(\\) failed|connection refused|bad gateway|gateway time-?out|service unavailable)"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": { "en": "System", "fr": "Système" },
|
||||
"presets": [
|
||||
{
|
||||
"id": "sys_auth",
|
||||
"name": { "en": "SSH and logins", "fr": "SSH et connexions" },
|
||||
"tags": [
|
||||
{
|
||||
"label": { "en": "login failures", "fr": "échecs de connexion" },
|
||||
"color": "#fca5a5",
|
||||
"pattern": "(?:Failed (?:password|publickey|none)|Invalid user|authentication failures?|Connection closed by (?:invalid|authenticating) user|maximum authentication attempts exceeded|FAILED (?:LOGIN|SU)|incorrect password attempts?|NOT in sudoers)"
|
||||
},
|
||||
{
|
||||
"label": { "en": "logins", "fr": "connexions" },
|
||||
"color": "#86efac",
|
||||
"pattern": "(?:Accepted (?:password|publickey|keyboard-interactive(?:/pam)?)|session opened for user|New session \\S+ of user)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "sys_sudo",
|
||||
"name": { "en": "sudo commands", "fr": "Commandes sudo" },
|
||||
"tags": [
|
||||
{
|
||||
"label": { "en": "sudo commands", "fr": "commandes sudo" },
|
||||
"color": "#fde68a",
|
||||
"caseSensitive": true,
|
||||
"pattern": "\\bCOMMAND=\\S+"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "sys_kernel",
|
||||
"name": { "en": "Kernel: OOM, crashes, disks", "fr": "Noyau : OOM, plantages, disques" },
|
||||
"tags": [
|
||||
{
|
||||
"label": { "en": "out of memory", "fr": "mémoire épuisée" },
|
||||
"color": "#f87171",
|
||||
"pattern": "(?:Out of memory|oom-kill(?:er)?|oom_reaper|Killed process \\d+|invoked oom-killer)"
|
||||
},
|
||||
{
|
||||
"label": { "en": "kernel errors", "fr": "erreurs noyau" },
|
||||
"color": "#fda4af",
|
||||
"pattern": "(?:Kernel panic|\\bBUG: |\\bOops\\b|Call Trace|segfault at|general protection fault|I/O error|EXT4-fs error|Buffer I/O error|blocked for more than \\d+ seconds|Hardware Error|soft lockup|hard LOCKUP)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "sys_systemd",
|
||||
"name": { "en": "systemd services", "fr": "Services systemd" },
|
||||
"tags": [
|
||||
{
|
||||
"label": { "en": "failed services", "fr": "services en échec" },
|
||||
"color": "#fca5a5",
|
||||
"pattern": "(?:Failed to start|failed with result|Main process exited, code=(?:exited|killed|dumped)|entered failed state|Start request repeated too quickly|Dependency failed)"
|
||||
},
|
||||
{
|
||||
"label": { "en": "service start/stop", "fr": "démarrage/arrêt de service" },
|
||||
"color": "#bbf7d0",
|
||||
"caseSensitive": true,
|
||||
"pattern": "\\b(?:Started|Starting|Stopped|Stopping|Reloaded|Reloading|Reached target)\\b"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "sys_firewall",
|
||||
"name": { "en": "Firewall and fail2ban", "fr": "Pare-feu et fail2ban" },
|
||||
"tags": [
|
||||
{
|
||||
"label": { "en": "firewall", "fr": "pare-feu" },
|
||||
"color": "#fdba74",
|
||||
"caseSensitive": true,
|
||||
"pattern": "(?:\\[UFW (?:BLOCK|ALLOW|AUDIT|LIMIT BLOCK)\\]|\\b(?:DROP|REJECT)\\b|\\b(?:Ban|Unban|Found) \\d{1,3}(?:\\.\\d{1,3}){3}\\b)"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Applications",
|
||||
"presets": [
|
||||
{
|
||||
"id": "app_docker",
|
||||
"name": { "en": "Docker and containers", "fr": "Docker et conteneurs" },
|
||||
"tags": [
|
||||
{
|
||||
"label": { "en": "container problems", "fr": "problèmes de conteneur" },
|
||||
"color": "#fcd34d",
|
||||
"pattern": "(?:\\bOOMKilled\\b|exited with code [1-9]\\d*|exit code: [1-9]\\d*|health_status: unhealthy|\\bunhealthy\\b|Back-off restarting|CrashLoopBackOff|container (?:die|kill|oom)\\b|restarting \\(\\d+\\))"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "app_db",
|
||||
"name": { "en": "Databases", "fr": "Bases de données" },
|
||||
"tags": [
|
||||
{
|
||||
"label": { "en": "database errors", "fr": "erreurs base de données" },
|
||||
"color": "#c4b5fd",
|
||||
"pattern": "(?:\\bdeadlock(?: detected| found)?\\b|duplicate key|too many (?:connections|clients)|lock wait timeout|slow query|could not connect to server|server has gone away|out of shared memory|terminating connection|Access denied for user|password authentication failed)"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": { "en": "General", "fr": "Général" },
|
||||
"presets": [
|
||||
{
|
||||
"id": "gen_levels",
|
||||
"name": { "en": "Log levels", "fr": "Niveaux de log" },
|
||||
"tags": [
|
||||
{
|
||||
"label": { "en": "fatal / critical", "fr": "fatal / critique" },
|
||||
"color": "#ef4444",
|
||||
"pattern": "\\b(?:fatal|crit(?:ical)?|panic|emerg(?:ency)?)\\b"
|
||||
},
|
||||
{
|
||||
"label": "info / notice",
|
||||
"color": "#bfdbfe",
|
||||
"pattern": "\\b(?:info|notice)\\b"
|
||||
},
|
||||
{
|
||||
"label": "debug / trace",
|
||||
"color": "#e5e7eb",
|
||||
"pattern": "\\b(?:debug|trace)\\b"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "gen_ip",
|
||||
"name": { "en": "IPv4 addresses", "fr": "Adresses IPv4" },
|
||||
"tags": [
|
||||
{
|
||||
"label": { "en": "IPv4 addresses", "fr": "adresses IPv4" },
|
||||
"color": "#a5f3fc",
|
||||
"pattern": "\\b(?:(?:25[0-5]|2[0-4]\\d|1?\\d?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|1?\\d?\\d)\\b"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,46 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBuiltinPresets(t *testing.T) {
|
||||
groups, err := parsePresets(builtinPresets)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(groups) == 0 {
|
||||
t.Fatal("no built-in presets")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadPresetsFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "presets.json")
|
||||
|
||||
if res := loadPresets(path); res.Source != "builtin" || res.Error != "" {
|
||||
t.Fatalf("missing file: got %q, error %q", res.Source, res.Error)
|
||||
}
|
||||
|
||||
os.WriteFile(path, []byte(`[{"group":"Mine","presets":[{"id":"a","name":{"en":"A","fr":"A fr"},
|
||||
"tags":[{"label":"x","pattern":"foo|bar","color":"#112233"},{"pattern":"a.b","color":"#445566","regex":false}]}]}]`), 0o644)
|
||||
res := loadPresets(path)
|
||||
if res.Source != "file" || res.Error != "" || res.Groups[0].Presets[0].Name["fr"] != "A fr" || res.Groups[0].Group["en"] != "Mine" {
|
||||
t.Fatalf("valid file: %+v", res)
|
||||
}
|
||||
|
||||
for _, bad := range []string{
|
||||
`not json`,
|
||||
`[{"group":"G","presets":[{"id":"a","name":"A","tags":[{"pattern":"(","color":"#112233"}]}]}]`,
|
||||
`[{"group":"G","presets":[{"id":"a","name":"A","tags":[{"pattern":"x","color":"red"}]}]}]`,
|
||||
`[{"group":"G","presets":[{"id":"a","name":"A","tags":[]}]}]`,
|
||||
`[{"group":"G","presets":[{"id":"a","name":"A","tags":[{"pattern":"x","color":"#112233"}]},{"id":"a","name":"B","tags":[{"pattern":"y","color":"#112233"}]}]}]`,
|
||||
} {
|
||||
os.WriteFile(path, []byte(bad), 0o644)
|
||||
if res := loadPresets(path); res.Source != "builtin" || res.Error == "" || len(res.Groups) == 0 {
|
||||
t.Errorf("%s: got %q, error %q", bad, res.Source, res.Error)
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
-86
@@ -49,20 +49,8 @@ const I18N = {
|
||||
confirmDelete: (p) => `Delete tag "${p}"?`,
|
||||
confirmReset: 'Replace all tags with the defaults (warning, error, ok)?',
|
||||
presetAria: 'Add a preset', presetPick: '+ Preset…',
|
||||
preset_http_status: 'HTTP status codes', preset_http_methods: 'HTTP methods',
|
||||
preset_http_probes: 'Probes and attacks', preset_http_bots: 'Bots and scripts',
|
||||
preset_http_errors: 'TLS/HTTPS and proxy errors',
|
||||
pl_probes: 'probes / attacks', pl_bots: 'bots / scripts', pl_tls: 'TLS errors', pl_proxy: 'proxy errors',
|
||||
presetGroupSys: 'System', presetGroupApps: 'Applications', presetGroupGen: 'General',
|
||||
preset_sys_auth: 'SSH and logins', preset_sys_sudo: 'sudo commands', preset_sys_kernel: 'Kernel: OOM, crashes, disks',
|
||||
preset_sys_systemd: 'systemd services', preset_sys_firewall: 'Firewall and fail2ban',
|
||||
preset_app_docker: 'Docker and containers', preset_app_db: 'Databases',
|
||||
preset_gen_levels: 'Log levels', preset_gen_ip: 'IPv4 addresses',
|
||||
pl_authFail: 'login failures', pl_authOk: 'logins', pl_sudo: 'sudo commands', pl_oom: 'out of memory',
|
||||
pl_kernel: 'kernel errors', pl_unitFail: 'failed services', pl_unitOk: 'service start/stop',
|
||||
pl_firewall: 'firewall', pl_docker: 'container problems', pl_db: 'database errors',
|
||||
pl_fatal: 'fatal / critical', pl_info: 'info / notice', pl_debug: 'debug / trace', pl_ip: 'IPv4 addresses',
|
||||
presetAdded: (n) => (n ? `${n} tag(s) added` : 'These tags are already in the list'),
|
||||
presetsFileErr: (f) => `Invalid presets file ${f}, built-in presets used: `,
|
||||
tagsLoadErr: 'Tags: ',
|
||||
err_pattern_required: 'The keyword is required',
|
||||
err_invalid_color: 'Invalid color (expected #rrggbb)',
|
||||
@@ -212,20 +200,8 @@ const I18N = {
|
||||
confirmDelete: (p) => `Supprimer le tag « ${p} » ?`,
|
||||
confirmReset: 'Remplacer tous les tags par les tags par défaut (warning, error, ok) ?',
|
||||
presetAria: 'Ajouter un préréglage', presetPick: '+ Préréglage…',
|
||||
preset_http_status: 'Codes HTTP', preset_http_methods: 'Méthodes HTTP',
|
||||
preset_http_probes: 'Sondes et attaques', preset_http_bots: 'Robots et scripts',
|
||||
preset_http_errors: 'Erreurs TLS/HTTPS et proxy',
|
||||
pl_probes: 'sondes / attaques', pl_bots: 'robots / scripts', pl_tls: 'erreurs TLS', pl_proxy: 'erreurs proxy',
|
||||
presetGroupSys: 'Système', presetGroupApps: 'Applications', presetGroupGen: 'Général',
|
||||
preset_sys_auth: 'SSH et connexions', preset_sys_sudo: 'Commandes sudo', preset_sys_kernel: 'Noyau : OOM, plantages, disques',
|
||||
preset_sys_systemd: 'Services systemd', preset_sys_firewall: 'Pare-feu et fail2ban',
|
||||
preset_app_docker: 'Docker et conteneurs', preset_app_db: 'Bases de données',
|
||||
preset_gen_levels: 'Niveaux de log', preset_gen_ip: 'Adresses IPv4',
|
||||
pl_authFail: 'échecs de connexion', pl_authOk: 'connexions', pl_sudo: 'commandes sudo', pl_oom: 'mémoire épuisée',
|
||||
pl_kernel: 'erreurs noyau', pl_unitFail: 'services en échec', pl_unitOk: 'démarrage/arrêt de service',
|
||||
pl_firewall: 'pare-feu', pl_docker: 'problèmes de conteneur', pl_db: 'erreurs base de données',
|
||||
pl_fatal: 'fatal / critique', pl_info: 'info / notice', pl_debug: 'debug / trace', pl_ip: 'adresses IPv4',
|
||||
presetAdded: (n) => (n ? `${n} tag(s) ajouté(s)` : 'Ces tags sont déjà dans la liste'),
|
||||
presetsFileErr: (f) => `Fichier de préréglages ${f} invalide, préréglages intégrés utilisés : `,
|
||||
tagsLoadErr: 'Tags : ',
|
||||
err_pattern_required: 'Le mot-clé est obligatoire',
|
||||
err_invalid_color: 'Couleur invalide (format #rrggbb attendu)',
|
||||
@@ -475,6 +451,7 @@ const state = {
|
||||
rows: [], // displayed records, newest first
|
||||
pending: [], // live messages received while scrolled down
|
||||
tags: [],
|
||||
presets: [], // preset groups from /api/presets
|
||||
matchers: [], // compiled tags + search terms
|
||||
mode: store.get('mode', 'simple'),
|
||||
live: store.get('live', '1') === '1',
|
||||
@@ -497,7 +474,6 @@ function applyLang() {
|
||||
for (const el of document.querySelectorAll('[data-i18n-title]')) el.title = t(el.dataset.i18nTitle) + (el.dataset.user ? ` (${el.dataset.user})` : '');
|
||||
for (const el of document.querySelectorAll('[data-i18n-aria]')) el.setAttribute('aria-label', t(el.dataset.i18nAria));
|
||||
for (const el of document.querySelectorAll('[data-i18n-ph]')) el.placeholder = t(el.dataset.i18nPh);
|
||||
for (const el of document.querySelectorAll('[data-i18n-label]')) el.label = t(el.dataset.i18nLabel);
|
||||
for (const b of document.querySelectorAll('#langSwitch [data-lang]')) b.setAttribute('aria-checked', String(b.dataset.lang === lang));
|
||||
}
|
||||
|
||||
@@ -514,6 +490,7 @@ function setLang(next) {
|
||||
renderHisto();
|
||||
renderStats();
|
||||
renderTagList();
|
||||
renderPresets();
|
||||
renderTimeSettings();
|
||||
renderPurge();
|
||||
renderInterface();
|
||||
@@ -2074,61 +2051,24 @@ $('#purgeBtn').addEventListener('click', async () => {
|
||||
|
||||
const PALETTE = ['#6366f1', '#0ea5e9', '#14b8a6', '#a855f7', '#ec4899', '#eab308', '#64748b', '#f97316'];
|
||||
|
||||
// Ready-made tags. The patterns are valid in both JavaScript and Go (RE2).
|
||||
// The HTTP ones cover nginx/Apache (common, combined), Traefik (CLF, JSON),
|
||||
// Caddy (JSON) and HAProxy (httplog); for status codes and methods only the
|
||||
// "hl" group is colored, not the context around it. The others target the
|
||||
// usual messages of sshd/PAM, sudo, the kernel, systemd, UFW/iptables,
|
||||
// fail2ban, Docker, PostgreSQL and MySQL/MariaDB.
|
||||
const HTTP_STATUS_CTX = '(?:" |"(?:status|DownstreamStatus|OriginStatus|status_code)": ?|(?:-1|\\d+)/(?:-1|\\d+)/(?:-1|\\d+)/(?:-1|\\d+)/\\+?\\d+ )';
|
||||
const httpStatus = (d, color) => ({ label: `HTTP ${d}xx`, pattern: `${HTTP_STATUS_CTX}(?<hl>${d}\\d\\d)\\b`, color });
|
||||
const httpMethod = (m, color) => ({ label: m.replace(/\|/g, '/'), pattern: `"(?<hl>${m})[ "]`, color, caseSensitive: true });
|
||||
const PRESETS = {
|
||||
http_status: () => [httpStatus(2, '#86efac'), httpStatus(3, '#93c5fd'), httpStatus(4, '#fdba74'), httpStatus(5, '#f87171')],
|
||||
http_methods: () => [httpMethod('GET|HEAD|OPTIONS', '#cbd5e1'), httpMethod('POST|PUT|PATCH', '#c4b5fd'), httpMethod('DELETE', '#f9a8d4')],
|
||||
http_probes: () => [{ label: t('pl_probes'), color: '#fda4af',
|
||||
pattern: '(?:wp-login\\.php|xmlrpc\\.php|wp-admin|phpmyadmin|/\\.env|/\\.git|/\\.aws|/cgi-bin/|\\.\\./|%2e%2e|/etc/passwd|<script|union(?:\\s|%20|\\+)+select)' }],
|
||||
http_bots: () => [{ label: t('pl_bots'), color: '#fde68a',
|
||||
pattern: '\\b(?:[a-z]*bot|crawler|spider|curl|wget|python-requests|Go-http-client|zgrab|masscan|nmap|sqlmap|nikto)\\b' }],
|
||||
http_errors: () => [
|
||||
{ label: t('pl_tls'), color: '#f0abfc',
|
||||
pattern: '(?:TLS handshake error|SSL_do_handshake\\(\\) failed|SSL handshake|SSL routines|certificate (?:has )?expired|certificate verify failed|bad certificate|unknown certificate|x509:)' },
|
||||
{ label: t('pl_proxy'), color: '#fdba74',
|
||||
pattern: '(?:upstream timed out|upstream prematurely closed|no live upstreams|connect\\(\\) failed|connection refused|bad gateway|gateway time-?out|service unavailable)' },
|
||||
],
|
||||
sys_auth: () => [
|
||||
{ label: t('pl_authFail'), color: '#fca5a5',
|
||||
pattern: '(?:Failed (?:password|publickey|none)|Invalid user|authentication failures?|Connection closed by (?:invalid|authenticating) user|maximum authentication attempts exceeded|FAILED (?:LOGIN|SU)|incorrect password attempts?|NOT in sudoers)' },
|
||||
{ label: t('pl_authOk'), color: '#86efac',
|
||||
pattern: '(?:Accepted (?:password|publickey|keyboard-interactive(?:/pam)?)|session opened for user|New session \\S+ of user)' },
|
||||
],
|
||||
sys_sudo: () => [{ label: t('pl_sudo'), color: '#fde68a', pattern: '\\bCOMMAND=\\S+', caseSensitive: true }],
|
||||
sys_kernel: () => [
|
||||
{ label: t('pl_oom'), color: '#f87171',
|
||||
pattern: '(?:Out of memory|oom-kill(?:er)?|oom_reaper|Killed process \\d+|invoked oom-killer)' },
|
||||
{ label: t('pl_kernel'), color: '#fda4af',
|
||||
pattern: '(?:Kernel panic|\\bBUG: |\\bOops\\b|Call Trace|segfault at|general protection fault|I/O error|EXT4-fs error|Buffer I/O error|blocked for more than \\d+ seconds|Hardware Error|soft lockup|hard LOCKUP)' },
|
||||
],
|
||||
sys_systemd: () => [
|
||||
{ label: t('pl_unitFail'), color: '#fca5a5',
|
||||
pattern: '(?:Failed to start|failed with result|Main process exited, code=(?:exited|killed|dumped)|entered failed state|Start request repeated too quickly|Dependency failed)' },
|
||||
{ label: t('pl_unitOk'), color: '#bbf7d0', caseSensitive: true,
|
||||
pattern: '\\b(?:Started|Starting|Stopped|Stopping|Reloaded|Reloading|Reached target)\\b' },
|
||||
],
|
||||
sys_firewall: () => [{ label: t('pl_firewall'), color: '#fdba74', caseSensitive: true,
|
||||
pattern: '(?:\\[UFW (?:BLOCK|ALLOW|AUDIT|LIMIT BLOCK)\\]|\\b(?:DROP|REJECT)\\b|\\b(?:Ban|Unban|Found) \\d{1,3}(?:\\.\\d{1,3}){3}\\b)' }],
|
||||
app_docker: () => [{ label: t('pl_docker'), color: '#fcd34d',
|
||||
pattern: '(?:\\bOOMKilled\\b|exited with code [1-9]\\d*|exit code: [1-9]\\d*|health_status: unhealthy|\\bunhealthy\\b|Back-off restarting|CrashLoopBackOff|container (?:die|kill|oom)\\b|restarting \\(\\d+\\))' }],
|
||||
app_db: () => [{ label: t('pl_db'), color: '#c4b5fd',
|
||||
pattern: '(?:\\bdeadlock(?: detected| found)?\\b|duplicate key|too many (?:connections|clients)|lock wait timeout|slow query|could not connect to server|server has gone away|out of shared memory|terminating connection|Access denied for user|password authentication failed)' }],
|
||||
gen_levels: () => [
|
||||
{ label: t('pl_fatal'), color: '#ef4444', pattern: '\\b(?:fatal|crit(?:ical)?|panic|emerg(?:ency)?)\\b' },
|
||||
{ label: t('pl_info'), color: '#bfdbfe', pattern: '\\b(?:info|notice)\\b' },
|
||||
{ label: t('pl_debug'), color: '#e5e7eb', pattern: '\\b(?:debug|trace)\\b' },
|
||||
],
|
||||
gen_ip: () => [{ label: t('pl_ip'), color: '#a5f3fc',
|
||||
pattern: '\\b(?:(?:25[0-5]|2[0-4]\\d|1?\\d?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|1?\\d?\\d)\\b' }],
|
||||
};
|
||||
// Ready-made tags, from the presets file (or the built-in presets.json):
|
||||
// see docs/presets.md. A text is a string or an object per language.
|
||||
const pickText = (x) => (typeof x === 'string' ? x : (x?.[lang] ?? x?.en ?? Object.values(x || {})[0] ?? ''));
|
||||
|
||||
async function loadPresets() {
|
||||
try {
|
||||
const res = await api('/api/presets');
|
||||
state.presets = res.groups || [];
|
||||
if (res.error) toast(t('presetsFileErr', res.file) + res.error);
|
||||
} catch (e) { toast(e.message); }
|
||||
renderPresets();
|
||||
}
|
||||
|
||||
function renderPresets() {
|
||||
$('#presetTags').innerHTML = `<option value="">${esc(t('presetPick'))}</option>`
|
||||
+ state.presets.map((g) => `<optgroup label="${esc(pickText(g.group))}">`
|
||||
+ g.presets.map((p) => `<option value="${esc(p.id)}">${esc(pickText(p.name))}</option>`).join('') + '</optgroup>').join('');
|
||||
}
|
||||
|
||||
async function loadTags() {
|
||||
try { state.tags = await api('/api/tags'); } catch (e) { toast(t('tagsLoadErr') + e.message); }
|
||||
@@ -2220,14 +2160,15 @@ $('#addTag').addEventListener('click', async () => {
|
||||
|
||||
// Adds a preset group, skipping tags whose pattern is already in the list.
|
||||
$('#presetTags').addEventListener('change', async (ev) => {
|
||||
const make = PRESETS[ev.target.value];
|
||||
const preset = state.presets.flatMap((g) => g.presets).find((p) => p.id === ev.target.value);
|
||||
ev.target.value = '';
|
||||
if (!make) return;
|
||||
if (!preset) return;
|
||||
let added = 0;
|
||||
try {
|
||||
for (const p of make()) {
|
||||
for (const p of preset.tags) {
|
||||
if (state.tags.some((x) => x.pattern === p.pattern)) continue;
|
||||
state.tags.push(await api('/api/tags', { method: 'POST', body: { regex: true, enabled: true, ...p } }));
|
||||
const tag = { ...p, label: pickText(p.label), regex: p.regex ?? true, enabled: true };
|
||||
state.tags.push(await api('/api/tags', { method: 'POST', body: tag }));
|
||||
added++;
|
||||
}
|
||||
} catch (e) { toast(e.message); }
|
||||
@@ -2248,6 +2189,7 @@ $('#resetTags').addEventListener('click', async () => {
|
||||
$('#settingsBtn').addEventListener('click', () => {
|
||||
renderTimeSettings();
|
||||
renderTagList();
|
||||
loadPresets();
|
||||
renderInterface();
|
||||
loadPurgeStatus();
|
||||
loadSyslog();
|
||||
|
||||
@@ -193,28 +193,6 @@
|
||||
<button id="addTag" class="btn primary" type="button" data-i18n="addTag">+ Add tag</button>
|
||||
<select id="presetTags" class="field" aria-label="Presets" data-i18n-aria="presetAria">
|
||||
<option value="" data-i18n="presetPick">+ Preset…</option>
|
||||
<optgroup label="HTTP/HTTPS">
|
||||
<option value="http_status" data-i18n="preset_http_status">HTTP status codes</option>
|
||||
<option value="http_methods" data-i18n="preset_http_methods">HTTP methods</option>
|
||||
<option value="http_probes" data-i18n="preset_http_probes">Probes and attacks</option>
|
||||
<option value="http_bots" data-i18n="preset_http_bots">Bots and scripts</option>
|
||||
<option value="http_errors" data-i18n="preset_http_errors">TLS/HTTPS and proxy errors</option>
|
||||
</optgroup>
|
||||
<optgroup label="System" data-i18n-label="presetGroupSys">
|
||||
<option value="sys_auth" data-i18n="preset_sys_auth">SSH and logins</option>
|
||||
<option value="sys_sudo" data-i18n="preset_sys_sudo">sudo commands</option>
|
||||
<option value="sys_kernel" data-i18n="preset_sys_kernel">Kernel: OOM, crashes, disks</option>
|
||||
<option value="sys_systemd" data-i18n="preset_sys_systemd">systemd services</option>
|
||||
<option value="sys_firewall" data-i18n="preset_sys_firewall">Firewall and fail2ban</option>
|
||||
</optgroup>
|
||||
<optgroup label="Applications" data-i18n-label="presetGroupApps">
|
||||
<option value="app_docker" data-i18n="preset_app_docker">Docker and containers</option>
|
||||
<option value="app_db" data-i18n="preset_app_db">Databases</option>
|
||||
</optgroup>
|
||||
<optgroup label="General" data-i18n-label="presetGroupGen">
|
||||
<option value="gen_levels" data-i18n="preset_gen_levels">Log levels</option>
|
||||
<option value="gen_ip" data-i18n="preset_gen_ip">IPv4 addresses</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</span>
|
||||
<button id="resetTags" class="btn ghost" type="button" data-i18n="resetTags">Restore default tags</button>
|
||||
|
||||
Reference in new issue
Block a user