Come raccogliere, standardizzare e centralizzare i log di Golang

tip / logs /golang /microservices

Le organizzazioni che dipendono dai sistemi distribuiti spesso scrivono le loro applicazioni in Go per sfruttare le caratteristiche di concorrenza come i canali e le goroutine (es, Heroku, Basecamp, Cockroach Labs, e Datadog). Se siete responsabili della costruzione o del supporto di applicazioni Go, una strategia di log ben ponderata può aiutarvi a capire il comportamento degli utenti, localizzare gli errori e monitorare le prestazioni delle vostre applicazioni.

Questo post vi mostrerà alcuni strumenti e tecniche per gestire i log di Golang. Inizieremo con la questione di quale pacchetto di log usare per diversi tipi di requisiti. Successivamente, spiegheremo alcune tecniche per rendere i tuoi log più ricercabili e affidabili, riducendo l’impronta delle risorse della tua configurazione di log e standardizzando i tuoi messaggi di log.

Conosci il tuo pacchetto di log

Go ti dà una ricchezza di opzioni quando scegli un pacchetto di log, e di seguito ne esploreremo diverse. Mentre logrus è la più popolare delle librerie che copriamo e ti aiuta a implementare un formato di log coerente, le altre hanno casi d’uso specializzati che vale la pena menzionare. Questa sezione esaminerà le librerie log, logrus e glog.

Usa log per semplicità

La libreria di log integrata in Golang, chiamata log, è dotata di un logger predefinito che scrive su errore standard e aggiunge un timestamp senza bisogno di configurazione. Potete usare questi log grezzi e pronti per lo sviluppo locale, quando ottenere un feedback veloce dal vostro codice può essere più importante che generare log ricchi e strutturati.

Per esempio, potete definire una funzione di divisione che restituisca un errore al chiamante, piuttosto che uscire dal programma, quando si tenta di dividere per 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)}

Perché il nostro esempio divide per zero, produrrà il seguente messaggio di log:

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

Utilizzare logrus per log formattati

Si consiglia di scrivere i log di Golang usando logrus, un pacchetto di log progettato per il logging strutturato che si adatta bene ai log in JSON. Il formato JSON rende possibile alle macchine di analizzare facilmente i vostri log di Golang. E poiché JSON è uno standard ben definito, rende semplice aggiungere contesto includendo nuovi campi – un parser dovrebbe essere in grado di raccoglierli automaticamente.

Utilizzando logrus, puoi definire campi standard da aggiungere ai tuoi log JSON usando la funzione WithFields, come mostrato sotto. Puoi poi fare chiamate al logger a diversi livelli, come Info(), Warn() e Error(). La libreria logrus scriverà il log come JSON automaticamente e inserirà i campi standard, insieme a qualsiasi campo definito al volo.

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

Il log risultante includerà il messaggio, il livello di log, il timestamp e i campi standard in un oggetto 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"}

Utilizza glog se sei preoccupato del volume

Alcune librerie di log ti permettono di abilitare o disabilitare il log a livelli specifici, il che è utile per tenere sotto controllo il volume dei log quando ti sposti tra sviluppo e produzione. Una di queste librerie è glog, che ti permette di usare dei flag sulla linea di comando (ad esempio, -v per la verbosità) per impostare il livello di log quando esegui il tuo codice. Potete poi usare una funzione V() nelle dichiarazioni if per scrivere i vostri log di Golang solo ad un certo livello di log.

Per esempio, potete usare glog per scrivere lo stesso errore “Can’t divide by zero” di prima, ma solo se state loggando al livello di verbosità 2. Puoi impostare la verbosità su qualsiasi intero a 32 bit firmato, o usare le funzioni Info(), Warning(), Error() e Fatal() per assegnare i livelli di verbosità da 0 a 3 (rispettivamente).

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

Puoi rendere la tua applicazione meno impegnativa in termini di risorse loggando solo certi livelli in produzione. Allo stesso tempo, se non c’è impatto sugli utenti, è spesso una buona idea registrare quante più interazioni possibili con la tua applicazione, poi usare un software di gestione dei log come Datadog per trovare i dati di cui hai bisogno per la tua indagine

