Go 插桩示例

本文档介绍了如何使用 OpenTelemetry SDK 和 OpenTelemetry 收集器对 Go 应用进行插桩,以收集跟踪记录和指标数据。此外,还介绍了如何将结构化 JSON 日志写入标准输出。如需试用插桩功能,请下载并运行示例应用。该应用会生成日志、指标和跟踪记录数据。

使用 OpenTelemetry 收集器时,您需要使用其 SDK 及 SDK 的 OTLP 进程内导出器对应用进行插桩。该插桩工具具有供应商中立性。您还需要部署一个 OpenTelemetry 收集器,用于从进程内导出器接收遥测数据,然后将该数据导出到您的 Google Cloud 项目。如需详细了解收集器,请参阅 Google 构建的 OpenTelemetry 收集器

如果您的环境支持使用收集器,我们建议您使用 OpenTelemetry 收集器来导出遥测数据。对于某些环境,您必须使用能够直接将数据发送到您的Google Cloud 项目的进程内导出器。如需了解进程内插桩,请参阅从 Trace 导出器迁移到 OTLP 端点

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

关于上下文

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

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

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

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

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

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

准备工作

  1. Install the Google Cloud CLI.

  2. 配置 gcloud CLI 以使用您的联合身份。

    如需了解详情,请参阅使用联合身份登录 gcloud CLI

  3. 如需初始化 gcloud CLI,请运行以下命令:

    gcloud init
  4. Create or select a Google Cloud project.

    Roles required to select or create a project

    • Select a project: Selecting a project doesn't require a specific IAM role—you can select any project that you've been granted a role on.
    • Create a project: To create a project, you need the Project Creator role (roles/resourcemanager.projectCreator), which contains the resourcemanager.projects.create permission. Learn how to grant roles.
    • Create a Google Cloud project:

      gcloud projects create PROJECT_ID

      Replace PROJECT_ID with a name for the Google Cloud project you are creating.

    • Select the Google Cloud project that you created:

      gcloud config set project PROJECT_ID

      Replace PROJECT_ID with your Google Cloud project name.

  5. Verify that billing is enabled for your Google Cloud project.

  6. Enable the Cloud Logging, Cloud Monitoring, Cloud Trace, and Telemetry APIs:

    Roles required to enable APIs

    To enable APIs, you need the Service Usage Admin IAM role (roles/serviceusage.serviceUsageAdmin), which contains the serviceusage.services.enable permission. Learn how to grant roles.

    gcloud services enable logging.googleapis.com monitoring.googleapis.com cloudtrace.googleapis.com telemetry.googleapis.com
  7. 如果您在 Cloud Shell、 Google Cloud资源或本地开发环境中运行示例,则只需具备本部分中列出的权限即可。对于生产应用,通常由服务账号提供用于写入日志、指标和跟踪记录数据的凭据。

    如需获得让示例应用写入日志、指标和跟踪记录数据所需的权限,请让管理员为您授予项目的以下 IAM 角色:

    如需获得查看日志、指标和跟踪记录数据所需的权限,请让管理员为您授予项目的以下 IAM 角色:

    如需详细了解如何授予角色,请参阅管理对项目、文件夹和组织的访问权限

    您也可以通过自定义角色或其他预定义角色来获取所需的权限。

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

    如需对应用进行插桩处理以收集跟踪记录和指标数据,并将结构化 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 a 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 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。OpenTelemetry Collector 使用 Google 导出器将日志和指标数据发送到您的项目。它使用 Telemetry API(该 API 使用 OTLP)将您的跟踪记录数据发送到您的项目。应用中的负载生成器会向应用的路由发出请求。

    下载并部署应用

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

    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
      
    5. 查看指标

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

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

      • Prometheus/http_client_duration_milliseconds/histogram 会记录客户端请求的持续时间,并将结果存储在直方图中。

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

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

        进入 Metrics Explorer

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

      2. 在 Google Cloud 控制台的工具栏中,选择您的 Google Cloud 项目。对于 App Hub 配置,请选择 App Hub 宿主项目或已启用应用的文件夹的管理项目。
      3. 指标元素中,展开选择指标菜单,在过滤条件栏中输入 http_server,然后使用子菜单选择一个特定资源类型和指标:
        1. 活跃资源菜单中,选择 Prometheus 目标
        2. 活跃指标类别菜单中,选择 HTTP
        3. 活跃指标菜单中,选择指标。
        4. 点击应用
      4. 如需添加用于从查询结果中移除时序的过滤条件,请使用过滤条件元素

      5. 配置数据的查看方式。

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

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

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

      查看跟踪记录

      跟踪记录数据可能需要几分钟的时间才可使用。例如,当您的项目收到跟踪记录数据时,Google Cloud Observability 可能需要创建数据库来存储这些数据。创建数据库可能需要几分钟时间,在此期间,您无法查看任何跟踪记录数据。

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

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

        转到 Trace 探索器

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

      2. 在此页面的表部分,选择 span 名称为 /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. 点击包含“处理/多请求”消息的日志条目中的 跟踪记录,然后选择查看跟踪记录详情

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

        您的日志数据可能会比跟踪记录数据早几分钟可用。如果您在通过按 ID 搜索跟踪记录或通过执行此任务中的步骤来查看跟踪记录数据时遇到错误,请等待一两分钟,然后重试相应操作。

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

      后续步骤