本文介绍通过OpenTelemetry Golang SDK将Golang应用的Trace数据接入到日志服务的操作步骤。

前提条件

  • 已创建Trace实例。更多信息,请参见创建Trace实例
  • 已安装Golang 1.13及以上版本的开发环境。

接入流程

  1. 初始化OpenTelemetry Provider。
  2. 判断是否符合半自动接入条件。
    • 如果符合,则您可以使用半自动方式接入Trace数据。

      当半自动方式无法覆盖您的所有场景时,余下场景您需要使用手动方式接入Trace数据。

    • 如果不符合,则您可以使用手动方式接入Trace数据。

步骤1:初始化OpenTelemetry Provider

为简化OpenTelemetry Provider的使用,日志服务提供SLS Provider用于快速构建并上传至日志服务的相关依赖项。
重要 您需要在创建Traces、注册Metrics之前,完成OpenTelemetry Provider的初始化。
您可以通过运行代码或配置环境变量完成OpenTelemetry Provider的初始化,详细说明如下:
  • 通过运行代码完成初始化。
    1. 添加依赖项。
      module opentelemetry-golang-sample
      
      go 1.13
      
      require (
          github.com/aliyun-sls/opentelemetry-go-provider-sls v0.9.0
          go.opentelemetry.io/otel v1.11.2
          go.opentelemetry.io/otel/metric v0.34.0
          go.opentelemetry.io/otel/trace v1.11.2
      )
    2. 配置初始化代码。

      如下代码中的变量需根据实际情况替换。关于变量的详细说明,请参见变量说明

      package main
      
      import (
          "github.com/aliyun-sls/opentelemetry-go-provider-sls/provider"
      )
      
      func main() {
      
          slsConfig, err := provider.NewConfig(provider.WithServiceName("${service}"),
              provider.WithServiceNamespace("${service.namespace}"),
              provider.WithServiceVersion("${version}"),
              provider.WithTraceExporterEndpoint("${endpoint}"),
              provider.WithMetricExporterEndpoint("${endpoint}"),
              provider.WithSLSConfig("${project}", "${instance}", "${access-key-id}", "${access-key-secret}"))
          // 使用panic(),表示如果初始化失败则程序直接异常退出,您也可以使用其他错误处理方式。
          if err != nil {
              panic(err)
          }
          if err := provider.Start(slsConfig); err != nil {
              panic(err)
          }
          defer provider.Shutdown(slsConfig)
          
          // 添加业务逻辑代码。
          ...
      }
      表 1. 变量说明
      变量说明示例
      ${service}服务名。根据您的实际场景取值。payment
      ${service.namespace}服务归属的命名空间。order
      ${version}服务版本号。建议按照va.b.c格式定义。v0.1.2
      ${endpoint}日志服务Project的接入地址,格式为${project}.${region-endpoint}:Port,其中:
      • ${project}:日志服务Project名称。
      • ${region-endpoint}:日志服务Project所在地域的访问域名,支持公网和阿里云内网(经典网络、VPC)。更多信息,请参见服务入口
      • Port:网络端口,固定为10010。
      说明
      • 如果配置为stdout,即provider.WithTraceExporterEndpoint("stdout"),表示将数据打印到标准输出。
      • 如果配置为空值,表示不上传Trace数据到日志服务。
      test-project.cn-hangzhou.log.aliyuncs.com:10010
      ${project}日志服务Project名称。test-project
      ${instance}Trace服务实例ID。更多信息,请参见创建Trace实例test-traces
      ${access-key-id}阿里云账号AccessKey ID。

      建议您使用只具备日志服务Project写入权限的RAM用户的AccessKey(包括AccessKey ID和AccessKey Secret)。授予RAM用户向指定Project写入数据权限的具体操作,请参见授权。如何获取AccessKey的具体操作,请参见访问密钥

      ${access-key-secret}阿里云账号AccessKey Secret。

      建议您使用只具备日志服务Project写入权限的RAM用户的AccessKey。

  • 通过配置环境变量完成初始化。
    配置方法环境变量是否必选说明默认值
    WithServiceNameSLS_OTEL_SERVICE_NAME服务名。根据您的实际场景取值即可。
    WithServiceNamespaceSLS_OTEL_SERVICE-NAMESPACE服务归属的命名空间。order
    WithServiceVersionSLS_OTEL_SERVICE_VERSION服务版本号。建议按照va.b.c格式定义。v0.1.0
    WithSLSConfigSLS_OTEL_PROJECT、SLS_OTEL_INSTANCE_ID、SLS_OTEL_ACCESS_KEY_ID、SLS_OTEL_ACCESS_KEY_SECRET日志服务资源信息,包括Project名称、Trace实例名称、具备Project只写权限的AccessKey ID和AccessKey Secret。授予RAM用户向指定Project写入数据权限的具体操作,请参见授权。如何获取AccessKey的具体操作,请参见访问密钥
    WithTraceExporterEndpointSLS_OTEL_TRACE_ENDPOINT日志服务Project的接入地址,格式为${project}.${region-endpoint}:Port,其中:
    • ${project}:日志服务Project名称。
    • ${region-endpoint}:日志服务Project所在地域的访问域名,支持内网和公网访问。更多信息,请参见服务入口
    • Port:网络端口,固定为10010。
    说明
    • 如果配置为stdout,表示将数据打印到标准输出。
    • 如果配置为空值,表示不上传Trace数据到日志服务。
    stdout
    WithTraceExporterInsecureSLS_OTEL_TRACE_INSECURE是否使用非安全方式传输。
    • true:使用非安全方式传输。
    • false:使用安全方式传输。
    说明 如果直接传输到日志服务,则必须配置为false
    false
    WithMetricExporterEndpointSLS_OTEL_METRIC_ENDPOINT日志服务Project的接入地址,格式为${project}.${region-endpoint}:Port,其中:
    • ${project}:日志服务Project名称。
    • ${region-endpoint}:日志服务Project所在地域的访问域名,支持内网和公网访问。更多信息,请参见服务入口
    • Port:网络端口,固定为10010。
    说明
    • 如果配置为stdout,表示将数据打印到标准输出。
    • 如果配置为空值,表示不上传时序数据到日志服务。
    stdout
    WithMetricExporterInsecureSLS_OTEL_METRIC_INSECURE是否使用非安全方式传输。
    • true:使用非安全方式传输。
    • false:使用安全方式传输。
    说明 如果直接传输到日志服务,则必须配置为false
    false
    WithResourceAttributes配置附加的Tag信息,例如环境、可用区等信息。
    WithResourceOTEL_RESOURCE_ATTRIBUTES配置附加的Tag信息,例如环境、可用区等信息。配置格式为key1=value1,key2=value2
    WithMetricReportingPeriodSLS_OTEL_METRIC_EXPORT_PERIODMetric输出间隔,建议设置区间为15s~60s。30s
    WithErrorHandler错误处理函数。
    WithErrorHandlerFunc错误处理函数。
    SLS_OTEL_ATTRIBUTES_ENV_KEYS配置附加的Tag信息,例如环境、可用区等信息。类似于OTEL_RESOURCE_ATTRIBUTES。区别在于SLS_OTEL_ATTRIBUTES_ENV_KEYS定义的Attribute Key,其实际值从对应的环境变量中读取。

    常用于K8s场景中将部分模板值填充到特定的环境变量中。配置格式为env-key-1|env-key-2|env-key-3

