Observability:使用 OpenTelemetry 手动检测 Go 应用程序

作者:Luca Wintergerst

DevOps 和 SRE 团队正在改变软件开发的流程。 DevOps 工程师专注于高效的软件应用程序和服务交付,而 SRE 团队是确保可靠性、可扩展性和性能的关键。 这些团队必须依赖全栈可观察性解决方案,使他们能够管理和监控系统,并确保问题在影响业务之前得到解决。

整个现代分布式应用程序堆栈的可观察性需要通常以仪表板的形式收集、处理和关联数据。 摄取所有系统数据需要跨堆栈、框架和提供程序安装代理,对于必须处理版本更改、兼容性问题和不随系统更改而扩展的专有代码的团队来说,这个过程可能具有挑战性且耗时。

得益于 OpenTelemetry (OTel),DevOps 和 SRE 团队现在拥有一种标准方法来收集和发送数据,该方法不依赖于专有代码,并且拥有大型社区支持,减少了供应商锁定。

在这篇博文中,我们将向你展示如何使用 OpenTelemetry 手动检测 Go 应用程序。 这种方法比使用自动检测稍微复杂一些

之前的博客中,我们还回顾了如何使用 OpenTelemetry 演示并将其连接到 Elastic®,以及 Elastic 与 OpenTelemetry 的一些功能。 在本博客中,我们将使用另一个演示应用程序,它有助于以简单的方式突出显示手动检测。

最后,我们将讨论 Elastic 如何支持与 Elastic 和 OpenTelemetry 代理一起运行的混合模式应用程序。 这样做的好处是不需要 otel-collector! 此设置你你能够根据最适合你业务的时间表,缓慢而轻松地将应用程序迁移到使用 Elastic 的 OTel。

应用程序、先决条件和配置

我们在这个博客中使用的应用程序称为 Elastiflix,一个电影流应用程序。 它由多个用 .NET、NodeJS、Go 和 Python 编写的微服务组成。

在我们检测示例应用程序之前,我们首先需要了解 Elastic 如何接收遥测数据。

OpenTelemetry 的 Elastic 配置选项

Elastic Observability 的所有 APM 功能均可通过 OTel 数据使用。 其中一些包括:

  • Service maps
  • 服务详细信息(延迟、吞吐量、失败的 transactions)
  • 服务之间的依赖关系、分布式追踪
  • transactions(跟踪)
  • 机器学习 (ML) 相关性
  • Log 相关性

除了 Elastic 的 APM 和遥测数据的统一视图之外,你还可以使用 Elastic 强大的机器学习功能来减少分析,并发出警报以帮助降低 MTTR。

先决条件

查看示例源代码

完整的源代码(包括本博客中使用的 Dockerfile)可以在 GitHub 上找到。 该存储库还包含相同的应用程序,但没有检测。 这使你可以比较每个文件并查看差异。

在开始之前,让我们先看一下未检测的代码。

这是我们可以接收 GET 请求的简单 go 应用程序。 请注意,此处显示的代码是稍微缩写的版本。

go 复制代码
1.  package main

3.  import (
4.  	"log"
5.  	"net/http"
6.  	"os"
7.  	"time"

9.  	"github.com/go-redis/redis/v8"

11.  	"github.com/sirupsen/logrus"

13.  	"github.com/gin-gonic/gin"
14.  	"strconv"
15.  	"math/rand"
16.  )

18.  var logger = &logrus.Logger{
19.  	Out:   os.Stderr,
20.  	Hooks: make(logrus.LevelHooks),
21.  	Level: logrus.InfoLevel,
22.  	Formatter: &logrus.JSONFormatter{
23.  		FieldMap: logrus.FieldMap{
24.  			logrus.FieldKeyTime:  "@timestamp",
25.  			logrus.FieldKeyLevel: "log.level",
26.  			logrus.FieldKeyMsg:   "message",
27.  			logrus.FieldKeyFunc:  "function.name", // non-ECS
28.  		},
29.  		TimestampFormat: time.RFC3339Nano,
30.  	},
31.  }

