How to collect, standardize, and centralize Golang logs

tip / logs /golang /microservices

Organisaatiot, jotka ovat riippuvaisia hajautetuista järjestelmistä, kirjoittavat sovelluksensa usein Go-kielellä, jotta ne voivat hyödyntää rinnakkaisuusominaisuuksia, kuten kanavia ja goroutiineja (esim, Heroku, Basecamp, Cockroach Labs ja Datadog). Jos olet vastuussa Go-sovellusten rakentamisesta tai tukemisesta, harkittu lokistrategia voi auttaa sinua ymmärtämään käyttäjien käyttäytymistä, paikallistamaan virheitä ja seuraamaan sovellusten suorituskykyä.

Tässä postauksessa esitellään joitakin työkaluja ja tekniikoita Golangin lokien hallintaan. Aloitamme kysymyksestä, mitä lokipakettia kannattaa käyttää erityyppisiin vaatimuksiin. Seuraavaksi kerromme joitain tekniikoita, joilla voit tehdä lokeistasi hakukelpoisempia ja luotettavampia, pienentää lokiasetuksesi resurssijalanjälkeä ja standardoida lokiviestejä.

Tuntemalla lokipakettisi

Go antaa sinulle runsaasti vaihtoehtoja, kun valitset lokipakettia, ja tarkastelemme alla useita niistä. Vaikka logrus on suosituin käsittelemistämme kirjastoista ja auttaa sinua toteuttamaan johdonmukaisen lokimuodon, muilla on erikoistuneita käyttötapauksia, jotka kannattaa mainita. Tässä osiossa tarkastellaan kirjastoja log, logrus ja glog.

Käytä logia yksinkertaisuuden vuoksi

Golangin sisäänrakennettu lokikirjasto nimeltä log sisältää oletusarvoisen lokin, joka kirjoittaa standardivirheeseen ja lisää aikaleiman ilman konfigurointia. Voit käyttää näitä karkeita ja valmiita lokeja paikallisessa kehityksessä, kun nopean palautteen saaminen koodistasi voi olla tärkeämpää kuin rikkaiden, jäsenneltyjen lokien tuottaminen.

Voit esimerkiksi määritellä jakofunktion, joka palauttaa virheilmoituksen kutsujalle sen sijaan, että poistuu ohjelmasta, kun yrität jakaa nollalla.

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

Koska esimerkkimme jakaa nollalla, se tulostaa seuraavan lokiviestin:

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

Käytä logrusta muotoiltuihin lokitiedostoihin

Suosittelemme Golangin lokien kirjoittamista käyttämällä logrusta, strukturoituun lokitiedostoon suunniteltua lokipakettia, joka soveltuu hyvin JSON-lokitukseen. JSON-muodon ansiosta koneiden on helppo jäsentää Golang-lokejasi. Ja koska JSON on hyvin määritelty standardi, sen avulla on suoraviivaista lisätä asiayhteyttä lisäämällä uusia kenttiä – jäsentäjän pitäisi pystyä poimimaan ne automaattisesti.

Käyttämällä logrusta voit määritellä standardikenttiä, jotka voit lisätä JSON-lokeihisi käyttämällä funktiota WithFields, kuten alla näkyy. Tämän jälkeen voit tehdä kutsuja logrukselle eri tasoilla, kuten Info(), Warn() ja Error(). Logrus-kirjasto kirjoittaa lokin JSON-muotoon automaattisesti ja lisää siihen vakiokentät sekä kentät, jotka olet määritellyt lennossa.

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

Tuloksena syntyvä loki sisältää viestin, lokin tason, aikaleiman ja vakiokentät JSON-oliossa:

{"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"}

Käytä glogia, jos olet huolissasi volyymista

Joidenkin lokikirjastojen avulla voit ottaa käyttöön tai poistaa käytöstä lokien kirjaamisen tietyillä tasoilla, mikä on hyödyllistä lokien volyymin pitämisessä kurissa siirryttäessä kehityksen ja tuotannon välillä. Yksi tällainen kirjasto on glog, jonka avulla voit käyttää komentorivin lippuja (esim. -v verbaalisuutta varten) asettaaksesi lokitason, kun suoritat koodia. Voit sitten käyttää V()-funktiota if-lausekkeissa kirjoittaaksesi Golangin lokit vain tietyllä lokitasolla.

Voit esimerkiksi käyttää glogia kirjoittaaksesi saman ”Can’t divide by zero” -virheen kuin aiemmin, mutta vain jos kirjaat lokitietoja verbosity-tasolla 2. Voit asettaa verbosityksi minkä tahansa merkityn 32-bittisen kokonaisluvun tai käyttää funktioita Info(), Warning(), Error() ja Fatal() määrittääksesi verbosity-tasot 03 (vastaavasti).

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

