Wie man Golang Logs sammelt, standardisiert und zentralisiert

tip / logs /golang /microservices

Organisationen, die auf verteilte Systeme angewiesen sind, schreiben ihre Anwendungen oft in Go, um die Vorteile von Gleichzeitigkeitsfunktionen wie Channels und Goroutines zu nutzen (z.B., Heroku, Basecamp, Cockroach Labs und Datadog). Wenn Sie für die Erstellung oder den Support von Go-Anwendungen verantwortlich sind, kann Ihnen eine gut durchdachte Logging-Strategie dabei helfen, das Benutzerverhalten zu verstehen, Fehler zu lokalisieren und die Leistung Ihrer Anwendungen zu überwachen.

In diesem Beitrag werden wir Ihnen einige Tools und Techniken zur Verwaltung von Golang-Logs vorstellen. Wir beginnen mit der Frage, welches Logging-Paket für die verschiedenen Anforderungen zu verwenden ist. Als nächstes werden wir einige Techniken erklären, um Ihre Logs durchsuchbarer und zuverlässiger zu machen, den Ressourcenbedarf Ihrer Logging-Einrichtung zu reduzieren und Ihre Log-Meldungen zu standardisieren.

Kennen Sie Ihr Logging-Paket

Go gibt Ihnen eine Fülle von Optionen, wenn Sie ein Logging-Paket auswählen, und wir werden im Folgenden einige davon untersuchen. Während logrus die beliebteste der von uns behandelten Bibliotheken ist und Ihnen hilft, ein konsistentes Protokollierungsformat zu implementieren, haben die anderen spezielle Anwendungsfälle, die es wert sind, erwähnt zu werden. Dieser Abschnitt befasst sich mit den Bibliotheken log, logrus und glog.

Nutzen Sie log der Einfachheit halber

Golangs eingebaute Logging-Bibliothek, genannt log, kommt mit einem Standard-Logger, der in den Standardfehler schreibt und einen Zeitstempel hinzufügt, ohne dass eine Konfiguration erforderlich ist. Sie können diese groben Protokolle für die lokale Entwicklung verwenden, wenn es wichtiger ist, schnelles Feedback von Ihrem Code zu erhalten, als umfangreiche, strukturierte Protokolle zu erstellen.

Sie können zum Beispiel eine Divisionsfunktion definieren, die einen Fehler an den Aufrufer zurückgibt, anstatt das Programm zu beenden, wenn Sie versuchen, durch Null zu teilen.

package mainimport ( "log" "errors" "fmt" )func divide(a float32, b float32) (float32, error) { if b == 0 { return 0, errors.New("can't divide by zero") } return a / b, nil}func main() { var a float32 = 10 var b float32 ret, err := divide(a,b) if err != nil{ log.Print(err) } fmt.Println(ret)}

Weil unser Beispiel durch Null dividiert, wird es die folgende Logmeldung ausgeben:

2019/01/31 11:48:00 can't divide by zero

Verwenden Sie logrus für formatierte Logs

Wir empfehlen, Golang-Logs mit logrus zu schreiben, einem Logging-Paket, das für strukturiertes Logging entwickelt wurde und gut für das Logging in JSON geeignet ist. Das JSON-Format ermöglicht es Maschinen, Ihre Golang-Protokolle leicht zu analysieren. Und da JSON ein gut definierter Standard ist, macht es das Hinzufügen von Kontext durch Einfügen neuer Felder einfach – ein Parser sollte in der Lage sein, sie automatisch zu erkennen.

Mit logrus können Sie Standardfelder definieren, die Sie Ihren JSON-Protokollen hinzufügen, indem Sie die Funktion WithFields verwenden, wie unten gezeigt. Sie können dann den Logger auf verschiedenen Ebenen aufrufen, z. B. Info(), Warn() und Error(). Die logrus-Bibliothek schreibt das Protokoll automatisch als JSON und fügt die Standardfelder sowie die von Ihnen definierten Felder ein.

