Genera seguimientos y métricas con Go

En este documento, se describe cómo modificar una app de Go para recopilar datos de seguimiento y métricas con el framework de código abierto de OpenTelemetry y cómo escribir registros JSON estructurados en salida estándar. En este documento, también se proporciona información sobre una app de muestra que puedes instalar y ejecutar. La app está configurada para generar métricas, seguimientos y registros.

Acerca del contexto

El contexto de OpenTelemetry es un mecanismo para transportar valores con alcance de ejecución en las APIs dentro de un proceso. Un uso importante del contexto es llevar el intervalo activo actual para que se pueda modificar o referenciar como el superior de cualquier intervalo nuevo cuando se cree. En síntesis:

  • El contexto hace referencia al mecanismo para propagar valores con alcance de ejecución, incluido el intervalo activo actual, en las APIs dentro de un proceso.

  • El contexto del intervalo es un objeto inmutable en cada intervalo que incluye el ID de seguimiento, el ID de intervalo, las marcas y el estado del seguimiento.

  • La propagación es el mecanismo que mueve el contexto entre los servicios y los procesos.

context.Context de la biblioteca estándar de Go también lleva valores con alcance a través de los límites de la API. Por lo general, las funciones del controlador en un servidor reciben un Context entrante y lo pasan a través de la cadena de llamadas a cualquier cliente que realice solicitudes salientes.

La biblioteca estándar de Go context.Context se usa como la implementación del contexto de OpenTelemetry en Go.

Antes de comenzar

Habilita las API de Cloud Logging API, Cloud Monitoring API, and Cloud Trace API.

Habilita las API

Instrumenta tu app para recopilar seguimientos, métricas y registros

Si deseas instrumentar tu app para recopilar datos de seguimiento y métricas, y escribir JSON estructurado en la salida estándar, realiza los pasos que se describen en las siguientes secciones de este documento:

  1. Configura la función principal
  2. Configura OpenTelemetry
  3. Configura el registro estructurado
  4. Agrega instrumentación al servidor HTTP
  5. Vincula intervalos de seguimiento con registros y métricas
  6. Agrega instrumentación al cliente HTTP
  7. Escribe registros estructurados

Configura la función principal

Si deseas configurar la app para que escriba registros estructurados y recopile métricas y datos de seguimiento mediante OpenTelemetry, actualiza la función main a fin de configurar el paquete de registro estructurado de Go, slog, y configurar OpenTelemetry.

En la siguiente muestra de código, se ilustra una función main que llama a dos funciones auxiliares, setupLogging() y setupOpenTelemetry(). Estas funciones auxiliares configuran el paquete de registro y 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)
	}
}

Después de configurar el paquete de registro, para vincular tus registros a los datos de seguimiento, debes pasar Context de Go al registrador. Para obtener más información, consulta la sección Escribe registros estructurados de este documento.

Configura OpenTelemetry

Para recopilar y exportar seguimientos y métricas mediante el protocolo OTLP, configura las instancias globales TracerProvider y MeterProvider. En la siguiente muestra de código, se ilustra la función setupOpenTelemetry, que se llama desde la función 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
}

En la muestra de código anterior, se configura el TextMapPropagator global a fin de usar el formato de contexto de seguimiento W3C para propagar el contexto de seguimiento. Esta configuración garantiza que los intervalos tengan la relación superior-secundaria correcta dentro de un seguimiento.

Para garantizar que se limpie toda la telemetría pendiente y que las conexiones se cierren de manera correcta, la función setupOpenTelemetry muestra una función llamada shutdown, que realiza esas acciones.

Configura el registro estructurado

Para incluir la información de seguimiento como parte de los registros con formato JSON escritos en el resultado estándar, configura el paquete de registro estructurado de Go, slog. En la siguiente muestra de código, se ilustra la función setupLogging, que se llama desde la función 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))
}

