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:
cedricandClaude Opus 5.5 committed 2026-10-03 16:05:47 +02:00
1 parent 4225a2c870
commit 688a7dc2e6
14 files changed
+680 -126

No files matched your search

+5
View File
@@ -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
View File
@@ -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
View File
@@ -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 |
+8 -8
View File
@@ -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 |
+6
View File
@@ -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)
+3
View File
@@ -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
+114
View File
@@ -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
View File
@@ -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)`.
+2 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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"
}
]
}
]
}
]
+46
View File
@@ -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
View File
@@ -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();
-22
View File
@@ -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>