Générer des traces et des métriques avec Go

Ce document explique comment modifier une application Go pour collecter des données de trace et de métrique à l'aide du framework Open Source OpenTelemetry et comment écrire des journaux JSON structurés afin d'utiliser la sortie standard. Il fournit également des informations sur un exemple d'application que vous pouvez installer et exécuter. L'application est configurée pour générer des métriques, des traces et des journaux.

À propos du contexte

Le contexte d'OpenTelemetry est un mécanisme permettant de transmettre des valeurs à l'échelle de l'exécution à travers les API d'un processus. Une utilisation importante du contexte consiste à porter le délai actif actuel afin qu'il puisse être modifié ou référencé en tant que parent de nouveaux délais lors de leur création. En résumé :

  • Le contexte fait référence au mécanisme permettant de propager les valeurs à l'échelle de l'exécution, y compris le délai actif actuel, entre les API d'un processus.

  • Le contexte de délai est un objet immuable sur chaque délai, qui inclut l'ID de trace, l'ID de délai, ainsi que les options et l'état de la trace.

  • La propagation est le mécanisme qui transfère le contexte entre les services et les processus.

Le fichier context.Context de la bibliothèque standard Go contient également des valeurs limitées au-delà des limites de l'API. En règle générale, les fonctions de gestionnaire d'un serveur reçoivent un Context entrant et le transmettent via la chaîne d'appel à tous les clients effectuant des requêtes sortantes.

La bibliothèque standard context.Context de Go est utilisée pour la mise en œuvre du contexte OpenTelemetry dans Go.

Avant de commencer

Activer les API Cloud Logging API, Cloud Monitoring API, and Cloud Trace API.

Activer les API

Instrumenter votre application pour collecter des traces, des métriques et des journaux

Pour instrumenter votre application afin de collecter des données de trace et de métrique, et d'écrire un code JSON structuré pour la sortie standard, procédez comme suit, comme décrit dans les sections suivantes de ce document:

  1. Configurer la fonction principale
  2. Configurer OpenTelementry
  3. Configurer la journalisation structurée
  4. Ajouter une instrumentation au serveur HTTP
  5. Associer les délais de trace aux journaux et aux métriques
  6. Ajouter une instrumentation au client HTTP
  7. Écrire des journaux structurés

Configurer la fonction principale

Pour configurer l'application afin qu'elle écrive des journaux structurés et pour collecter des métriques et des données de trace à l'aide d'OpenTelemetry, mettez à jour la fonction main pour configurer le package de journalisation structurée Go, slog, et pour configurer OpenTelemetry.

L'exemple de code suivant illustre une fonction main qui appelle deux fonctions d'assistance, setupLogging() et setupOpenTelemetry(). Ces fonctions d'assistance configurent le package de journalisation et OpenTelemetry:

func main() {
	ctx := context.Background()

	// Setup logging
	setupLogging()

	// Setup metrics, tracing, and context propagation
	shutdown, err := setupOpenTelemetry(ctx)
	if err != nil {
		slog.ErrorContext(ctx, "error setting up OpenTelemetry", slog.Any("error", err))
		os.Exit(1)
	}

	// Run the http server, and shutdown and flush telemetry after it exits.
	slog.InfoContext(ctx, "server starting...")
	if err = errors.Join(runServer(), shutdown(ctx)); err != nil {
		slog.ErrorContext(ctx, "server exited with error", slog.Any("error", err))
		os.Exit(1)
	}
}

Après avoir configuré le package de journalisation, pour associer vos journaux à vos données de trace, vous devez transmettre le Context Go à l'enregistreur. Pour en savoir plus, consultez la section Écrire des journaux structurés de ce document.

Configurer OpenTelementry

Pour collecter et exporter des traces et des métriques à l'aide du protocole OTLP, configurez les instances globales TracerProvider et MeterProvider. L'exemple de code suivant illustre la fonction setupOpenTelemetry, qui est appelée à partir de la fonction main:

func setupOpenTelemetry(ctx context.Context) (shutdown func(context.Context) error, err error) {
	var shutdownFuncs []func(context.Context) error

	// shutdown combines shutdown functions from multiple OpenTelemetry
	// components into a single function.
	shutdown = func(ctx context.Context) error {
		var err error
		for _, fn := range shutdownFuncs {
			err = errors.Join(err, fn(ctx))
		}
		shutdownFuncs = nil
		return err
	}

	// Configure Context Propagation to use the default W3C traceparent format
	otel.SetTextMapPropagator(autoprop.NewTextMapPropagator())

	// Configure Trace Export to send spans as OTLP
	texporter, err := autoexport.NewSpanExporter(ctx)
	if err != nil {
		err = errors.Join(err, shutdown(ctx))
		return
	}
	tp := trace.NewTracerProvider(trace.WithBatcher(texporter))
	shutdownFuncs = append(shutdownFuncs, tp.Shutdown)
	otel.SetTracerProvider(tp)

	// Configure Metric Export to send metrics as OTLP
	mreader, err := autoexport.NewMetricReader(ctx)
	if err != nil {
		err = errors.Join(err, shutdown(ctx))
		return
	}
	mp := metric.NewMeterProvider(
		metric.WithReader(mreader),
	)
	shutdownFuncs = append(shutdownFuncs, mp.Shutdown)
	otel.SetMeterProvider(mp)

	return shutdown, nil
}

L'exemple de code précédent configure le paramètre TextMapPropagator global pour utiliser le format Contexte de trace W3C afin de propager le contexte de trace. Cette configuration garantit que les délais ont la bonne relation parent-enfant au sein d'une trace.

Pour garantir que toutes les données de télémétrie en attente sont vidées et que les connexions sont fermées correctement, la fonction setupOpenTelemetry renvoie une fonction nommée shutdown, qui effectue ces actions.

Configurer la journalisation structurée

Pour inclure les informations de trace dans les journaux au format JSON écrits dans la sortie standard, configurez le package de journalisation structurée Go, slog. L'exemple de code suivant illustre la fonction setupLogging, qui est appelée à partir de la fonction main:

func setupLogging() {
	// Use json as our base logging format.
	jsonHandler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ReplaceAttr: replacer})
	// Add span context attributes when Context is passed to logging calls.
	instrumentedHandler := handlerWithSpanContext(jsonHandler)
	// Set this handler as the global slog handler.
	slog.SetDefault(slog.New(instrumentedHandler))
}

Le code précédent appelle la fonction handlerWithSpanContext, qui extrait des informations de l'instance Context et les ajoute en tant qu'attributs à un journal. Ces attributs peuvent ensuite être utilisés pour mettre en corrélation un journal et une trace:

  • logging.googleapis.com/trace: nom de ressource de la trace associée à l'entrée de journal.
  • logging.googleapis.com/spanId: ID de délai avec la trace associée à l'entrée de journal.
  • logging.googleapis.com/trace_sampled: la valeur de ce champ doit être true ou false.

Pour en savoir plus sur ces champs, consultez la structure LogEntry.

func handlerWithSpanContext(handler slog.Handler) *spanContextLogHandler {
	return &spanContextLogHandler{Handler: handler}
}

// spanContextLogHandler is an slog.Handler which adds attributes from the
// span context.
type spanContextLogHandler struct {
	slog.Handler
}

// Handle overrides slog.Handler's Handle method. This adds attributes from the
// span context to the slog.Record.
func (t *spanContextLogHandler) Handle(ctx context.Context, record slog.Record) error {
	// Get the SpanContext from the golang Context.
	if s := trace.SpanContextFromContext(ctx); s.IsValid() {
		// Add trace context attributes following Cloud Logging structured log format described
		// in https://cloud.google.com/logging/docs/structured-logging#special-payload-fields
		record.AddAttrs(
			slog.Any("logging.googleapis.com/trace", s.TraceID()),
		)
		record.AddAttrs(
			slog.Any("logging.googleapis.com/spanId", s.SpanID()),
		)
		record.AddAttrs(
			slog.Bool("logging.googleapis.com/trace_sampled", s.TraceFlags().IsSampled()),
		)
	}
	return t.Handler.Handle(ctx, record)
}

func replacer(groups []string, a slog.Attr) slog.Attr {
	// Rename attribute keys to match Cloud Logging structured log format
	switch a.Key {
	case slog.LevelKey:
		a.Key = "severity"
		// Map slog.Level string values to Cloud Logging LogSeverity
		// https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#LogSeverity
		if level := a.Value.Any().(slog.Level); level == slog.LevelWarn {
			a.Value = slog.StringValue("WARNING")
		}
	case slog.TimeKey:
		a.Key = "timestamp"
	case slog.MessageKey:
		a.Key = "message"
	}
	return a
}

Ajouter une instrumentation au serveur HTTP

Pour ajouter une instrumentation de trace et de métrique aux requêtes traitées par le serveur HTTP, utilisez OpenTelemetry. L'exemple suivant utilise le gestionnaire otelhttp pour propager le contexte, et pour l'instrumentation des traces et des métriques:

func runServer() error {
	handleHTTP("/single", handleSingle)
	handleHTTP("/multi", handleMulti)

	return http.ListenAndServe(":8080", nil)
}