El código anterior llama a la función handlerWithSpanContext, que extrae información de la instancia Context y agrega esa información como atributos a un registro. Estos atributos se pueden usar para correlacionar un registro con un seguimiento:

  • logging.googleapis.com/trace: el nombre del recurso del seguimiento asociado a la entrada de registro.
  • logging.googleapis.com/spanId: el ID de intervalo con el seguimiento asociado a la entrada de registro.
  • logging.googleapis.com/trace_sampled: el valor de este campo debe ser true o false.

Para obtener más información sobre estos campos, consulta la estructura 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
}

Agrega instrumentación al servidor HTTP

Para agregar instrumentación de seguimiento y de métricas a las solicitudes que controla el servidor HTTP, usa OpenTelemetry. En el siguiente ejemplo, se usa el controlador otelhttp para propagar el contexto y para la instrumentación de métricas y seguimientos:

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

En el código anterior, el controlador otelhttp usa las instancias globales TracerProvider, MeterProvider y TextMapPropagator. La función setupOpenTelemetry configura estas instancias.

Intervalos de seguimiento de vínculos con registros y métricas

Para vincular los intervalos de cliente y servidor, y asociar las métricas y los registros, pasa la instancia Context de Go a la solicitud HTTP y cuando escribas registros. En el siguiente ejemplo, se ilustra un controlador de ruta que extrae la instancia de Context de Go y la pasa al registrador y a la función callSingle, que realiza una solicitud HTTP saliente:

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

En el código anterior, la llamada a función r.Context() recupera el Context de Go de la solicitud HTTP.

Agrega instrumentación al cliente HTTP

Para incorporar el contexto de seguimiento en las solicitudes HTTP salientes y agregar instrumentación de seguimiento y de métricas, llama a la función otelhttp.Get. En el siguiente ejemplo, la función callSingle realiza esta acción:

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

En el código anterior, el controlador otelhttp usa las instancias globales TracerProvider, MeterProvider y TextMapPropagator. La función setupOpenTelemetry configura estas instancias.

Escribe registros estructurados

Para escribir registros estructurados que se vinculen a un seguimiento, usa slog, el paquete de registro estructurado de Go, y pasa la instancia Context de Go al registrador. La instancia de Go Context es obligatoria cuando deseas vincular un registro a un intervalo. Por ejemplo, la siguiente instrucción muestra cómo llamar al método InfoContext para slog y, además, ilustra cómo agregar el campo subRequests a la instancia JSON:

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

Ejecuta una app de ejemplo configurada para recopilar telemetría

En la aplicación de ejemplo, se usan formatos independientes del proveedor, incluido JSON para los registros y OTLP para las métricas y los seguimientos. Para enrutar la telemetría a Google Cloud, esta muestra usa el Collector de OpenTelemetry configurado con los exportadores de Google. El generador de cargas de la app envía solicitudes a las rutas de la app.

Descarga e implementa la app

Para ejecutar la muestra, haz lo siguiente:

  1. En la consola de Google Cloud, activa Cloud Shell.

    Activar Cloud Shell

    En la parte inferior de la consola de Google Cloud, se inicia una sesión de Cloud Shell en la que se muestra una ventana de línea de comandos. Cloud Shell es un entorno de shell con Google Cloud CLI ya instalada y con valores ya establecidos para el proyecto actual. La sesión puede tardar unos segundos en inicializarse.

  2. Clona el repositorio:

    git clone https://github.com/GoogleCloudPlatform/golang-samples
    
  3. Ve al directorio de OpenTelemetry:

    cd golang-samples/opentelemetry/instrumentation
    
  4. Compila y ejecuta la muestra:

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

    Si no ejecutas en Cloud Shell, ejecuta la aplicación con la variable de entorno GOOGLE_APPLICATION_CREDENTIALS que apunta a un archivo de credenciales. Las credenciales predeterminadas de la aplicación proporcionan un archivo de credenciales en $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
    

Consulta tus métricas