33.  func main() {
34.  	delayTime, _ := strconv.Atoi(os.Getenv("TOGGLE_SERVICE_DELAY"))

36.  	redisHost := os.Getenv("REDIS_HOST")
37.  	if redisHost == "" {
38.  		redisHost = "localhost"
39.  	}

41.  	redisPort := os.Getenv("REDIS_PORT")
42.  	if redisPort == "" {
43.  		redisPort = "6379"
44.  	}

46.  	applicationPort := os.Getenv("APPLICATION_PORT")
47.  	if applicationPort == "" {
48.  		applicationPort = "5000"
49.  	}

51.  	// Initialize Redis client
52.  	rdb := redis.NewClient(&redis.Options{
53.  		Addr:     redisHost + ":" + redisPort,
54.  		Password: "",
55.  		DB:       0,
56.  	})

58.  	// Initialize router
59.  	r := gin.New()
60.  	r.Use(logrusMiddleware)

62.  	r.GET("/favorites", func(c *gin.Context) {
63.  		// artificial sleep for delayTime
64.  		time.Sleep(time.Duration(delayTime) * time.Millisecond)

66.  		userID := c.Query("user_id")

68.  		contextLogger(c).Infof("Getting favorites for user %q", userID)

70.  		favorites, err := rdb.SMembers(c.Request.Context(), userID).Result()
71.  		if err != nil {
72.  			contextLogger(c).Error("Failed to get favorites for user %q", userID)
73.  			c.String(http.StatusInternalServerError, "Failed to get favorites")
74.  			return
75.  		}

77.  		contextLogger(c).Infof("User %q has favorites %q", userID, favorites)

79.  		c.JSON(http.StatusOK, gin.H{
80.  			"favorites": favorites,
81.  		})
82.  	})

84.  	// Start server
85.  	logger.Infof("App startup")
86.  	log.Fatal(http.ListenAndServe(":"+applicationPort, r))
87.  	logger.Infof("App stopped")
88.  }

分步指南

步骤 0. 登录你的 Elastic Cloud 帐户

本博客假设你有 Elastic Cloud 帐户 - 如果没有,请按照说明开始使用 Elastic Cloud

步骤 1. 安装并初始化 OpenTelemetry

第一步,我们需要向应用程序添加一些额外的包。

markdown 复制代码
1.  import (
2.        "github.com/go-redis/redis/extra/redisotel/v8"
3.        "go.opentelemetry.io/otel"
4.        "go.opentelemetry.io/otel/attribute"
5.        "go.opentelemetry.io/otel/exporters/otlp/otlptrace"
6.      "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"

8.  	"go.opentelemetry.io/otel/propagation"

10.  	"google.golang.org/grpc/credentials"
11.  	"crypto/tls"

13.        sdktrace "go.opentelemetry.io/otel/sdk/trace"

15.  	"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"

17.  	"go.opentelemetry.io/otel/trace"
18.  	"go.opentelemetry.io/otel/codes"
19.  )

此代码导入必要的 OpenTelemetry 包,包括用于跟踪、导出和检测特定库(如 Redis)的包。

接下来我们读取 OTEL_EXPORTER_OTLP_ENDPOINT 变量并初始化导出器。

go 复制代码
1.  var (
2.      collectorURL = os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT")
3.  )
4.  var tracer trace.Tracer

