Sådan indsamler, standardiserer og centraliserer du Golang-logfiler

tip / logs /golang /microservices

Organisationer, der er afhængige af distribuerede systemer, skriver ofte deres applikationer i Go for at drage fordel af samtidighedsfunktioner som kanaler og goroutiner (f.eks, Heroku, Basecamp, Cockroach Labs og Datadog). Hvis du er ansvarlig for at bygge eller understøtte Go-applikationer, kan en velovervejet logningsstrategi hjælpe dig med at forstå brugeradfærd, lokalisere fejl og overvåge dine applikationers ydeevne.

Dette indlæg vil vise dig nogle værktøjer og teknikker til administration af Golang-logs. Vi begynder med spørgsmålet om, hvilken logføringspakke der skal bruges til forskellige typer krav. Derefter forklarer vi nogle teknikker til at gøre dine logfiler mere søgbare og pålidelige, reducere ressourceaftrykket fra din logningsopsætning og standardisere dine logmeddelelser.

Kend din logningspakke

Go giver dig et væld af muligheder, når du vælger en logningspakke, og vi vil undersøge flere af disse nedenfor. Mens logrus er det mest populære af de biblioteker, vi dækker, og hjælper dig med at implementere et konsistent logningsformat, har de andre specialiserede anvendelsesmuligheder, som er værd at nævne. Dette afsnit vil gennemgå bibliotekerne log, logrus og glog.

Brug log for enkelhed

Golangs indbyggede logningsbibliotek, kaldet log, leveres med en standardlogger, der skriver til standardfejl og tilføjer et tidsstempel uden behov for konfiguration. Du kan bruge disse grove logfiler til lokal udvikling, når det kan være vigtigere at få hurtig feedback fra din kode end at generere rige, strukturerede logfiler.

For eksempel kan du definere en divisionsfunktion, der returnerer en fejl til den, der kalder den, i stedet for at afslutte programmet, når du forsøger at dividere med 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)}

Da vores eksempel dividerer med nul, vil det udsende følgende logmeddelelse:

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

Brug logrus til formaterede logfiler

Vi anbefaler at skrive Golang-logfiler ved hjælp af logrus, en logføringspakke, der er designet til struktureret logføring, og som er velegnet til logføring i JSON. JSON-formatet gør det muligt for maskiner at analysere dine Golang-logs nemt. Og da JSON er en veldefineret standard, gør det det nemt at tilføje kontekst ved at inkludere nye felter – en parser burde kunne opfange dem automatisk.

Med logrus kan du definere standardfelter, der skal tilføjes til dine JSON-logs, ved hjælp af funktionen WithFields, som vist nedenfor. Du kan derefter foretage kald til loggeren på forskellige niveauer, f.eks. Info(), Warn() og Error(). Logrus-biblioteket vil skrive loggen som JSON automatisk og indsætte standardfelterne sammen med eventuelle felter, du har defineret undervejs.

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 resulterende log vil indeholde besked, logniveau, tidsstempel og standardfelter i et 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"}

Brug glog, hvis du er bekymret for volumen

I nogle logningsbiblioteker kan du aktivere eller deaktivere logning på bestemte niveauer, hvilket er nyttigt for at holde logvolumen i skak, når du flytter mellem udvikling og produktion. Et sådant bibliotek er glog, som giver dig mulighed for at bruge flag på kommandolinjen (f.eks. -v for verbosity) til at indstille logningsniveauet, når du kører din kode. Du kan derefter bruge en V()-funktion i if-angivelser til kun at skrive dine Golang-logfiler på et bestemt logniveau.

For eksempel kan du bruge glog til at skrive den samme “Can’t divide by zero”-fejl fra tidligere, men kun hvis du logger på verbosity-niveauet 2. Du kan indstille verbosity til et vilkårligt signeret 32-bit heltal eller bruge funktionerne Info(), Warning(), Error() og Fatal() til at tildele verbosity-niveauerne 0 til 3 (henholdsvis).

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

