How to collect, standardize, and centralize Golang logs

tip / logs /golang /microservices

Az elosztott rendszerektől függő szervezetek gyakran Go nyelven írják alkalmazásaikat, hogy kihasználják az olyan párhuzamossági funkciókat, mint a csatornák és goroutines (pl., Heroku, Basecamp, Cockroach Labs és Datadog). Ha Go alkalmazások készítéséért vagy támogatásáért felel, egy jól átgondolt naplózási stratégia segíthet a felhasználói viselkedés megértésében, a hibák lokalizálásában és az alkalmazások teljesítményének nyomon követésében.

Ez a bejegyzés néhány eszközt és technikát mutat be a Golang naplók kezeléséhez. Azzal a kérdéssel kezdjük, hogy milyen naplózási csomagot használjunk a különböző típusú követelményekhez. Ezután ismertetünk néhány technikát, amelyekkel kereshetőbbé és megbízhatóbbá teheti a naplóit, csökkentheti a naplózási beállítás erőforrásigényét, és szabványosíthatja a naplóüzeneteket.

Tudja meg a naplózási csomagját

A Go rengeteg lehetőséget ad a naplózási csomag kiválasztásakor, és ezek közül az alábbiakban néhányat megvizsgálunk. Míg az általunk tárgyalt könyvtárak közül a logrus a legnépszerűbb, és segít egy konzisztens naplózási formátum megvalósításában, a többinek speciális felhasználási esetei vannak, amelyeket érdemes megemlíteni. Ez a rész a log, a logrus és a glog könyvtárakat fogja áttekinteni.

A log használata az egyszerűség kedvéért

Golang beépített naplózási könyvtára, a log, egy alapértelmezett naplózót tartalmaz, amely a standard hibaüzenetbe ír és időbélyeget ad hozzá, konfiguráció nélkül. Ezeket a nyers és kész naplókat helyi fejlesztéshez használhatjuk, amikor a gyors visszajelzés a kódról fontosabb lehet, mint a gazdag, strukturált naplók generálása.

Meghatározhatunk például egy olyan osztásfüggvényt, amely a programból való kilépés helyett hibát küld vissza a hívónak, amikor nullával próbál osztani.

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

Mert példánk nullával oszt, a következő naplóüzenetet fogja kiadni:

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

A logrus használata formázott naplókhoz

A Golang naplók írásához a logrus használatát javasoljuk, amely egy strukturált naplózásra tervezett naplócsomag, amely jól használható JSON-ban történő naplózásra. A JSON formátum lehetővé teszi, hogy a gépek könnyen elemezzék a Golang naplóit. És mivel a JSON egy jól definiált szabvány, egyszerűvé teszi a kontextus hozzáadását új mezők hozzáadásával – egy elemzőnek automatikusan fel kell tudnia venni őket.

A logrus használatával a WithFields függvény használatával definiálhatja a JSON-naplókhoz hozzáadandó szabványos mezőket, ahogy az alább látható. Ezután különböző szinteken hívhatja a naplózót, például Info(), Warn() és Error(). A logrus könyvtár automatikusan megírja a naplót JSON-ként, és beilleszti a szabványos mezőket, valamint az Ön által menet közben definiált mezőket.

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

A kapott napló tartalmazza az üzenetet, a naplószintet, az időbélyeget és a szabványos mezőket egy JSON objektumban:

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

Használja a glogot, ha aggódik a mennyiség miatt

Néhány naplókönyvtár lehetővé teszi, hogy bizonyos szinteken engedélyezze vagy letiltsa a naplózást, ami hasznos, ha a fejlesztés és a termelés közötti váltáskor kordában akarja tartani a napló mennyiségét. Az egyik ilyen könyvtár a glog, amely lehetővé teszi, hogy a parancssorban zászlókkal (pl. -v a szóbeliséghez) állítsuk be a naplózási szintet a kód futtatásakor. Ezután egy V() függvényt használhatsz a if utasításokban, hogy a Golang naplózását csak egy bizonyos naplózási szinten írd meg.

Például a glog segítségével megírhatod ugyanazt a “Can’t divide by zero” hibát, mint korábban, de csak akkor, ha a naplózás a 2 verbozitási szinten történik. A szóbeliséget bármilyen előjeles 32 bites egész számra beállíthatja, vagy a Info(), Warning(), Error() és Fatal() függvények segítségével a 0-től 3-ig (illetve)

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

Azzal teheti kevésbé erőforrás-igényessé az alkalmazást, hogy termelésben csak bizonyos szinteket naplóz. Ugyanakkor, ha nincs hatása a felhasználókra, gyakran jó ötlet, ha minél több interakciót naplózol az alkalmazásoddal, majd a Datadoghoz hasonló naplókezelő szoftverekkel megtalálod a vizsgálathoz szükséges adatokat