步骤2:接入数据

  • (推荐)半自动接入

    OpenTelemetry提供众多基础库的自动埋点方案,如果您的业务依赖于这些基础库,则可以使用这些基础库的自动埋点方案来接入数据。关于基础库的更多信息,请参见Golang自动埋点方案

    • net、http接入

      如下示例基于go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.37.0版本创建。更多信息,请参见otel-http-example

      如下代码中的变量需根据实际情况替换。关于变量的详细说明,请参见变量说明

      package main
      
      import (
      	"fmt"
      	"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
      	"go.opentelemetry.io/otel/attribute"
      	"go.opentelemetry.io/otel/metric/global"
      	"io"
      	"net/http"
      	"time"
      
      	"github.com/aliyun-sls/opentelemetry-go-provider-sls/provider"
      
      	"go.opentelemetry.io/otel/trace"
      )
      
      func main() {
      
      	slsConfig, err := provider.NewConfig(provider.WithServiceName("${service}"),
      		provider.WithServiceNamespace("${service.namespace}"),
      		provider.WithServiceVersion("${version}"),
      		provider.WithTraceExporterEndpoint("${endpoint}"),
      		provider.WithMetricExporterEndpoint("${endpoint}"),
      		provider.WithSLSConfig("${project}", "${instance}", "${access-key-id}", "${access-key-secret}"))
      	// 使用panic(),表示如果初始化失败则程序直接异常退出,您也可以使用其他错误处理方式。
      	if err != nil {
      		panic(err)
      	}
      	if err := provider.Start(slsConfig); err != nil {
      		panic(err)
      	}
      	defer provider.Shutdown(slsConfig)
      
      	// 如果您需要分析应用中的指标数据,可以注册相关的Metric指标。
      	labels := []attribute.KeyValue{
      		attribute.String("label1", "value1"),
      	}
      	meter := global.Meter("aliyun.sls")
      	sayDavidCount, _ := meter.Int64Counter("say_david_count")
      
      	helloHandler := func(w http.ResponseWriter, req *http.Request) {
      		if time.Now().Unix()%10 == 0 {
      			_, _ = io.WriteString(w, "Hello, world!\n")
      		} else {
      			// 如果您需要记录一些事件,可以获取Context中的Span并添加Event。
      			ctx := req.Context()
      			span := trace.SpanFromContext(ctx)
      			span.AddEvent("say : Hello, I am david", trace.WithAttributes(attribute.KeyValue{
      				Key:   "label-key-1",
      				Value: attribute.StringValue("label-value-1"),
      			}))
      
      			_, _ = io.WriteString(w, "Hello, I am david!\n")
      			sayDavidCount.Add(req.Context(), 1, labels...)
      		}
      	}
      
      	// 使用otel net/http的自动注入方式,只需要使用otelhttp.NewHandler包裹http.Handler。
      	otelHandler := otelhttp.NewHandler(http.HandlerFunc(helloHandler), "Hello")
      
      	http.Handle("/hello", otelHandler)
      	fmt.Println("Now listen port 8080, you can visit 127.0.0.1:8080/hello .")
      	err = http.ListenAndServe(":8080", nil)
      	if err != nil {
      		panic(err)
      	}
      }
      
    • mux接入

      下述示例基于go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.37.0 版本创建,新版本中接口可能出现一定的改动,最新示例请参见otel-mux-example

      如下代码中的变量需根据实际情况替换。关于变量的详细说明,请参见变量说明

      package main
      
      import (
      	"context"
      	"fmt"
      	"go.opentelemetry.io/otel/attribute"
      	"go.opentelemetry.io/otel/metric/global"
      	"net/http"
      
      	"github.com/aliyun-sls/opentelemetry-go-provider-sls/provider"
      
      	"github.com/gorilla/mux"
      	"go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux"
      	"go.opentelemetry.io/otel/trace"
      )
      
      func main() {
      
      	slsConfig, err := provider.NewConfig(provider.WithServiceName("${service}"),
      		provider.WithServiceNamespace("${service.namespace}"),
      		provider.WithServiceVersion("${version}"),
      		provider.WithTraceExporterEndpoint("${endpoint}"),
      		provider.WithMetricExporterEndpoint("${endpoint}"),
      		provider.WithSLSConfig("${project}", "${instance}", "${access-key-id}", "${access-key-secret}"))
      	// 使用panic(),表示如果初始化失败则程序直接异常退出,您也可以使用其他错误处理方式。
      	if err != nil {
      		panic(err)
      	}
      	if err := provider.Start(slsConfig); err != nil {
      		panic(err)
      	}
      	defer provider.Shutdown(slsConfig)
      
      	// 如果您需要分析应用中的指标数据,可以注册相关的Metric指标。
      	labels := []attribute.KeyValue{
      		attribute.String("label1", "value1"),
      	}
      	meter := global.Meter("aliyun.sls")
      	callUsersCount, _ := meter.Int64Counter("call_users_count")
      
      	r := mux.NewRouter()
      	r.Use(otelmux.Middleware("my-server"))
      	r.HandleFunc("/users/{id:[0-9]+}", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
      		vars := mux.Vars(r)
      		id := vars["id"]
      		callUsersCount.Add(r.Context(), 1, labels...)
      		name := getUser(r.Context(), id)
      		reply := fmt.Sprintf("user %s (id %s)\n", name, id)
      		_, _ = w.Write(([]byte)(reply))
      	}))
      	http.Handle("/", r)
      	fmt.Println("Now listen port 8080, you can visit 127.0.0.1:8080/users/xxx .")
      	_ = http.ListenAndServe(":8080", nil)
      }
      
      func getUser(ctx context.Context, id string) string {
      	if id == "123" {
      		return "otelmux tester"
      	}
      	// 如果您需要记录一些事件,可以获取Context中的Span并添加Event。
      	span := trace.SpanFromContext(ctx)
      	span.AddEvent("unknown user id : "+id, trace.WithAttributes(attribute.KeyValue{
      		Key:   "label-key-1",
      		Value: attribute.StringValue("label-value-1"),
      	}))
      	return "unknown"
      }
      
  • 手动接入

    如下代码中的变量需根据实际情况替换。关于变量的详细说明,请参见变量说明

    // Copyright The AliyunSLS Authors
    //
    // Licensed under the Apache License, Version 2.0 (the "License");
    // you may not use this file except in compliance with the License.
    // You may obtain a copy of the License at
    //
    //     http://www.apache.org/licenses/LICENSE-2.0
    //
    // Unless required by applicable law or agreed to in writing, software
    // distributed under the License is distributed on an "AS IS" BASIS,
    // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    // See the License for the specific language governing permissions and
    // limitations under the License.
    
    package main
    
    import (
    	"context"
    	"errors"
    	"fmt"
    	"go.opentelemetry.io/otel/attribute"
    	"go.opentelemetry.io/otel/metric/global"
    	"go.opentelemetry.io/otel/metric/instrument"
    	"math/rand"
    	"time"
    
    	"github.com/aliyun-sls/opentelemetry-go-provider-sls/provider"
    
    	"go.opentelemetry.io/otel"
    	"go.opentelemetry.io/otel/codes"
    	"go.opentelemetry.io/otel/trace"
    )
    
    func main() {
    	slsConfig, err := provider.NewConfig(provider.WithServiceName("payment"),
    		provider.WithServiceVersion("v0.1.0"),
    		provider.WithTraceExporterEndpoint("stdout"),
    		provider.WithMetricExporterEndpoint("stdout"),
    		provider.WithSLSConfig("test-project", "test-otel", "access-key-id", "access-key-secret"))
    	// 使用panic(),表示如果初始化失败则程序直接异常退出,您也可以使用其他错误处理方式。
    	if err != nil {
    		panic(err)
    	}
    	if err := provider.Start(slsConfig); err != nil {
    		panic(err)
    	}
    	defer provider.Shutdown(slsConfig)
    
    	mockTrace()
    	mockMetrics()
    }
    
    func mockMetrics() {
    	// 附加标签信息。
    	labels := []attribute.KeyValue{
    		attribute.String("label1", "value1"),
    	}
    
    	meter := global.Meter("ex.com/basic")
    
    	meter.Float64ObservableCounter("randval", instrument.WithFloat64Callback(func(ctx context.Context, observer instrument.Float64Observer) error {
    		observer.Observe(rand.Float64(), labels...)
    		return nil
    	}))
    
    	temperature, _ := meter.Float64Counter("temperature")
    	interrupts, _ := meter.Int64Counter("interrupts")
    
    	ctx := context.Background()
    
    	for {
    		temperature.Add(ctx, 100+10*rand.NormFloat64(), labels...)
    		interrupts.Add(ctx, int64(rand.Intn(100)), labels...)
    
    		time.Sleep(time.Second * time.Duration(rand.Intn(10)))
    	}
    }
    
    func mockTrace() {
    
    	tracer := otel.Tracer("ex.com/basic")
    
    	ctx0 := context.Background()
    
    	ctx1, finish1 := tracer.Start(ctx0, "foo")
    	defer finish1.End()
    
    	ctx2, finish2 := tracer.Start(ctx1, "bar")
    	defer finish2.End()
    
    	ctx3, finish3 := tracer.Start(ctx2, "baz")
    	defer finish3.End()
    
    	ctx := ctx3
    	getSpan(ctx)
    	addAttribute(ctx)
    	addEvent(ctx)
    	recordException(ctx)
    	createChild(ctx, tracer)
    }
    
    // example of getting the current span
    // 获取当前的Span。
    func getSpan(ctx context.Context) {
    	span := trace.SpanFromContext(ctx)
    	fmt.Printf("current span: %v\n", span)
    }
    
    // example of adding an attribute to a span
    // 向Span中添加属性值。
    func addAttribute(ctx context.Context) {
    	span := trace.SpanFromContext(ctx)
    	span.SetAttributes(attribute.KeyValue{
    		Key:   "label-key-1",
    		Value: attribute.StringValue("label-value-1")})
    }
    
    // example of adding an event to a span
    // 向Span中添加事件。
    func addEvent(ctx context.Context) {
    	span := trace.SpanFromContext(ctx)
    	span.AddEvent("event1", trace.WithAttributes(
    		attribute.String("event-attr1", "event-string1"),
    		attribute.Int64("event-attr2", 10)))
    }
    
    // example of recording an exception
    // 记录Span结果以及错误信息。
    func recordException(ctx context.Context) {
    	span := trace.SpanFromContext(ctx)
    	span.RecordError(errors.New("exception has occurred"))
    	span.SetStatus(codes.Error, "internal error")
    }
    
    // example of creating a child span
    // 创建子Span。
    func createChild(ctx context.Context, tracer trace.Tracer) {
    	// span := trace.SpanFromContext(ctx)
    	_, childSpan := tracer.Start(ctx, "child")
    	defer childSpan.End()
    	fmt.Printf("child span: %v\n", childSpan)
    }
    

后续步骤