Systemd-Journal mit jq filtern
Journalctl bietet in der Version 239 noch keine Möglichkeit, Einträge anhand eines Kriteriums auszuschließen. Bei der Recherche bin ich auf jq (Debian-Paket: jq, IRC-Kanal: #jq) gestoßen, das ein Stream-Editor für JSON ist. Ähnlich wie man mit sed einen Strom von Textzeilen verarbeiten kann, lässt sich jq zum Filtern und Modifizieren eines Stroms von JSON-Blöcken verwenden. Da journalctl auch JSON als Ausgabeformat unterstützt, habe ich mir mit beiden einen entsprechenden Filter gebastelt.
Am Anfang stand die Frage, welche Fehlermeldungen seit gestern aufgetreten
sind. Eigentlich keine komplizierte Sachen, denn journalctl hat den Filter
-perr
. Dabei kamen aber sehr viele Meldungen von
Synapse, die alle anderen Fehlermeldungen überdeckt
haben. Daher brauchte ich also ein »alle Fehlermeldungen seit gestern, ohne die
Meldungen von Synapse« und das unterstützt journalctl (noch) nicht.
JSON ist ein leichtgewichtiges Datenformat, das man auch recht gut lesen und
von Hand bearbeiten kann. Journalctl unterstützt die Ausgabe der Logeinträge
als JSON, mit man nicht den Kampf mit den Zeilenenden bei sed hat, denn
viele von den Fehlermeldungen von Synapse enthalten Zeilenumbrüche. Der
einfachste Ausdruck für jq ist .
, der das komplette Objekt, das eingelesen
wurde, weitergibt. Mit der Kommandozeilenoption -C
kann man die Einfärbung der
Ausgabe aktivieren. Ein jq -C .
ist wie ein sed ''
– also ein guter
Anfang:
journalctl -perr --since yesterday |jq -C . |less
Die Abfrage- und Bearbeitungssprache von jq ist sehr mächtig – schon allein weil ein JSON-Objekt komplexer als eine Textzeile ist und mehr Operationen darauf ermöglicht. Die vollständige Beschreibung gibt es in der Man-Page. Jq bearbeitet die Eingabe wie einen Strom von JSON-Objekten, die verschiedene Stufen einer Verarbeitungskette durchlaufen. In den Stufen kann das Objekt entfernt oder verändert werden und am Ende der Kette wird das Objekt ausgegeben.
Somit ist mein nächstes Ziel, die Entfernung aller Einträge, die von Synapse
kommen. Diese sind dadurch gekennzeichnet, dass deren Feld SYSTEMDUNIT den
Wert matrix-synapse.service hat. Für die Ausgabe sollen also alle Objekt
ausgewählt werden, auf die dies nicht zutrifft. Der Ausruck für jq lautet
select(._SYSTEMD_UNIT != "matrix-synapse.service")
. Mit dem Term
.NAME
greift man auf den Wert des Feldes NAME im
aktuellen Objekt zu.
Da viele Informationen in den JSON-Objekten nicht notwendig sind und sich die
Ausgabe in Textzeilen schöner zu erfassen wäre, soll eben aus dem komplexen
JSON-Objekt am Ende eine JSON-Zeichenfolge werden. Hierfür bietet jq
Textersetzung (engl. string interpolation) an. Innerhalb von ""
kann man mit
\(…)
beliebige jq-Ausdrücke einbauen. Am Ende soll eine Ausgabe mit Uhrzeit,
Dienstname und Meldung entstehen. Der entsprechende Ausdruck ist also
"\(._SOURCE_REALTIME_TIMESTAMP) \(.SYSLOG_IDENTIFIER): \(.MESSAGE)"
.
Die Stufen in der Verarbeitungskette werden bei jq, ähnlich wie in der Shell,
mit |
(Pipe) getrennt. Da das Ergebnis der gesamten Kette eine Zeichenfolge
ist, kann man mit der Option -r
die Ausgabe der unnötigen Anführungszeichen
unterdrücken.
journalctl -perr --since yesterday |jq -r 'select(._SYSTEMD_UNIT != "matrix-synapse.service")
| "\(._SOURCE_REALTIME_TIMESTAMP) \(.SYSLOG_IDENTIFIER): \(.MESSAGE)"
' |less
Die Zeitangaben in Mikrosekunden sind jedoch nicht sonderlich lesbar. Die
Funktion todate
kann diese in ein lesbares Format umwandeln, aber möchte als
Eingabe die Sekunden als Zahl haben. Daher braucht es an dieser Stelle eine
Unterkette, in der die Werte in dem Feld in eine Zahl umgewandelt und durch eine
Million geteilt werden, bevor sich an todate gehen:
._SOURCE_REALTIME_TIMESTAMP | tonumber |. / 1000000 |todate
Bei mir kam dann die Warnung: jq: error (at <stdin>:4885): null (null) cannot
be parsed as a number
. Offenbar enthalten nicht alle Objekte das Feld
SOURCEREALTIME_TIMESTAMP. Aber jq hat den Operator //
, wie es ihn auch
in Perl gibt bzw. wie man ||
auch in Javascript verwendet: wenn der linke
Ausdruck zu keinem Wert ergibt, wird der rechte Wert genommen. Da die Objekte
alle noch ._REALTIMETIMESTAMP enthalten, soll dieser als Ersatzwert
verwendet werden. Analog ist es mit .SYSLOG_IDENTIFIER, das auch nicht in
allen Objekten vorkommt, weshalb der Ersatz .UNIT verwendet werden soll
journalctl -perr --since yesterday |jq -r 'select(._SYSTEMD_UNIT != "matrix-synapse.service")
| "\(._SOURCE_REALTIME_TIMESTAMP // .__REALTIME_TIMESTAMP | tonumber |. / 1000000 |todate) \(.SYSLOG_IDENTIFIER // .UNIT): \(.MESSAGE)"
' |less
Damit hatte ich einen Filter, der mir die Fehlermeldungen ohne die Meldungen von Synapse liefert. Gepackt von dem Möglichkeiten, die jq bietet, wollte ich dann die kompletten Meldungen des Tages so reduzieren, dass ich sie überblicken konnte. Hierbei störten die Meldungen der Matrix-Whatsapp-Verbindung, die sich leicht durch Erweiterung des select-Ausdrucks entfernen ließen.
Allerdings gab es auch viele Meldungen von networkd, die mit Switching to
DNS
begannen, und ich wollte auch nur genau diese entfernen. Hierfür musste ich
bei select wieder eine Unterkette einfügen, denn not ist kein Operator bei
jq, sondern eine Funktion. Also musste ich den Wert im Feld MESSAGE auf den
Beginn prüfen und dieses Ergebnis negieren: .MESSAGE
|startswith("Switching to DNS") |not
. Am Ende hatte ich den folgenden Ausdruck,
der das Log recht übersichtlich gestaltetet:
journalctl --since today |jq -r '
select(._SYSTEMD_UNIT != "matrix-synapse.service" and ._SYSTEMD_UNIT != "matrix-whatsapp.service"
and (.MESSAGE |startswith("Switching to DNS") |not))
| "\(._SOURCE_REALTIME_TIMESTAMP // .__REALTIME_TIMESTAMP | tonumber |. / 1000000 |todate) \(.SYSLOG_IDENTIFIER // .UNIT): \(.MESSAGE)"
' |less
Aktuelle Version eines Projekts bei Github
Von Riot werden kontinuierlich neue Versionen veröffentlicht. Die Aktualisierung meiner Installation wollte ich daher vereinfachen bis hin zu einer vollständigen Automatisierung. Leider habe ich keine beständige URL gefunden, über die ich immer die letzte Version beziehen kann. Aber github hat eine API, über die man Informationen zur letzten Veröffentlichung abfragen kann: https://api.github.com/repos/vector-im/riot-web/releases/latest. Als Antwort bekommt man ein JSON-Objekt, aus dem ich mit jq die notwendigen Informationen extrahiere.
Die Versionsangabe steht im Feld name, das sich leicht mit .name
auswählen
lässt und da es eine Zeichenfolge ist, lassen sich mit er Option -r
die
Anführungszeichen unterdrücken:
ver=$(jq -r .name <<<$release_data)
Mit einer Veröffentlichung können mehrere Dateien herausgegeben werden. Deshalb
ist in dem Feld assets eine Menge (Array), dessen Elemente das Feld
browser_download_url enthalten. Bei einer solchen Kette von Selektionen
braucht man keine einzelnen Stufen (|
), sondern kann diese aneinander
schreiben: .assets[].browser_download_url
. Von diesen URLs soll dann
diejenige, die auf .tar.gz endet, verwendet werden.
url=$(jq -r '.assets[].browser_download_url|select(endswith(".tar.gz"))' <<<$release_data)
Das gesamte Skript lautet dann:
#!/bin/zsh
# https://github.com/vector-im/riot-web/releases/tag/latest
release_data=$(curl -s 'https://api.github.com/repos/vector-im/riot-web/releases/latest')
ver=$(jq -r .name <<<$release_data)
url=$(jq -r '.assets[].browser_download_url|select(endswith(".tar.gz"))' <<<$release_data)
echo "latest version: $ver"
cd $0:h
tgt=m.jo-so.de
<span class="createlink">version) </span> && exit 0
mv $tgt $tgt.old
mkdir $tgt
curl -Ls $url |tar xzf - -C $tgt --strip=1
mv $tgt.old/config.json $tgt
diff -u $tgt.old/config.sample.json $tgt/config.sample.json
Alternativ dazu bietet jq noch einen anderen Weg zur Auswahl der URL. Mit ..
werden alle Elemente in beliebiger Tiefe auswählt; ähnlich dem Operator **
bei
Pfaden in der Zsh. Aus einer Menge von Elementen kann man mit der Funktion
strings die Zeichenfolgen herausfilter und mit match gegen einen regulären
Ausdruck prüfen. Die Abfrage ist damit etwas kürzer (bei anderen Aufgaben kann
sie erheblich kürzer werden), aber nicht mehr so zielsicher und robust.
jq -r '..|strings|select(match("http.*\\.tar\\.gz$"))'
Hängt man an einen Feldnamen ein Fragezeichen, so gibt jq keine Fehlermeldung
aus, sollte das Feld nicht existieren, sondern null. Um etwas mehr Sicherheit
bei der Abfrage zu haben, kann man also aus der Menge aller Elemente, jeweils
das Feld browser_download_url auswählen, sollte es vorkommen:
..|.browser_download_url?
Aus dieser Ergebnismenge muss man allerdings die null-Werte entfernen, was
mit dem Filter strings oder der Prüfung . != null
möglich ist:
jq -r '..|.browser_download_url?|strings|select(endswith(".tar.gz"))'
jq -r '..|.browser_download_url?|select(. != null and endswith(".tar.gz"))'