7.  func initTracer() func(context.Context) error {
8.  	tracer = otel.Tracer("go-favorite-otel-manual")

10.  	// remove https:// from the collector URL if it exists
11.  	collectorURL = strings.Replace(collectorURL, "https://", "", 1)
12.  	secretToken := os.Getenv("ELASTIC_APM_SECRET_TOKEN")
13.  	if secretToken == "" {
14.  		log.Fatal("ELASTIC_APM_SECRET_TOKEN is required")
15.  	}

17.  	secureOption := otlptracegrpc.WithInsecure()
18.      exporter, err := otlptrace.New(
19.          context.Background(),
20.          otlptracegrpc.NewClient(
21.              secureOption,
22.              otlptracegrpc.WithEndpoint(collectorURL),
23.  			otlptracegrpc.WithHeaders(map[string]string{
24.  				"Authorization": "Bearer " + secretToken,
25.  			}),
26.  			otlptracegrpc.WithTLSCredentials(credentials.NewTLS(&tls.Config{})),
27.          ),
28.      )

30.      if err != nil {
31.          log.Fatal(err)
32.      }

34.      otel.SetTracerProvider(
35.          sdktrace.NewTracerProvider(
36.              sdktrace.WithSampler(sdktrace.AlwaysSample()),
37.              sdktrace.WithBatcher(exporter),
38.          ),
39.      )
40.  	otel.SetTextMapPropagator(
41.  		propagation.NewCompositeTextMapPropagator(
42.  			propagation.Baggage{},
43.  			propagation.TraceContext{},
44.  		),
45.  	)
46.      return exporter.Shutdown
47.  }

为了检测到 Redis 的连接,我们将向其添加一个跟踪钩子(hook),为了检测 Gin,我们将添加 OTel 中间件。 这将自动捕获与我们的应用程序的所有交互,因为 Gin 将被完全检测。 此外,所有到 Redis 的传出连接也将被检测。

markdown 复制代码
 1.        // Initialize Redis client
2.  	rdb := redis.NewClient(&redis.Options{
3.  		Addr:     redisHost + ":" + redisPort,
4.  		Password: "",
5.  		DB:       0,
6.  	})
7.  	rdb.AddHook(redisotel.NewTracingHook())
8.  	// Initialize router
9.  	r := gin.New()
10.  	r.Use(logrusMiddleware)
11.  	r.Use(otelgin.Middleware("go-favorite-otel-manual"))

添加自定义 span

现在我们已经添加并初始化了所有内容,我们可以添加自定义跨度。

如果我们想为应用程序的一部分提供额外的检测,我们只需启动一个自定义 span,然后推迟结束该 span。

css 复制代码
 2.  // start otel span
3.  ctx := c.Request.Context()
4.  ctx, span := tracer.Start(ctx, "add_favorite_movies")
5.  defer span.End()

为了进行比较,这是我们示例应用程序的检测代码。 你可以在 GitHub 上找到完整的源代码。

go 复制代码
1.  package main

3.  import (
4.  	"log"
5.  	"net/http"
6.  	"os"
7.  	"time"
8.  	"context"

10.  	"github.com/go-redis/redis/v8"
11.  	"github.com/go-redis/redis/extra/redisotel/v8"

14.  	"github.com/sirupsen/logrus"

16.  	"github.com/gin-gonic/gin"

18.    "go.opentelemetry.io/otel"
19.    "go.opentelemetry.io/otel/attribute"
20.    "go.opentelemetry.io/otel/exporters/otlp/otlptrace"
21.    "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"

23.  	"go.opentelemetry.io/otel/propagation"

25.  	"google.golang.org/grpc/credentials"
26.  	"crypto/tls"

28.    sdktrace "go.opentelemetry.io/otel/sdk/trace"

30.  	"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"

32.  	"go.opentelemetry.io/otel/trace"

34.  	"strings"
35.  	"strconv"
36.  	"math/rand"
37.  	"go.opentelemetry.io/otel/codes"

39.  )

41.  var tracer trace.Tracer

