Organisationer som är beroende av distribuerade system skriver ofta sina program i Go för att kunna dra nytta av funktioner för samtidighet, till exempel kanaler och goroutiner (t.ex., Heroku, Basecamp, Cockroach Labs och Datadog). Om du är ansvarig för att bygga eller stödja Go-applikationer kan en väl genomtänkt loggningsstrategi hjälpa dig att förstå användarbeteende, lokalisera fel och övervaka prestandan hos dina applikationer.
Det här inlägget kommer att visa dig några verktyg och tekniker för att hantera Golang-loggar. Vi börjar med frågan om vilket loggningspaket som ska användas för olika typer av krav. Därefter förklarar vi några tekniker för att göra dina loggar mer sökbara och tillförlitliga, minska resursavtrycket från din loggningsinstallation och standardisera dina loggningsmeddelanden.
- Känn ditt loggningspaket
- Använd log för enkelhet
- Använd logrus för formaterade loggar
- Använd glog om du är orolig för volymen
- Bästa metoder för att skriva och lagra Golang-loggar
- Undvik att deklarera goroutiner för loggning
- Skriv dina loggar till en fil
- Implementera ett standardgränssnitt för loggning
- Centralisera Golang-loggar
- Spåra Golang-loggar över mikrotjänster
- Snygga och omfattande Golang-loggar
Känn ditt loggningspaket
Go ger dig en mängd alternativ när du väljer ett loggningspaket, och vi kommer att utforska flera av dessa nedan. Medan logrus är det mest populära av de bibliotek som vi tar upp och hjälper dig att implementera ett konsekvent loggningsformat, har de andra specialiserade användningsområden som är värda att nämna. Det här avsnittet kommer att granska biblioteken log, logrus och glog.
Använd log för enkelhet
Golangs inbyggda loggningsbibliotek, som kallas log
, levereras med en standardloggare som skriver till standardfel och lägger till en tidsstämpel utan att du behöver konfigurera. Du kan använda dessa grova och färdiga loggar för lokal utveckling, när det kan vara viktigare att få snabb återkoppling från koden än att generera rika, strukturerade loggar.
Du kan till exempel definiera en divisionsfunktion som returnerar ett fel till anroparen, i stället för att avsluta programmet, när du försöker dividera med noll.
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)}
För att vårt exempel dividerar med noll kommer det att ge ut följande loggmeddelande:
2019/01/31 11:48:00 can't divide by zero
Använd logrus för formaterade loggar
Vi rekommenderar att du skriver Golang-loggar med hjälp av logrus, ett loggningspaket som är utformat för strukturerad loggning och som är väl lämpat för loggning i JSON. JSON-formatet gör det möjligt för maskiner att enkelt analysera dina Golangloggar. Och eftersom JSON är en väldefinierad standard är det enkelt att lägga till kontext genom att inkludera nya fält – en analysator bör kunna plocka upp dem automatiskt.
Med logrus kan du definiera standardfält att lägga till i dina JSON-loggar med hjälp av funktionen WithFields
, som visas nedan. Du kan sedan göra anrop till loggningsfunktionen på olika nivåer, t.ex. Info()
, Warn()
och Error()
. Logrus-biblioteket skriver loggen som JSON automatiskt och infogar standardfälten tillsammans med eventuella fält som du har definierat i farten.
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")}
Den resulterande loggen kommer att innehålla meddelande, loggnivå, tidsstämpel och standardfält i ett 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"}
Använd glog om du är orolig för volymen
En del loggningsbibliotek gör det möjligt att aktivera eller inaktivera loggning på specifika nivåer, vilket är användbart för att hålla loggningsvolymen i schack när du går mellan utveckling och produktion. Ett sådant bibliotek är glog
, som låter dig använda flaggor på kommandoraden (t.ex. -v
för verbosity) för att ställa in loggningsnivån när du kör din kod. Du kan sedan använda en V()
-funktion i if
-angivelser för att skriva dina Golang-loggar endast på en viss loggningsnivå.
Du kan till exempel använda glog för att skriva samma ”Can’t divide by zero”-fel från tidigare, men endast om du loggar på verbosity-nivån 2
. Du kan ställa in verbosity till vilket signerat 32-bitars heltal som helst eller använda funktionerna Info()
, Warning()
, Error()
och Fatal()
för att tilldela verbosity-nivåerna 0
till 3
(respektive).
if err != nil && glog.V(2){ glog.Warning(err) }
Du kan göra din applikation mindre resurskrävande genom att logga endast vissa nivåer i produktionen. Samtidigt, om det inte finns någon påverkan på användarna, är det ofta en bra idé att logga så många interaktioner med din applikation som möjligt, och sedan använda logghanteringsprogram som Datadog för att hitta de data du behöver för din undersökning
Bästa metoder för att skriva och lagra Golang-loggar
När du väl har valt ett loggningsbibliotek vill du också planera för var i koden du ska göra anrop till loggningsfunktionen, hur du ska lagra dina loggar och hur du ska göra nytta av dem. I det här avsnittet rekommenderar vi en rad bästa metoder för att organisera dina Golang-loggar:
- Företag anrop till loggaren från din huvudprogramprocess, inte inom goroutiner.
- Skriv loggar från ditt program till en lokal fil, även om du skickar dem till en central plattform senare.
- Standardisera dina loggar med en uppsättning fördefinierade meddelanden.
- Sänd dina loggar till en central plattform så att du kan analysera och aggregera dem.
- Använd HTTP-huvuden och unika ID:n för att logga användarens beteende i olika mikrotjänster.
Undvik att deklarera goroutiner för loggning
Det finns två anledningar till att undvika att skapa egna goroutiner för att hantera skrivning av loggar. För det första kan det leda till samtidighetsproblem, eftersom dubbletter av loggaren skulle försöka få tillgång till samma io.Writer
. För det andra startar loggningsbibliotek vanligtvis goroutiner själva och hanterar eventuella samtidighetsproblem internt, och att starta egna goroutiner kommer bara att störa.
Skriv dina loggar till en fil
Även om du skickar dina loggar till en central plattform rekommenderar vi att du först skriver dem till en fil på din lokala maskin. Du vill försäkra dig om att dina loggar alltid är tillgängliga lokalt och inte går förlorade i nätverket. Att skriva till en fil innebär dessutom att du kan frikoppla uppgiften att skriva dina loggar från uppgiften att skicka dem till en central plattform. Dina program själva behöver inte upprätta anslutningar eller strömma dina loggar, och du kan överlåta dessa jobb till specialiserad programvara som Datadog Agent. Om du kör dina Go-applikationer i en containeriserad infrastruktur som inte redan innehåller beständig lagring – t.ex. containrar som körs på AWS Fargate – kanske du vill konfigurera ditt logghanteringsverktyg så att det samlar in loggar direkt från containrarnas STDOUT- och STDERR-strömmar (detta hanteras på olika sätt i Docker och Kubernetes).
Implementera ett standardgränssnitt för loggning
När de skriver anrop till loggningsenheter från sin kod använder team ofta olika attributnamn för att beskriva samma sak. Inkonsekventa attribut kan förvirra användarna och göra det omöjligt att korrelera loggar som borde ingå i samma bild. Till exempel kan två utvecklare logga samma fel, ett saknat klientnamn vid hantering av en uppladdning, på olika sätt.
Ett bra sätt att genomdriva standardisering är att skapa ett gränssnitt mellan din programkod och loggningsbiblioteket. Gränssnittet innehåller fördefinierade loggmeddelanden som implementerar ett visst format, vilket gör det lättare att undersöka problem genom att loggmeddelanden kan sökas, grupperas och filtreras.
I det här exemplet deklarerar vi en Event
-typ med ett fördefinierat meddelande. Sedan använder vi Event
-meddelanden för att göra anrop till en loggare. Lagkamrater kan skriva Golang-loggar genom att tillhandahålla en minimal mängd anpassad information och låta programmet göra arbetet med att implementera ett standardformat.
Först skriver vi ett logwrapper
-paket som utvecklare kan inkludera i sin kod.
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)}
För att använda vårt loggningsgränssnitt behöver vi bara inkludera det i vår kod och göra anrop till en instans av StandardLogger
.
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")}
När vi kör vår kod får vi följande JSON-logg:
{"level":"error","msg":"Invalid value for argument: client: nil","time":"2019-03-04T11:21:07-05:00"}
Centralisera Golang-loggar
Om din applikation distribueras över ett kluster av värdar är det inte hållbart att SSH:a in i varje värddator för att svansa, greppa och undersöka dina loggar. Ett mer skalbart alternativ är att skicka loggar från lokala filer till en central plattform.
En lösning är att använda Golang syslog-paketet för att vidarebefordra loggar från hela infrastrukturen till en enda syslog-server.
Anpassa och effektivisera din Golang-logghantering med Datadog.
En annan är att använda en logghanteringslösning. Datadog kan till exempel skräddarsy dina loggfiler och vidarebefordra loggar till en central plattform för bearbetning och analys.
Du kan använda attribut för att grafera värdena för vissa loggfält över tiden, sorterade efter grupp. Du kan till exempel spåra antalet fel per service
för att få veta om det finns en incident i en av dina tjänster. Genom att visa loggar från endast tjänsten go-logging-demo
kan vi se hur många felloggar den här tjänsten har producerat under ett visst intervall.
Du kan också använda attribut för att undersöka möjliga orsaker, till exempel för att se om en topp i antalet felloggar hör till en viss värd. Du kan sedan skapa en automatiserad varning baserat på värdena i dina loggar.
Spåra Golang-loggar över mikrotjänster
När du felsöker ett fel är det ofta till hjälp att se vilket beteendemönster som ledde till felet, även om beteendet involverar ett antal mikrotjänster. Du kan uppnå detta med distribuerad spårning, genom att visualisera i vilken ordning din applikation utför funktioner, databasfrågor och andra uppgifter, och följa dessa exekveringssteg när de tar sig fram genom ett nätverk. Ett sätt att implementera distribuerad spårning i dina loggar är att skicka kontextuell information som HTTP-huvuden.
I det här exemplet tar en mikrotjänst emot en begäran och kontrollerar om det finns ett spårnings-ID i x-trace
-huvudet och genererar ett om det inte finns. När vi gör en begäran till en annan mikrotjänst genererar vi sedan ett nytt spanID – för denna och för varje begäran – och lägger till det i huvudet x-span
.
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)}
Mikrotjänster i nedströmsledet använder x-span
-huvudet för inkommande begäranden för att specificera föräldrarna till de spanID:er som de genererar, och skickar den informationen som x-parent
-huvudet till nästa mikrotjänst i kedjan.
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))}
Om ett fel uppstår i en av våra mikrotjänster kan vi använda attributen trace
, parent
och span
för att se vilken väg en begäran har tagit, så att vi vet vilka värdar – och eventuellt vilka delar av programkoden – vi ska undersöka.
I den första mikrotjänsten:
{"appname":"go-logging","level":"debug","msg":"Hello from Microservice One","trace":"eUBrVfdw","time":"2017-03-02T15:29:26+01:00","span":"UzWHRihF"}
I den andra:
{"appname":"go-logging","level":"debug","msg":"Hello from Microservice Two","parent":"UzWHRihF","trace":"eUBrVfdw","time":"2017-03-02T15:29:26+01:00","span":"DPRHBMuE"}
Om du vill gräva djupare i Golangs spårningsmöjligheter kan du använda ett spårningsbibliotek som OpenTracing eller en övervakningsplattform med stöd för distribuerad spårning av Go-applikationer. Datadog kan till exempel automatiskt bygga en karta över tjänster med hjälp av data från sitt Golang-spårningsbibliotek, visualisera trender i dina spårningar över tid och informera dig om tjänster med ovanliga förfrågningsfrekvenser, felfrekvenser eller latenstider.
Snygga och omfattande Golang-loggar
I det här inlägget har vi lyft fram fördelarna och kompromisserna med flera Go-loggningsbibliotek. Vi har också rekommenderat sätt att se till att dina loggar är tillgängliga och åtkomliga när du behöver dem, och att informationen de innehåller är konsekvent och lätt att analysera.
För att börja analysera alla dina Go-loggar med Datadog kan du registrera dig för en gratis provperiod.