A Golang naplók írásának és tárolásának legjobb gyakorlatai

Ha már kiválasztottál egy naplókönyvtárat, azt is meg kell tervezned, hogy a kódodban hol fogod meghívni a naplózót, hogyan fogod tárolni a naplókat, és hogyan fogod értelmezni őket. Ebben a szakaszban a Golang-naplók rendszerezéséhez egy sor legjobb gyakorlatot ajánlunk:

  • A naplózót a fő alkalmazási folyamaton belül hívja meg, ne a goroutine-okon belül.
  • Az alkalmazás naplóit helyi fájlba írja, még akkor is, ha később egy központi platformra küldi őket.
  • Szabványosítsa a naplóit egy sor előre definiált üzenettel.
  • Küldje a naplóit egy központi platformra, hogy elemezni és összesíteni tudja őket.
  • Használjon HTTP-fejléceket és egyedi azonosítókat a felhasználói viselkedés naplózásához a mikroszolgáltatások között.

Kerülje a goroutine-ok deklarálását a naplózáshoz

Két oka van annak, hogy ne hozzon létre saját goroutine-okat a naplók írásához. Először is, ez párhuzamossági problémákhoz vezethet, mivel a naplózó duplikátumai megpróbálnának hozzáférni ugyanahhoz a io.Writerhoz. Másodszor, a naplókönyvtárak általában maguk indítanak goroutine-okat, amelyek az esetleges párhuzamossági problémákat belsőleg kezelik, és a saját goroutine-ok indítása csak zavarja őket.

Fájlba írja a naplóit

Még ha a naplóit egy központi platformra küldi is, azt javasoljuk, hogy először a helyi gépén lévő fájlba írja őket. Biztosítani szeretné, hogy a naplói mindig elérhetőek legyenek helyben, és ne vesszenek el a hálózatban. Ráadásul a fájlba írás azt jelenti, hogy szétválaszthatja a naplók írásának feladatát a központi platformra való küldés feladatától. Maguknak az alkalmazásoknak nem kell kapcsolatot létrehozniuk vagy a naplóit streamelniük, és ezeket a feladatokat olyan speciális szoftverekre bízhatja, mint a Datadog Agent. Ha Go alkalmazásait olyan konténeres infrastruktúrában futtatja, amely még nem tartalmaz tartós tárolót – például az AWS Fargate-en futó konténereket -, akkor érdemes a naplókezelő eszközét úgy konfigurálni, hogy a naplókat közvetlenül a konténerek STDOUT és STDERR adatfolyamaiból gyűjtse (ezt a Docker és a Kubernetes másképp kezeli).

Szabványos naplózási felület megvalósítása

A kódjukból a naplókhoz való hívások írása során a csapatok gyakran különböző attribútumneveket használnak ugyanazon dolog leírására. Az ellentmondásos attribútumok összezavarhatják a felhasználókat, és lehetetlenné teszik az olyan naplók korrelációját, amelyeknek ugyanannak a képnek a részét kellene képezniük. Például két fejlesztő különböző módon naplózhatja ugyanazt a hibát, a hiányzó ügyfélnevet egy feltöltés kezelésénél.

Golang naplózza ugyanazt a hibát különböző helyeken különböző üzenetekkel.
Golang naplózza ugyanazt a hibát különböző helyeken különböző üzenetekkel.

A szabványosítás kikényszerítésének jó módja, ha létrehoz egy interfészt az alkalmazáskód és a naplókönyvtár között. Az interfész előre definiált naplóüzeneteket tartalmaz, amelyek egy bizonyos formátumot valósítanak meg, megkönnyítve a problémák kivizsgálását azáltal, hogy a naplóüzenetek kereshetők, csoportosíthatók és szűrhetők.

Golang egy hiba esetén egy szabványos interfész segítségével naplóz egy egységes üzenetet.
A Golang egy hiba esetén egy szabványos interfész segítségével naplóz egy konzisztens üzenet létrehozásához.

Ebben a példában egy Event típust deklarálunk egy előre definiált üzenettel. Ezután Event üzeneteket fogunk használni egy logger meghívására. A csapattagok úgy írhatnak Golang-naplókat, hogy minimális mennyiségű egyéni információt adnak meg, és az alkalmazásra bízzák a szabványos formátum megvalósításának munkáját.

Először is írunk egy logwrapper csomagot, amelyet a fejlesztők beépíthetnek a kódjukba.

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

A naplózási felületünk használatához csak be kell építenünk a kódunkba, és meg kell hívnunk egy StandardLogger példányt.

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

Amikor futtatjuk a kódunkat, a következő JSON naplót kapjuk:

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

A Golang naplóinak központosítása