package mainimport ( log "github.com/sirupsen/logrus")func main() { log.SetFormatter(&log.JSONFormatter{}) standardFields := log.Fields{ "hostname": "staging-1", "appname": "foo-app", "session": "1ce3f6v", } log.WithFields(standardFields).WithFields(log.Fields{"string": "foo", "int": 1, "float": 1.1}).Info("My first ssl event from Golang")}

Das resultierende Protokoll enthält die Nachricht, den Log-Level, den Zeitstempel und die Standardfelder in einem JSON-Objekt:

{"appname":"foo-app","float":1.1,"hostname":"staging-1","int":1,"level":"info","msg":"My first ssl event from Golang","session":"1ce3f6v","string":"foo","time":"2019-03-06T13:37:12-05:00"}

Verwenden Sie glog, wenn Sie sich über den Umfang Gedanken machen

Einige Protokollierungsbibliotheken ermöglichen es Ihnen, die Protokollierung auf bestimmten Ebenen zu aktivieren oder zu deaktivieren, was nützlich ist, um den Umfang des Protokolls in Schach zu halten, wenn Sie zwischen Entwicklung und Produktion wechseln. Eine dieser Bibliotheken ist glog, die es Ihnen ermöglicht, Flags auf der Kommandozeile zu verwenden (z.B. -v für die Ausführlichkeit), um die Protokollierungsstufe zu setzen, wenn Sie Ihren Code ausführen. Sie können dann eine V()-Funktion in if-Anweisungen verwenden, um Ihre Golang-Protokolle nur auf einem bestimmten Log-Level zu schreiben.

Zum Beispiel können Sie glog verwenden, um den gleichen „Kann nicht durch Null teilen“-Fehler von vorher zu schreiben, aber nur, wenn Sie auf dem Verbosity-Level 2 protokollieren. Sie können die Ausführlichkeit auf jede vorzeichenbehaftete 32-Bit-Ganzzahl setzen oder die Funktionen Info(), Warning(), Error() und Fatal() verwenden, um die Ausführlichkeitsstufen 0 bis 3 zuzuweisen.

 if err != nil && glog.V(2){ glog.Warning(err) }

Sie können Ihre Anwendung weniger ressourcenintensiv machen, indem Sie in der Produktion nur bestimmte Stufen protokollieren. Gleichzeitig ist es, wenn es keine Auswirkungen auf die Benutzer gibt, oft eine gute Idee, so viele Interaktionen mit Ihrer Anwendung wie möglich zu protokollieren und dann eine Protokollverwaltungssoftware wie Datadog zu verwenden, um die Daten zu finden, die Sie für Ihre Untersuchung benötigen

Best Practices für das Schreiben und Speichern von Golang-Protokollen

Wenn Sie sich für eine Protokollierungsbibliothek entschieden haben, müssen Sie auch planen, an welcher Stelle in Ihrem Code Sie Aufrufe an den Logger machen, wie Sie Ihre Protokolle speichern und wie Sie sie sinnvoll nutzen können. In diesem Abschnitt empfehlen wir eine Reihe von Best Practices für die Organisation Ihrer Golang-Protokolle:

  • Aufrufe an den Logger aus Ihrem Hauptanwendungsprozess heraus, nicht innerhalb von Goroutines.
  • Schreiben Sie die Protokolle Ihrer Anwendung in eine lokale Datei, auch wenn Sie sie später an eine zentrale Plattform senden.
  • Standardisieren Sie Ihre Protokolle mit einer Reihe von vordefinierten Nachrichten.
  • Senden Sie Ihre Protokolle an eine zentrale Plattform, damit Sie sie analysieren und aggregieren können.
  • Verwenden Sie HTTP-Header und eindeutige IDs, um das Benutzerverhalten über Microservices hinweg zu protokollieren.

Vermeiden Sie die Deklaration von Goroutines für die Protokollierung