Migliori pratiche per scrivere e memorizzare i log di Golang

Una volta che hai scelto una libreria di log, vorrai anche pianificare dove nel tuo codice fare chiamate al logger, come memorizzare i tuoi log e come dargli un senso. In questa sezione, raccomanderemo una serie di buone pratiche per organizzare i vostri log di Golang:

  • Fate le chiamate al logger dall’interno del processo principale della vostra applicazione, non all’interno delle goroutine.
  • Scrivete i log della vostra applicazione in un file locale, anche se li spedirete successivamente a una piattaforma centrale.
  • Standardizzate i vostri log con un insieme di messaggi predefiniti.
  • Invia i tuoi log a una piattaforma centrale in modo da poterli analizzare e aggregare.
  • Utilizza le intestazioni HTTP e gli ID unici per registrare il comportamento degli utenti nei microservizi.

Evitare di dichiarare goroutine per i log

Ci sono due motivi per evitare di creare le proprie goroutine per gestire la scrittura dei log. Primo, può portare a problemi di concorrenza, poiché i duplicati del logger tenterebbero di accedere allo stesso io.Writer. In secondo luogo, le librerie di log di solito avviano esse stesse delle goroutine, gestendo internamente qualsiasi problema di concorrenza, e avviare le proprie goroutine non farà altro che interferire.

Scrivi i tuoi log su un file

Anche se stai spedendo i tuoi log a una piattaforma centrale, ti consigliamo di scriverli prima su un file sulla tua macchina locale. Vorrai assicurarti che i tuoi log siano sempre disponibili localmente e non si perdano nella rete. Inoltre, scrivere su un file significa che puoi disaccoppiare il compito di scrivere i tuoi log dal compito di inviarli a una piattaforma centrale. Le vostre applicazioni stesse non avranno bisogno di stabilire connessioni o trasmettere i vostri log, e potrete lasciare questi lavori a software specializzati come l’agente Datadog. Se stai eseguendo le tue applicazioni Go all’interno di un’infrastruttura containerizzata che non include già l’archiviazione persistente – ad esempio, i container in esecuzione su AWS Fargate – potresti voler configurare il tuo strumento di gestione dei log per raccogliere i log direttamente dai flussi STDOUT e STDERR dei tuoi container (questo è gestito diversamente in Docker e Kubernetes).

Implementare un’interfaccia standard di logging

Quando scrivi le chiamate ai logger dall’interno del loro codice, i team spesso usano nomi di attributi diversi per descrivere la stessa cosa. Attributi incoerenti possono confondere gli utenti e rendere impossibile correlare log che dovrebbero far parte dello stesso quadro. Per esempio, due sviluppatori potrebbero registrare lo stesso errore, un nome client mancante durante la gestione di un upload, in modi diversi.

Golang registra lo stesso errore con messaggi diversi da posizioni diverse.
Golang registra lo stesso errore con messaggi diversi da posizioni diverse.

Un buon modo per imporre la standardizzazione è creare un’interfaccia tra il codice dell’applicazione e la libreria di log. L’interfaccia contiene messaggi di log predefiniti che implementano un certo formato, rendendo più facile investigare i problemi assicurando che i messaggi di log possano essere cercati, raggruppati e filtrati.

Golang registra un errore usando un'interfaccia standard per creare un messaggio coerente.
Golang registra un errore usando un’interfaccia standard per creare un messaggio coerente.

In questo esempio, dichiareremo un tipo Event con un messaggio predefinito. Poi useremo i messaggi Event per fare chiamate a un logger. I team possono scrivere i log di Golang fornendo una quantità minima di informazioni personalizzate, lasciando che l’applicazione faccia il lavoro di implementare un formato standard.

Prima di tutto, scriveremo un pacchetto logwrapper che gli sviluppatori possono includere nel loro codice.

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

Per usare la nostra interfaccia di log, dobbiamo solo includerla nel nostro codice e fare chiamate a un’istanza di 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")}

