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

La journalisation structurée est l’antidote. Elle transforme chaque ligne de journal en un petit événement avec des champs stables, permettant aux outils de rechercher et d’agréger de manière fiable. Pour comprendre comment les journaux se connectent 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 est évolutive
La journalisation structurée est une forme de journalisation où un enregistrement n’est pas seulement une chaîne de caractères, mais un message accompagné d’attributs clé-valeur typés. L’idée est ennuyeuse de la meilleure manière possible : une fois que les journaux sont lisibles par machine, un incident cesse d’être une compétition de grep.
Une comparaison rapide :
Texte brut (priorité à l’humain, hostile aux outils)
failed to charge card user=42 amount=19.99 ms=842 err=timeout
Structuré (priorité aux outils, tout en restant lisible)
{"msg":"failed to charge card","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 résident 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 en Go comme interface de journalisation partagée
Go possède le 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) apporte la journalisation structurée dans la bibliothèque standard et formalise une forme commune pour les enregistrements de journaux : heure, niveau, message et attributs. Pour un résumé compact du langage et des 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 passé. Dans les 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 fournir 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. La vraie valeur se cache dans la cohérence des clés.
Gestionnaire (Handler)
Un gestionnaire est la façon dont les enregistrements deviennent des octets. Le gestionnaire intégré TextHandler écrit une sortie clé=valeur, tandis que JSONHandler écrit du JSON délimité par des lignes. Les gestionnaires sont aussi l’endroit où la réfaction, le renommage des clés et le routage de sortie ont tendance à se produire.
Une fonctionnalité sous-estimée est que slog peut se placer devant du code existant. Lorsque vous définissez un enregistreur slog par défaut, les fonctions slog de niveau supérieur l’utilisent, et le package log classique peut aussi y être redirigé. Cela rend la migration incrémentielle possible.
Groupes
Les groupes résolvent le problème de “chaque sous-système utilise id”. Vous pouvez regrouper un ensemble d’attributs pour une requête (request.method, request.path) ou nommer un sous-système entier avec WithGroup afin que les clés ne se chevauchent pas.
Une configuration slog pour la production
La configuration suivante atteint les objectifs habituels.
Les exemples utilisent un petit package logx ; pour savoir où ces packages résident généralement dans un module réel, consultez Structure de projet Go : Pratiques & Modèles.
- un événement JSON par ligne
- journaux écrits sur stdout pour la collecte
- métadonnées de service stables attachées une seule fois
- journalisation consciente du contexte pour les IDs de requête et de trace
- réfaction 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 quand disponible
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
// Réfaction 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 mineur avec de grandes conséquences : le gestionnaire JSON intégré utilise des clés standards (time, level, msg, source). Lorsque votre backend de journaux attend un schéma différent, ReplaceAttr est la soupape de décompression qui vous permet de normaliser les clés sans réécrire les sites d’appel.
Le schéma importe plus que l’enregistreur
La plupart des échecs de “journalisation structurée” sont des échecs de schéma.
Les champs essentiels qui continuent à payer leur loyer
Chaque backend de journaux 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
- component (ou subsystem)
- event (un nom stable pour l’événement 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 (catégorie stable)
Remarquez le schéma : 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’une charge utile JSON, mais cela devient douloureux lorsqu’un backend traite certains champs comme des labels indexés ou des clés de flux. Les IDs utilisateur, les adresses IP, les jetons de requête aléatoires et les URLs complètes ont tendance à exploser les combinaisons.
Le résultat pratique est simple : gardez les labels et les clés d’index ennuyeux (service, environnement, région), et gardez les champs à haute cardinalité à l’intérieur de la charge utile structurée pour le filtrage au moment de l’interrogation.
Corrélation avec les IDs de requête et les traces
La corrélation est le point où les journaux cessent d’être du simple texte et commencent à se comporter comme de la télémétrie.
L’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 passe à cause d’elle. Il fonctionne souvent même sans traçage distribué, et il reste utile lorsque les traces sont échantillonnées. Pour une vue complète de la manière dont les IDs de requête et autres métadonnées doivent être stockés et récupérés depuis le contexte — y compris les modèles de clés typées et les exemples de middleware — consultez Go context.Context Bien Fait.
Un modèle courant consiste à attacher un enregistreur 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 traces avec W3C Trace Context et OpenTelemetry
W3C Trace Context définit une manière standard de propager l’identité de trace (pour HTTP, via traceparent et tracestate). OpenTelemetry s’appuie sur cela afin que les IDs de trace et les IDs de span puissent être extraits du contexte.
Cet exemple de middleware journalise à la fois request_id et les identifiants de trace lorsqu’ils sont 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 à “que s’est-il 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 comptage 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 détaille une configuration complète de Grafana que vous pouvez pointer vers des backends de journaux et de métriques courants.
C’est aussi là que les niveaux de journal commencent à prendre de l’importance. Les journaux de débogage sont souvent précieux, mais c’est aussi là que se cachent le coût et le bruit. L’utilisation d’un niveau dynamique (LevelVar) permet au système de rester silencieux par défaut, tout en permettant des détails ciblés lorsqu’ils sont nécessaires.
Pensées finales
La journalisation structurée en Go n’est plus un débat de bibliothèque. La partie intéressante est de savoir si vos enregistrements de journaux sont cohérents, corrélables et abordables à stocker.
Lorsque vos journaux portent des champs stables comme event, request_id et trace_id, ils cessent d’être des “chaînes écrites par quelqu’un” et commencent à être 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 de 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 et les performances du stockage des journaux, la documentation de Grafana Loki recommande fortement des labels bornés et statiques et met en garde contre les labels à haute cardinalité créant trop de flux et un index énorme, ce qui est directement pertinent lors de la décision de ce qui devient un label versus ce qui reste comme un champ JSON non indexé.