Gerar traces e métricas com o Go

Neste documento, descrevemos como modificar um aplicativo Go para coletar dados de trace e métrica usando o framework de código aberto OpenTelemetry e como gravar registros JSON estruturados para saída padrão. Este documento também fornece informações sobre um app de amostra que pode ser instalado e executado. O app está configurado para gerar métricas, traces e registros.

Sobre o contexto

O Context do OpenTelemetry é um mecanismo para transportar valores com escopo de execução entre APIs dentro de um processo. Um uso importante do contexto é carregar o período ativo atual para que ele possa ser modificado ou referenciado como pai de qualquer novo período quando ele é criado. Para resumir:

  • Contexto refere-se ao mecanismo para propagar valores com escopo de execução, incluindo o período ativo atual, entre APIs dentro de um processo.

  • O contexto do período é um objeto imutável em todos os períodos que inclui o ID do trace, o ID do período e as sinalizações e o estado do trace.

  • A propagação é o mecanismo que move o contexto entre serviços e processos.

O context.Context da biblioteca padrão do Go também carrega valores de escopo nos limites da API. Normalmente, as funções de gerenciador em um servidor recebem uma Context de entrada e a transmitem pela cadeia de chamadas para todos os clientes que fazem solicitações de saída.

A biblioteca padrão do Go context.Context é usada como a implementação do OpenTelemetry Context em Go.

Antes de começar

Ative as APIs Cloud Logging API, Cloud Monitoring API, and Cloud Trace API.

Ative as APIs

Instrumentar o app para coletar traces, métricas e registros

Para instrumentar o app para coletar dados de rastreamento e métrica e gravar um JSON estruturado para saída padrão, siga as etapas a seguir, conforme descrito nas próximas seções deste documento:

  1. Configurar a função principal
  2. Configurar o OpenTelemetry
  3. Configurar a geração de registros estruturados
  4. Adicionar instrumentação ao servidor HTTP
  5. Vincular períodos de trace a registros e métricas
  6. Adicionar instrumentação ao cliente HTTP
  7. Gravar registros estruturados

Configurar a função principal

Para configurar o app para gravar registros estruturados e coletar métricas e dados de rastreamento usando o OpenTelemetry, atualize a função main para configurar o pacote de geração de registros estruturados do Go, slog, e para configurar o OpenTelemetry.

O exemplo de código a seguir ilustra uma função main que chama duas funções auxiliares, setupLogging() e setupOpenTelemetry(). Estas funções auxiliares configuram o pacote de geração de registros e o 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)
	}
}

Depois de configurar o pacote de geração de registros, é necessário transmitir o Context do Go para o logger para vincular seus registros aos dados de rastreamento. Para mais informações, consulte a seção Gravar registros estruturados deste documento.

Configurar o OpenTelemetry

Para coletar e exportar traces e métricas usando o protocolo OTLP (em inglês), configure as instâncias globais TracerProvider e MeterProvider. O exemplo de código a seguir ilustra a função setupOpenTelemetry, que é chamada na função 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
}

O exemplo de código anterior configura o TextMapPropagator global para usar o formato Contexto de Trace do W3C para propagar o contexto de rastro. Essa configuração garante que os períodos tenham o relacionamento pai-filho correto em um trace.

Para garantir que toda a telemetria pendente seja limpa e que as conexões sejam fechadas corretamente, a função setupOpenTelemetry retorna uma função chamada shutdown, que executa essas ações.

Configurar a geração de registros estruturados

Para incluir as informações de trace como parte dos registros formatados em JSON gravados na saída padrão, configure o pacote de geração de registros estruturados do Go, slog. O exemplo de código a seguir ilustra a função setupLogging, que é chamada na função 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))
}

O código anterior chama a função handlerWithSpanContext, que extrai informações da instância Context e as adiciona como atributos a um registro. Esses atributos podem ser usados para correlacionar um registro com um trace:

  • logging.googleapis.com/trace: nome do recurso do trace associado à entrada de registro.
  • logging.googleapis.com/spanId: o ID do período com o trace associado à entrada de registro.
  • logging.googleapis.com/trace_sampled: o valor desse campo precisa ser true ou false.

Para mais informações sobre esses campos, consulte a estrutura 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
}

Adicionar instrumentação ao servidor HTTP

Para adicionar instrumentação de rastreamento e métrica às solicitações processadas pelo servidor HTTP, use o OpenTelemetry. O exemplo a seguir usa o gerenciador otelhttp para propagar o contexto e para instrumentação de rastreamento e métrica:

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

No código anterior, o gerenciador otelhttp usa as instâncias globais TracerProvider, MeterProvider e TextMapPropagator. A função setupOpenTelemetry configura essas instâncias.

Vincular períodos de trace a registros e métricas

Para vincular períodos do servidor e do cliente e associar métricas e registros, transmita a instância Context do Go para a solicitação HTTP e ao gravar registros. O exemplo a seguir ilustra um gerenciador de rota que extrai a instância Context do Go e a transmite para o registrador e a função callSingle, que faz uma solicitação HTTP de saída:

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

No código anterior, a chamada de função r.Context() recupera o Context do Go da solicitação HTTP.

Adicionar instrumentação ao cliente HTTP

Para injetar o contexto de rastreamento em solicitações HTTP de saída e adicionar instrumentação de trace e métrica, chame a função otelhttp.Get. No exemplo a seguir, a função callSingle executa essa ação:

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

No código anterior, o gerenciador otelhttp usa as instâncias globais TracerProvider, MeterProvider e TextMapPropagator. A função setupOpenTelemetry configura essas instâncias.

