使用 Go 生成跟踪记录和指标

本文档介绍了如何修改 Go 应用以使用开源 OpenTelemetry 框架收集跟踪记录和指标数据,以及如何将结构化 JSON 日志写入标准输出。本文档还介绍了您可以安装和运行的示例应用。该应用已配置为生成指标、跟踪记录和日志。

如需详细了解插桩,请参阅以下文档:

关于上下文

OpenTelemetry 的上下文是一种在进程内跨 API 携带执行范围的值的机制。上下文的一个重要用途是携带当前活跃 span,以便进行修改,或在创建任何新 span 时作为其父级引用。总结:

  • 上下文是指在进程内跨 API 传播执行范围的值(包括当前活跃 span)的机制。

  • Span Context 是每个 span 上的不可变对象,其中包括跟踪记录 ID、span ID 以及跟踪记录的标志和状态。

  • 传播是在服务和进程之间移动上下文的机制。

Go 标准库的 context.Context 也可跨 API 边界携带限定了范围的值。通常,服务器中的处理程序函数会接收传入的 Context,并通过调用链将其传递给发出传出请求的任何客户端。

Go 的标准库 context.Context 用作 Go 中 OpenTelemetry 上下文的实现。

准备工作

Enable the Cloud Logging, Cloud Monitoring, and Cloud Trace APIs.

Enable the APIs

对应用进行插桩处理以收集跟踪记录、指标和日志

如需对应用进行插桩处理以收集跟踪记录和指标数据,并将结构化 JSON 写入标准输出,请执行以下步骤,如本文档后续部分所述:

  1. 配置主函数
  2. 配置 OpenTelemetry
  3. 配置结构化日志记录
  4. 向 HTTP 服务器添加插桩
  5. 将跟踪记录 span 与日志和指标相关联
  6. 向 HTTP 客户端添加插桩
  7. 写入结构化日志

配置主函数

如需配置应用以使用 OpenTelemetry 写入结构化日志以及收集指标和跟踪记录数据,请更新 main 函数以配置 Go 结构化日志记录软件包 slog 并配置 OpenTelemetry。

以下代码示例展示了会调用两个辅助函数(setupLogging()setupOpenTelemetry())的 main 函数。以下辅助函数用于配置日志记录软件包和 OpenTelemetry。

如需查看完整示例,请点击 更多,然后选择在 GitHub 上查看

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

配置日志记录软件包后,如需将日志与跟踪记录数据相关联,您必须将 Go Context 传递给日志记录器。如需了解详情,请参阅本文档的写入结构化日志部分。

配置 OpenTelemetry

如需使用 OTLP 协议收集和导出跟踪记录和指标,请配置全局 TracerProviderMeterProvider 实例。以下代码示例展示了从 main 函数调用的 setupOpenTelemetry 函数:

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
}

上述代码示例将全局 TextMapPropagator 配置为使用 W3C 跟踪上下文格式来传播跟踪记录上下文。此配置可确保 span 在跟踪记录中具有正确的父子关系。

为了确保所有待处理的遥测数据都被清空并正常关闭连接,setupOpenTelemetry 函数会返回一个名为 shutdown 的函数,该函数会执行这些操作。

配置结构化日志记录

如需将跟踪记录信息作为写入标准输出的 JSON 格式日志的一部分添加,请配置 Go 结构化日志记录软件包 slog。以下代码示例展示了从 main 函数调用的 setupLogging 函数:

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

上述代码调用 handlerWithSpanContext 函数,该函数从 Context 实例中提取信息,并将该信息作为属性添加到日志中。然后,您可以使用这些属性将日志与跟踪记录相关联:

  • logging.googleapis.com/trace:与日志条目关联的跟踪记录的资源名称。
  • logging.googleapis.com/spanId:与日志条目关联的跟踪记录的 span ID。
  • logging.googleapis.com/trace_sampled:此字段的值必须是 truefalse

如需详细了解这些字段,请参阅 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
}

向 HTTP 服务器添加插桩

如需向 HTTP 服务器处理的请求添加跟踪记录和指标插桩,请使用 OpenTelemetry。以下示例使用 otelhttp 处理程序传播上下文,以及进行跟踪记录和指标插桩:

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

在前面的代码中,otelhttp 处理程序使用全局 TracerProviderMeterProviderTextMapPropagator 实例。setupOpenTelemetry 函数会配置这些实例。

将跟踪记录 span 与日志和指标相关联

如需关联服务器和客户端 span 并关联指标和日志,请在写入日志时将 Go Context 实例传递给 HTTP 请求。以下示例演示了一个路由处理程序,该处理程序提取 Go Context 实例并将该实例传递给日志记录器和 callSingle 函数,该函数发出传出 HTTP 请求:

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

	err := computeSubrequests(r, subRequests)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadGateway)
		return
	}

	fmt.Fprintln(w, "ok")
}

在前面的代码中,函数调用 r.Context() 从 HTTP 请求中检索 Go Context

向 HTTP 客户端添加插桩

如需将跟踪上下文注入传出 HTTP 请求并添加跟踪记录和指标插桩,请调用 otelhttp.Get 函数。在以下示例中,callSingle 函数会执行此操作:

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