// handleHTTP handles the http HandlerFunc on the specified route, and uses
// otelhttp for context propagation, trace instrumentation, and metric
// instrumentation.
func handleHTTP(route string, handleFn http.HandlerFunc) {
	instrumentedHandler := otelhttp.NewHandler(otelhttp.WithRouteTag(route, handleFn), route)

	http.Handle(route, instrumentedHandler)
}

Dans le code précédent, le gestionnaire otelhttp utilise les instances globales TracerProvider, MeterProvider et TextMapPropagator. La fonction setupOpenTelemetry configure ces instances.

Associer les délais de trace aux journaux et aux métriques

Pour associer des délais serveur et client, et pour associer des métriques et des journaux, transmettez l'instance Go Context à la requête HTTP et lorsque vous écrivez des journaux. L'exemple suivant illustre un gestionnaire de routage qui extrait l'instance Go Context et la transmet à l'enregistreur et à la fonction callSingle, qui envoie une requête HTTP sortante:

func handleMulti(w http.ResponseWriter, r *http.Request) {
	subRequests := 3 + rand.Intn(4)
	// Write a structured log with the request context, which allows the log to
	// be linked with the trace for this request.
	slog.InfoContext(r.Context(), "handle /multi request", slog.Int("subRequests", subRequests))

	// Make 3-7 http requests to the /single endpoint.
	for i := 0; i < subRequests; i++ {
		if err := callSingle(r.Context()); err != nil {
			http.Error(w, err.Error(), http.StatusBadGateway)
			return
		}
	}

	fmt.Fprintln(w, "ok")
}

Dans le code précédent, l'appel de fonction r.Context() récupère le Context Go à partir de la requête HTTP.

Ajouter une instrumentation au client HTTP

Pour injecter le contexte de trace dans les requêtes HTTP sortantes et ajouter une instrumentation de trace et de métrique, appelez la fonction otelhttp.Get. Dans l'exemple suivant, la fonction callSingle effectue cette action:

func callSingle(ctx context.Context) error {
	// otelhttp.Get makes an http GET request, just like net/http.Get.
	// In addition, it records a span, records metrics, and propagates context.
	res, err := otelhttp.Get(ctx, "http://localhost:8080/single")
	if err != nil {
		return err
	}

	return res.Body.Close()
}

Dans le code précédent, le gestionnaire otelhttp utilise les instances globales TracerProvider, MeterProvider et TextMapPropagator. La fonction setupOpenTelemetry configure ces instances.

Écrire des journaux structurés

Pour écrire des journaux structurés qui renvoient vers une trace, utilisez le package de journalisation structurée de Go, slog, et transmettez l'instance Go Context à l'enregistreur. L'instance Go Context est requise lorsque vous souhaitez associer un journal à un délai. Par exemple, l'instruction suivante montre comment appeler la méthode InfoContext pour slog et comment ajouter le champ subRequests à l'instance JSON:

slog.InfoContext(r.Context(), "handle /multi request", slog.Int("subRequests", subRequests))

Exécuter un exemple d'application configuré pour collecter les données de télémétrie

L'exemple d'application utilise des formats indépendants du fournisseur, y compris JSON pour les journaux et OTLP pour les métriques et les traces. Pour acheminer la télémétrie vers Google Cloud, cet exemple utilise le Collector OpenTelemetry configuré avec les exportateurs Google. Le générateur de charge de l'application envoie des requêtes aux routes de l'application.

Télécharger et déployer l'application

Pour exécuter l'exemple , procédez comme suit :

  1. Dans la console Google Cloud, activez Cloud Shell.

    Activer Cloud Shell

    En bas de la fenêtre de la console Google Cloud, une session Cloud Shell démarre et affiche une invite de ligne de commande. Cloud Shell est un environnement shell dans lequel Google Cloud CLI est déjà installé, et dans lequel des valeurs sont déjà définies pour votre projet actuel. L'initialisation de la session peut prendre quelques secondes.

  2. Clonez le dépôt :

    git clone https://github.com/GoogleCloudPlatform/golang-samples
    
  3. Accédez au répertoire OpenTelemetry:

    cd golang-samples/opentelemetry/instrumentation
    
  4. Compilez et exécutez l'exemple.

    docker compose up --abort-on-container-exit
    

    Si vous n'exécutez pas sur Cloud Shell, exécutez l'application avec la variable d'environnement GOOGLE_APPLICATION_CREDENTIALS pointant vers un fichier d'identifiants. Les identifiants par défaut de l'application fournissent un fichier d'identifiants à l'adresse $HOME/.config/gcloud/application_default_credentials.json.

    # Set environment variables
    export GOOGLE_CLOUD_PROJECT="PROJECT_ID"
    export GOOGLE_APPLICATION_CREDENTIALS="$HOME/.config/gcloud/application_default_credentials.json"
    export USERID="$(id -u)"
    
    # Run
    docker compose -f docker-compose.yaml -f docker-compose.creds.yaml up --abort-on-container-exit
    

