Comment collecter, standardiser et centraliser les logs Golang

tip / logs /golang /microservices

Les organisations qui dépendent de systèmes distribués écrivent souvent leurs applications en Go pour profiter des fonctionnalités de concurrence comme les canaux et les goroutines (par ex, Heroku, Basecamp, Cockroach Labs et Datadog). Si vous êtes responsable de la construction ou du support des applications Go, une stratégie de journalisation bien pensée peut vous aider à comprendre le comportement des utilisateurs, à localiser les erreurs et à surveiller les performances de vos applications.

Ce post vous montrera quelques outils et techniques pour gérer les journaux Golang. Nous commencerons par la question de savoir quel paquet de journalisation utiliser pour différents types d’exigences. Ensuite, nous expliquerons certaines techniques pour rendre vos journaux plus consultables et fiables, réduire l’empreinte des ressources de votre configuration de journalisation, et standardiser vos messages de journal.

Connaître votre paquet de journalisation

Go vous donne une richesse d’options lors du choix d’un paquet de journalisation, et nous allons explorer plusieurs d’entre eux ci-dessous. Alors que logrus est la plus populaire des bibliothèques que nous couvrons, et vous aide à mettre en œuvre un format de journalisation cohérent, les autres ont des cas d’utilisation spécialisés qui méritent d’être mentionnés. Cette section examinera les bibliothèques log, logrus, et glog.

Utiliser log pour la simplicité

La bibliothèque de journalisation intégrée de Golang, appelée log, est livrée avec un logger par défaut qui écrit sur l’erreur standard et ajoute un horodatage sans avoir besoin de configuration. Vous pouvez utiliser ces journaux bruts et prêts pour le développement local, lorsque l’obtention d’un retour rapide de votre code peut être plus important que la génération de journaux riches et structurés.

Par exemple, vous pouvez définir une fonction de division qui renvoie une erreur à l’appelant, plutôt que de quitter le programme, lorsque vous tentez de diviser par zéro.

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

Parce que notre exemple divise par zéro, il produira le message de journal suivant:

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

Utiliser logrus pour des journaux formatés

Nous recommandons d’écrire les journaux Golang en utilisant logrus, un paquet de journalisation conçu pour la journalisation structurée qui est bien adapté à la journalisation en JSON. Le format JSON permet aux machines d’analyser facilement vos journaux Golang. Et puisque JSON est un standard bien défini, il rend simple l’ajout de contexte en incluant de nouveaux champs – un analyseur syntaxique devrait pouvoir les capter automatiquement.

En utilisant logrus, vous pouvez définir des champs standard à ajouter à vos journaux JSON en utilisant la fonction WithFields, comme indiqué ci-dessous. Vous pouvez ensuite faire des appels au logger à différents niveaux, comme Info(), Warn() et Error(). La bibliothèque logrus écrira automatiquement le journal au format JSON et insérera les champs standard, ainsi que tous les champs que vous avez définis à la volée.

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

Le journal résultant comprendra le message, le niveau du journal, l’horodatage et les champs standard dans un objet 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"}

Utiliser glog si vous êtes préoccupé par le volume

Certaines bibliothèques de journalisation vous permettent d’activer ou de désactiver la journalisation à des niveaux spécifiques, ce qui est utile pour garder le volume du journal sous contrôle lors du passage du développement à la production. Une telle bibliothèque est glog, qui vous permet d’utiliser des drapeaux à la ligne de commande (par exemple, -v pour la verbosité) pour définir le niveau de journalisation lorsque vous exécutez votre code. Vous pouvez ensuite utiliser une fonction V() dans les déclarations if pour écrire vos journaux Golang uniquement à un certain niveau de journalisation.

Par exemple, vous pouvez utiliser glog pour écrire la même erreur « Can’t divide by zero » de tout à l’heure, mais seulement si vous enregistrez au niveau de verbosité de 2. Vous pouvez définir la verbosité à n’importe quel entier 32 bits signé, ou utiliser les fonctions Info(), Warning(), Error() et Fatal() pour attribuer les niveaux de verbosité 0 à 3 (respectivement).

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