Es gibt zwei Gründe, die Erstellung eigener Goroutines für die Protokollierung zu vermeiden. Erstens kann es zu Gleichzeitigkeitsproblemen führen, da Duplikate des Loggers versuchen würden, auf denselben io.Writer zuzugreifen. Zweitens starten die Logging-Bibliotheken in der Regel selbst Goroutines, die alle Gleichzeitigkeitsprobleme intern verwalten, und das Starten eigener Goroutines würde nur stören.

Schreiben Sie Ihre Logs in eine Datei

Selbst wenn Sie Ihre Logs an eine zentrale Plattform senden, empfehlen wir, sie zunächst in eine Datei auf Ihrem lokalen Rechner zu schreiben. Sie möchten sicherstellen, dass Ihre Protokolle immer lokal verfügbar sind und nicht im Netzwerk verloren gehen. Außerdem bedeutet das Schreiben in eine Datei, dass Sie die Aufgabe, Ihre Protokolle zu schreiben, von der Aufgabe, sie an eine zentrale Plattform zu senden, entkoppeln können. Ihre Anwendungen selbst müssen keine Verbindungen aufbauen oder Ihre Protokolle streamen, und Sie können diese Aufgaben spezialisierter Software wie dem Datadog Agent überlassen. Wenn Sie Ihre Go-Anwendungen in einer containerisierten Infrastruktur ausführen, die noch keinen persistenten Speicher enthält – z. B. Container, die auf AWS Fargate laufen -, sollten Sie Ihr Protokollverwaltungstool so konfigurieren, dass es Protokolle direkt aus den STDOUT- und STDERR-Streams Ihrer Container sammelt (dies wird in Docker und Kubernetes anders gehandhabt).

Implementieren Sie eine Standard-Protokollierungsschnittstelle

Beim Schreiben von Aufrufen zu Protokollierern innerhalb ihres Codes verwenden Teams oft unterschiedliche Attributnamen, um dieselbe Sache zu beschreiben. Inkonsistente Attribute können Benutzer verwirren und machen es unmöglich, Protokolle, die Teil desselben Bildes sein sollten, zu korrelieren. Beispielsweise könnten zwei Entwickler denselben Fehler, einen fehlenden Client-Namen bei der Verarbeitung eines Uploads, auf unterschiedliche Weise protokollieren.

Golang protokolliert denselben Fehler mit unterschiedlichen Meldungen an verschiedenen Stellen.
Golang protokolliert denselben Fehler mit unterschiedlichen Meldungen an verschiedenen Stellen.

Eine gute Möglichkeit, eine Standardisierung zu erzwingen, besteht darin, eine Schnittstelle zwischen Ihrem Anwendungscode und der Protokollierungsbibliothek zu schaffen. Die Schnittstelle enthält vordefinierte Protokollnachrichten, die ein bestimmtes Format implementieren, was die Untersuchung von Problemen erleichtert, indem sichergestellt wird, dass Protokollnachrichten gesucht, gruppiert und gefiltert werden können.

Golang protokolliert einen Fehler unter Verwendung einer Standardschnittstelle, um eine konsistente Nachricht zu erstellen.
Golang protokolliert einen Fehler mit Hilfe einer Standardschnittstelle, um eine konsistente Nachricht zu erstellen.

In diesem Beispiel deklarieren wir einen Event Typ mit einer vordefinierten Nachricht. Dann werden wir Event Nachrichten verwenden, um einen Logger aufzurufen. Teammitglieder können Golang-Protokolle schreiben, indem sie eine minimale Menge an benutzerdefinierten Informationen bereitstellen und der Anwendung die Arbeit der Implementierung eines Standardformats überlassen.

Zuerst schreiben wir ein logwrapper-Paket, das Entwickler in ihren Code einbinden können.

