Journalisation structurée en Go avec slog pour l'observabilité et l'alerting
Journaux JSON interrogeables connectés aux traces.
Les journaux (logs) sont une interface de débogage que vous pouvez encore utiliser lorsque le système est en feu. Le problème, c’est que les journaux texte brut vieillissent mal : dès que vous avez besoin de filtrage, d’agrégation et d’alertes, vous commencez à parser des phrases.

La journalisation structurée est l’antidote. Elle transforme chaque ligne de journal en un petit événement avec des champs stables, afin que les outils puissent rechercher et agréger de manière fiable. Pour comprendre comment les journaux s’intègrent aux métriques, aux tableaux de bord et aux alertes dans la pile globale, consultez le Guide d’Observabilité : Surveillance, Métriques, Prometheus & Grafana.
Qu’est-ce que la journalisation structurée et pourquoi elle s’adapte à l’échelle
La journalisation structurée est une forme de journalisation où un enregistrement n’est pas simplement une chaîne de caractères, mais un
message accompagné d’attributs de type clé-valeur. L’idée est ennuyeuse à la bonne manière :
dès que les journaux sont lisibles par machine, un incident n’est plus un concours de grep.
Une comparaison rapide :
Texte brut (priorité humain, hostile aux outils)
échec de la facturation de la carte utilisateur=42 montant=19.99 ms=842 err=timeout
Structuré (priorité outil, toujours lisible)
{"msg":"échec de la facturation de la carte","user_id":42,"amount":19.99,"duration_ms":842,"error":"timeout"}
En production, il est utile de considérer les journaux comme un flux d’événements émis par le processus, tandis que le routage et le stockage se trouvent en dehors de l’application. Ce modèle mental vous pousse à écrire un événement par ligne et à garder les événements faciles à expédier et à retraiter.
Slog dans Go comme interface de journalisation partagée
Go dispose du package log classique depuis toujours, mais les services modernes ont besoin
de niveaux et de champs. Le package log/slog (Go 1.21 et versions ultérieures) intègre la
journalisation structurée dans la bibliothèque standard et formalise une forme commune pour les
enregistrements de journal : heure, niveau, message et attributs.
Pour un rappel concis sur la langue et les commandes en parallèle de ce guide, consultez la Fiche de référence Go.
Les éléments clés du modèle sont :
Enregistrement (Record)
Un enregistrement est ce qui s’est produit. En termes de slog, il contient l’heure, le niveau, le message,
et un ensemble d’attributs. Vous créez des enregistrements via des méthodes comme Info et Error,
ou via Log lorsque vous souhaitez spécifier le niveau explicitement.
Attributs
Les attributs sont les paires clé-valeur qui rendent les journaux interrogeables. Si vous journalisez le
même concept sous trois clés différentes (user, userId, uid), vous obtenez trois
ensembles de données différents. Des clés cohérentes sont là où se cache la vraie valeur.
Gestionnaire (Handler)
Un gestionnaire est la manière dont les enregistrements deviennent des octets. Le gestionnaire TextHandler intégré écrit
une sortie clé=valeur, tandis que JSONHandler écrit du JSON délimité par des lignes. Les gestionnaires sont
aussi l’endroit où se produisent généralement la rédaction (redaction), le renommage des clés et le routage de la sortie.
Une fonctionnalité sous-estimée est que slog peut se placer devant le code existant. Lorsque
vous définissez un journal slog par défaut, les fonctions slog de niveau supérieur l’utilisent, et le package
log classique peut également être redirigé vers celui-ci. Cela rend la migration incrémentale
possible.
Groupes
Les groupes résolvent le problème « chaque sous-système utilise id ». Vous pouvez regrouper un ensemble d'
attributs pour une requête (request.method, request.path) ou nommer tout un sous-système avec WithGroup afin que les clés ne collisionnent pas.
Configuration slog adaptée à la production
La configuration suivante atteint les objectifs habituels.
Les exemples utilisent un petit package logx ; pour savoir où ce type de package se trouve généralement dans un module réel, consultez Structure de projet Go : Pratiques & Modèles.
- un événement JSON par ligne
- journaux écrits vers
stdoutpour la collecte - métadonnées de service stables attachées une seule fois
- journalisation consciente du contexte pour les ID de requête et de trace
- redaction centralisée pour les clés sensibles
package logx
import (
"log/slog"
"os"
)
var level slog.LevelVar // par défaut INFO
func New() *slog.Logger {
opts := &slog.HandlerOptions{
Level: &level, // peut être modifié à l'exécution
AddSource: true, // inclure le fichier et la ligne lorsque disponible
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
// Redaction centralisée : cohérente et difficile à contourner par accident.
switch a.Key {
case "password", "token", "authorization", "api_key":
return slog.String(a.Key, "[redacted]")
}
return a
},
}
h := slog.NewJSONHandler(os.Stdout, opts)
return slog.New(h).With(
"service", os.Getenv("SERVICE_NAME"),
"env", os.Getenv("ENV"),
"version", os.Getenv("VERSION"),
)
}
func SetLevel(l slog.Level) { level.Set(l) }
Un détail minuscule aux grandes conséquences : le gestionnaire JSON intégré utilise des
clés standard (time, level, msg, source). Lorsque votre backend de journalisation attend un
schéma différent, ReplaceAttr est la soupape de sécurité qui vous permet de normaliser les clés
sans réécrire les sites d’appel.
Le schéma compte plus que le journalier
La plupart des échecs de « journalisation structurée » sont des échecs de schéma.
Champs essentiels qui continuent de payer leur loyer
Tout backend de journalisation stockera un horodatage, un niveau et un message. En pratique, un schéma d’application utile ajoute souvent un petit ensemble de champs stables :
- service, env, version
- composant (ou sous-système)
- événement (un nom stable pour ce qui s’est produit)
- request_id (quand une requête existe)
- trace_id et span_id (quand le traçage existe)
- error (chaîne) et error_kind (bac stable)
Remarquez le modèle : ces champs répondent à des questions opérationnelles, pas à la curiosité des développeurs.
Les conventions sémantiques sont une astuce de cohérence peu coûteuse
Si vous utilisez déjà OpenTelemetry, ses conventions sémantiques fournissent un vocabulaire standard pour les attributs à travers les signaux de télémétrie. Même si vous n’exportez pas les journaux via OpenTelemetry, emprunter des noms d’attributs réduit la taxe « comment avons-nous appelé ce champ dans le service B ».
Haute cardinalité et pourquoi les journaux deviennent coûteux
La haute cardinalité signifie « trop de valeurs uniques ». C’est acceptable à l’intérieur d’un payload JSON, mais cela devient douloureux lorsqu’un backend traite certains champs comme des étiquettes indexées ou des clés de flux. Les ID d’utilisateur, les adresses IP, les jetons de requête aléatoires et les URL complètes ont tendance à faire exploser les combinaisons.
Le résultat pratique est simple : gardez les étiquettes et les clés d’indexation ennuyeuses (service, environnement, région), et gardez les champs à haute cardinalité à l’intérieur du payload structuré pour le filtrage au moment de la requête.
Corrélation avec les ID de requête et les traces
La corrélation est le point où les journaux cessent d’être simplement du texte et commencent à se comporter comme de la télémétrie.
ID de requête comme clé de corrélation à la friction la plus faible
Un ID de requête est le pont le plus simple entre une requête entrante et tout ce qui se produit à cause d’elle. Il fonctionne généralement même sans traçage distribué, et il reste utile lorsque les traces sont échantillonnées.
Un modèle courant consiste à attacher un journalier par requête au contexte :
package logx
import (
"context"
"log/slog"
)
type ctxKey struct{}
func WithLogger(ctx context.Context, l *slog.Logger) context.Context {
return context.WithValue(ctx, ctxKey{}, l)
}
func FromContext(ctx context.Context) *slog.Logger {
if l, ok := ctx.Value(ctxKey{}).(*slog.Logger); ok && l != nil {
return l
}
return slog.Default()
}
Corrélation de trace avec W3C Trace Context et OpenTelemetry
W3C Trace Context définit une méthode standard pour propager l’identité de trace (pour HTTP,
via traceparent et tracestate). OpenTelemetry s’appuie sur cela pour que les ID de trace et
les ID de span puissent être extraits du contexte.
Cet exemple de middleware journalise à la fois request_id et les identifiants de trace lorsque
disponibles :
package middleware
import (
"crypto/rand"
"encoding/hex"
"net/http"
"go.opentelemetry.io/otel/trace"
"log/slog"
"example.com/project/logx"
)
func requestID() string {
var b [16]byte
_, _ = rand.Read(b[:])
return hex.EncodeToString(b[:])
}
func WithRequestLogger(base *slog.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rid := r.Header.Get("X-Request-Id")
if rid == "" {
rid = requestID()
}
l := base.With(
"request_id", rid,
"method", r.Method,
"path", r.URL.Path,
)
if sc := trace.SpanContextFromContext(r.Context()); sc.IsValid() {
l = l.With(
"trace_id", sc.TraceID().String(),
"span_id", sc.SpanID().String(),
)
}
ctx := logx.WithLogger(r.Context(), l)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
Une fois que les champs de corrélation existent, la ligne de journal devient un index vers d’autres données. La différence lors d’un incident en direct n’est pas subtile.
Transformer les journaux structurés en signaux de surveillance et d’alerte
Les journaux sont excellents pour répondre à « qu’est-ce qui s’est passé ». Les alertes concernent généralement « à quelle fréquence et à quel point c’est grave ».
Une approche pratique consiste à traiter certains événements de journal comme des compteurs :
- event=payment_failed
- event=db_timeout
- event=cache_miss
De nombreuses plateformes peuvent dériver des métriques basées sur les journaux en comptant les enregistrements correspondants sur une fenêtre. Les journaux structurés rendent ce compte résilient, car il est basé sur une valeur de champ plutôt que sur une correspondance de texte fragile. Lorsque vous êtes prêt à visualiser et explorer ces signaux, Installer et utiliser Grafana sur Ubuntu : Guide complet vous guide à travers une configuration complète de Grafana que vous pouvez pointer vers des backends de journal et de métriques courants.
C’est aussi là que les niveaux de journal commencent à avoir de l’importance. Les journaux de débogage sont souvent précieux,
mais c’est aussi là que se cachent les coûts et le bruit. Utiliser un niveau dynamique (LevelVar)
permet au système de rester silencieux par défaut, tout en permettant un détail ciblé
lorsque nécessaire.
Pensées finales
La journalisation structurée dans Go n’est plus un débat de bibliothèque. La partie intéressante est de savoir si vos enregistrements de journal sont cohérents, corrélables et abordables à stocker.
Lorsque vos journaux transportent des champs stables comme event, request_id et trace_id, ils
cessent d’être des « chaînes écrites par quelqu’un » et deviennent un ensemble de données sur lequel vous pouvez opérer.
Notes
L’équipe Go a introduit log/slog dans Go 1.21 et a souligné que les journaux structurés
utilisent des paires clé-valeur afin qu’ils puissent être analysés, filtrés, recherchés et analysés
de manière fiable, et a également noté la motivation de fournir un cadre commun partagé
à travers l’écosystème.
La documentation du package log/slog définit le modèle d’enregistrement (heure, niveau,
message, paires clé-valeur) et les gestionnaires intégrés (TextHandler pour clé=valeur
et JSONHandler pour JSON délimité par des lignes), et documente l’intégration SetDefault
avec le package log classique.
Pour la corrélation distribuée, la spécification W3C Trace Context standardise
la propagation traceparent et tracestate, et OpenTelemetry spécifie que son
SpanContext est conforme à W3C Trace Context et expose TraceId et SpanId,
rendant la corrélation journal-trace simple lorsqu’un span est présent.
Pour le coût de stockage et la performance des journaux, la documentation de Grafana Loki recommande fortement des étiquettes bornées et statiques et avertit que les étiquettes à haute cardinalité créent trop de flux et un index énorme, ce qui est directement pertinent lors de la décision de ce qui devient une étiquette versus ce qui reste comme un champ JSON non indexé.