Vous pouvez rendre votre application moins gourmande en ressources en ne journalisant que certains niveaux en production. En même temps, s’il n’y a pas d’impact sur les utilisateurs, c’est souvent une bonne idée de journaliser autant d’interactions avec votre application que possible, puis d’utiliser un logiciel de gestion des journaux comme Datadog pour trouver les données dont vous avez besoin pour votre enquête

Best practices for writing and storing Golang logs

Une fois que vous avez choisi une bibliothèque de journalisation, vous voudrez également planifier où dans votre code faire des appels au logger, comment stocker vos journaux, et comment leur donner un sens. Dans cette section, nous recommanderons une série de bonnes pratiques pour organiser vos journaux Golang :

  • Faites des appels au logger à partir de votre processus d’application principal, pas à l’intérieur des goroutines.
  • Écrivez les journaux de votre application dans un fichier local, même si vous les expédierez plus tard vers une plateforme centrale.
  • Normalisez vos journaux avec un ensemble de messages prédéfinis.
  • Envoyez vos journaux à une plateforme centrale pour pouvoir les analyser et les agréger.
  • Utilisez les en-têtes HTTP et les identifiants uniques pour consigner le comportement des utilisateurs à travers les microservices.

Évitez de déclarer des goroutines pour la journalisation

Il y a deux raisons d’éviter de créer vos propres goroutines pour gérer l’écriture des journaux. Premièrement, cela peut entraîner des problèmes de concurrence, car des doublons du logger tenteraient d’accéder au même io.Writer. Deuxièmement, les bibliothèques de journalisation démarrent généralement elles-mêmes des goroutines, gérant tout problème de concurrence en interne, et le démarrage de vos propres goroutines ne fera qu’interférer.

Écrire vos journaux dans un fichier

Même si vous expédiez vos journaux vers une plateforme centrale, nous vous recommandons de les écrire d’abord dans un fichier sur votre machine locale. Vous voudrez vous assurer que vos journaux sont toujours disponibles localement et ne sont pas perdus dans le réseau. En outre, l’écriture dans un fichier signifie que vous pouvez découpler la tâche d’écriture de vos journaux de la tâche d’envoi à une plate-forme centrale. Vos applications elles-mêmes n’auront pas besoin d’établir des connexions ou de diffuser vos journaux, et vous pouvez laisser ces tâches à des logiciels spécialisés comme l’agent Datadog. Si vous exécutez vos applications Go au sein d’une infrastructure conteneurisée qui n’inclut pas déjà un stockage persistant – par exemple, des conteneurs exécutés sur AWS Fargate – vous pouvez configurer votre outil de gestion des journaux pour collecter les journaux directement à partir des flux STDOUT et STDERR de vos conteneurs (ceci est géré différemment dans Docker et Kubernetes).

Mettre en place une interface de journalisation standard

Lorsqu’elles écrivent des appels aux loggers depuis leur code, les équipes utilisent souvent différents noms d’attributs pour décrire la même chose. Des attributs incohérents peuvent confondre les utilisateurs et rendre impossible la corrélation de journaux qui devraient faire partie de la même image. Par exemple, deux développeurs peuvent consigner la même erreur, un nom de client manquant lors de la gestion d’un téléchargement, de différentes manières.

Golang consigne la même erreur avec des messages différents provenant de différents emplacements.
Golang consigne la même erreur avec des messages différents provenant de différents emplacements.

Une bonne façon de faire respecter la normalisation est de créer une interface entre votre code d’application et la bibliothèque de consignation. L’interface contient des messages de journalisation prédéfinis qui mettent en œuvre un certain format, ce qui facilite l’investigation des problèmes en garantissant que les messages de journalisation peuvent être recherchés, regroupés et filtrés.

Golang consigne une erreur en utilisant une interface standard pour créer un message cohérent.
Golang consigne une erreur en utilisant une interface standard pour créer un message cohérent.

Dans cet exemple, nous allons déclarer un type Event avec un message prédéfini. Ensuite, nous utiliserons les messages Event pour faire des appels à un logger. Les coéquipiers peuvent écrire des journaux Golang en fournissant une quantité minimale d’informations personnalisées, laissant l’application faire le travail d’implémentation d’un format standard.

D’abord, nous allons écrire un paquet logwrapper que les développeurs peuvent inclure dans leur code.

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

Pour utiliser notre interface de journalisation, il suffit de l’inclure dans notre code et de faire des appels à une instance de 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")}