package logwrapperimport ( "github.com/sirupsen/logrus")// Event stores messages to log later, from our standard interfacetype Event struct { id int message string}// StandardLogger enforces specific log message formatstype StandardLogger struct { *logrus.Logger}// NewLogger initializes the standard loggerfunc NewLogger() *StandardLogger {var baseLogger = logrus.New()var standardLogger = &StandardLogger{baseLogger}standardLogger.Formatter = &logrus.JSONFormatter{}return standardLogger}// Declare variables to store log messages as new Eventsvar ( invalidArgMessage = Event{1, "Invalid arg: %s"} invalidArgValueMessage = Event{2, "Invalid value for argument: %s: %v"} missingArgMessage = Event{3, "Missing arg: %s"})// InvalidArg is a standard error messagefunc (l *StandardLogger) InvalidArg(argumentName string) { l.Errorf(invalidArgMessage.message, argumentName)}// InvalidArgValue is a standard error messagefunc (l *StandardLogger) InvalidArgValue(argumentName string, argumentValue string) { l.Errorf(invalidArgValueMessage.message, argumentName, argumentValue)}// MissingArg is a standard error messagefunc (l *StandardLogger) MissingArg(argumentName string) { l.Errorf(missingArgMessage.message, argumentName)}

Um unsere Protokollierungsschnittstelle zu verwenden, müssen wir sie nur in unseren Code einbinden und eine Instanz von StandardLogger aufrufen.

