Kapitel 4. Rezepte protokollieren
Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com
4.0 Einleitung
Unter Logging versteht man die Aufzeichnung von Ereignissen, die während der Ausführung eines Programms auftreten. Diese Tätigkeit wird beim Programmieren oft unterschätzt, weil sie zusätzliche Arbeit bedeutet, die sich für den Programmierer nicht sofort auszahlt.
Während des normalen Betriebs eines Programms ist die Protokollierung ein Overhead, der Verarbeitungszyklen in Anspruch nimmt, um in eine Datei, eine Datenbank oder sogar auf den Bildschirm zu schreiben. Darüber hinaus können nicht verwaltete Logs Probleme verursachen. Der klassische Fall, dass die Logdateien so groß werden, dass sie den gesamten verfügbaren Speicherplatz belegen und den Server zum Absturz bringen, ist nur zu real und passiert zu oft.
Wenn jedoch etwas passiert und du die Abfolge der Ereignisse herausfinden willst, die dazu geführt haben, werden die Protokolle zu einer unschätzbaren diagnostischen Ressource. Logs können auch in Echtzeit überwacht werden und bei Bedarf können Warnmeldungen verschickt werden.
4.1 In Protokolle schreiben
Diskussion
Go stellt in der Standardbibliothek das Paket log
zur Verfügung, mit dem du Ereignisse protokollieren kannst, während das Programm läuft. Es hat eine Standardimplementierung, die in den Standardfehler schreibt und einen Zeitstempel hinzufügt. Das bedeutet, dass du es sofort für die Protokollierung verwenden kannst, ohne es konfigurieren oder einrichten zu müssen, wenn du in den Standardfehler schreiben willst.
Das Paket log
bietet mehrere Funktionen, mit denen du Logs schreiben kannst. Es gibt vor allem drei Gruppen von Funktionen:
-
Druckt die Protokolle an den Logger
- Tödlich
-
Druckt in den Logger und ruft
os.Exit
mit einem Exit-Code von 1 auf - Panik
-
Druckt in den Logger und ruft
panic
Jedes Set besteht aus drei Funktionen; Print
hat zum Beispiel Print
und Printf
, die eine Formatierung ermöglichen, und Println
fügt nach dem Drucken einen Zeilenumbruch hinzu.
Hier ist ein Beispiel mit Print
:
func
main
()
{
str
:=
"abcdefghi"
num
,
err
:=
strconv
.
ParseInt
(
str
,
10
,
64
)
if
err
!=
nil
{
log
.
Println
(
"Cannot parse string:"
,
err
)
}
fmt
.
Println
(
"Number is"
,
num
)
}
Wenn in diesem Beispiel ein Fehler auftritt, rufst du die Funktion Println
auf, die in den Standard-Logger ausgibt. Das siehst du in der Befehlszeile, wenn du das Programm ausführst:
% go run main.go 2022/01/23 18:39:06 Cannot parse string: strconv.ParseInt: parsing "abcdefghi": invalid syntax Number is 0
Du wirst auch sehen, dass das Programm nicht anhält und bis zur letzten Anweisung des Programms weiterläuft. Mit anderen Worten: Println
protokolliert einfach und macht weiter. Was ist der Unterschied zu fmt.Println
? Eigentlich gar nicht - das Einzige, was in der Zeile hinzugefügt wird, ist das Datum.
Als Nächstes wirf einen Blick auf Fatal
. Du änderst nur eine Zeile des Codes, um Fatalln
zu verwenden:
func
main
()
{
str
:=
"abcdefghi"
num
,
err
:=
strconv
.
ParseInt
(
str
,
10
,
64
)
if
err
!=
nil
{
log
.
Fatalln
(
"Cannot parse string"
,
err
)
}
fmt
.
Println
(
"Number is"
,
num
)
}
Wenn du es ausführst, solltest du Folgendes sehen:
% go run main.go 2022/01/23 18:42:10 Cannot parse string strconv.ParseInt: parsing "abcdefghi": invalid syntax exit status 1
Beachte, dass die letzte Anweisung nicht ausgeführt wird und das Programm mit dem Exit-Code 1 endet. Der Exit-Code 1 ist ein Sammelbegriff für allgemeine Fehler, d.h. irgendetwas ist mit dem Programm schief gelaufen, und deshalb muss es beendet werden.
Schließlich kannst du Panic
verwenden. Wenn du die eingebaute Funktion panic
aufrufst, hält sie die aktuelle Goroutine an, führt den aufgeschobenen Code aus und kehrt zur aufrufenden Funktion zurück, wodurch eine weitere panic
ausgelöst wird, die schließlich zu main
aufsteigt und schließlich beendet wird. Mehr über die eingebaute Funktion panic
erfährst du in Rezept 3.7:
func
main
()
{
str
:=
"abcdefghi"
num
:=
conv
(
str
)
fmt
.
Println
(
"Number is"
,
num
)
}
func
conv
(
str
string
)
(
num
int64
)
{
defer
fmt
.
Println
(
"deferred code in function conv"
)
num
,
err
:=
strconv
.
ParseInt
(
str
,
10
,
64
)
if
err
!=
nil
{
log
.
Panicln
(
"Cannot parse string"
,
err
)
}
fmt
.
Println
(
"end of function conv"
)
return
}
Die Funktion Panicln
druckt in den Standard-Logger und gerät in Panik. Wenn du den Code ausführst, siehst du Folgendes:
% go run main.go 2022/01/23 18:48:20 Cannot parse string strconv.ParseInt: parsing "abcdefghi": invalid syntax deferred code in function conv panic: Cannot parse string strconv.ParseInt: parsing "abcdefghi": invalid syntax ... exit status 2
Der verzögerte Code in der Funktion conv
läuft, aber die letzte Anweisung im Programm nicht. Interessanterweise siehst du den Exit Code 2, was technisch nicht korrekt ist, denn traditionell bedeutet Exit Code 2 so etwas wie "falsche Argumente". Seit Go Version 1.17.6 ist dieser kleine Fehler immer noch in der Warteschlange und wartet darauf, behoben zu werden.
4.2 Ändern, was vom Standard-Logger aufgezeichnet wird
Diskussion
Das Standardverhalten des Standard-Loggers fügt die Felder für Datum und Uhrzeit zu jeder Zeile des Protokolls hinzu. Zum Beispiel mit dieser Codezeile:
log
.
Println
(
"Some event happened"
)
wirst du dies auf dem Bildschirm sehen:
2022/01/24 10:46:44 Some event happened
Mit dem Paket log
kannst du zusätzlich zu den Standardfeldern für Datum und Uhrzeit weitere Informationen hinzufügen. Du kannst diese Felder mit der Funktion SetFlag
hinzufügen. Zu den zur Verfügung stehenden Feldern gehören:
- Datum
-
Das Datum in der lokalen Zeitzone
- Zeit
- Mikrosekunden
- UTC
-
Verwende die Zeitzone UTC anstelle der lokalen Zeitzone, wenn Datums- oder Zeitfelder gesetzt sind
- Lange Datei
- Kurze Datei
- Position des Nachrichtenpräfixes
-
Verschiebe das Präfix (von
SetPrefix
) vom Anfang der Zeile vor den Anfang der Nachricht
Hier sind einige Beispiele. Du beginnst damit, dass du nur das Datumsfeld im Protokoll einstellst:
log
.
SetFlags
(
log
.
Ldate
)
log
.
Println
(
"Some event happened"
)
Das ergibt:
2022/01/24 Some event happened
Wenn du die Zeit mit Mikrosekundenangaben hinzufügen möchtest, tust du dies:
log
.
SetFlags
(
log
.
Ldate
|
log
.
Lmicroseconds
)
log
.
Println
(
"Some event happened"
)
Mit dem or
Operator für die Flags richtest du die verschiedenen Felder ein, die mit dem Log verwendet werden sollen. Hier ist das Ergebnis von vorher:
2022/01/24 20:43:54.595365 Some event happened
Die Dateifelder sind interessant, weil du mit ihnen anhand der Logs feststellen kannst, wo dieProbleme im Code liegen:
log
.
SetFlags
(
log
.
Ldate
|
log
.
Lshortfile
)
log
.
Println
(
"Some event happened"
)
Er gibt dir zusätzliche Informationen über den Dateinamen und die Zeile, in der dasProblem aufgetreten ist:
2022/01/24 20:51:02 logging.go:20: Some event happened
4.3 Protokollierung in Datei
Diskussion
Bisher hast du gelernt, wie man die Logs in den Standardfehler schreibt, vor allem auf den Bildschirm, wenn du sie in der Kommandozeile ausführst. Was ist, wenn du sie in eine Logdatei schreiben willst, was in den meisten Fällen der Fall ist?
Die Antwort ist ziemlich einfach. Du benutzt SetOutput
, um die Ausgabe in eine Datei umzuleiten.
Schau dir zuerst das Logfile an. Du willst eine neue Datei zum Erstellen oder Anhängen öffnen, und sie ist nur zum Schreiben gedacht:
file
,
err
:=
os
.
OpenFile
(
"app.log"
,
os
.
O_APPEND
|
os
.
O_CREATE
|
os
.
O_WRONLY
,
0644
)
if
err
!=
nil
{
log
.
Fatal
(
err
)
}
defer
file
.
Close
()
Du rufst SetOutput
mit der Datei als Parameter auf und fährst dann mit der Protokollierung fort:
log
.
SetOutput
(
file
)
log
.
Println
(
"Some event happened"
)
Die Ausgabe wird unter in die app.log-Datei statt auf den Bildschirm geschrieben.
Wie du wahrscheinlich aus dem Code entnommen hast, sollte das Einrichten der Logs, die in eine Datei geschrieben werden sollen, nur einmal gemacht werden. Was ist, wenn du gleichzeitig auf den Bildschirm und in die Datei schreiben willst? Du könntest die Protokollausgabe jedes Mal zurücksetzen (tue das nicht) oder vielleicht zwei Logger erstellen, einen für Standardfehler und einen für eine Datei, und die Logger dann in separatenAnweisungen aufrufen.
Oder du kannst Go's MultiWriter
Funktion im io
Paket verwenden, die einen Writer erstellt, der seine Schreibvorgänge auf alle bereitgestellten Writer dupliziert:
file
,
err
:=
os
.
OpenFile
(
"app.log"
,
os
.
O_APPEND
|
os
.
O_CREATE
|
os
.
O_WRONLY
,
0644
)
if
err
!=
nil
{
log
.
Fatal
(
err
)
}
defer
file
.
Close
()
writer
:=
io
.
MultiWriter
(
os
.
Stderr
,
file
)
log
.
SetOutput
(
writer
)
log
.
Println
(
"Some event happened"
)
log
.
Println
(
"Another event happened"
)
Auf diese Weise schreibst du gleichzeitig auf den Bildschirm und in die Datei. Du kannst sogar in mehr als zwei Writer schreiben!
4.4 Log-Ebenen verwenden
Diskussion
Logdaten sind in der Regel ziemlich groß. Mit Hilfe von Log-Ebenen kannst du sie überschaubarer machen und die Priorität der Ereignisse bestimmen. Die Log-Levels geben den Schweregrad eines Ereignisses an und damit seine Bedeutung. Es ist ein einfacher Mechanismus: Du schaust dir zuerst die Log-Levels mit dem höchsten Schweregrad an, filterst die Logs mit dem niedrigsten Schweregrad heraus und verringerst so die Ermüdung durch Alarme. Beispiele für Protokollstufen von hoch bis niedrig sind:
-
Tödlich
-
Fehler
-
Warnen
-
Info
-
Debuggen
Um Loglevel für deine Logs einzurichten, kannst du den Level zu jeder Zeile des Logs hinzufügen. Am einfachsten ist es, die Funktion SetPrefix
zu verwenden:
log
.
SetPrefix
(
"INFO "
)
log
.
Println
(
"Some event happened"
)
Wenn du die Funktion SetPrefix
mit dem Loglevel als Präfix aufrufst, setzt du den Loglevel an den Anfang der Zeile:
INFO 2022/01/26 00:48:15 Some event happened
Das Problem ist natürlich, dass du jedes Mal, wenn du eine Log-Zeile mit einer anderen Log-Ebene als die vorherige Zeile hinzufügen willst, SetPrefix
erneut aufrufen musst. Das ist keine praktikable Lösung.
Eine andere Methode besteht darin, neue Logger im Voraus zu erstellen, wobei jeder Logger eine einzelne Log-Ebene darstellt:
var
(
info
*
log
.
Logger
debug
*
log
.
Logger
)
func
init
()
{
info
=
log
.
New
(
os
.
Stderr
,
"INFO\t"
,
log
.
LstdFlags
)
debug
=
log
.
New
(
os
.
Stderr
,
"DEBUG\t"
,
log
.
LstdFlags
)
}
Dazu verwendest du die Funktion New
, die einen Logger zurückgibt, aber du kannst auch das Präfix und die Felder festlegen, die dem Log hinzugefügt werden sollen.
Um Ereignisse mit verschiedenen Protokollierungsstufen zu protokollieren, musst du nur die entsprechenden Logger verwenden:
info
.
Println
(
"Some informational event happened"
)
debug
.
Println
(
"Some debugging event happened"
)
Das wird auf dem Bildschirm angezeigt:
INFO 2022/01/26 00:53:03 Some informational event happened DEBUG 2022/01/26 00:53:03 Some debugging event happened
Du kannst auch die Protokollierung für bestimmte Log-Ebenen ausschalten, um die Größe der Logdatei zu verringern. Wenn du zum Beispiel ein Programm entwickelst, kannst du Debug-Ereignisse protokollieren, aber sobald du es in der Produktion einsetzt, möchtest du das nicht mehr tun. Eine gängige Praxis ist es, Umgebungsvariablen zu verwenden, um anzugeben, ob du in der Entwicklung oder in der Produktion arbeitest.
Umgebungsvariablen sind benannte Werte, die Teil der Umgebung sind, in der Programme laufen. Umgebungsvariablen werden in der Befehlszeile gesetzt, und Programme können während der Laufzeit auf diese Variablen zugreifen.
Für Unix-basierte Systeme wie Linux und macOS kannst du zum Beispiel eine Umgebungsvariable ENV
wie folgt setzen:
$ export ENV=development
Um die Umgebungsvariable in der Kommandozeile abzurufen, kannst du echo
verwenden und ein $
vor den Variablennamen setzen:
$ echo $ENV
Auch in Windows-Systemen kannst du die Umgebungsvariable auf diese Weise setzen:
$ set ENV=development
Um die Umgebungsvariable in Windows zu erhalten, kannst du echo
verwenden und ein %
vor und nach dem Variablennamen hinzufügen:
$ echo %ENV%
Das Abrufen der Umgebungsvariablen in Go ist einfach. Du kannst die Funktion os.Getenv
verwenden, der du den Namen der Umgebungsvariablen übergibst, und du erhältst den Wert der Umgebungsvariablen. Wie im vorherigen Beispiel setzen wir dieUmgebungsvariable ENV
auf production
:
$ export ENV=production
Wenn du diesen Code ausführst, wirst du sehen, dass das Debug-Ereignis nicht gedruckt wird:
info
.
Println
(
"Some informational event happened"
)
if
os
.
Getenv
(
"ENV"
)
!=
"production"
{
debug
.
Println
(
"Some debugging event happened"
)
}
Wenn du in die Entwicklungsumgebung wechselst, wird das Debug-Ereignis erneut gedruckt.
Auf hast du zu Beginn dieses Rezepts gelernt, dass einer der Gründe für die Verwendung von Log-Ebenen darin besteht, bestimmte Log-Ebenen zu priorisieren und herauszufiltern. Auf einem Unix-basierten System kannst du dies ganz einfach mit dem Befehl grep
tun.
Angenommen, du hast eine Datei namens logfile.log mit den folgenden Einträgen:
INFO 2023/01/06 00:21:32 Some informational event happened DEBUG 2023/01/06 00:21:32 Some debugging event happened INFO 2023/01/06 00:21:35 Another informational event happened WARN 2023/01/06 00:23:35 A warning event happened WARN 2023/01/06 00:33:11 Another warning event happened ERROR 2023/01/06 00:33:11 An error event happened
Du willst dir zuerst alle Fehlerereignisse ansehen, damit du mit grep
nur Fehlerereignisse herausfiltern kannst:
$ grep "^ERROR" logfile.log
Auf siehst du nur das Fehlerereignis. Das ^
vor grep
bedeutet, dass du nur die Zeilen sehen willst, die mit ERROR
beginnen:
ERROR 2023/01/06 00:33:11 An error event happened
Was, wenn du alle Log-Ereignisse außer den Debug-Ereignissen sehen willst? Du kannst die Debug-Ereignisse einfach mit dem v
Flag in grep
ausschließen:
$ grep -v "^DEBUG" logfile.log
Dies führt dazu, dass alle Ereignisse außer den Debug-Ereignissen angezeigt werden:
INFO 2023/01/06 00:21:32 Some informational event happened INFO 2023/01/06 00:21:35 Another informational event happened WARN 2023/01/06 00:23:35 A warning event happened WARN 2023/01/06 00:33:11 Another warning event happened ERROR 2023/01/06 00:33:11 An error event happened
Die Verwendung von grep
ist nur der Anfang. grep
ist ein mächtiges Werkzeug, aber es gibt noch viele andere Tools zur Loganalyse, die du verwenden kannst.
4.5 Protokollierung mit dem System Log Service
Diskussion
Syslog ist ein standardisiertes netzwerkbasiertes Logging-Protokoll. Es ist seit langem der De-facto-Standard für die Protokollierung von Systemereignissen und wurde in den 1980er Jahren von Eric Allman im Rahmen des Sendmail-Projekts entwickelt. Das Protokoll wurde in RFC 3164 von der Internet Engineering Task Force (IETF) dokumentiert. Später standardisierte die IETF es in RFC 5424.
Eine Syslog-Nachricht (wie in RFC 3164) besteht aus drei Teilen:
- Priorität
- Kopfzeile
-
Enthält den Zeitstempel und den Hostnamen oder die IP-Adresse des Rechners
- Nachricht
-
Beinhaltet den Tag und den Inhalt
Die Einrichtung beschreibt den Typ des Systems, das die Logmeldung sendet. Sie ermöglicht es, dass Log-Meldungen von verschiedenen Einrichtungen unterschiedlich behandelt werden. In den RFCs sind 24 Einrichtungen definiert; hier sind einige davon:
-
0 (Kernel)
-
1 (Benutzerebene)
-
2 (Post)
-
3 (System-Daemons)
-
4 (Sicherheits-/Autorisierungsmeldungen)
Der Schweregrad ist ähnlich wie der Log-Level. Syslog definiert acht verschiedene Stufen, wobei 0 die höchste und 7 die niedrigste ist:
-
0 (Notfall)
-
1 (Warnung)
-
2 (Kritisch)
-
3 (Fehler)
-
4 (Warnung)
-
5 (Hinweis)
-
6 (informativ)
-
7 (Debug)
Der Zeitstempel und der Hostname oder die IP-Adresse sind selbsterklärend. Der Tag ist der Name des Programms, das die Meldung erzeugt hat, während der Inhalt die Details der Logmeldung enthält.
Syslog ist nicht einheitlich in verschiedenen Betriebssystemen implementiert. Eine beliebte Implementierung von Syslog ist rsyslog, die Standard-Syslog-Implementierung in vielen Linux-Varianten wie Debian, Ubuntu, openSUSE und anderen.
Go bietet ein log/syslog
Paket als Teil der Standardbibliothek, um eine Schnittstelle zu Syslog zu schaffen. Allerdings funktioniert es nicht auf allen Systemen. Zum Beispiel funktioniert es nicht mit Windows, weil es nicht in Windows implementiert ist.
Das Beispiel in diesem Rezept basiert auf der Ausführung gegen rsyslog auf Ubuntu 20.04 und sollte auf Systemen mit rsyslog funktionieren. Ich habe es jedoch nicht auf allen Systemen und Implementierungen ausprobiert.
Bevor wir mit dem Go-Code beginnen, musst du rsyslog so einrichten, dass die Priorität, die Kopfzeile und die Nachrichtenteile angezeigt werden. In rsyslog wird dies mithilfe eines Templates in der rsyslog-Konfigurationsdatei erledigt.
Beginne mit der Bearbeitung der Konfigurationsdatei /etc/rsyslog.conf:
$ sudo vi /etc/rsyslog.conf
Füge die Templating-Konfiguration nach dieser Zeile $ActionFileDefaultTemplate RSYSLOG_TraditionalFileFormat
in die Konfigurationsdatei ein:
$template gosyslogs,"%syslogseverity-text% %syslogfacility-text% %hostname% %timegenerated% %syslogtag% %msg%\n" $ActionFileDefaultTemplate gosyslogs
In dieser Konfiguration nennst du die Vorlage gosyslogs
. Du stellst sie so ein, dass zuerst der Schweregrad angezeigt wird, gefolgt von der Einrichtung, dann der Hostname und der Zeitstempel und schließlich der Tag und die Nachricht.
Sobald du diese Datei gespeichert hast, starte rsyslog neu:
sudo service rsyslog restart
Jetzt, wo du rsyslog eingerichtet hast, kannst du dir den Code ansehen. Das Senden von Logmeldungen an syslog mit dem Paket syslog
ist relativ einfach:
var
logger
*
log
.
Logger
func
init
()
{
var
err
error
logger
,
err
=
syslog
.
NewLogger
(
syslog
.
LOG_USER
|
syslog
.
LOG_NOTICE
,
0
)
if
err
!=
nil
{
log
.
Fatal
(
"cannot write to syslog: "
,
err
)
}
}
func
main
()
{
logger
.
(
"hello syslog!"
)
}
Mit der Funktion NewLogger
erstellst du einen Logger und übergibst die Syslog-Prioritätsflags, die du setzen willst. Das Paket syslog
bietet Flags für den Facility und den Severity Level. Du kannst sie miteinander kombinieren, um den Facility Code und den Schweregrad zu senden. Im Fall des vorangegangenen Codes sendest du syslog.LOG_USER
, um den Facility Code des Benutzers anzugeben, und syslog.LOG_NOTICE
, um den Schweregrad der Meldung anzugeben.
Führe den Code zuerst in der Datei main.go aus:
$ go run main.go
Überprüfe nun die Syslogs. Führe dies in der Kommandozeile aus:
$ sudo tail /var/log/syslog
Du solltest eine Reihe von Logmeldungen sehen, aber irgendwo am Ende sollte etwas wie das hier stehen:
notice user myhostname Jan 26 15:30:08 /tmp/go-build2223050573/b001/exe/ main[163995]: hello syslog!
Das erste Feld ist notice
, der Schweregrad , gefolgt von user
, dem Code der Einrichtung. Das nächste Feld ist myhostname
, das ist der Hostname, gefolgt vom Zeitstempel.
Das nächste Feld ist das Tag, das Feld /tmp/go-build2223050573/b001/exe/main[163995]
in der Logmeldung. Warum wird angezeigt, dass es sich im Verzeichnis /tmp
befindet? Das liegt daran, dass du go run
verwendest. Wenn du den Code kompilierst und die Binärdatei ausführst, sieht es anders aus. Das letzte Feld in der Logmeldung sind die Details des Logs, die du mit logger
ausdruckst.
Get Go Kochbuch now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.