Organizace, které jsou závislé na distribuovaných systémech, často píší své aplikace v jazyce Go, aby mohly využívat funkce souběhu, jako jsou kanály a goroutiny (např, Heroku, Basecamp, Cockroach Labs a Datadog). Pokud jste zodpovědní za vytváření nebo podporu aplikací v jazyce Go, může vám dobře promyšlená strategie logování pomoci pochopit chování uživatelů, lokalizovat chyby a sledovat výkon aplikací.
V tomto příspěvku se seznámíte s některými nástroji a technikami pro správu logů v jazyce Golang. Začneme otázkou, který balík pro logování použít pro různé druhy požadavků. Dále si vysvětlíme některé techniky pro lepší prohledávání a spolehlivost logů, snížení náročnosti nastavení logování na zdroje a standardizaci zpráv logů.
- Znejte svůj balík pro logování
- Pro jednoduchost použijte log
- Pro formátované protokoly použijte logrus
- Použijete glog, pokud vám záleží na objemu
- Nejlepší postupy pro psaní a ukládání logů Golang
- Vyhněte se deklarování goroutin pro protokolování
- Zapisujte své logy do souboru
- Zavedení standardního rozhraní pro logování
- Centralizace logů Golang
- Sledování protokolů Golang napříč mikroslužbami
- Čisté a komplexní protokoly Golang
Znejte svůj balík pro logování
Go vám při výběru balíku pro logování dává bohaté možnosti a my se několika z nich budeme zabývat níže. Zatímco logrus je nejoblíbenější z knihoven, kterými se zabýváme, a pomůže vám implementovat konzistentní formát protokolování, ostatní mají specializované případy použití, které stojí za zmínku. V této části prozkoumáme knihovny log, logrus a glog.
Pro jednoduchost použijte log
Vestavěná knihovna logování v jazyce Golang s názvem log
je dodávána s výchozím loggerem, který zapisuje do standardní chyby a přidává časovou značku bez nutnosti konfigurace. Tyto hrubé a hotové protokoly můžete použít při lokálním vývoji, kdy může být rychlé získání zpětné vazby z kódu důležitější než generování bohatých strukturovaných protokolů.
Například můžete definovat funkci dělení, která při pokusu o dělení nulou vrátí volajícímu chybu, místo aby ukončila program.
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)}
Protože náš příklad dělí nulou, vypíše následující zprávu protokolu:
2019/01/31 11:48:00 can't divide by zero
Pro formátované protokoly použijte logrus
Doporučujeme psát protokoly v systému Golang pomocí logrus, což je protokolovací balík určený pro strukturované protokolování, který se dobře hodí pro protokolování v JSON. Formát JSON umožňuje strojům snadno analyzovat vaše logy Golang. A protože JSON je dobře definovaný standard, umožňuje jednoduše přidávat kontext zahrnutím nových polí – parser by je měl být schopen automaticky zachytit.
Pomocí logrusu můžete definovat standardní pole, která přidáte do svých logů JSON, pomocí funkce WithFields
, jak je uvedeno níže. Poté můžete provádět volání loggeru na různých úrovních, například Info()
, Warn()
a Error()
. Knihovna logrus automaticky zapíše protokol jako JSON a vloží standardní pole spolu se všemi poli, která jste definovali za běhu.
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")}
Výsledný protokol bude obsahovat zprávu, úroveň protokolu, časovou značku a standardní pole v objektu JSON:
{"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"}
Použijete glog, pokud vám záleží na objemu
Některé knihovny pro protokolování umožňují povolit nebo zakázat protokolování na určitých úrovních, což je užitečné pro udržení objemu protokolu pod kontrolou při přechodu mezi vývojem a produkcí. Jednou z takových knihoven je glog
, která umožňuje pomocí příznaků na příkazovém řádku (např. -v
pro hlasitost) nastavit úroveň protokolování při spouštění kódu. Pomocí funkce V()
pak můžete v příkazech if
zapisovat logy Golangu pouze na určité úrovni logování.
Například pomocí funkce glog můžete vypsat stejnou chybu „Nelze dělit nulou“ jako dříve, ale pouze pokud logujete na úrovni verbosity 2
. Úroveň verbosity můžete nastavit na libovolné 32bitové celé číslo se znaménkem nebo můžete použít funkce Info()
, Warning()
, Error()
a Fatal()
pro přiřazení úrovní verbosity 0
až 3
(v tomto pořadí).
if err != nil && glog.V(2){ glog.Warning(err) }
Můžete svou aplikaci učinit méně náročnou na zdroje tím, že v produkci budete logovat pouze určité úrovně. Zároveň, pokud to nemá žádný dopad na uživatele, je často dobré logovat co nejvíce interakcí s aplikací a pak pomocí softwaru pro správu logů, jako je Datadog, najít data, která potřebujete pro své vyšetřování
Nejlepší postupy pro psaní a ukládání logů Golang
Jakmile si vyberete knihovnu pro logování, budete také chtít naplánovat, kde ve svém kódu budete volat logger, jak ukládat logy a jak jim dávat smysl. V této části vám doporučíme řadu osvědčených postupů pro uspořádání logů Golangu:
- Volání loggeru provádějte z hlavního procesu aplikace, nikoli z goroutin.
- Zapisujte logy z aplikace do lokálního souboru, i když je později odešlete na centrální platformu.
- Standardizujte své logy pomocí sady předdefinovaných zpráv.
- Odesílejte protokoly na centrální platformu, abyste je mohli analyzovat a agregovat.
- Používejte hlavičky HTTP a jedinečná ID k protokolování chování uživatelů napříč mikroslužbami.
Vyhněte se deklarování goroutin pro protokolování
Existují dva důvody, proč se vyhnout vytváření vlastních goroutin pro zpracování zápisu protokolů. Zaprvé to může vést k problémům se souběžností, protože duplikáty loggeru by se pokoušely přistupovat ke stejnému io.Writer
. Za druhé, knihovny pro logování obvykle samy spouštějí goroutiny, které interně spravují případné problémy se souběžností, a spouštění vlastních goroutin by je pouze rušilo.
Zapisujte své logy do souboru
I když odesíláte své logy na centrální platformu, doporučujeme je nejprve zapsat do souboru na lokálním počítači. Budete chtít mít jistotu, že vaše protokoly budou vždy k dispozici lokálně a neztratí se v síti. Zápis do souboru navíc znamená, že můžete oddělit úlohu zápisu protokolů od úlohy jejich odesílání do centrální platformy. Vaše aplikace samy nebudou muset navazovat spojení ani streamovat vaše protokoly a tyto úlohy můžete přenechat specializovanému softwaru, jako je Datadog Agent. Pokud provozujete své aplikace Go v rámci kontejnerové infrastruktury, která ještě neobsahuje trvalé úložiště – např. kontejnery běžící na AWS Fargate – možná budete chtít nakonfigurovat svůj nástroj pro správu logů tak, aby shromažďoval logy přímo z proudů STDOUT a STDERR vašich kontejnerů (v Dockeru a Kubernetes se to řeší jinak).
Zavedení standardního rozhraní pro logování
Při psaní volání loggerů ze svého kódu používají týmy často různé názvy atributů pro popis stejné věci. Nekonzistentní atributy mohou uživatele mást a znemožňují korelaci logů, které by měly tvořit součást stejného obrazu. Například dva vývojáři mohou stejnou chybu, chybějící jméno klienta při zpracování odesílání, logovat různými způsoby.
Dobrým způsobem, jak prosadit standardizaci, je vytvořit rozhraní mezi kódem aplikace a knihovnou logování. Rozhraní obsahuje předdefinované zprávy protokolu, které implementují určitý formát, což usnadňuje zkoumání problémů tím, že zajišťuje možnost vyhledávání, seskupování a filtrování zpráv protokolu.
V tomto příkladu budeme deklarovat typ Event
s předdefinovanou zprávou. Poté použijeme zprávy Event
k volání loggeru. Týmoví spolupracovníci mohou psát protokoly Golang tak, že poskytnou minimální množství vlastních informací a nechají na aplikaci, aby provedla implementaci standardního formátu.
Nejprve napíšeme balíček logwrapper
, který mohou vývojáři zahrnout do svého kódu.
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)}
Chceme-li používat naše rozhraní pro protokolování, musíme jej pouze zahrnout do našeho kódu a provést volání instance 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")}
Při spuštění našeho kódu získáme následující log JSON:
{"level":"error","msg":"Invalid value for argument: client: nil","time":"2019-03-04T11:21:07-05:00"}
Centralizace logů Golang
Pokud je vaše aplikace nasazena na clusteru hostitelů, není udržitelné, abyste se do každého z nich připojovali pomocí SSH, abyste mohli tailovat, grepovat a zkoumat logy. Škálovatelnější alternativou je předávat protokoly z místních souborů na centrální platformu.
Jedním řešením je použití balíčku Golang syslog pro předávání protokolů z celé infrastruktury na jediný server syslog.
Přizpůsobte a zefektivněte správu protokolů Golang pomocí Datadogu.
Dalším řešením je použití řešení pro správu protokolů. Datadog například dokáže sledovat vaše soubory protokolů a předávat protokoly centrální platformě ke zpracování a analýze.
Pomocí atributů můžete vykreslit hodnoty určitých polí protokolu v průběhu času, seřazené podle skupin. Můžete například sledovat počet chyb podle service
, abyste věděli, zda došlo k incidentu v některé z vašich služeb. Při zobrazení protokolů pouze ze služby go-logging-demo
můžeme vidět, kolik chybových protokolů tato služba v daném intervalu vyprodukovala.
Pomocí atributů můžete také proniknout do možných příčin, například zjistit, zda nárůst chybových protokolů patří konkrétnímu hostiteli. Na základě hodnot protokolů pak můžete vytvořit automatické upozornění.
Sledování protokolů Golang napříč mikroslužbami
Při řešení problémů s chybou je často užitečné zjistit, jaký vzorec chování k ní vedl, i když se toto chování týká řady mikroslužeb. Toho můžete dosáhnout pomocí distribuovaného trasování, které vizualizuje pořadí, v jakém aplikace provádí funkce, dotazy do databáze a další úlohy, a sleduje tyto kroky provádění při jejich cestě sítí. Jedním ze způsobů, jak implementovat distribuované trasování v rámci protokolů, je předávat kontextové informace jako hlavičky HTTP.
V tomto příkladu jedna mikroslužba přijme požadavek a zkontroluje, zda je v hlavičce x-trace
ID trasování, a pokud neexistuje, vygeneruje ho. Při požadavku na jinou mikroslužbu pak vygenerujeme nové spanID – pro tento i pro každý další požadavek – a přidáme ho do hlavičky 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)}
Mikroslužby nižšího řádu používají hlavičky x-span
příchozích požadavků k určení rodičů spanů, které generují, a tuto informaci odešlou jako hlavičku x-parent
další mikroslužbě v řetězci.
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))}
Pokud se v některé z mikroslužeb vyskytne chyba, můžeme pomocí atributů trace
, parent
a span
zjistit, jakou cestou se požadavek vydal, což nám umožní zjistit, které hostitele – a případně které části kódu aplikace – je třeba prozkoumat.
V první mikroslužbě:
{"appname":"go-logging","level":"debug","msg":"Hello from Microservice One","trace":"eUBrVfdw","time":"2017-03-02T15:29:26+01:00","span":"UzWHRihF"}
V druhé:
{"appname":"go-logging","level":"debug","msg":"Hello from Microservice Two","parent":"UzWHRihF","trace":"eUBrVfdw","time":"2017-03-02T15:29:26+01:00","span":"DPRHBMuE"}
Pokud chcete proniknout hlouběji do možností trasování v systému Golang, můžete použít trasovací knihovnu, například OpenTracing, nebo monitorovací platformu, která podporuje distribuované trasování aplikací Go. Například Datadog dokáže automaticky sestavit mapu služeb pomocí dat ze své knihovny pro trasování Golang, vizualizovat trendy v trasování v čase a informovat vás o službách s neobvyklým počtem požadavků, chybovostí nebo latencí.
Čisté a komplexní protokoly Golang
V tomto příspěvku jsme zdůraznili výhody a kompromisy několika knihoven pro protokolování Go. Doporučili jsme také způsoby, jak zajistit, aby vaše logy byly k dispozici a přístupné, když je potřebujete, a aby informace, které obsahují, byly konzistentní a daly se snadno analyzovat.
Chcete-li začít analyzovat všechny své logy Go pomocí Datadogu, přihlaste se k bezplatné zkušební verzi.