Afficher vos métriques

L'instrumentation OpenTelementry dans l'exemple d'application génère des métriques Prometheus que vous pouvez afficher à l'aide de l'explorateur de métriques:

  • Prometheus/http_server_duration/histogram enregistre la durée des requêtes du serveur et stocke les résultats dans un histogramme.

  • Prometheus/http_server_request_content_length_total/counter enregistre la longueur du contenu de la requête pour les routes HTTP /multi et /single. Les mesures pour cette métrique sont cumulatives, ce qui signifie que chaque valeur représente le total depuis le début de la collecte des valeurs.

  • Prometheus/http_server_response_content_length_total/counter enregistre la longueur du contenu de la réponse pour les routes HTTP /multi et /single. Les mesures pour cette métrique sont cumulatives.

Pour afficher les métriques générées par l'exemple d'application, procédez comme suit :
  1. Dans le panneau de navigation de la console Google Cloud, sélectionnez Surveillance, puis  Explorateur de métriques :

    Accéder à l'explorateur de métriques

  2. Dans l'élément Métrique, développez le menu Sélectionner une métrique, saisissez http_server dans la barre de filtre, puis utilisez les sous-menus pour sélectionner un type de ressource et des métriques spécifiques :
    1. Dans le menu Ressources actives, sélectionnez Cible Prometheus.
    2. Dans le menu Catégories de métriques actives, sélectionnez Http.
    3. Dans le menu Métriques actives, sélectionnez une métrique.
    4. Cliquez sur Appliquer.
  3. Configurez le mode d'affichage des données.

    Lorsque les mesures d'une métrique sont cumulatives, l'explorateur de métriques normalise automatiquement les données mesurées par période d'alignement, ce qui permet d'afficher un taux dans le graphique. Pour en savoir plus, consultez la section Genres, types et conversions.

    Lorsque des valeurs entières ou doubles sont mesurées, par exemple avec les deux métriques counter, l'explorateur de métriques additionne automatiquement toutes les séries temporelles. Pour afficher les données des routes HTTP /multi et /single, définissez le premier menu de l'entrée Agrégation sur Aucun.

    Pour plus d'informations sur la configuration d'un graphique, consultez la page Sélectionner des métriques lors de l'utilisation de l'explorateur de métriques.

Afficher vos traces

Pour afficher vos données de trace, procédez comme suit:

  1. Dans le panneau de navigation de la console Google Cloud, sélectionnez Trace, puis Explorateur Trace:

    Accéder à Explorateur Trace

  2. Dans le graphique à nuage de points, sélectionnez une trace avec l'URI /multi.
  3. Dans le graphique de Gantt du panneau Détails des traces, sélectionnez le délai intitulé /multi.

    Un panneau contenant des informations sur la requête HTTP s'affiche. Ces informations incluent la méthode, le code d'état, le nombre d'octets et le user-agent de l'appelant.

  4. Pour afficher les journaux associés à cette trace, sélectionnez l'onglet Logs & Events (Journaux et événements).

    Cet onglet affiche les journaux individuels. Pour afficher les détails de l'entrée de journal, développez-la. Vous pouvez également cliquer sur Afficher les journaux et afficher le journal à l'aide de l'explorateur de journaux.

Pour en savoir plus sur l'utilisation de l'explorateur Cloud Trace, consultez la page Rechercher et explorer des traces.

Afficher les journaux

L'explorateur de journaux vous permet d'inspecter vos journaux et d'afficher les traces associées, lorsqu'elles existent.

  1. Dans le panneau de navigation de la console Google Cloud, sélectionnez Logging, puis Explorateur de journaux :

    Accéder à l'explorateur de journaux

  2. Recherchez un journal avec la description suivante : handle /multi request.

    Pour afficher les détails du journal, développez l'entrée de journal. Le champ jsonPayload contient une entrée intitulée subRequests. Cette entrée a été ajoutée par une instruction dans la fonction handleMulti.

  3. Cliquez sur Traces sur une entrée de journal contenant le message "handle /multi request", puis sélectionnez Afficher les détails des traces.

    Un panneau Trace details (Informations sur la trace) s'ouvre et affiche la trace sélectionnée.

Pour en savoir plus sur l'utilisation de l'explorateur de journaux, consultez la page Afficher les journaux à l'aide de l'explorateur de journaux.

Étapes suivantes