Anleitung: mapper_parsing_exception - failed to parse aus einem ELK-Appender debuggen
Symptom
Im Service-Log taucht regelmäßig auf:
... PerformanceLogRejectedException: mapper_parsing_exception - failed to parse
at ...ElasticsearchWriter.sendData(...)
...
Die Application-Exception-Queue füllt sich mit identischen Einträgen.
Wichtig zum Verständnis: Diese Exception wird in vielen Logback-Appender-Implementierungen konstruiert und in die Exception-Queue gelegt, nicht geworfen. Die Anwendung läuft normal weiter, nur die betroffenen Log-Dokumente gehen verloren. Der Stacktrace ist der Capture-Stack zum Erzeugungszeitpunkt — kein echter Crash.
Außerdem: viele Logger-Bibliotheken serialisieren die ES-Bulk-Antwort verkürzt und droppen das caused_by. Die Meldung im Service-Log enthält die eigentliche Ursache dann nicht. Ohne den nachfolgenden manuellen Replay rätselt man.
Schritt 1 — Den fehlgeschlagenen Body greifen
Der Body wird in der Regel bereits geloggt, aber auf DEBUG-Level. Logging-Level am betroffenen Logger im laufenden Pod anziehen (kein Redeploy nötig):
curl -X POST \
"http://<pod-host-or-portforward>/actuator/loggers/<logger-name>" \
-H "Content-Type: application/json" \
-d '{"configuredLevel":"DEBUG"}'
Den <logger-name> aus dem Stacktrace ableiten (die Klasse, in der die Exception konstruiert wird, ist meist auch der relevante Logger). Ein paar Minuten warten, dann in den Pod-Logs nach einer Zeile suchen, die mit dem rejected Body endet — meist erkennbar an einem failed body: o. Ä. davor. Der Block besteht aus zwei Zeilen:
{"index":{"_index":"<index-name>"}}
{"@timestamp":"...", ... }
Beide zusammen sind das NDJSON, das der Service an /_bulk schicken wollte.
Logging hinterher wieder zurücksetzen:
curl -X POST \
"http://<pod-host>/actuator/loggers/<logger-name>" \
-H "Content-Type: application/json" \
-d '{"configuredLevel":null}'
Schritt 2 — Body als NDJSON-Datei ablegen
cat > /tmp/fail.ndjson <<'NDJSON'
{"index":{"_index":"<index-name>"}}
{"@timestamp":"...", ...}
NDJSON
Pflicht-Details, sonst antwortet ES mit nichts Brauchbarem:
- Zwei Zeilen, genau in der Reihenfolge: erst
action, dannsource. - Datei MUSS mit einem Newline enden (Heredoc tut das automatisch).
- Content-Type beim Senden ist
application/x-ndjson(nichtapplication/json).
Schritt 3 — Manuell an ES senden, volle Fehlermeldung lesen
ES-URL und Auth aus dem Service-Deployment holen (Env-Var bzw. konfigurierte BasicAuth — selbe Credentials wie die laufende Anwendung):
ES_URL="<scheme://host:port/_bulk>"
U="<es-user>"
P="<es-password>"
curl -sk -u "$U:$P" -H 'Content-Type: application/x-ndjson' \
--data-binary @/tmp/fail.ndjson "$ES_URL" | jq .
Flags-Cheatsheet:
-ssilent, kein Progress-Bar-kself-signed Cert akzeptieren-uBasicAuth--data-binarydamit Newlines nicht verschluckt werden (-dwürde sie entfernen → Bulk kaputt)- ohne
jqals Fallback:python3 -m json.tool
Wenn die Antwort leer / der Befehl scheinbar nichts tut:
curl -vk -u "$U:$P" -H 'Content-Type: application/x-ndjson' \
--data-binary @/tmp/fail.ndjson "$ES_URL" 2>&1 | tail -40
-v zeigt HTTP-Status (401 = Auth falsch, 404 = URL falsch, 415 = Content-Type falsch, 200 = OK, dann Body interpretieren).
Schritt 4 — caused_by interpretieren
Antwort hat die Form:
{
"took": 0,
"errors": true,
"items": [{ "index": {
"status": 400,
"error": {
"type": "mapper_parsing_exception",
"reason": "failed to parse",
"caused_by": { "type": "...", "reason": "..." }
}
}}]
}
Vier häufige caused_by-Muster und ihr Fix:
A) Field-Limit überschritten
"type": "illegal_argument_exception",
"reason": "Limit of total fields [1000] has been exceeded while adding new fields [N]"
→ Index-Schema ist gewachsen, neuer Code emittiert weitere dynamische Keys. Fix sofort, am betroffenen Index:
curl -k -u "$U:$P" -H 'Content-Type: application/json' -XPUT \
"<scheme://host:port>/<index-name>/_settings" -d '{
"index.mapping.total_fields.limit": 5000
}'
Wirkt live, schon im Buffer hängende Dokumente kommen durch.
Dauerhaft für künftige Indizes (Pattern an die Namenskonvention anpassen):
curl -k -u "$U:$P" -H 'Content-Type: application/json' -XPUT \
"<scheme://host:port>/_index_template/<template-name>" -d '{
"index_patterns": ["<prefix>-*"],
"template": {
"settings": { "index.mapping.total_fields.limit": 5000 }
}
}'
Greift erst bei Neuanlage des nächsten Index.
B) Typ-Konflikt (Skalar vs. Objekt)
"reason": "object mapping for [path.to.field] tried to parse field [...] as object, but found a concrete value"
→ Eine Code-Stelle schickt field als String/Zahl, ES hat es schon als Objekt mit Subfeldern gemappt (oder umgekehrt). Das Feld im reason lokalisieren, im Repo nach den Stellen suchen, die es schreiben — dann entweder Code vereinheitlichen oder das Feld via Reindex mit korrektem Mapping neu anlegen.
C) Feldtyp lässt sich nicht ändern
"reason": "mapper [path.to.field] cannot be changed from type [long] to [text]"
→ Erster Wert hat den Typ festgenagelt. ES erlaubt keinen In-Place-Mapping-Change. Optionen: a) Code so anpassen, dass immer der ursprüngliche Typ kommt; b) auf nächsten Index-Rollover warten; c) ignore_malformed: true für das Feld setzen (silently drop).
D) Datumsformat passt nicht
"reason": "failed to parse field [@timestamp] of type [date] ..."
"caused_by.reason": "could not parse date [...]"
→ Default-ES-Date akzeptiert strict_date_optional_time||epoch_millis. Wenn der Logger ein abweichendes Format generiert, ES-seitig das Mapping um die Variante erweitern oder Logger-seitig auf ISO-8601 zwingen.
Schritt 5 — Verifizieren
Nach dem Fix denselben Body nochmal abschicken:
curl -sk -u "$U:$P" -H 'Content-Type: application/x-ndjson' \
--data-binary @/tmp/fail.ndjson "$ES_URL" | jq '.errors, .items[0].index.status'
Erwartetes Ergebnis:
false
201
→ Service-Logs kontrollieren, die Reject-Exception darf nicht mehr nachkommen.
Diagnose-Toolkit für später
Aktuelles Mapping einsehen:
curl -sk -u "$U:$P" "<scheme://host:port>/<index-name>/_mapping?pretty"
Feldanzahl pro Index (grobe Limit-Annäherung):
curl -sk -u "$U:$P" "<scheme://host:port>/<index-name>/_field_caps?fields=*" \
| jq '.fields | length'
Diff zwischen zwei Indizes (zeigt Schema-Drift, ideal um zu sehen welche Felder zwischen zwei Zeiträumen neu dazukamen):
for IDX in <index-A> <index-B>; do
curl -sk -u "$U:$P" "<scheme://host:port>/$IDX/_mapping" \
| jq -r --arg i "$IDX" '
.[$i].mappings | paths(.type? != null) as $p
| ($p | join(".")) + " = " + (getpath($p).type)' \
| sort -u > "/tmp/$IDX.txt"
done
diff /tmp/<index-A>.txt /tmp/<index-B>.txt
Was strukturell anfällig macht
- Logback-Appender mit
rawJsonMessage=truemergen jede Logger-Message als JSON ins ES-Dokument. Wenn der Code-Pfad freie String-Keys einsammelt (pro Endpoint/Methode/Thread eigene Keys), wächst das Mapping unbeschränkt. Mittext+keyword-Multifields zählt jeder Stringwert doppelt → Limit erreicht man schneller als gedacht. - Periodisch rotierende Indizes (z. B. monatlich) starten jedes Mal bei Default-Settings inkl. 1000-Feld-Limit. Index-Template ist der dauerhafte Hebel.
- Logger-Bibliotheken, die
caused_byaus der Bulk-Antwort wegwerfen, machen Live-Diagnose unmöglich. So lange das nicht behoben ist, ist der manuelle Replay-Schritt zwingend.