Organizacje, które zależą od systemów rozproszonych, często piszą swoje aplikacje w Go, aby wykorzystać funkcje współbieżności, takie jak kanały i goroutines (np, Heroku, Basecamp, Cockroach Labs i Datadog). Jeśli jesteś odpowiedzialny za budowanie lub wspieranie aplikacji Go, dobrze przemyślana strategia logowania może pomóc Ci zrozumieć zachowanie użytkowników, zlokalizować błędy i monitorować wydajność Twoich aplikacji.
Ten post pokaże Ci kilka narzędzi i technik do zarządzania logami Golanga. Zaczniemy od pytania o to, którego pakietu logów użyć dla różnych rodzajów wymagań. Następnie wyjaśnimy kilka technik, dzięki którym twoje dzienniki będą bardziej wyszukiwalne i niezawodne, zmniejszymy zużycie zasobów twojej konfiguracji logowania i ustandaryzujemy twoje komunikaty dziennika.
- Znaj swój pakiet logowania
- Użyj log dla prostoty
- Użyj logrusa do sformatowanych dzienników
- Użyj glog, jeśli martwisz się o objętość
- Najlepsze praktyki pisania i przechowywania logów Golang
- Unikaj deklarowania goroutines dla logowania
- Zapisz swoje logi do pliku
- Implementacja standardowego interfejsu logowania
- Centralizacja logów Golanga
- Śledzenie logów Golanga w mikroserwisach
- Czyste i wyczerpujące logi Golanga
Znaj swój pakiet logowania
Go daje ci mnóstwo opcji przy wyborze pakietu logowania, a my zbadamy kilka z nich poniżej. Podczas gdy logrus jest najpopularniejszą z omawianych przez nas bibliotek i pomaga zaimplementować spójny format logowania, pozostałe mają specjalistyczne zastosowania, o których warto wspomnieć. W tej sekcji omówimy biblioteki log, logrus i glog.
Użyj log dla prostoty
Wbudowana biblioteka logowania Golanga, zwana log
, jest dostarczana z domyślnym loggerem, który zapisuje do standardowego błędu i dodaje znacznik czasu bez potrzeby konfiguracji. Możesz używać tych surowych i gotowych logów do lokalnego rozwoju, gdy uzyskanie szybkiej informacji zwrotnej z kodu może być ważniejsze niż generowanie bogatych, ustrukturyzowanych logów.
Na przykład, możesz zdefiniować funkcję dzielenia, która zwraca błąd do wywołującego, zamiast kończyć program, gdy próbujesz podzielić przez zero.
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)}
Ponieważ nasz przykład dzieli przez zero, wyświetli następujący komunikat dziennika:
2019/01/31 11:48:00 can't divide by zero
Użyj logrusa do sformatowanych dzienników
Zalecamy pisanie dzienników Golanga przy użyciu logrusa, pakietu logowania zaprojektowanego do strukturalnego rejestrowania, który jest dobrze przystosowany do rejestrowania w JSON. Format JSON umożliwia maszynom łatwe parsowanie logów Golanga. A ponieważ JSON jest dobrze zdefiniowanym standardem, ułatwia to dodawanie kontekstu poprzez włączanie nowych pól – parser powinien być w stanie je automatycznie wyłapać.
Używając logrusa, możesz zdefiniować standardowe pola do dodania do logów JSON za pomocą funkcji WithFields
, jak pokazano poniżej. Następnie możesz wykonywać wywołania do loggera na różnych poziomach, takich jak Info()
, Warn()
i Error()
. Biblioteka logrus automatycznie zapisze log jako JSON i wstawi standardowe pola, wraz z polami, które zdefiniowałeś w locie.
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")}
Wynikowy dziennik będzie zawierał wiadomość, poziom dziennika, znacznik czasu i standardowe pola w obiekcie 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"}
Użyj glog, jeśli martwisz się o objętość
Niektóre biblioteki logowania pozwalają włączyć lub wyłączyć logowanie na określonych poziomach, co jest przydatne do utrzymania objętości dziennika w ryzach podczas przechodzenia między rozwojem a produkcją. Jedną z takich bibliotek jest glog
, która pozwala na użycie flag w wierszu poleceń (np. -v
dla verbosity) do ustawienia poziomu logowania podczas uruchamiania kodu. Następnie możesz użyć funkcji V()
w if
instrukcjach, aby pisać logi Golanga tylko na określonym poziomie logowania.
Na przykład możesz użyć glog, aby napisać ten sam błąd „Can’t divide by zero” z wcześniej, ale tylko wtedy, gdy logujesz na poziomie verbosity 2
. Możesz ustawić verbosity na dowolną podpisaną 32-bitową liczbę całkowitą lub użyć funkcji Info()
, Warning()
, Error()
i Fatal()
do przypisania poziomów verbosity od 0
do 3
(odpowiednio).
if err != nil && glog.V(2){ glog.Warning(err) }
Możesz uczynić swoją aplikację mniej zasobochłonną, logując tylko niektóre poziomy w produkcji. Jednocześnie, jeśli nie ma wpływu na użytkowników, często dobrym pomysłem jest rejestrowanie tak wielu interakcji z aplikacją, jak to tylko możliwe, a następnie użycie oprogramowania do zarządzania logami, takiego jak Datadog, aby znaleźć dane potrzebne do zbadania sprawy
Najlepsze praktyki pisania i przechowywania logów Golang
Po wybraniu biblioteki logowania, będziesz także chciał zaplanować, gdzie w kodzie wykonać wywołania do loggera, jak przechowywać logi i jak nadać im sens. W tej sekcji zalecamy serię najlepszych praktyk organizowania logów Golanga:
- Wywołania do loggera wykonuj z głównego procesu aplikacji, a nie z goroutines.
- Zapisuj logi z aplikacji do lokalnego pliku, nawet jeśli później wyślesz je do centralnej platformy.
- Standaryzuj swoje logi za pomocą zestawu predefiniowanych komunikatów.
- Wysyłaj swoje logi do centralnej platformy, abyś mógł je analizować i agregować.
- Używaj nagłówków HTTP i unikalnych identyfikatorów do rejestrowania zachowań użytkowników w mikroserwisach.
Unikaj deklarowania goroutines dla logowania
Istnieją dwa powody, aby unikać tworzenia własnych goroutines do obsługi pisania logów. Po pierwsze, może to prowadzić do problemów ze współbieżnością, ponieważ duplikaty rejestratora będą próbowały uzyskać dostęp do tego samego io.Writer
. Po drugie, biblioteki logujące zazwyczaj same uruchamiają goroutines, zarządzając wewnętrznie wszelkimi problemami współbieżności, a uruchamianie własnych goroutines będzie tylko przeszkadzać.
Zapisz swoje logi do pliku
Nawet jeśli wysyłasz swoje logi do centralnej platformy, zalecamy zapisanie ich najpierw do pliku na twojej lokalnej maszynie. Będziesz chciał mieć pewność, że logi są zawsze dostępne lokalnie i nie zostaną utracone w sieci. Dodatkowo, zapisywanie do pliku oznacza, że możesz oddzielić zadanie zapisywania logów od zadania wysyłania ich do centralnej platformy. Twoje aplikacje nie będą musiały nawiązywać połączeń ani przesyłać logów, a te zadania możesz pozostawić wyspecjalizowanemu oprogramowaniu, takiemu jak Datadog Agent. Jeśli uruchamiasz swoje aplikacje Go w infrastrukturze kontenerowej, która nie zawiera jeszcze trwałej pamięci masowej – np. kontenery działające na AWS Fargate – możesz chcieć skonfigurować swoje narzędzie do zarządzania logami, aby zbierało logi bezpośrednio ze strumieni STDOUT i STDERR kontenerów (jest to obsługiwane inaczej w Docker i Kubernetes).
Implementacja standardowego interfejsu logowania
Podczas pisania wywołań do loggerów z poziomu swojego kodu, zespoły często używają różnych nazw atrybutów do opisania tej samej rzeczy. Niespójne atrybuty mogą dezorientować użytkowników i uniemożliwiać skorelowanie logów, które powinny być częścią tego samego obrazu. Na przykład, dwóch programistów może rejestrować ten sam błąd, brakującą nazwę klienta podczas obsługi wysyłania, na różne sposoby.
Dobrym sposobem na wymuszenie standaryzacji jest stworzenie interfejsu między kodem aplikacji a biblioteką logowania. Interfejs zawiera predefiniowane komunikaty dziennika, które implementują określony format, ułatwiając badanie problemów poprzez zapewnienie, że komunikaty dziennika mogą być przeszukiwane, grupowane i filtrowane.
W tym przykładzie zadeklarujemy typ Event
z predefiniowanym komunikatem. Następnie użyjemy Event
wiadomości do wykonania wywołań do loggera. Członkowie zespołu mogą pisać logi Golanga, dostarczając minimalną ilość niestandardowych informacji, pozwalając aplikacji wykonać pracę związaną z implementacją standardowego formatu.
Po pierwsze, napiszemy pakiet logwrapper
, który programiści mogą włączyć do swojego kodu.
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)}
Aby użyć naszego interfejsu logowania, musimy tylko włączyć go do naszego kodu i wykonać wywołania do instancji 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")}
Gdy uruchomimy nasz kod, otrzymamy następujący log JSON:
{"level":"error","msg":"Invalid value for argument: client: nil","time":"2019-03-04T11:21:07-05:00"}
Centralizacja logów Golanga
Jeśli twoja aplikacja jest rozmieszczona na klastrze hostów, nie jest zrównoważone, aby SSH do każdego z nich w celu ogonowania, grepowania i badania logów. Bardziej skalowalną alternatywą jest przekazywanie logów z plików lokalnych do centralnej platformy.
Jednym z rozwiązań jest użycie pakietu Golang syslog do przekazywania logów z całej infrastruktury do pojedynczego serwera syslog.
Dostosuj i usprawnij zarządzanie logami Golang za pomocą Datadog.
Innym rozwiązaniem jest użycie rozwiązania do zarządzania logami. Datadog, na przykład, może dopasować pliki dziennika i przekazać dzienniki do centralnej platformy do przetwarzania i analizy.
Możesz użyć atrybutów do wykresu wartości określonych pól dziennika w czasie, posortowanych według grupy. Na przykład, możesz śledzić liczbę błędów według service
, aby dać ci znać, czy w jednej z twoich usług wystąpił jakiś incydent. Pokazując logi tylko z usługi go-logging-demo
, możemy zobaczyć, ile logów błędów ta usługa wyprodukowała w danym przedziale czasowym.
Możesz również użyć atrybutów, aby zagłębić się w możliwe przyczyny, na przykład sprawdzając, czy wzrost liczby logów błędów należy do określonego hosta. Możesz wtedy stworzyć automatyczny alert oparty na wartościach twoich logów.
Śledzenie logów Golanga w mikroserwisach
Podczas rozwiązywania problemów z błędem, często pomocne jest sprawdzenie, jaki wzorzec zachowania doprowadził do niego, nawet jeśli to zachowanie dotyczy wielu mikroserwisów. Można to osiągnąć za pomocą rozproszonego śledzenia, wizualizując kolejność wykonywania funkcji, zapytań do bazy danych i innych zadań przez aplikację, a następnie śledząc te kroki wykonania w trakcie ich drogi przez sieć. Jednym ze sposobów implementacji rozproszonego śledzenia w dziennikach jest przekazywanie informacji kontekstowych jako nagłówków HTTP.
W tym przykładzie jedna mikroserwis otrzymuje żądanie i sprawdza, czy w nagłówku x-trace
znajduje się identyfikator śledzenia, generując go, jeśli nie istnieje. Podczas wykonywania żądania do innej mikroserwisu, generujemy nowy spanID dla tego i dla każdego żądania i dodajemy go do nagłówka 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)}
Mikroserwisy downstream używają nagłówków x-span
przychodzących żądań, aby określić rodziców przęseł, które generują, i wysyłają te informacje jako nagłówek x-parent
do następnego mikroserwisu w łańcuchu.
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))}
Jeśli w jednej z naszych mikroserwisów wystąpi błąd, możemy użyć atrybutów trace
, parent
i span
, aby zobaczyć drogę, jaką przebyło żądanie, dzięki czemu będziemy wiedzieć, które hosty – i ewentualnie które części kodu aplikacji – należy zbadać.
W pierwszej mikroserwisie:
{"appname":"go-logging","level":"debug","msg":"Hello from Microservice One","trace":"eUBrVfdw","time":"2017-03-02T15:29:26+01:00","span":"UzWHRihF"}
W drugiej:
{"appname":"go-logging","level":"debug","msg":"Hello from Microservice Two","parent":"UzWHRihF","trace":"eUBrVfdw","time":"2017-03-02T15:29:26+01:00","span":"DPRHBMuE"}
Jeśli chcesz bardziej zagłębić się w możliwości śledzenia Golanga, możesz użyć biblioteki śledzącej, takiej jak OpenTracing, lub platformy monitorującej, która obsługuje rozproszone śledzenie aplikacji Go. Na przykład Datadog może automatycznie budować mapę usług przy użyciu danych z biblioteki śledzenia Golanga; wizualizować trendy w śledzeniu w czasie; i informować o usługach z nietypowymi wskaźnikami żądań, błędów lub opóźnień.
Czyste i wyczerpujące logi Golanga
W tym poście przedstawiliśmy zalety i wady kilku bibliotek logowania Go. Zaproponowaliśmy również sposoby zapewnienia, że logi są dostępne wtedy, gdy są potrzebne, a informacje w nich zawarte są spójne i łatwe do przeanalizowania.
Aby rozpocząć analizowanie wszystkich logów Go z Datadog, zarejestruj się na bezpłatną wersję próbną.
.