Du kan gøre dit program mindre ressourcekrævende ved kun at logge visse niveauer i produktionen. Samtidig er det, hvis der ikke er nogen indvirkning på brugerne, ofte en god idé at logge så mange interaktioner med din applikation som muligt og derefter bruge loghåndteringssoftware som Datadog til at finde de data, du har brug for til din undersøgelse

Bedste praksis for skrivning og lagring af Golang-logs

Når du har valgt et logningsbibliotek, skal du også planlægge, hvor i din kode du skal foretage kald til loggeren, hvordan du skal gemme dine logs, og hvordan du skal give mening til dem. I dette afsnit anbefaler vi en række bedste praksis for organisering af dine Golang-logfiler:

  • Før kald til loggeren fra din hovedprogramproces, ikke i goroutiner.
  • Skriv logfiler fra dit program til en lokal fil, selv om du senere vil sende dem til en central platform.
  • Standardiser dine logfiler med et sæt foruddefinerede meddelelser.
  • Send dine logfiler til en central platform, så du kan analysere og aggregere dem.
  • Brug HTTP-headers og unikke ID’er til at logge brugeradfærd på tværs af microservices.

Undgå at deklarere goroutiner til logning

Der er to grunde til at undgå at oprette dine egne goroutiner til at håndtere skrivning af logfiler. For det første kan det føre til problemer med samtidighed, da dubletter af loggeren vil forsøge at få adgang til den samme io.Writer. For det andet starter logningsbiblioteker normalt selv goroutiner og håndterer eventuelle samtidighedsproblemer internt, og hvis du starter dine egne goroutiner, vil det kun forstyrre.

Skriv dine logs til en fil

Selv hvis du sender dine logs til en central platform, anbefaler vi, at du først skriver dem til en fil på din lokale maskine. Du vil gerne sikre dig, at dine logfiler altid er tilgængelige lokalt og ikke går tabt i netværket. Desuden betyder det at skrive til en fil, at du kan afkoble opgaven med at skrive dine logfiler fra opgaven med at sende dem til en central platform. Dine programmer behøver ikke selv at etablere forbindelser eller streame dine logs, og du kan overlade disse opgaver til specialiseret software som Datadog Agent. Hvis du kører dine Go-programmer i en containeriseret infrastruktur, der ikke allerede omfatter vedvarende lagring – f.eks. containere, der kører på AWS Fargate – kan du konfigurere dit loghåndteringsværktøj til at indsamle logs direkte fra dine containeres STDOUT- og STDERR-strømme (dette håndteres anderledes i Docker og Kubernetes).

Implementer en standard logningsgrænseflade

Når de skriver kald til loggere fra deres kode, bruger teams ofte forskellige attributnavne til at beskrive den samme ting. Inkonsistente attributter kan forvirre brugerne og gøre det umuligt at korrelere logs, der burde være en del af det samme billede. To udviklere kan f.eks. logge den samme fejl, et manglende klientnavn ved håndtering af en upload, på forskellige måder.

Golang logger for den samme fejl med forskellige meddelelser fra forskellige steder.
Golang logger for den samme fejl med forskellige meddelelser fra forskellige steder.

En god måde at gennemtvinge standardisering er at oprette en grænseflade mellem din programkode og logningsbiblioteket. Grænsefladen indeholder foruddefinerede logmeddelelser, der implementerer et bestemt format, hvilket gør det lettere at undersøge problemer ved at sikre, at logmeddelelser kan søges, grupperes og filtreres.

Golang logger for en fejl ved hjælp af en standardgrænseflade for at skabe en ensartet meddelelse.
Golang logger for en fejl ved hjælp af en standardgrænseflade for at oprette en konsistent meddelelse.

I dette eksempel vil vi deklarere en Event type med en foruddefineret meddelelse. Derefter bruger vi Event-meddelelser til at foretage kald til en logger. Holdkammerater kan skrive Golang-logfiler ved at angive et minimum af brugerdefinerede oplysninger og lade programmet gøre arbejdet med at implementere et standardformat.

Først skriver vi en logwrapper-pakke, som udviklere kan inkludere i deres kode.

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)}

For at bruge vores logningsgrænseflade behøver vi kun at inkludere den i vores kode og foretage kald til en instans af 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ører vores kode, får vi følgende JSON-log:

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