Ha az alkalmazásunkat több host fürtön keresztül telepítjük, nem fenntartható, hogy mindegyikbe SSH-n keresztül lépjünk be, hogy taileljük, grepeljük és vizsgáljuk a naplóinkat. Skalálhatóbb alternatíva, ha a naplófájlokat a helyi fájlokból egy központi platformra továbbítja.

Az egyik megoldás a Golang syslog csomag használata, amellyel az egész infrastruktúrából származó naplófájlokat egyetlen syslog-kiszolgálóra továbbítja.

A Golang naplókezelését a Datadoggal testre szabhatja és egyszerűsítheti.

A másik megoldás egy naplókezelő megoldás használata. A Datadog például képes követni a naplófájljait, és továbbítani a naplófájlokat egy központi platformra feldolgozás és elemzés céljából.

A Datadog Log Explorer nézetben megjelenítheti a különböző forrásokból származó Golang-naplókat.

Attribútumok segítségével csoportosítással ábrázolhatja bizonyos naplómezők értékeit az időben. Például nyomon követheti a hibák számát service szerint, hogy értesüljön arról, ha valamelyik szolgáltatásában incidens történt. Ha csak a go-logging-demo szolgáltatás naplóit mutatjuk, láthatjuk, hogy ez a szolgáltatás hány hibanaplót produkált egy adott intervallumban.

A Golang-naplók állapot szerinti csoportosítása.

Attribútumok segítségével a lehetséges okokat is feltárhatja, például megnézheti, hogy a hibanaplók kiugrása egy adott állomáshoz tartozik-e. Ezután a naplók értékei alapján automatikus riasztást hozhat létre.

A Golang-naplók nyomon követése mikroszolgáltatásokon keresztül

Hiba elhárításakor gyakran hasznos látni, hogy milyen viselkedési minta vezetett a hibához, még akkor is, ha ez a viselkedés több mikroszolgáltatást érint. Ezt az elosztott nyomkövetéssel érheti el, megjelenítve azt a sorrendet, amelyben az alkalmazás függvényeket, adatbázis-lekérdezéseket és egyéb feladatokat hajt végre, és követve ezeket a végrehajtási lépéseket, ahogyan azok egy hálózaton keresztül haladnak. Az elosztott nyomkövetés megvalósításának egyik módja a naplókon belül a kontextuális információk HTTP-fejlécek formájában történő átadása.

Ebben a példában az egyik mikroszolgáltatás fogad egy kérést, és ellenőrzi, hogy a x-trace fejlécben van-e nyomkövetési azonosító, és generál egyet, ha nem létezik. Amikor egy másik mikroszolgáltatáshoz intézünk egy kérést, akkor egy új spanID-t generálunk – erre és minden kérésre -, és hozzáadjuk a x-span fejléchez.

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

A bejövő kérések x-span fejlécét a bejövő kérések x-span fejlécében adjuk meg az általuk generált spanok szüleit, és ezt az információt a x-parent fejléc formájában elküldjük a láncban következő mikroszolgáltatásnak.

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

Ha valamelyik mikroszolgáltatásunkban hiba lép fel, a trace, parent és span attribútumok segítségével láthatjuk, hogy egy kérés milyen útvonalon haladt, így megtudhatjuk, hogy mely hosztokat – és esetleg az alkalmazás kódjának mely részeit – kell megvizsgálnunk.

Az első mikroszolgáltatásban:

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

A másodikban:

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

Ha mélyebben bele akarunk ásni a Golang nyomkövetési lehetőségeibe, használhatunk egy olyan nyomkövetési könyvtárat, mint az OpenTracing, vagy egy olyan felügyeleti platformot, amely támogatja a Go alkalmazások elosztott nyomkövetését. A Datadog például a Golang nyomkövetési könyvtár adataiból automatikusan képes a szolgáltatások térképét létrehozni; a nyomkövetések időbeli trendjeit vizualizálni; és tájékoztatni a szokatlan kérésszámú, hibaarányú vagy késleltetési idejű szolgáltatásokról.

Egy példa a mikroszolgáltatások közötti kérések nyomkövetését bemutató vizualizációra.
Egy példa a mikroszolgáltatások közötti kérések nyomvonalát bemutató vizualizációra.

Tiszta és átfogó Golang naplók

Ebben a bejegyzésben több Go naplókönyvtár előnyeit és kompromisszumait emeltük ki. Javasoltunk olyan módszereket is, amelyekkel biztosíthatja, hogy a naplói elérhetőek és hozzáférhetőek legyenek, amikor szüksége van rájuk, és hogy a bennük található információk konzisztensek és könnyen elemezhetőek legyenek.

Az összes Go naplójának Datadoggal történő elemzéséhez regisztráljon egy ingyenes próbaverzióra.

Vélemény, hozzászólás?

Az e-mail-címet nem tesszük közzé.