
Filebeat et stacktraces Java : arrêter de perdre la moitié de l'erreur
Ouvrez Kibana sur un projet Java fraîchement branché à Filebeat, cherchez
une erreur, et vous tomberez souvent sur ce spectacle : la première ligne
de l’exception dans un document, puis quarante documents orphelins
contenant chacun un at com.example... solitaire. La stacktrace est là,
mais éparpillée - impossible de la lire, impossible de compter les
erreurs, impossible d’alerter dessus.
La cause est souvent simple : Filebeat lit ligne par ligne mais une stacktrace Java s’étale sur plusieurs lignes. Voyons comment recoller les morceaux, avec une application de test pour vérifier chaque étape.
L’application de test
Pour travailler sur du concret, une mini-application qui logge du trafic normal et lève une exception (qui va générer une stacktrace) toutes les cinq secondes :
package fr.juery;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.SQLException;
import java.util.UUID;
public class LogGenerator {
private static final Logger log = LoggerFactory.getLogger(LogGenerator.class);
public static void main(String[] args) throws InterruptedException {
while (true) {
log.info("Handling command {}", UUID.randomUUID());
Thread.sleep(1000);
try {
loadCommand();
} catch (Exception e) {
log.error("Failed to handle command", e);
}
Thread.sleep(4000);
}
}
static void loadCommand() {
try {
throw new SQLException("Connection refused: connect");
} catch (SQLException e) {
throw new IllegalStateException("Unable to load command", e);
}
}
}Avec un pattern Logback classique :
<configuration>
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>./app.log</file>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<logger name="fr.juery" level="debug" additivity="false">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</logger>
<root level="error">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>
</configuration>Le fichier produit ressemble à ceci - notez que seule la première ligne commence par une date :
2026-07-19 15:09:56.923 INFO [main] fr.juery.LogGenerator - Handling command af08c406-b83f-4973-b84a-da1b6803fbef
2026-07-19 15:09:57.931 ERROR [main] fr.juery.LogGenerator - Failed to handle command
java.lang.IllegalStateException: Unable to load command
at fr.juery.LogGenerator.loadCommand(LogGenerator.java:29)
at fr.juery.LogGenerator.main(LogGenerator.java:17)
Caused by: java.sql.SQLException: Connection refused: connect
at fr.juery.LogGenerator.loadCommand(LogGenerator.java:27)
... 1 common frames omittedC’est cette propriété qu’on va exploiter : une ligne qui ne commence pas par un timestamp appartient au message précédent.
Étape 1 - le parser multiline de Filebeat
Depuis Filebeat 7.16, l’entrée recommandée est filestream, et le
regroupement multiligne se déclare dans la liste parsers :
filebeat.inputs:
- type: filestream
id: demo-java-app
paths:
- /var/log/app/app.log
parsers:
- multiline:
type: pattern
pattern: '^\d{4}-\d{2}-\d{2}' # A line that starts with a date...
negate: true # ...DOESN'T match with this pattern ?
match: after # concatenate it with the previous line
max_lines: 200
timeout: 5s
output.elasticsearch:
hosts: ["https://elasticsearch:9200"]Les trois options se lisent ensemble et méritent d’être décodées une fois pour toutes :
patterndécrit la première ligne d’un événement (ici : commence par une dateYYYY-MM-DD).negate: true+match: aftersignifie : « toute ligne qui ne ressemble pas à un début d’événement est rattachée après la ligne précédente ». C’est la combinaison à retenir pour les logs applicatifs Java - lesat ...,Caused by:et... N common frames omittedne commencent jamais par une date, ils sont donc absorbés.max_lines(500 par défaut) : au-delà, les lignes sont jetées. 200 suffit largement pour une stacktrace, même avec troisCaused by.
Deux pièges classiques à ce stade :
- Ancrer le pattern. Sans
^, une date en plein milieu d’un message (« import du 2026-07-01 échoué ») déclencherait un faux découpage. - Le
timeout: si l’application écrit sa stacktrace lentement (buffer, GC…), Filebeat attend jusqu’à 5 s de silence avant d’expédier l’événement. Inutile d’y toucher, mais c’est lui qui explique le léger délai d’apparition des erreurs.
Après redémarrage de Filebeat, chaque erreur arrive dans Elasticsearch en
un seul document : le champ message contient les 40 lignes,
stacktrace complète incluse. Premier objectif atteint.
Étape 2 - extraire les champs qui rendent le document exploitable
Un gros bloc de texte dans message, c’est lisible mais pas requêtable.
Pour filtrer sur log.level: ERROR ou agréger par classe d’exception, il
faut découper la première ligne en champs. Sans Logstash, un pipeline
d’ingestion Elasticsearch fait très bien le travail :
{
"processors": [
{
"grok": {
"field": "message",
"patterns": [
"%{TIMESTAMP_ISO8601:app.timestamp} %{LOGLEVEL:log.level}\\s+\\[%{DATA:app.thread}\\] %{JAVACLASS:log.logger} - %{GREEDYMULTILINE:app.message}"
],
"pattern_definitions": {
"GREEDYMULTILINE": "(.|\\n)*"
}
}
},
{
"grok": {
"field": "app.message",
"patterns": [
"%{DATA:app.short_message}\\n%{JAVACLASS:error.type}(: %{DATA:error.message})?\\n%{GREEDYMULTILINE:error.stack_trace}"
],
"pattern_definitions": { "GREEDYMULTILINE": "(.|\\n)*" },
"ignore_failure": true
}
},
{
"date": {
"field": "app.timestamp",
"formats": ["yyyy-MM-dd HH:mm:ss.SSS"],
"timezone": "Europe/Paris"
}
}
]
}Le deuxième grok, marqué ignore_failure, ne s’applique qu’aux messages
qui contiennent effectivement une stacktrace : il isole la classe de
l’exception dans error.type, son message dans error.message et la
pile dans error.stack_trace - les noms de champs standard ECS, ceux que
Kibana et les règles d’alerte connaissent déjà.
Reste à dire à Filebeat d’envoyer les documents dans ce pipeline :
output.elasticsearch:
hosts: ["https://elasticsearch:9200"]
pipeline: java-logsÉtape 3 - vérifier le résultat
Avant d’attendre Kibana, testez le pipeline avec l’API _simulate, en
collant un événement multiligne tel que Filebeat l’envoie :
curl -X POST "localhost:9200/_ingest/pipeline/java-logs/_simulate" \
-H 'Content-Type: application/json' -d '
{
"docs": [{ "_source": { "message": "2026-07-19 15:10:02.944 ERROR [main] fr.juery.LogGenerator - Failed to handle command\njava.lang.IllegalStateException: Unable to load command\n\tat fr.juery.LogGenerator.loadCommand(LogGenerator.java:29)\n\tat fr.juery.LogGenerator.main(LogGenerator.java:17)\nCaused by: java.sql.SQLException: Connection refused: connect\n\tat fr.juery.LogGenerator.loadCommand(LogGenerator.java:27)\n\t... 1 common frames omitted" } }]
}'Le document simulé doit ressortir avec tous les champs découpés :
{
"log.level": "ERROR",
"log.logger": "fr.juery.LogGenerator",
"app.thread": "main",
"app.short_message": "Failed to handle command",
"error.type": "java.lang.IllegalStateException",
"error.message": "Unable to load command",
"error.stack_trace": "\tat fr.juery.LogGenerator.loadCommand(...)"
}Dans Kibana, on peut maintenant écrire les requêtes qui étaient impossibles au début :
log.level: "ERROR" and error.type: "java.lang.IllegalStateException"Et surtout construire une visualisation « top 10 des error.type sur
24 h » - le tableau de bord qui révèle en un coup d’œil l’exception qui
noie les logs.
L’alternative qui supprime le problème : logger en JSON
Tout ce qui précède répare un format pensé pour les humains. L’autre école consiste à produire directement du JSON avec ecs-logging-java :
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>./app.log.json</file>
<encoder class="co.elastic.logging.logback.EcsEncoder"/>
</appender>Chaque événement devient une ligne JSON unique - stacktrace comprise,
échappée dans le champ error.stack_trace. Côté Filebeat, plus de
multiline ni de grok, un simple parser ndjson :
parsers:
- ndjson:
target: ""
overwrite_keys: trueSi vous avez la main sur l’application (sur son code), c’est la meilleure option : zéro regex, zéro risque de pattern qui casse au prochain changement de format de log. La configuration multiline reste indispensable pour tout ce que vous ne contrôlez pas - applications tierces, legacy, logs de serveurs d’applications.
Une stacktrace éparpillée sur 40 documents ne sert à personne. Le trio
patternancré sur le timestamp +negate: true+match: afterrecolle l’événement dans Filebeat ; un pipeline d’ingestion en extraiterror.typeeterror.stack_trace; et si vous pouvez modifier l’application, loggez en JSON ECS et supprimez le problème à la source.