Lorsque nous exécutons notre code, nous obtiendrons le journal JSON suivant:

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

Centraliser les journaux Golang

Si votre application est déployée sur un cluster d’hôtes, il n’est pas viable de se connecter en SSH à chacun d’entre eux afin de tailer, grep, et d’enquêter sur vos journaux. Une alternative plus évolutive consiste à transmettre les journaux des fichiers locaux à une plateforme centrale.

Une solution consiste à utiliser le paquet syslog de Golang pour transmettre les journaux de toute votre infrastructure à un seul serveur syslog.

Personnalisez et rationalisez votre gestion des journaux Golang avec Datadog.

Une autre solution consiste à utiliser une solution de gestion des journaux. Datadog, par exemple, peut faire la queue de vos fichiers journaux et transmettre les journaux à une plateforme centrale pour le traitement et l’analyse.

La vue Datadog Log Explorer peut afficher les journaux Golang provenant de diverses sources.

Vous pouvez utiliser des attributs pour représenter graphiquement les valeurs de certains champs de journaux au fil du temps, triés par groupe. Par exemple, vous pouvez suivre le nombre d’erreurs par service pour vous indiquer s’il y a un incident dans l’un de vos services. En montrant les journaux du seul service go-logging-demo, nous pouvons voir combien de journaux d’erreurs ce service a produit dans un intervalle donné.

Groupement des journaux Golang par état.

Vous pouvez également utiliser les attributs pour approfondir les causes possibles, par exemple voir si un pic de journaux d’erreurs appartient à un hôte spécifique. Vous pouvez ensuite créer une alerte automatisée basée sur les valeurs de vos journaux.

Tracer les journaux Golang à travers les microservices

Lorsque vous dépannez une erreur, il est souvent utile de voir quel modèle de comportement y a conduit, même si ce comportement implique un certain nombre de microservices. Vous pouvez y parvenir grâce au traçage distribué, en visualisant l’ordre dans lequel votre application exécute des fonctions, des requêtes de base de données et d’autres tâches, et en suivant ces étapes d’exécution au fur et à mesure qu’elles font leur chemin dans un réseau. Une façon de mettre en œuvre le traçage distribué au sein de vos journaux est de transmettre des informations contextuelles en tant qu’en-têtes HTTP.

Dans cet exemple, un microservice reçoit une requête et vérifie la présence d’un ID de trace dans l’en-tête x-trace, en générant un s’il n’existe pas. Lorsqu’on fait une requête à un autre microservice, on génère alors un nouveau spanID – pour cette requête et pour chaque requête – et on l’ajoute à l’en-tête 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)}

Les microservices en aval utilisent les en-têtes x-span des requêtes entrantes pour spécifier les parents des spans qu’ils génèrent, et envoient cette information comme en-tête x-parent au microservice suivant dans la chaîne.

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

Si une erreur se produit dans l’un de nos microservices, nous pouvons utiliser les attributs trace, parent et span pour voir le chemin qu’une requête a pris, ce qui nous permet de savoir quels hôtes – et éventuellement quelles parties du code de l’application – il faut examiner.

Dans le premier microservice:

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

Dans le second:

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

Si vous voulez creuser plus profondément dans les possibilités de traçage de Golang, vous pouvez utiliser une bibliothèque de traçage comme OpenTracing ou une plateforme de surveillance qui prend en charge le traçage distribué pour les applications Go. Par exemple, Datadog peut construire automatiquement une carte des services à l’aide des données de sa bibliothèque de traçage Golang, visualiser les tendances de vos traces dans le temps et vous signaler les services présentant des taux de demandes, des taux d’erreurs ou des temps de latence inhabituels.

Un exemple de visualisation montrant des traces de demandes entre microservices.
Un exemple de visualisation montrant des traces de requêtes entre microservices.

Des logs Golang propres et complets

Dans ce billet, nous avons mis en évidence les avantages et les inconvénients de plusieurs bibliothèques de logs Go. Nous avons également recommandé des moyens de s’assurer que vos logs sont disponibles et accessibles lorsque vous en avez besoin, et que les informations qu’ils contiennent sont cohérentes et faciles à analyser.

Pour commencer à analyser tous vos logs Go avec Datadog, inscrivez-vous à un essai gratuit.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.