La instrumentación de OpenTelemetry en la app de ejemplo genera métricas de Prometheus que puedes ver mediante el Explorador de métricas:

  • Prometheus/http_server_duration/histogram registra la duración de las solicitudes del servidor y almacena los resultados en un histograma.

  • Prometheus/http_server_request_content_length_total/counter registra la longitud del contenido de la solicitud para las rutas HTTP /multi y /single. Las mediciones de esta métrica son acumulativas, lo que significa que cada valor representa el total desde que comenzó la recopilación de valores.

  • Prometheus/http_server_response_content_length_total/counter registra la longitud del contenido de la respuesta para las rutas HTTP /multi y /single. Las mediciones de esta métrica son acumulativas.

Para ver las métricas que genera la app de muestra, haz lo siguiente:
  1. En el panel de navegación de la consola de Google Cloud, elige Monitoring y, luego,  Explorador de métricas:

    Ir al Explorador de métricas

  2. En el elemento Métrica, expande el menú Seleccionar una métrica, ingresa http_server en la barra de filtros y, luego, usa los submenús para seleccionar un métrica y tipo de recurso específicos:
    1. En el menú Recursos activos, elige Destino de Prometheus.
    2. En el menú Categorías de métricas activas, elige Http.
    3. En el menú Métricas activas, selecciona una métrica.
    4. Haz clic en Aplicar.
  3. Configura cómo se ven los datos.

    Cuando las mediciones de una métrica son acumulativas, el Explorador de métricas normaliza automáticamente los datos medidos por el período de alineación, lo que hace que el gráfico muestre una frecuencia. Para obtener más información, consulta Categorías, tipos y conversiones.

    Cuando se miden valores de números enteros o dobles, como con las dos métricas counter, el Explorador de métricas suma automáticamente todas las series temporales. Para ver los datos de las rutas HTTP /multi y /single, establece el primer menú de la entrada Agregación como Ninguna.

    Para obtener más información sobre la configuración de un gráfico, consulta elige métricas cuando uses el Explorador de métricas.

Ve tus seguimientos

Para ver tus datos de seguimiento, haz lo siguiente:

  1. En el panel de navegación de la consola de Google Cloud, selecciona Trace y, luego, Explorador de seguimiento:

    Ve al Explorador de seguimiento

  2. En el gráfico de dispersión, selecciona un seguimiento con el URI de /multi.
  3. En el diagrama de Gantt del panel Detalles de seguimiento, selecciona el intervalo etiquetado como /multi.

    Se abre un panel que muestra información sobre la solicitud HTTP. Estos detalles incluyen el método, el código de estado, la cantidad de bytes y el usuario-agente del emisor.

  4. Para ver los registros asociados con este seguimiento, selecciona la pestaña Registros y eventos.

    La pestaña muestra registros individuales. Para ver los detalles de la entrada de registro, expande la entrada de registro. También puedes hacer clic en Ver registros y ver el registro con el Explorador de registros.

Si deseas obtener más información para usar el explorador de Cloud Trace, consulta Busca y explora seguimientos.

Mira los registros

En el Explorador de registros, puedes inspeccionar tus registros y, también, puedes ver los seguimientos asociados, cuando existen.

  1. En el panel de navegación de la consola de Google Cloud, elige Logging y, luego, Explorador de registros:

    Ir al Explorador de registros

  2. Ubica un registro con la descripción de handle /multi request.

    Para ver los detalles del registro, expande la entrada de registro. En el campo jsonPayload, hay una entrada etiquetada como subRequests. Esta entrada se agregó mediante una declaración en la función handleMulti.

  3. Haz clic en Seguimientos en una entrada de registro con el mensaje "Controla solicitudes múltiples" y, luego, selecciona Ver detalles de seguimiento.

    Se abrirá el panel Detalles de seguimiento y se mostrará el seguimiento seleccionado.

Para obtener más información sobre el uso del Explorador de registros, consulta Visualiza registros con el Explorador de registros.

¿Qué sigue?