在前面的代码中,otelhttp 处理程序使用全局 TracerProviderMeterProviderTextMapPropagator 实例。setupOpenTelemetry 函数会配置这些实例。

写入结构化日志

如需写入链接到跟踪记录的结构化日志,请使用 Go 的结构化日志记录软件包 slog,并将 Go Context 实例传递给日志记录器。如果您想要将日志关联到 span,则需要 Go Context 实例。例如,以下语句显示了如何为 slog 调用 InfoContext 方法,并说明了如何将字段 subRequests 添加到 JSON 实例:

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

运行配置为收集遥测数据的示例应用

示例应用使用不受制于供应商的格式,包括 JSON(用于日志)和 OTLP(用于指标和跟踪记录)。为了将遥测路由到 Google Cloud,此示例使用配置了 Google 导出器的 OpenTelemetry Collector。应用中的负载生成器会向应用的路由发出请求。

下载并部署应用

如需运行示例,请执行以下操作:

  1. In the Google Cloud console, activate Cloud Shell.

    Activate Cloud Shell

    At the bottom of the Google Cloud console, a Cloud Shell session starts and displays a command-line prompt. Cloud Shell is a shell environment with the Google Cloud CLI already installed and with values already set for your current project. It can take a few seconds for the session to initialize.

  2. 克隆代码库:

    git clone https://github.com/GoogleCloudPlatform/golang-samples
    
  3. 转到 OpenTelemetry 目录:

    cd golang-samples/opentelemetry/instrumentation
    
  4. 构建并运行示例:

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

    如果您未在 Cloud Shell 上运行,请使用指向凭据文件的 GOOGLE_APPLICATION_CREDENTIALS 环境变量运行应用。应用默认凭据提供了一个凭据文件 ($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
    

查看指标

示例应用中的 OpenTelemetry 插桩生成 Prometheus 指标,您可以使用 Metrics Explorer 查看这些指标:

  • Prometheus/http_server_duration/histogram 会记录服务器请求的持续时间,并将结果存储在直方图中。

  • Prometheus/http_server_request_content_length_total/counter 记录 /multi/single HTTP 路由的请求内容长度。此指标的测量结果是累积的,这意味着每个值代表从开始收集值以来的总数。

  • Prometheus/http_server_response_content_length_total/counter 记录 /multi/single HTTP 路由的响应内容长度。此指标的测量值是累计的。

如需查看示例应用生成的指标,请执行以下操作:
  1. 在 Google Cloud 控制台中,转到 Metrics Explorer 页面:

    进入 Metrics Explorer

    如果您使用搜索栏查找此页面,请选择子标题为监控的结果。

  2. 指标元素中,展开选择指标菜单,在过滤栏中输入 http_server,然后使用子菜单选择一个特定资源类型和指标:
    1. 活跃资源菜单中,选择 Prometheus 目标
    2. 活跃指标类别菜单中,选择 HTTP
    3. 活跃指标菜单中,选择指标。
    4. 点击应用
  3. 配置数据的查看方式。

    如果指标的测量结果是累积的,则 Metrics Explorer 会自动按校准时间段对测量数据进行归一化,从而使图表显示速率。如需了解详情,请参阅种类、类型和转换

    测量整数或双精度值时(例如使用两个 counter 指标),Metrics Explorer 会自动对所有时序求和。如需查看 /multi/single HTTP 路由的数据,请将聚合条目的第一个菜单设置为

    如需详细了解如何配置图表,请参阅使用 Metrics Explorer 时选择指标

查看跟踪记录

如需查看跟踪记录数据,请执行以下操作:

  1. 在 Google Cloud 控制台中,转到 Trace 探索器页面:

    转到 Trace 探索器

    您也可以使用搜索栏查找此页面。

  2. 在散点图中,选择 URI 为 /multi 的跟踪记录。
  3. 跟踪记录详情面板的甘特图中,选择标记为 /multi 的 span。

    此时会打开一个面板,其中显示 HTTP 请求的相关信息。这些详细信息包括方法、状态代码、字节数以及调用方的用户代理。

  4. 如需查看与此跟踪记录关联的日志,请选择日志和事件标签页。

    该标签页会显示各个日志。如需查看日志条目的详细信息,请展开日志条目。您还可以点击查看日志,并使用 Logs Explorer 查看日志。

如需详细了解如何使用 Cloud Trace 探索器,请参阅查找和探索跟踪记录

查看日志

在 Logs Explorer 中,您可以检查日志,还可以查看关联的跟踪记录(如果存在)。

  1. 在 Google Cloud 控制台中,转到 Logs Explorer 页面。

    前往 Logs Explorer

    如果您使用搜索栏查找此页面,请选择子标题为 Logging 的结果。

  2. 找到具有 handle /multi request 说明的日志。

    如需查看日志的详细信息,请展开日志条目。在 jsonPayload 字段中,有一个名为 subRequests 的条目。此条目由 handleMulti 函数中的语句添加。

  3. 点击包含“处理/多请求”消息的日志条目中的 跟踪记录,然后选择查看跟踪记录详情

    跟踪记录详情面板随即会打开并显示所选跟踪记录。

如需详细了解如何使用 Logs Explorer,请参阅使用 Logs Explorer 查看日志

后续步骤