Voit tehdä sovelluksestasi vähemmän resursseja vievän loggaamalla vain tietyt tasot tuotannossa. Samaan aikaan, jos sillä ei ole vaikutusta käyttäjiin, on usein hyvä idea lokata mahdollisimman paljon vuorovaikutusta sovelluksen kanssa ja käyttää sitten Datadogin kaltaista lokienhallintaohjelmistoa etsiessäsi tutkimuksessa tarvitsemasi tiedot

Parhaat käytännöt Golangin lokien kirjoittamiseen ja tallentamiseen

Kun olet valinnut lokikirjaston, sinun on myös suunniteltava, missä kohtaa koodia teet kutsuja lokinhallintaohjelmalle, miten tallennat lokit ja miten voit hyödyntää niitä. Tässä osiossa suosittelemme joukon parhaita käytäntöjä Golang-lokiesi järjestämiseen:

  • Tehdä kutsuja loggaajalle sovelluksen pääprosessista, ei goroutiineista.
  • Kirjoita lokit sovelluksestasi paikalliseen tiedostoon, vaikka toimittaisitkin ne myöhemmin keskitettyyn alustaan.
  • Vakioi lokiesi joukolla ennalta määriteltyjä viestejä.
  • Lähetä lokit keskitettyyn alustaan, jotta voit analysoida ja koota ne yhteen.
  • Käytä HTTP-otsikoita ja yksilöllisiä tunnuksia käyttäjän käyttäytymisen kirjaamiseen mikropalveluiden välillä.

Vältä gorutiinien julistamista lokien kirjoittamista varten

On kaksi syytä välttää omien gorutiinien luomista lokien kirjoittamista varten. Ensinnäkin, se voi johtaa samanaikaisuusongelmiin, koska lokinpitäjän kaksoiskappaleet yrittäisivät käyttää samaa io.Writer. Toiseksi lokikirjastot yleensä käynnistävät goroutiineja itse, jolloin ne hoitavat rinnakkaisuusongelmat sisäisesti, ja omien goroutiinien käynnistäminen vain häiritsee.

Kirjoita lokit tiedostoon

Sitäkin huolimatta, että lähetät lokit keskitettyyn alustaan, suosittelemme, että kirjoitat ne ensin tiedostoon paikallisella koneellasi. Haluat varmistaa, että lokit ovat aina saatavilla paikallisesti, eivätkä ne häviä verkkoon. Lisäksi tiedostoon kirjoittaminen tarkoittaa, että voit irrottaa lokien kirjoittamisen tehtävästä ja niiden lähettämisestä keskitettyyn alustaan. Sovelluksiesi ei tarvitse itse luoda yhteyksiä tai suoratoistaa lokejasi, ja voit jättää nämä tehtävät Datadog Agentin kaltaisille erikoisohjelmistoille. Jos käytät Go-sovelluksiasi kontti-infrastruktuurissa, joka ei jo sisällä pysyvää tallennustilaa – esimerkiksi AWS Fargaten päällä toimivat kontit – saatat haluta konfiguroida lokienhallintatyökalusi keräämään lokit suoraan konttiesi STDOUT- ja STDERR-virroista (tätä käsitellään eri tavalla Dockerissa ja Kubernetesissa).

Toteuta vakiomuotoinen kirjausrajapinta

Kirjoittaessasi kutsuja lokinhallintatyökaluille koodinsa sisältä tiimit tiimit käyttävät usein eri attribuuttien nimiä kuvailemaan samoja asioita. Epäjohdonmukaiset attribuutit voivat hämmentää käyttäjiä ja tehdä mahdottomaksi korreloida lokit, joiden pitäisi olla osa samaa kuvaa. Rajapinta sisältää ennalta määritellyt lokiviestit, jotka toteuttavat tietyn muodon, mikä helpottaa ongelmien tutkimista varmistamalla, että lokiviestejä voidaan hakea, ryhmitellä ja suodattaa.

Golang kirjaa virheen käyttämällä standardiliittymää yhtenäisen viestin luomiseksi.
Golang lokittaa virheen käyttämällä vakiorajapintaa yhtenäisen viestin luomiseksi.

Tässä esimerkissä julistetaan Event-tyyppi, jolla on ennalta määritetty viesti. Sitten käytämme Event-viestejä tehdäksemme kutsuja loggaajalle. Työkaverit voivat kirjoittaa Golangin lokitiedostoja antamalla minimaalisen määrän mukautettuja tietoja ja antamalla sovelluksen tehdä työn vakiomuodon toteuttamisesta.

Aluksi kirjoitamme logwrapper-paketin, jonka kehittäjät voivat sisällyttää koodiinsa.

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

Käyttääksemme lokirajapintaamme meidän tarvitsee vain sisällyttää se koodiimme ja tehdä kutsuja StandardLogger-instanssille.

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

Kun suoritamme koodimme, saamme seuraavan JSON-lokin:

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

Golangin lokien keskittäminen