43.  func initTracer() func(context.Context) error {
44.  	tracer = otel.Tracer("go-favorite-otel-manual")

46.  	collectorURL = strings.Replace(collectorURL, "https://", "", 1)

48.  	secureOption := otlptracegrpc.WithInsecure()

50.  	// split otlpHeaders by comma and convert to map
51.  	headers := make(map[string]string)
52.  	for _, header := range strings.Split(otlpHeaders, ",") {
53.  		headerParts := strings.Split(header, "=")

55.  		if len(headerParts) == 2 {
56.  			headers[headerParts[0]] = headerParts[1]
57.  		}
58.  	}

60.      exporter, err := otlptrace.New(
61.          context.Background(),
62.          otlptracegrpc.NewClient(
63.              secureOption,
64.              otlptracegrpc.WithEndpoint(collectorURL),
65.  			otlptracegrpc.WithHeaders(headers),
66.  			otlptracegrpc.WithTLSCredentials(credentials.NewTLS(&tls.Config{})),
67.          ),
68.      )

70.      if err != nil {
71.          log.Fatal(err)
72.      }

74.      otel.SetTracerProvider(
75.          sdktrace.NewTracerProvider(
76.              sdktrace.WithSampler(sdktrace.AlwaysSample()),
77.              sdktrace.WithBatcher(exporter),
78.              //sdktrace.WithResource(resources),
79.          ),
80.      )
81.  	otel.SetTextMapPropagator(
82.  		propagation.NewCompositeTextMapPropagator(
83.  			propagation.Baggage{},
84.  			propagation.TraceContext{},
85.  		),
86.  	)
87.      return exporter.Shutdown
88.  }

90.  var (
91.    collectorURL = os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT")
92.  	otlpHeaders = os.Getenv("OTEL_EXPORTER_OTLP_HEADERS")
93.  )

96.  var logger = &logrus.Logger{
97.  	Out:   os.Stderr,
98.  	Hooks: make(logrus.LevelHooks),
99.  	Level: logrus.InfoLevel,
100.  	Formatter: &logrus.JSONFormatter{
101.  		FieldMap: logrus.FieldMap{
102.  			logrus.FieldKeyTime:  "@timestamp",
103.  			logrus.FieldKeyLevel: "log.level",
104.  			logrus.FieldKeyMsg:   "message",
105.  			logrus.FieldKeyFunc:  "function.name", // non-ECS
106.  		},
107.  		TimestampFormat: time.RFC3339Nano,
108.  	},
109.  }

111.  func main() {
112.  	cleanup := initTracer()
113.    defer cleanup(context.Background())

115.  	redisHost := os.Getenv("REDIS_HOST")
116.  	if redisHost == "" {
117.  		redisHost = "localhost"
118.  	}

120.  	redisPort := os.Getenv("REDIS_PORT")
121.  	if redisPort == "" {
122.  		redisPort = "6379"
123.  	}

125.  	applicationPort := os.Getenv("APPLICATION_PORT")
126.  	if applicationPort == "" {
127.  		applicationPort = "5000"
128.  	}

130.  	// Initialize Redis client
131.  	rdb := redis.NewClient(&redis.Options{
132.  		Addr:     redisHost + ":" + redisPort,
133.  		Password: "",
134.  		DB:       0,
135.  	})
136.  	rdb.AddHook(redisotel.NewTracingHook())

139.  	// Initialize router
140.  	r := gin.New()
141.  	r.Use(logrusMiddleware)
142.  	r.Use(otelgin.Middleware("go-favorite-otel-manual"))

145.  	// Define routes
146.  	r.GET("/", func(c *gin.Context) {
147.  		contextLogger(c).Infof("Main request successful")
148.  		c.String(http.StatusOK, "Hello World!")
149.  	})

151.  	r.GET("/favorites", func(c *gin.Context) {
152.  		// artificial sleep for delayTime
153.  		time.Sleep(time.Duration(delayTime) * time.Millisecond)

155.  		userID := c.Query("user_id")

157.  		contextLogger(c).Infof("Getting favorites for user %q", userID)

159.  		favorites, err := rdb.SMembers(c.Request.Context(), userID).Result()
160.  		if err != nil {
161.  			contextLogger(c).Error("Failed to get favorites for user %q", userID)
162.  			c.String(http.StatusInternalServerError, "Failed to get favorites")
163.  			return
164.  		}

166.  		contextLogger(c).Infof("User %q has favorites %q", userID, favorites)

168.  		c.JSON(http.StatusOK, gin.H{
169.  			"favorites": favorites,
170.  		})
171.  	})

173.  	// Start server
174.  	logger.Infof("App startup")
175.  	log.Fatal(http.ListenAndServe(":"+applicationPort, r))
176.  	logger.Infof("App stopped")
177.  }