Quando eseguiamo il nostro codice, otterremo il seguente log JSON:

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

Centralizzare i log di Golang

Se la vostra applicazione è distribuita su un cluster di host, non è sostenibile entrare con SSH in ognuno di essi per fare tail, grep e investigare i vostri log. Un’alternativa più scalabile è quella di passare i log dai file locali a una piattaforma centrale.

Una soluzione è quella di utilizzare il pacchetto syslog di Golang per inoltrare i log da tutta la vostra infrastruttura a un singolo server syslog.

Personalizza e ottimizza la gestione dei log di Golang con Datadog.

Un’altra è quella di utilizzare una soluzione di gestione dei log. Datadog, per esempio, può seguire i vostri file di log e inoltrare i log a una piattaforma centrale per l’elaborazione e l’analisi.

La vista Datadog Log Explorer può mostrare i log di Golang da varie fonti.

Si possono usare gli attributi per tracciare i valori di certi campi di log nel tempo, ordinati per gruppo. Per esempio, si potrebbe tracciare il numero di errori per service per farvi sapere se c’è un incidente in uno dei vostri servizi. Mostrando i log del solo servizio go-logging-demo, possiamo vedere quanti log di errore questo servizio ha prodotto in un dato intervallo.

Raggruppando i log di Golang per stato.

Puoi anche usare gli attributi per approfondire le possibili cause, per esempio vedere se un picco nei log di errore appartiene a un host specifico. Puoi quindi creare un allarme automatico basato sui valori dei tuoi log.

Traccia i log di Golang attraverso i microservizi

Quando risolvi un errore, è spesso utile vedere quale schema di comportamento lo ha portato, anche se questo comportamento coinvolge un certo numero di microservizi. È possibile ottenere questo risultato con il tracing distribuito, visualizzando l’ordine in cui l’applicazione esegue funzioni, query al database e altri compiti, e seguendo questi passi di esecuzione mentre si fanno strada attraverso una rete. Un modo per implementare il tracing distribuito nei log è quello di passare informazioni contestuali come intestazioni HTTP.

In questo esempio, un microservizio riceve una richiesta e verifica la presenza di un ID di traccia nell’intestazione x-trace, generandone uno se non esiste. Quando si fa una richiesta a un altro microservizio, generiamo quindi un nuovo spanID – per questa e per ogni richiesta – e lo aggiungiamo all’intestazione 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)}

I microservizi a valle usano le intestazioni x-span delle richieste in arrivo per specificare i genitori degli span che generano, e inviano queste informazioni come intestazione x-parent al microservizio successivo nella catena.

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

Se si verifica un errore in uno dei nostri microservizi, possiamo usare gli attributi trace, parent, e span per vedere il percorso che una richiesta ha preso, permettendoci di sapere quali host – e possibilmente quali parti del codice dell’applicazione – indagare.

Nel primo microservizio:

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

Nel secondo:

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

Se volete scavare più a fondo nelle possibilità di tracciamento di Golang, potete usare una libreria di tracciamento come OpenTracing o una piattaforma di monitoraggio che supporti il tracciamento distribuito per applicazioni Go. Per esempio, Datadog può costruire automaticamente una mappa dei servizi usando i dati della sua libreria di tracing di Golang; visualizzare le tendenze nelle vostre tracce nel tempo; e farvi sapere dei servizi con tassi di richiesta, tassi di errore o latenza insoliti.

Un esempio di visualizzazione che mostra le tracce delle richieste tra microservizi.
Un esempio di visualizzazione che mostra le tracce delle richieste tra microservizi.

Registri Golang puliti e completi

In questo post, abbiamo evidenziato i benefici e i compromessi di diverse librerie di log di Go. Abbiamo anche raccomandato modi per assicurare che i vostri log siano disponibili e accessibili quando ne avete bisogno, e che le informazioni che contengono siano coerenti e facili da analizzare.

Per iniziare ad analizzare tutti i vostri log di Go con Datadog, iscrivetevi per una prova gratuita.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.