Logowanie w Go: Choosing a System and Using it

Jesteś stosunkowo nowy w języku Go. Prawdopodobnie używasz go do napisania aplikacji internetowej lub serwera i musisz utworzyć plik dziennika. Więc, robisz szybkie wyszukiwanie w sieci i okazuje się, że istnieje mnóstwo opcji logowania w Go. Skąd wiesz, którą z nich wybrać? Ten artykuł pozwoli Ci odpowiedzieć na to pytanie.

Przyjrzymy się wbudowanemu pakietowi log i określimy, do jakich projektów się nadaje, zanim zbadamy inne rozwiązania logowania, które są powszechne w ekosystemie Go.

Co logować

Nie muszę ci mówić, jak ważne jest logowanie. Logi są używane przez każdą produkcyjną aplikację sieciową, aby pomóc programistom i operacjom:

  • Wykrywanie błędów w kodzie aplikacji
  • Odkrywanie problemów z wydajnością
  • Wykonywanie pośmiertnej analizy przestojów i incydentów bezpieczeństwa

Dane, które faktycznie logujesz, będą zależeć od typu aplikacji, którą budujesz. Przez większość czasu, będziesz miał pewne wariacje w następujących przypadkach:

  • Znacznik czasu, kiedy zdarzenie wystąpiło lub dziennik został wygenerowany
  • Poziomy dziennika, takie jak debug, error lub info
  • Dane kontekstowe, aby pomóc zrozumieć, co się stało i umożliwić łatwe odtworzenie sytuacji

Czego nie rejestrować

Ogólnie rzecz biorąc, nie powinieneś rejestrować żadnej formy wrażliwych danych biznesowych lub informacji umożliwiających identyfikację osób. Obejmuje to, ale nie ogranicza się do:

  • Nazwisk
  • Adresów IP
  • Numerów kart kredytowych

Te ograniczenia mogą sprawić, że dzienniki będą mniej użyteczne z perspektywy inżynierskiej, ale dzięki nim Twoja aplikacja będzie bezpieczniejsza. W wielu przypadkach przepisy, takie jak GDPR i HIPAA, mogą zabraniać rejestrowania danych osobowych.

Wprowadzenie pakietu log

Biblioteka standardowa Go ma wbudowany logpakiet, który zapewnia większość podstawowych funkcji logowania. Chociaż nie ma on poziomów dziennika (takich jak debug, ostrzeżenie lub błąd), nadal zapewnia wszystko, czego potrzebujesz, aby skonfigurować podstawową strategię logowania.

Oto najbardziej podstawowy przykład logowania:

package mainimport "log"func main() { log.Println("Hello world!")}

Kod powyżej drukuje tekst „Hello world!” do standardowego błędu, ale zawiera również datę i czas, co jest przydatne przy filtrowaniu komunikatów dziennika według daty.

2019/12/09 17:21:53 Hello world!

Domyślnie pakiet log drukuje do standardowego strumienia wyjściowego błędu (stderr), ale można sprawić, by pisał do plików lokalnych lub dowolnego miejsca docelowego, które obsługuje interfejs io.Writer. Dodaje również znacznik czasu do komunikatu dziennika bez dodatkowej konfiguracji.

Wypisywanie do pliku

Jeśli potrzebujesz przechowywać komunikaty dziennika w pliku, możesz to zrobić, tworząc nowy plik lub otwierając istniejący plik i ustawiając go jako wyjście dziennika. Oto przykład:

package mainimport ( "log" "os")func main() { // If the file doesn't exist, create it or append to the file file, err := os.OpenFile("logs.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666) if err != nil { log.Fatal(err) } log.SetOutput(file) log.Println("Hello world!")}

Gdy uruchomimy kod, do logs.txt.

2019/12/09 17:22:47 Hello world!

Jak wspomniano wcześniej, możesz w zasadzie wyprowadzać swoje logi do dowolnego miejsca docelowego, które implementuje interfejs io.Writer, więc masz dużo elastyczności przy decydowaniu, gdzie logować wiadomości w swojej aplikacji.

Tworzenie niestandardowych loggerów

Pomimo że pakiet log implementuje predefiniowany logger, który zapisuje do standardowego błędu, możemy tworzyć niestandardowe typy loggerów za pomocą metody log.New().

Podczas tworzenia nowego loggera należy przekazać trzy argumenty do log.New():

  • out: Dowolny typ implementujący interfejs io.Writer, czyli taki, do którego będą zapisywane dane dziennika
  • prefix: Ciąg znaków, który jest dołączany do początku każdego wiersza dziennika
  • flag: Zestaw stałych, które pozwalają nam określić, jakie właściwości logowania mają być zawarte w każdym wpisie generowanym przez logger (więcej na ten temat w następnym rozdziale)

Możemy wykorzystać tę cechę do tworzenia niestandardowych loggerów. Oto przykład, który implementuje loggery Info, Warning i Error:

package mainimport ( "log" "os")var ( WarningLogger *log.Logger InfoLogger *log.Logger ErrorLogger *log.Logger)func init() { file, err := os.OpenFile("logs.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666) if err != nil { log.Fatal(err) } InfoLogger = log.New(file, "INFO: ", log.Ldate|log.Ltime|log.Lshortfile) WarningLogger = log.New(file, "WARNING: ", log.Ldate|log.Ltime|log.Lshortfile) ErrorLogger = log.New(file, "ERROR: ", log.Ldate|log.Ltime|log.Lshortfile)}func main() { InfoLogger.Println("Starting the application...") InfoLogger.Println("Something noteworthy happened") WarningLogger.Println("There is something you should know about") ErrorLogger.Println("Something went wrong")}

Po utworzeniu lub otwarciu pliku logs.txt na początku funkcji init, inicjalizujemy trzy zdefiniowane loggery przez podanie miejsca docelowego, łańcucha prefiksu i flag logu.

W funkcji main rejestratory są wykorzystywane przez wywołanie funkcji Println, która zapisuje nowy wpis do pliku dziennika. Po uruchomieniu tego programu do pliku logs.txt zostanie zapisana następująca treść.

INFO: 2019/12/09 12:01:06 main.go:26: Starting the application...INFO: 2019/12/09 12:01:06 main.go:27: Something noteworthy happenedWARNING: 2019/12/09 12:01:06 main.go:28: There is something you should know aboutERROR: 2019/12/09 12:01:06 main.go:29: Something went wrong

Zauważ, że w tym przykładzie logujemy do pojedynczego pliku, ale możesz użyć oddzielnego pliku dla każdego loggera, przekazując inny plik podczas tworzenia loggera.

Flagi log

Możesz użyć stałych flag logu, aby wzbogacić komunikat logu o dodatkowe informacje kontekstowe, takie jak plik, numer wiersza, data i czas. Na przykład, przepuszczenie komunikatu „Coś poszło nie tak” przez logger z kombinacją flag pokazaną poniżej:

log.Ldate|log.Ltime|log.Lshortfile

wydrukuje

2019/12/09 12:01:06 main.go:29: Something went wrong

Niestety, nie ma kontroli nad kolejnością, w jakiej się pojawiają, ani nad formatem, w jakim są prezentowane.

Wprowadzanie frameworków logowania

Używanie pakietu log jest świetne dla lokalnego rozwoju, gdy uzyskanie szybkiej informacji zwrotnej jest ważniejsze niż generowanie bogatych, ustrukturyzowanych logów. Poza tym, najprawdopodobniej lepiej będzie użyć frameworka logowania.

Główną zaletą używania szkieletu logowania jest to, że pomaga on standaryzować dane logów. Oznacza to, że:

  • Łatwiej jest czytać i rozumieć dane dziennika.
  • Łatwiej jest zbierać dzienniki z kilku źródeł i podawać je do centralnej platformy w celu analizy.

Dodatkowo, logowanie jest całkiem dobrze rozwiązanym problemem. Po co wymyślać koło na nowo?

Wybór frameworka logowania

Decyzja, którego frameworka użyć, może być wyzwaniem, ponieważ istnieje kilka opcji do wyboru.

Dwa najpopularniejsze frameworki logowania dla Go wydają się beglog i logrus. Popularność gloga jest zaskakująca, ponieważ nie był on aktualizowany od kilku lat. logrus jest lepiej utrzymany i używany w popularnych projektach, takich jak Docker, więc na nim się skupimy.

Rozpoczęcie pracy z logrusem

Instalacja logrusa jest tak prosta, jak wykonanie poniższej komendy w terminalu:

go get "github.com/Sirupsen/logrus"

Jedną z najwspanialszych rzeczy w logrusie jest to, że jest całkowicie kompatybilny z pakietem log biblioteki standardowej, więc możesz wszędzie zastąpić import logów przez log "github.com/sirupsen/logrus" i po prostu będzie działać!

Zmodyfikujmy nasz wcześniejszy przykład „hello world”, który używał pakietu log i użyjmy zamiast niego logrus:

package mainimport ( log "github.com/sirupsen/logrus")func main() { log.Println("Hello world!")}

Wykonanie tego kodu daje wynik:

INFO Hello world!

Nie może być łatwiej!

Logowanie w JSON

logrus jest dobrze przystosowane do strukturalnego logowania w JSON, które – jako że JSON jest dobrze zdefiniowanym standardem – ułatwia zewnętrznym usługom parsowanie logów, a także sprawia, że dodawanie kontekstu do komunikatu dziennika jest stosunkowo proste dzięki użyciu pól, jak pokazano poniżej:

package mainimport ( log "github.com/sirupsen/logrus")func main() { log.SetFormatter(&log.JSONFormatter{}) log.WithFields( log.Fields{ "foo": "foo", "bar": "bar", }, ).Info("Something happened")}

Generowany log będzie obiektem JSON, który zawiera komunikat, poziom dziennika, znacznik czasu i dołączone pola.

{"bar":"bar","foo":"foo","level":"info","msg":"Something happened","time":"2019-12-09T15:55:24+01:00"}

Jeśli nie jesteś zainteresowany wyprowadzaniem swoich logów jako JSON, pamiętaj, że istnieje kilka formaterów firm trzecich dla logrusa, które możesz zobaczyć na jego stronie Github. Możesz nawet napisać swój własny formatter, jeśli wolisz.

Poziomy logów

W przeciwieństwie do standardowego pakietu logów, logrus obsługuje poziomy logów.

logrus ma siedem poziomów dziennika: Trace, Debug, Info, Warn, Error, Fatal i Panic. Powaga każdego poziomu wzrasta w miarę schodzenia w dół listy.

log.Trace("Something very low level.")log.Debug("Useful debugging information.")log.Info("Something noteworthy happened!")log.Warn("You should probably take a look at this.")log.Error("Something failed but I'm not quitting.")// Calls os.Exit(1) after logginglog.Fatal("Bye.")// Calls panic() after logginglog.Panic("I'm bailing.")

Ustawiając poziom logowania na loggerze, można logować tylko wpisy, które są potrzebne w zależności od środowiska. Domyślnie logrus rejestruje wszystko, co ma poziom Info lub wyższy (Warn, Error, Fatal lub Panic).

package mainimport ( log "github.com/sirupsen/logrus")func main() { log.SetFormatter(&log.JSONFormatter{}) log.Debug("Useful debugging information.") log.Info("Something noteworthy happened!") log.Warn("You should probably take a look at this.") log.Error("Something failed but I'm not quitting.")}

Wykonanie powyższego kodu spowoduje wyświetlenie następujących danych wyjściowych:

{"level":"info","msg":"Something noteworthy happened!","time":"2019-12-09T16:18:21+01:00"}{"level":"warning","msg":"You should probably take a look at this.","time":"2019-12-09T16:18:21+01:00"}{"level":"error","msg":"Something failed but I'm not quitting.","time":"2019-12-09T16:18:21+01:00"}

Zauważ, że komunikat poziomu Debug nie został wydrukowany. Aby włączyć go do logów, ustaw log.Level na równe log.DebugLevel:

log.SetLevel(log.DebugLevel)

Wrap up

W tym poście, zbadaliśmy użycie wbudowanego pakietu logów i ustaliliśmy, że powinien on być używany tylko dla trywialnych aplikacji lub podczas budowania szybkiego prototypu. Dla wszystkiego innego, użycie głównego nurtu logowania jest koniecznością.

Spojrzeliśmy również na sposoby zapewnienia, że informacje zawarte w twoich logach są spójne i łatwe do przeanalizowania, zwłaszcza gdy agregujesz je na scentralizowanej platformie.

Dzięki za przeczytanie!

Leave a Reply