步骤 2. 使用环境变量运行 Docker 镜像

正如 OTEL 文档中所指定的,我们将使用环境变量并传入 APM 代理配置部分中找到的配置值。

由于 Elastic 本身接受 OTLP,因此我们只需要提供 OTEL Exporter 需要发送数据的端点和身份验证,以及一些其他环境变量。

在 Elastic Cloud 和 Kibana® 中从哪里获取这些变量

你可以从路径 /app/home#/tutorial/apm 下的 Kibana 复制端点和 token。

你需要复制 OTEL_EXPORTER_OTLP_ENDPOINT 以及 OTEL_EXPORTER_OTLP_HEADERS。

构建镜像

arduino 复制代码
docker build -t  go-otel-manual-image .

运行镜像

ini 复制代码
1.  docker run \
2.         -e OTEL_EXPORTER_OTLP_ENDPOINT="<REPLACE WITH OTEL_EXPORTER_OTLP_ENDPOINT>" \
3.         -e OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer <REPLACE WITH TOKEN>" \
4.         -e OTEL_RESOURCE_ATTRIBUTES="service.version=1.0,deployment.environment=production,service. \
5.         -p 5000:5000 \
6.         go-otel-manual-image

你现在可以发出一些请求来生成跟踪数据。 请注意,这些请求预计会返回错误,因为此服务依赖于你当前未运行的 Redis 连接。 如前所述,你可以在此处找到使用 Docker compose 的更完整示例。

bash 复制代码
1.  curl localhost:500/favorites
2.  # or alternatively issue a request every second

4.  while true; do curl "localhost:5000/favorites"; sleep 1; done;

跟踪如何显示在 Elastic 中?

现在该服务已完成检测,在查看 Node.js 服务的 transaction 部分时,你应该会在 Elastic APM 中看到以下输出:

结论

在这篇博客中,我们讨论了以下内容:

  • 如何使用 OpenTelemetry 手动检测 Go
  • 如何正确初始化 OpenTelemetry 并添加自定义范围
  • 如何使用 Elastic 轻松设置 OTLP ENDPOINT 和 OTLP HEADERS,而不需要收集器

希望它能够提供一个易于理解的使用 OpenTelemetry 检测 Go 的演练,以及将跟踪发送到 Elastic 是多么容易。

有关 OpenTelemetry with Elastic 的其他资源:

开发者资源:

常规配置和用例资源:

原文:Manual instrumentation of Go applications with OpenTelemetry | Elastic Blog

相关推荐
Mephisto.java1 小时前
【大数据学习 | Spark】Spark的改变分区的算子
大数据·elasticsearch·oracle·spark·kafka·memcache
mqiqe1 小时前
Elasticsearch 分词器
python·elasticsearch
小马爱打代码1 小时前
Elasticsearch简介与实操
大数据·elasticsearch·搜索引擎
java1234_小锋10 小时前
Elasticsearch是如何实现Master选举的?
大数据·elasticsearch·搜索引擎
梦幻通灵16 小时前
ES分词环境实战
大数据·elasticsearch·搜索引擎
Elastic 中国社区官方博客16 小时前
Elasticsearch 中的热点以及如何使用 AutoOps 解决它们
大数据·运维·elasticsearch·搜索引擎·全文检索
小黑屋说YYDS1 天前
ElasticSearch7.x入门教程之索引概念和基础操作(三)
elasticsearch
Java 第一深情1 天前
Linux上安装单机版ElasticSearch6.8.1
linux·elasticsearch·全文检索
KevinAha2 天前
Elasticsearch 6.8 分析器
elasticsearch
wuxingge2 天前
elasticsearch7.10.2集群部署带认证
运维·elasticsearch