Centraliser Golang-logs

Hvis din applikation er implementeret på tværs af en klynge af værter, er det ikke bæredygtigt at SSH’e ind på hver enkelt vært for at hale, grepe og undersøge dine logs. Et mere skalerbart alternativ er at videregive logfiler fra lokale filer til en central platform.

En løsning er at bruge Golang-syslog-pakken til at videresende logfiler fra hele din infrastruktur til en enkelt syslog-server.

Apas og strømlin din Golang-loghåndtering med Datadog.

En anden løsning er at bruge en loghåndteringsløsning. Datadog kan f.eks. følge dine logfiler og videresende logfiler til en central platform til behandling og analyse.

Visningen Datadog Log Explorer kan vise Golang-logfiler fra forskellige kilder.

Du kan bruge attributter til at vise værdierne for visse logfelter over tid i grafer, sorteret efter gruppe. Du kan f.eks. spore antallet af fejl efter service, så du kan få at vide, om der er en hændelse i en af dine tjenester. Ved at vise logs fra kun go-logging-demo-tjenesten kan vi se, hvor mange fejllogs denne tjeneste har produceret i et givet interval.

Gruppering af Golang-logs efter status.

Du kan også bruge attributter til at bore ned i mulige årsager, f.eks. se, om en spids i fejllogs tilhører en bestemt vært. Du kan derefter oprette en automatiseret advarsel baseret på værdierne i dine logfiler.

Spore Golang-logfiler på tværs af mikrotjenester

Når du foretager fejlfinding på en fejl, er det ofte nyttigt at se, hvilket adfærdsmønster der førte til fejlen, selv om denne adfærd involverer en række mikrotjenester. Du kan opnå dette med distribueret sporing, hvor du visualiserer den rækkefølge, i hvilken din applikation udfører funktioner, databaseforespørgsler og andre opgaver, og følger disse eksekveringstrin, mens de bevæger sig gennem et netværk. En måde at implementere distribueret sporing i dine logfiler er at videregive kontekstuelle oplysninger som HTTP-headere.

I dette eksempel modtager en mikrotjeneste en anmodning og kontrollerer, om der findes et sporings-id i x-trace-headeren, og genererer et, hvis det ikke findes. Når vi foretager en anmodning til en anden mikroservice, genererer vi derefter et nyt spanID – for denne og for hver anmodning – og tilføjer det til headeren 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 bruger x-span-headerne i indgående anmodninger til at angive forældrene til de spans, de genererer, og sender disse oplysninger som x-parent-headeren til den næste mikroservice i kæden.

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))}

Hvis der opstår en fejl i en af vores microservices, kan vi bruge trace-, parent– og span-attributterne til at se den rute, som en anmodning har taget, så vi ved, hvilke værter – og muligvis hvilke dele af programkoden – vi skal undersøge.

I den første mikroservice:

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

I den anden:

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

Hvis du vil grave dybere ned i Golang-sporingens muligheder, kan du bruge et sporingsbibliotek som OpenTracing eller en overvågningsplatform, der understøtter distribueret sporing til Go-programmer. Datadog kan f.eks. automatisk opbygge et kort over tjenester ved hjælp af data fra sit Golang-sporingsbibliotek, visualisere tendenser i dine spor over tid og give dig besked om tjenester med usædvanlige anmodningsrater, fejlrater eller latenstid.

Eksempel på en visualisering, der viser spor af anmodninger mellem microservices.
Et eksempel på en visualisering, der viser spor af forespørgsler mellem microservices.

Rene og omfattende Golang-logs

I dette indlæg har vi fremhævet fordelene og kompromiserne ved flere Go-logbiblioteker. Vi har også anbefalet metoder til at sikre, at dine logfiler er tilgængelige og tilgængelige, når du har brug for dem, og at de oplysninger, de indeholder, er konsistente og nemme at analysere.

For at begynde at analysere alle dine Go-logfiler med Datadog skal du tilmelde dig en gratis prøveperiode.

Skriv et svar

Din e-mailadresse vil ikke blive publiceret.