package mainimport ( li "<PATH_TO_PACKAGE>/logwrapper")func main() { var standardLogger = := li.NewLogger() // You can then call a method of our standard logger in the context of an error // you would like to log. standardLogger.InvalidArgValue("client", "nil")}

Wenn wir unseren Code ausführen, erhalten wir das folgende JSON-Protokoll:

{"level":"error","msg":"Invalid value for argument: client: nil","time":"2019-03-04T11:21:07-05:00"}

Golang-Protokolle zentralisieren

Wenn Ihre Anwendung über einen Cluster von Hosts verteilt ist, ist es nicht nachhaltig, sich per SSH in jeden einzelnen einzuloggen, um Ihre Protokolle zu verfolgen, zu durchsuchen und zu untersuchen. Eine skalierbarere Alternative ist die Weiterleitung von Protokollen aus lokalen Dateien an eine zentrale Plattform.

Eine Lösung ist die Verwendung des Golang syslog-Pakets, um Protokolle aus der gesamten Infrastruktur an einen einzigen Syslog-Server weiterzuleiten.

Anpassen und optimieren Sie Ihr Golang-Protokollmanagement mit Datadog.

Eine andere Möglichkeit ist die Verwendung einer Protokollmanagementlösung. Datadog zum Beispiel kann Ihre Log-Dateien verfolgen und Logs an eine zentrale Plattform zur Verarbeitung und Analyse weiterleiten.

Die Datadog Log Explorer-Ansicht kann Golang-Logs aus verschiedenen Quellen anzeigen.

Sie können Attribute verwenden, um die Werte bestimmter Log-Felder im Laufe der Zeit grafisch darzustellen, sortiert nach Gruppen. Sie können zum Beispiel die Anzahl der Fehler nach service verfolgen, um zu erfahren, ob es einen Vorfall in einem Ihrer Dienste gibt. Wenn man nur die Protokolle des Dienstes go-logging-demo anzeigt, kann man sehen, wie viele Fehlerprotokolle dieser Dienst in einem bestimmten Intervall erzeugt hat.

Gruppierung der Golang-Protokolle nach Status.

Sie können auch Attribute verwenden, um mögliche Ursachen aufzuschlüsseln, z. B. um zu sehen, ob eine Spitze der Fehlerprotokolle zu einem bestimmten Host gehört. Sie können dann eine automatisierte Warnung basierend auf den Werten Ihrer Protokolle erstellen.

Verfolgen Sie Golang-Protokolle über Microservices hinweg

Bei der Fehlersuche ist es oft hilfreich zu sehen, welches Verhaltensmuster zu dem Fehler geführt hat, selbst wenn dieses Verhalten eine Reihe von Microservices betrifft. Sie können dies mit verteiltem Tracing erreichen, indem Sie die Reihenfolge visualisieren, in der Ihre Anwendung Funktionen, Datenbankabfragen und andere Aufgaben ausführt, und diese Ausführungsschritte auf ihrem Weg durch ein Netzwerk verfolgen. Eine Möglichkeit, verteiltes Tracing in Ihren Protokollen zu implementieren, besteht darin, Kontextinformationen als HTTP-Header zu übermitteln.

In diesem Beispiel empfängt ein Microservice eine Anfrage und prüft, ob eine Trace-ID im x-trace-Header vorhanden ist, und erzeugt eine, wenn diese nicht existiert. Bei einer Anfrage an einen anderen Microservice wird dann eine neue spanID – für diese und für jede Anfrage – generiert und dem Header x-span hinzugefügt.

func microService1(w http.ResponseWriter, r *http.Request) { client := &http.Client{} trace := r.Header.Get("x-trace") if ( trace == "") { trace = generateTraceId() } span := generateSpanId() // Hit the second microservice with the appropriate headers reqService2, _ := http.NewRequest("GET", "<ADDRESS>", nil) reqService2.Header.Add("x-trace", trace) reqService2.Header.Add("x-span", span) resService2, _ := client.Do(reqService2)}

Die nachgelagerten Microservices verwenden die x-span-Header eingehender Anfragen, um die Eltern der von ihnen generierten Spans anzugeben, und senden diese Informationen als x-parent-Header an den nächsten Microservice in der Kette.

func microService2(w http.ResponseWriter, r *http.Request) { trace := r.Header.Get("x-trace") span := generateSpanId() parent := r.Header.Get("x-span") if (trace == "") { w.Header().Set("x-parent", parent) } w.Header().Set("x-trace", trace) w.Header().Set("x-span", span) if (parent == "") { w.Header().Set("x-parent", span) } w.WriteHeader(http.StatusOK) io.WriteString(w, fmt.Sprintf(aResponseMessage, 2, trace, span, parent))}

Wenn in einem unserer Microservices ein Fehler auftritt, können wir die Attribute trace, parent und span verwenden, um den Weg zu sehen, den eine Anfrage genommen hat, und erfahren so, welche Hosts – und möglicherweise welche Teile des Anwendungscodes – zu untersuchen sind.

Im ersten Microservice:

{"appname":"go-logging","level":"debug","msg":"Hello from Microservice One","trace":"eUBrVfdw","time":"2017-03-02T15:29:26+01:00","span":"UzWHRihF"}

Im zweiten:

{"appname":"go-logging","level":"debug","msg":"Hello from Microservice Two","parent":"UzWHRihF","trace":"eUBrVfdw","time":"2017-03-02T15:29:26+01:00","span":"DPRHBMuE"}

Wenn Sie tiefer in die Golang-Tracing-Möglichkeiten einsteigen möchten, können Sie eine Tracing-Bibliothek wie OpenTracing oder eine Monitoring-Plattform verwenden, die verteiltes Tracing für Go-Anwendungen unterstützt. Datadog kann zum Beispiel automatisch eine Karte von Diensten erstellen, indem es Daten aus seiner Golang-Tracing-Bibliothek verwendet; Trends in Ihren Traces im Laufe der Zeit visualisieren; und Sie über Dienste mit ungewöhnlichen Anforderungsraten, Fehlerraten oder Latenzzeiten informieren.

Ein Beispiel für eine Visualisierung, die Traces von Anforderungen zwischen Microservices zeigt.
Ein Beispiel für eine Visualisierung, die Spuren von Anfragen zwischen Microservices zeigt.

Saubere und umfassende Golang-Logs

In diesem Beitrag haben wir die Vorteile und Nachteile verschiedener Go-Logging-Bibliotheken aufgezeigt. Wir haben auch empfohlen, wie Sie sicherstellen können, dass Ihre Logs verfügbar und zugänglich sind, wenn Sie sie brauchen, und dass die darin enthaltenen Informationen konsistent und leicht zu analysieren sind.

Um mit der Analyse aller Ihrer Go-Logs mit Datadog zu beginnen, melden Sie sich für eine kostenlose Testversion an.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.