Gravar registros estruturados

Para gravar registros estruturados vinculados a um trace, use o pacote de geração de registros estruturados do Go, slog, e transmita a instância Context do Go para o logger. A instância Context do Go é necessária para vincular um registro a um período. Por exemplo, a instrução a seguir mostra como chamar o método InfoContext para slog e ilustra como adicionar o campo subRequests à instância JSON:

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

Executar um app de exemplo configurado para coletar telemetria

O aplicativo de exemplo usa formatos neutros em relação a fornecedores, incluindo JSON para registros e OTLP para métricas e traces. Para rotear a telemetria para o Google Cloud, este exemplo usa o Collector do OpenTelemetry configurado com os exportadores do Google. O gerador de carga no app emite solicitações para as rotas dele.

Fazer o download e implantar o app

Para executar a amostra:

  1. No Console do Google Cloud, ative o Cloud Shell.

    Ativar o Cloud Shell

    Na parte inferior do Console do Google Cloud, uma sessão do Cloud Shell é iniciada e exibe um prompt de linha de comando. O Cloud Shell é um ambiente shell com a CLI do Google Cloud já instalada e com valores já definidos para o projeto atual. A inicialização da sessão pode levar alguns segundos.

  2. Clone o repositório:

    git clone https://github.com/GoogleCloudPlatform/golang-samples
    
  3. Acesse o diretório do OpenTelemetry:

    cd golang-samples/opentelemetry/instrumentation
    
  4. Crie e execute a amostra:

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

    Se você não estiver usando o Cloud Shell, execute o aplicativo com a variável de ambiente GOOGLE_APPLICATION_CREDENTIALS apontando para um arquivo de credenciais. O Application Default Credentials fornece um arquivo de credenciais em $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
    

Ver suas métricas

A instrumentação do OpenTelemetry no app de amostra gera métricas do Prometheus, que podem ser visualizadas usando o Metrics Explorer:

  • Prometheus/http_server_duration/histogram registra a duração das solicitações do servidor e armazena os resultados em um histograma.

  • Prometheus/http_server_request_content_length_total/counter registra o tamanho do conteúdo da solicitação para as rotas HTTP /multi e /single. As medidas dessa métrica são cumulativas, o que significa que cada valor representa o total desde o início da coleta de valores.

  • Prometheus/http_server_response_content_length_total/counter registra o tamanho do conteúdo da resposta para as rotas HTTP /multi e /single. As medições dessa métrica são cumulativas.

Para visualizar as métricas geradas pelo app de exemplo, faça o seguinte:
  1. No painel de navegação do console do Google Cloud, selecione Monitoramento e  Metrics Explorer:

    Acesse o Metrics explorer

  2. No elemento Metric, expanda o menu Selecionar uma métrica, digite http_server na barra de filtro e use os submenus para selecionar um tipo de recurso e métrica específicos:
    1. No menu Active resources, selecione Prometheus Target.
    2. No menu Categorias de métrica ativas, selecione Instância.
    3. No menu Métricas ativas, selecione uma métrica de faturamento†.
    4. Clique em Aplicar.
  3. Configure a visualização dos dados.

    Quando as medições de uma métrica são cumulativas, o Metrics Explorer normaliza automaticamente os dados medidos pelo período de alinhamento, o que resulta na exibição de uma taxa no gráfico. Para mais informações, consulte Tipos, tipos e conversões.

    Quando valores inteiros ou duplos são medidos, como acontece com as duas métricas counter, o Metrics Explorer soma automaticamente todas as séries temporais. Para visualizar os dados das rotas HTTP /multi e /single, defina o primeiro menu da entrada Agregação como Nenhum.

    Para mais informações sobre como configurar um gráfico, consulte Selecionar métricas ao usar o Metrics Explorer.

Visualizar os rastros

Para visualizar os dados de trace, faça o seguinte:

  1. No painel de navegação do console do Google Cloud, selecione Trace e, em seguida, Trace Explorer:

    Acessar o Explorador de traces

  2. No gráfico de dispersão, selecione um rastro com o URI de /multi.
  3. No diagrama de Gantt no painel Detalhes do trace, selecione o período rotulado como /multi.

    Um painel é aberto com informações sobre a solicitação HTTP. Esses detalhes incluem o método, o código de status, o número de bytes e o user agent do autor da chamada.

  4. Para visualizar os registros associados a esse trace, selecione a guia Registros e eventos.

    A guia mostra registros individuais. Para exibir os detalhes da entrada de registro, expanda a entrada de registro. Também é possível clicar em Ver registros e ver o registro usando a Análise de registros.

Para mais informações sobre como usar o explorador do Cloud Trace, consulte Encontrar e explorar traces.

Acessar os registros

Na Análise de registros, é possível inspecionar os registros e visualizar os traces associados, quando eles existirem.

  1. No painel de navegação do console do Google Cloud, selecione Logging e clique em Análise de registros:

    Acessar a Análise de registros

  2. Localize um registro com a descrição de handle /multi request.

    Para ver os detalhes do registro, expanda a entrada. No campo jsonPayload, há uma entrada identificada como subRequests. Essa entrada foi adicionada por uma instrução na função handleMulti.

  3. Clique em Traces em uma entrada de registro com a mensagem "handle /multi request" e selecione View trace details.

    O painel Detalhes do trace é aberto e mostra o trace selecionado.

Para mais informações sobre como usar a Análise de registros, consulte Ver registros usando a Análise de registros.

A seguir