Organisaties die afhankelijk zijn van gedistribueerde systemen schrijven hun applicaties vaak in Go om voordeel te halen uit concurrency functies zoals kanalen en goroutines (bijv, Heroku, Basecamp, Cockroach Labs, en Datadog). Als je verantwoordelijk bent voor het bouwen of ondersteunen van Go applicaties, kan een goed doordachte logging strategie je helpen om gebruikersgedrag te begrijpen, fouten te lokaliseren, en de prestaties van je applicaties te monitoren.
Deze post zal je een aantal tools en technieken laten zien voor het beheren van Golang logs. We zullen beginnen met de vraag welk logging pakket te gebruiken voor verschillende soorten eisen. Vervolgens zullen we enkele technieken uitleggen om je logs beter doorzoekbaar en betrouwbaarder te maken, de resource footprint van je logging setup te verkleinen, en je log berichten te standaardiseren.
- Ken je logging pakket
- Gebruik log voor eenvoud
- Gebruik logrus voor geformatteerde logs
- Gebruik glog als u zich zorgen maakt over het volume
- Best practices voor het schrijven en opslaan van Golang logs
- Vermijd het declareren van goroutines voor logging
- Schrijf uw logs naar een bestand
- Een standaard loginterface implementeren
- Logs van Golang centraliseren
- Track Golang logs across microservices
- Schone en uitgebreide Golang-logboeken
Ken je logging pakket
Go geeft je een schat aan opties bij het kiezen van een logging pakket, en we zullen er een aantal van hieronder verkennen. Hoewel logrus de meest populaire van de bibliotheken is die we behandelen, en je helpt om een consistent logging formaat te implementeren, hebben de anderen gespecialiseerde gebruikssituaties die het vermelden waard zijn. Deze sectie zal een overzicht geven van de bibliotheken log, logrus, en glog.
Gebruik log voor eenvoud
Golang’s ingebouwde log bibliotheek, log
genaamd, wordt geleverd met een standaard logger die naar standaard error schrijft en een timestamp toevoegt zonder dat configuratie nodig is. U kunt deze ruwe en kant-en-klare logs gebruiken voor lokale ontwikkeling, wanneer het krijgen van snelle feedback van uw code belangrijker kan zijn dan het genereren van rijke, gestructureerde logs.
U kunt bijvoorbeeld een delingsfunctie definiëren die een fout teruggeeft aan de aanroeper, in plaats van het programma te verlaten, wanneer u probeert te delen door nul.
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)}
Omdat ons voorbeeld door nul deelt, zal het de volgende logboodschap uitvoeren:
2019/01/31 11:48:00 can't divide by zero
Gebruik logrus voor geformatteerde logs
We raden aan om Golang logs te schrijven met logrus, een logging pakket ontworpen voor gestructureerde logging dat zeer geschikt is voor het loggen in JSON. Het JSON formaat maakt het mogelijk voor machines om gemakkelijk je Golang logs te parsen. En omdat JSON een goed gedefinieerde standaard is, is het eenvoudig om context toe te voegen door nieuwe velden op te nemen – een parser zou in staat moeten zijn om ze automatisch op te pikken.
Tijdens het gebruik van logrus, kun je standaard velden definiëren om toe te voegen aan je JSON logs door gebruik te maken van de functie WithFields
, zoals hieronder te zien is. U kunt dan aanroepen doen naar de logger op verschillende niveaus, zoals Info()
, Warn()
en Error()
. De logrus bibliotheek zal het log automatisch als JSON schrijven en de standaard velden invoegen, samen met de velden die je onderweg hebt gedefinieerd.
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")}
Het resulterende logboek bevat het bericht, het logniveau, de tijdstempel en de standaardvelden in een JSON-object:
{"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"}
Gebruik glog als u zich zorgen maakt over het volume
Enkele logboekbibliotheken stellen u in staat om het loggen op specifieke niveaus in of uit te schakelen, wat handig is om het logboekvolume onder controle te houden wanneer u van ontwikkeling naar productie gaat. Een van die bibliotheken is glog
, waarmee u op de commandoregel vlaggen kunt gebruiken (bijv. -v
voor verbosity) om het logging-niveau in te stellen wanneer u uw code uitvoert. Je kunt dan een V()
functie in if
statements gebruiken om je Golang logs alleen op een bepaald log niveau te schrijven.
Voorbeeld, je kunt glog gebruiken om dezelfde “Can’t divide by zero” fout van eerder te schrijven, maar alleen als je logt op het verbosity niveau van 2
. U kunt de verbosity instellen op elk ondertekend 32-bit geheel getal, of de functies Info()
, Warning()
, Error()
, en Fatal()
gebruiken om verbosity-niveaus 0
tot en met 3
(respectievelijk) toe te wijzen.
if err != nil && glog.V(2){ glog.Warning(err) }
U kunt uw applicatie minder resource-intensief maken door alleen bepaalde niveaus in productie te loggen. Tegelijkertijd, als er geen impact is op gebruikers, is het vaak een goed idee om zoveel mogelijk interacties met je applicatie te loggen, en dan log management software zoals Datadog te gebruiken om de data te vinden die je nodig hebt voor je onderzoek
Best practices voor het schrijven en opslaan van Golang logs
Als je eenmaal een logging library hebt gekozen, wil je ook plannen waar in je code je aanroepen naar de logger moet maken, hoe je je logs moet opslaan, en hoe je ze moet interpreteren. In deze sectie zullen we een aantal best practices aanbevelen voor het organiseren van je Golang logs:
- Roep aanroepen naar de logger vanuit je hoofd applicatie proces, niet binnen goroutines.
- Schrijf logs van je applicatie naar een lokaal bestand, zelfs als je ze later naar een centraal platform stuurt.
- Standaardiseer je logs met een set van voorgedefinieerde berichten.
- Zend uw logs naar een centraal platform, zodat u ze kunt analyseren en aggregeren.
- Gebruik HTTP-headers en unieke ID’s om gebruikersgedrag over microservices te loggen.
Vermijd het declareren van goroutines voor logging
Er zijn twee redenen om te vermijden dat u uw eigen goroutines maakt om het schrijven van logs af te handelen. Ten eerste kan het leiden tot concurrency problemen, omdat duplicaten van de logger zouden proberen om toegang te krijgen tot dezelfde io.Writer
. Ten tweede, logging bibliotheken starten meestal zelf goroutines, die intern eventuele concurrency problemen oplossen, en het starten van uw eigen goroutines zal alleen maar storen.
Schrijf uw logs naar een bestand
Zelfs als u uw logs naar een centraal platform stuurt, raden we aan om ze eerst naar een bestand op uw lokale machine te schrijven. U wilt er zeker van zijn dat uw logs altijd lokaal beschikbaar zijn en niet verloren gaan in het netwerk. Bovendien betekent het schrijven naar een bestand dat je de taak van het schrijven van je logs kunt loskoppelen van de taak om ze naar een centraal platform te sturen. Je applicaties zelf hoeven geen verbindingen tot stand te brengen of je logs te streamen, en je kunt deze taken overlaten aan gespecialiseerde software zoals de Datadog Agent. Als u uw Go-toepassingen binnen een containerinfrastructuur uitvoert die nog geen persistente opslag bevat – bijvoorbeeld containers die op AWS Fargate draaien – kunt u uw logboekbeheerprogramma configureren om logboeken rechtstreeks van de STDOUT- en STDERR-streams van uw containers te verzamelen (dit wordt anders afgehandeld in Docker en Kubernetes).
Een standaard loginterface implementeren
Wanneer teams vanuit hun code logboekaanroepen schrijven, gebruiken ze vaak verschillende attribuutnamen om hetzelfde te beschrijven. Inconsistente attributen kunnen gebruikers verwarren en maken het onmogelijk om logs te correleren die deel zouden moeten uitmaken van hetzelfde plaatje. Twee ontwikkelaars kunnen bijvoorbeeld dezelfde fout, een ontbrekende clientnaam bij het afhandelen van een upload, op verschillende manieren loggen.
Een goede manier om standaardisatie af te dwingen is het maken van een interface tussen uw applicatiecode en de logboekbibliotheek. De interface bevat vooraf gedefinieerde logberichten die een bepaalde indeling implementeren, waardoor het eenvoudiger wordt om problemen te onderzoeken door ervoor te zorgen dat logberichten kunnen worden doorzocht, gegroepeerd en gefilterd.
In dit voorbeeld declareren we een Event
-type met een vooraf gedefinieerde melding. Dan zullen we Event
berichten gebruiken om aanroepen naar een logger te maken. Teamgenoten kunnen Golang logs schrijven door een minimale hoeveelheid aangepaste informatie te verstrekken, en de applicatie het werk te laten doen van het implementeren van een standaard formaat.
Eerst zullen we een logwrapper
pakket schrijven dat ontwikkelaars in hun code kunnen opnemen.
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)}
Om onze logging interface te gebruiken, hoeven we het alleen maar in onze code op te nemen en aanroepen te doen naar een instantie van 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")}
Als we onze code uitvoeren, krijgen we het volgende JSON-log:
{"level":"error","msg":"Invalid value for argument: client: nil","time":"2019-03-04T11:21:07-05:00"}
Logs van Golang centraliseren
Als je applicatie wordt ingezet op een cluster van hosts, is het niet duurzaam om op elke host te SSH-en om je logs te bekijken, te grep-en en te onderzoeken. Een meer schaalbaar alternatief is om logs van lokale bestanden door te sturen naar een centraal platform.
Een oplossing is om het Golang syslog package te gebruiken om logs van je hele infrastructuur door te sturen naar een enkele syslog server.
Het beheer van Golang logs aanpassen en stroomlijnen met Datadog.
Een andere oplossing is om een log management oplossing te gebruiken. Datadog kan bijvoorbeeld uw logbestanden volgen en logs doorsturen naar een centraal platform voor verwerking en analyse.
U kunt attributen gebruiken om de waarden van bepaalde logvelden in de loop van de tijd in grafieken weer te geven, gesorteerd op groep. U zou bijvoorbeeld het aantal fouten per service
kunnen bijhouden om u te laten weten of er een incident is in een van uw services. Door de logs van alleen de go-logging-demo
-service weer te geven, kunnen we zien hoeveel foutlogs deze service in een bepaald interval heeft geproduceerd.
U kunt ook attributen gebruiken om dieper in te gaan op mogelijke oorzaken, bijvoorbeeld om te zien of een piek in foutlogs aan een specifieke host toebehoort. Je kunt dan een geautomatiseerde waarschuwing maken op basis van de waarden van je logs.
Track Golang logs across microservices
Bij het troubleshooten van een fout, is het vaak handig om te zien welk gedragspatroon tot de fout heeft geleid, zelfs als dat gedrag een aantal microservices betreft. U kunt dit bereiken met gedistribueerde tracering, het visualiseren van de volgorde waarin uw applicatie functies, database queries en andere taken uitvoert, en het volgen van deze uitvoeringsstappen terwijl ze hun weg door een netwerk maken. Een manier om gedistribueerde tracering in je logs te implementeren is door contextuele informatie door te geven als HTTP headers.
In dit voorbeeld ontvangt een microservice een verzoek en controleert op een trace ID in de x-trace
header, en genereert er een als die niet bestaat. Bij het doen van een verzoek aan een andere microservice genereren we vervolgens een nieuw spanID-voor dit en voor elk verzoek- en voegen dat toe aan de header 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)}
Downstream microservices gebruiken de x-span
headers van inkomende verzoeken om de ouders te specificeren van de spans die ze genereren, en sturen die informatie als de x-parent
header naar de volgende microservice in de keten.
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))}
Als er een fout optreedt in een van onze microservices, kunnen we de trace
, parent
, en span
attributen gebruiken om de route te zien die een verzoek heeft genomen, zodat we weten welke hosts – en mogelijk welke delen van de applicatiecode – we moeten onderzoeken.
In de eerste microservice:
{"appname":"go-logging","level":"debug","msg":"Hello from Microservice One","trace":"eUBrVfdw","time":"2017-03-02T15:29:26+01:00","span":"UzWHRihF"}
In de tweede:
{"appname":"go-logging","level":"debug","msg":"Hello from Microservice Two","parent":"UzWHRihF","trace":"eUBrVfdw","time":"2017-03-02T15:29:26+01:00","span":"DPRHBMuE"}
Als je dieper wilt graven in de traceermogelijkheden van Golang, kun je een tracing-bibliotheek gebruiken zoals OpenTracing of een monitoringplatform dat gedistribueerde tracing voor Go-toepassingen ondersteunt. Datadog kan bijvoorbeeld automatisch een kaart van services maken met gegevens uit zijn Golang-tracingbibliotheek, trends in uw traces in de loop van de tijd visualiseren en u op de hoogte stellen van services met ongebruikelijke request rates, error rates of latency.
Schone en uitgebreide Golang-logboeken
In dit bericht hebben we de voordelen en nadelen van verschillende Go-logboekbibliotheken belicht. We hebben ook manieren aanbevolen om ervoor te zorgen dat uw logboeken beschikbaar en toegankelijk zijn wanneer u ze nodig hebt, en dat de informatie die ze bevatten consistent en eenvoudig te analyseren is.
Om te beginnen met het analyseren van al uw Go-logboeken met Datadog, kunt u zich aanmelden voor een gratis proefversie.