09 - Le filtre Grok : parser n'importe quel log
Ce que tu vas apprendre
- La syntaxe Grok et comment elle fonctionne par-dessus les regex
- Les 20 patterns built-in les plus utiles
- Parser des logs Apache, Nginx et syslog
- Creer tes propres patterns custom
- Utiliser le Grok Debugger pour tester
- Les erreurs courantes et comment les éviter
Prerequisites
- Savoir ce qu'est un filtre Logstash (voir article 03)
- Des bases en regex aident (mais ne sont pas obligatoires)
Grok, c'est des regex deguisees
La première fois que j'ai ouvert un pipeline Logstash en production, j'ai vu ca :
%{COMBINEDAPACHELOG}
Un mot en majuscules entre %{}. Ca ne ressemblait a rien que je connaissais. En fait, c'est juste un alias pour une regex. COMBINEDAPACHELOG est un pattern predefini qui se développé en une regex de 200 caractères.
Grok, c'est ca : une bibliothèque de regex nommees. Au lieu d'écrire (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}), tu ecris %{IP}. Au lieu de memoriser les groupes de capture, tu donnes un nom au champ directement.
La syntaxe
Pattern simple
%{PATTERN_NAME:field_name}
PATTERN_NAME: le nom du pattern (une regex predefinie)field_name: le nom du champ dans lequel stocker le résultat
Exemple : %{IP:client_ip} matche une adresse IP et la stocke dans le champ client_ip.
Pattern sans capture
%{PATTERN_NAME}
Sans :field_name, le pattern matche mais ne capture pas dans un champ. Utile pour "consommer" du texte sans le garder.
Pattern avec type
%{PATTERN_NAME:field_name:type}
Le troisieme élément force un type : int ou float.
%{INT:status_code:int}
%{NUMBER:duration:float}
Ca évité de faire un mutate { convert => ... } apres.
Les 20 patterns les plus utiles
Logstash inclut des centaines de patterns. Voici ceux que tu utiliseras 90% du temps :
| Pattern | Matche | Exemple |
|---|---|---|
WORD |
Un mot (lettres, chiffres, underscore) | hello, user_42 |
DATA |
N'importe quoi (non-greedy) | abc 123 !@# |
GREEDYDATA |
N'importe quoi (greedy, va jusqu'a la fin) | tout le reste de la ligne |
INT |
Un entier | 42, -7 |
NUMBER |
Un nombre (entier ou decimal) | 3.14, 42 |
IP |
Une adresse IPv4 | 192.168.1.10 |
IPV6 |
Une adresse IPv6 | ::1 |
IPORHOST |
IP ou hostname | 192.168.1.10, server01 |
HOSTNAME |
Un hostname | api.example.com |
URI |
Une URL complète | https://example.com/path?q=1 |
URIPATH |
Le chemin d'une URL | /api/users/42 |
TIMESTAMP_ISO8601 |
Date ISO 8601 | 2026-03-31T14:23:01.456Z |
HTTPDATE |
Date format log HTTP | 31/Mar/2026:14:23:01 +0000 |
LOGLEVEL |
Niveau de log | INFO, ERROR, WARN |
EMAILADDRESS |
user@example.com |
|
UUID |
UUID v4 | 550e8400-e29b-41d4-a716-446655440000 |
MAC |
Adresse MAC | 00:1A:2B:3C:4D:5E |
PATH |
Chemin de fichier | /var/log/app.log |
POSINT |
Entier positif | 200, 42 |
SPACE |
Espaces | , \t |
Et les patterns composes :
| Pattern | Description |
|---|---|
COMBINEDAPACHELOG |
Ligne de log Apache/Nginx combined |
COMMONAPACHELOG |
Ligne de log Apache common |
SYSLOGLINE |
Ligne syslog complète |
SYSLOGBASE |
En-tête syslog (timestamp + host + process) |
Exemple 1 : parser un log Apache combined
Le format Apache combined est le format de log le plus repandu pour les serveurs web :
192.168.1.10 - john [31/Mar/2026:14:23:01 +0000] "GET /api/users HTTP/1.1" 200 1234 "https://example.com" "Mozilla/5.0"
Le pipeline :
input {
file {
path => "/data/apache.log"
start_position => "beginning"
sincedb_path => "/dev/null"
}
}
filter {
grok {
match => { "message" => "%{COMBINEDAPACHELOG}" }
}
}
output {
stdout { codec => rubydebug }
}
Résultat :
json{
"clientip": "192.168.1.10",
"ident": "-",
"auth": "john",
"timestamp": "31/Mar/2026:14:23:01 +0000",
"verb": "GET",
"request": "/api/users",
"httpversion": "1.1",
"response": "200",
"bytes": "1234",
"referrer": "\"https://example.com\"",
"agent": "\"Mozilla/5.0\""
}
Un seul pattern, et tu as 11 champs proprement extraits. C'est la puissance de Grok : des annees de regex deja ecrites et testees.
Exemple 2 : parser un log applicatif custom
Tes propres logs n'utilisent pas un format standard. Tu dois écrire le pattern toi-meme. Prenons ce format :
2026-03-31 14:23:01.456 ERROR [api-users] [req-abc123] POST /users 500 5023ms - Connection refused to PostgreSQL
Decomposons :
TIMESTAMP LEVEL SERVICE REQUEST_ID METHOD URL STATUS DURATION MESSAGE
2026-03-31 14:23:01 ERROR [api-users] [req-abc123] POST /users 500 5023ms Connection refused...
Le pattern Grok :
filter {
grok {
match => {
"message" => "%{TIMESTAMP_ISO8601:log_timestamp} %{LOGLEVEL:level}\s+\[%{DATA:service}\] \[%{DATA:request_id}\] %{WORD:http_method} %{URIPATH:url} %{INT:status_code:int} %{INT:duration:int}ms - %{GREEDYDATA:error_message}"
}
}
}
Construction du pattern, morceau par morceau :
%{TIMESTAMP_ISO8601:log_timestamp}— la date%{LOGLEVEL:level}— INFO, ERROR, WARN...\s+— un ou plusieurs espaces (le LOGLEVEL a une largeur variable)\[%{DATA:service}\]— le nom du service entre crochets\[%{DATA:request_id}\]— l'ID de requête entre crochets%{WORD:http_method}— GET, POST, PUT...%{URIPATH:url}— le chemin URL%{INT:status_code:int}— le code HTTP (converti en entier)%{INT:duration:int}ms— la duree suivie de "ms"-— le separateur litteral%{GREEDYDATA:error_message}— tout le reste
GREEDYDATA est toujours en dernier. C'est le "attrape-tout" qui prend le reste de la ligne.
Exemple 3 : parser du syslog
Mar 31 14:23:01 server01 sshd[12345]: Accepted publickey for admin from 10.0.0.5 port 54321 ssh2
filter {
grok {
match => { "message" => "%{SYSLOGBASE} %{GREEDYDATA:syslog_message}" }
}
}
SYSLOGBASE extrait le timestamp, le hostname, le programme et le PID. GREEDYDATA capture le message.
Creer des patterns custom
Quand les patterns built-in ne suffisent pas, tu créés les tiens. Deux méthodes.
Méthode 1 : regex inline
Tu melanges des patterns Grok et de la regex brute dans le meme match :
filter {
grok {
match => {
"message" => "user=%{WORD:username} action=(?<action>[a-z_]+) duration=(?<duration>\d+)ms"
}
}
}
La syntaxe (?<field_name>regex) est une capture nommee standard. Ca marche, mais ca devient vite illisible.
Méthode 2 : fichier de patterns
Cree un fichier de patterns dans un dossier dédié :
logstash/
├── pipeline/
│ └── main.conf
└── patterns/
└── custom
Le fichier custom (pas d'extension) :
# logstash/patterns/custom
APP_TIMESTAMP %{YEAR}-%{MONTHNUM}-%{MONTHDAY} %{HOUR}:%{MINUTE}:%{SECOND}\.%{INT}
APP_LOGLEVEL (TRACE|DEBUG|INFO|WARN|ERROR|FATAL)
SERVICE_NAME [a-zA-Z][a-zA-Z0-9_-]*
REQUEST_ID req-[a-f0-9]+
APP_LOG %{APP_TIMESTAMP:timestamp} %{APP_LOGLEVEL:level}\s+\[%{SERVICE_NAME:service}\] \[%{REQUEST_ID:request_id}\] %{GREEDYDATA:body}
Le format : NOM_PATTERN regex_ou_composition_de_patterns. Un pattern par ligne.
Utilisation dans le pipeline :
filter {
grok {
patterns_dir => ["/usr/share/logstash/patterns"]
match => { "message" => "%{APP_LOG}" }
}
}
N'oublie pas de monter le dossier dans Docker :
yamllogstash:
volumes:
- ./logstash/pipeline/:/usr/share/logstash/pipeline/
- ./logstash/patterns/:/usr/share/logstash/patterns/:ro
Les patterns custom rendent les pipelines plus lisibles. %{APP_LOG} est plus comprehensible que 200 caractères de regex.
Le Grok Debugger
Écrire un pattern Grok du premier coup, ca n'arrive jamais. Tu as besoin d'un outil pour tester.
Dans Kibana
Ouvre Kibana (http://localhost:5601), va dans "Dev Tools" > "Grok Debugger". Colle ta ligne de log, colle ton pattern, Kibana te montre les champs extraits en temps réel.
En ligne
Le site grokdebugger.com fait la meme chose sans Kibana. Colle ta ligne, colle ton pattern, vois le résultat.
Mon workflow
- Je prends 5 lignes de log representatifs (cas normal + erreur + edge cases)
- J'ouvre le Grok Debugger dans Kibana
- Je construis le pattern morceau par morceau, de gauche a droite
- Je teste chaque ligne
- Quand ca matche les 5 lignes, je l'intégré dans le pipeline
Plusieurs patterns pour un champ
Parfois, tes logs ont plusieurs formats. Les lignes normales et les lignes d'erreur n'ont pas la meme structure. Grok accepte un tableau de patterns :
filter {
grok {
match => {
"message" => [
"%{TIMESTAMP_ISO8601:ts} %{LOGLEVEL:level} \[%{DATA:service}\] %{WORD:method} %{URIPATH:url} %{INT:status:int} %{INT:duration:int}ms",
"%{TIMESTAMP_ISO8601:ts} %{LOGLEVEL:level} \[%{DATA:service}\] %{GREEDYDATA:error_message}"
]
}
break_on_match => true
}
}
Avec break_on_match => true (le défaut), Grok essaie les patterns dans l'ordre et s'arrêté au premier qui matche. Le premier pattern est pour les lignes normales (avec method, url, status), le deuxieme est un fallback pour les lignes d'erreur.
Gestion des échecs de parsing
Quand Grok ne matche aucun pattern, il ajoute le tag _grokparsefailure a l'événement. L'événement n'est pas supprime, il continue dans le pipeline avec le tag.
filter {
grok {
match => { "message" => "%{COMBINEDAPACHELOG}" }
tag_on_failure => ["_grokparsefailure"]
}
}
output {
if "_grokparsefailure" in [tags] {
file {
path => "/data/grok-failures.log"
codec => json_lines
}
}
if "_grokparsefailure" not in [tags] {
elasticsearch {
hosts => ["http://elasticsearch:9200"]
index => "logs-%{+YYYY.MM.dd}"
}
}
}
Les événements mal parses vont dans un fichier a part. Tu peux les analyser pour ajuster tes patterns.
En production, surveille le nombre de _grokparsefailure. Si ca monte, c'est qu'un format de log a change ou qu'un nouveau service écrit dans un format que ton pipeline ne connaît pas.
Performance : Grok est lent
Il faut le dire : Grok est le filtre le plus lent de Logstash. Chaque pattern est une regex compilee et exécutée sur le texte. Plus le pattern est complexe, plus c'est lent.
Quelques regles pour limiter les degats :
1. Ancre tes patterns. Si tu sais que la ligne commence par un timestamp, commence le pattern par ^%{TIMESTAMP_ISO8601}. L'ancre ^ évité que le moteur regex essaie de matcher a chaque position.
2. Evite les patterns optionnels longs. (%{GREEDYDATA:extra})? a la fin d'un pattern oblige le moteur a backtracker. Si le champ est parfois absent, utilise deux patterns avec break_on_match.
3. Prefere Dissect quand c'est possible. Si tes logs ont un separateur fixe (espace, pipe, tab), Dissect est 5 a 10 fois plus rapide. On le voit dans l'article suivant.
4. Un Grok, pas cinq. Chaque grok {} dans le pipeline est une passe sur le texte. Extrais tout dans un seul match si possible.
Sur paltemps.fr, je mesure le temps de filtre avec l'API _node/stats. Si un pipeline passe plus de temps en filtre qu'en output, c'est souvent Grok le coupable.
Supprimer le champ message apres parsing
Apres un Grok reussi, le champ message contient encore la ligne brute. Si tu n'en as plus besoin, supprime-le pour economiser de l'espace dans Elasticsearch :
filter {
grok {
match => { "message" => "%{COMBINEDAPACHELOG}" }
remove_field => ["message"]
}
}
Attention : ne supprime message que si ton Grok a bien extrait tous les champs. Sinon tu perds la donnee brute.
Résumé
- Grok transforme des lignes de texte en champs structures grâce à des patterns nommes
%{PATTERN:field}matche et capture,%{PATTERN:field:int}convertit en entierCOMBINEDAPACHELOG,SYSLOGLINEetTIMESTAMP_ISO8601couvrent la majorite des cas- Cree des patterns custom dans un fichier dédié pour les formats maison
- Utilise le Grok Debugger (Kibana ou en ligne) pour tester avant d'intégrer
- Grok est lent : ancre tes patterns, préféré Dissect pour les formats fixes
Precedent : 08 - Les codecs | Suivant : 10 - Le filtre Dissect