Jos sovelluksesi on käytössä useiden isäntien klusterissa, ei ole kestävää SSH:lla kirjautua jokaiseen isäntäkoneeseen hännänvarsi- ja grep-toimintojen sekä lokien tutkimisen vuoksi. Skaalautuvampi vaihtoehto on välittää lokit paikallisista tiedostoista keskitettyyn alustaan.

Yksi ratkaisuksi on käyttää Golangin syslog-pakettia, joka välittää lokit kaikkialta infrastruktuuristasi yhdelle syslog-palvelimelle.

Räätälöi ja virtaviivaista Golangin lokien hallintaa Datadogin avulla.

Toiseksi vaihtoehdoksi on myös lokienhallintaratkaisujen käyttö. Esimerkiksi Datadog voi seurata lokitiedostojasi ja välittää lokit keskitettyyn alustaan käsiteltäväksi ja analysoitavaksi.

Datadogin Log Explorer -näkymässä voit näyttää Golang-lokit eri lähteistä.

Ominaisuuksien avulla voit esittää graafisesti ryhmittäin lajiteltuna tiettyjen lokikenttien arvot ajan kuluessa. Voit esimerkiksi seurata virheiden määrää service:n mukaan, jotta tiedät, jos jossakin palvelussasi on häiriö. Kun näytetään vain go-logging-demo-palvelun lokit, nähdään, kuinka monta virhelokia tämä palvelu on tuottanut tietyllä aikavälillä.

Golangin lokien ryhmittely tilan mukaan.

Ominaisuuksien avulla voit myös tarkastella mahdollisia syitä, esimerkiksi nähdä, kuuluuko virhelokien piikki tiettyyn isäntäkoneeseen. Voit sitten luoda automaattisen hälytyksen lokien arvojen perusteella.

Track Golang logs across microservices

Vianmäärityksessä on usein hyödyllistä nähdä, millainen käyttäytymismalli johti virheeseen, vaikka kyseiseen käyttäytymiseen liittyisi useita mikropalveluja. Voit saavuttaa tämän hajautetulla jäljityksellä, visualisoimalla järjestyksen, jossa sovelluksesi suorittaa funktioita, tietokantakyselyjä ja muita tehtäviä, ja seuraamalla näitä suoritusvaiheita niiden kulkiessa verkon läpi. Yksi tapa toteuttaa hajautettu jäljitys lokien sisällä on välittää asiayhteystietoja HTTP-otsakkeina.

Tässä esimerkissä yksi mikropalvelu vastaanottaa pyynnön ja tarkistaa, onko x-trace-otsakkeessa jäljitystunnusta, ja luo sellaisen, jos sitä ei ole. Kun tehdään pyyntö toiselle mikropalvelulle, luodaan tämän jälkeen uusi spanID – tätä ja jokaista pyyntöä varten – ja lisätään se otsikkoon 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)}

Alempana olevat mikropalvelut käyttävät saapuvien pyyntöjen x-span-otsakkeita määrittelemään tuottamiensa spanien vanhemmat ja lähettävät tämän tiedon x-parent-otsakkeena ketjun seuraavalle mikropalvelulle.

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

Jos jossakin mikropalvelussamme tapahtuu virhe, voimme käyttää trace-, parent– ja span-attribuutteja nähdäksemme, mitä reittiä pyyntö on kulkenut, jolloin tiedämme, mitkä isännät – ja mahdollisesti mitkä sovelluskoodin osat – on tutkittava.

Ensimmäisessä mikropalvelussa:

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

Kakkosessa:

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

Jos haluat syventyä syvällisemmin Golangin jäljitysmahdollisuuksiin, voit käyttää jäljityskirjastoa, kuten OpenTracingia, tai monitorointialustaa, joka tukee hajautettua jäljitystä Go-sovelluksille. Esimerkiksi Datadog voi automaattisesti rakentaa kartan palveluista käyttäen sen Golang-jäljityskirjaston tietoja, visualisoida jälkien trendejä ajan mittaan ja ilmoittaa palveluista, joissa on epätavallisia pyyntöjen, virheiden tai viiveiden määriä.

Esimerkki visualisoinnista, jossa näkyvät mikropalveluiden välisten pyyntöjen jäljet.
Esimerkki visualisoinnista, joka näyttää mikropalveluiden välisten pyyntöjen jäljet.

Puhtaat ja kattavat Golang-lokit

Tässä postauksessa olemme tuoneet esiin useiden Go:n lokikirjastojen hyötyjä ja kompromisseja. Olemme myös suositelleet tapoja varmistaa, että lokit ovat saatavilla ja käytettävissä silloin, kun niitä tarvitaan, ja että niiden sisältämät tiedot ovat johdonmukaisia ja helposti analysoitavissa.

Aloittaaksesi kaikkien Go-lokiesi analysoinnin Datadogin avulla, rekisteröidy ilmaiseen kokeilujaksoon.

Vastaa

Sähköpostiosoitettasi ei julkaista.