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

相关推荐
一次旅行20 小时前
【效率秘籍】Shell 函数+别名组合拳,让 80% 的重复命令变 1 个字母
大数据·elasticsearch·搜索引擎
暖和_白开水20 小时前
数据分析agent(九):es_client_manager.py 和分词器ik_max_word问题
elasticsearch·数据分析·c#
码农学院21 小时前
Elasticsearch构建汽车零配件智能搜索与匹配系统
大数据·elasticsearch·汽车
Elastic 中国社区官方博客2 天前
不到 5 分钟完成本地部署:Jina embedding 模型现已支持本地部署
大数据·人工智能·elasticsearch·搜索引擎·embedding·jina
二进制流水搬运工2 天前
入职第一天拉了23个仓库,我写了个脚本解放双手
大数据·elasticsearch·搜索引擎
Elasticsearch2 天前
在不到一小时内将 Datadog Kubernetes 仪表板迁移到 Elastic Observability
elasticsearch
.柒宇.2 天前
Elasticsearch 核心概念与系统架构详解
elasticsearch·系统架构
风行無痕2 天前
单机Elasticsearch 8.19.18安全升级到9.4.3的过程
大数据·elasticsearch·搜索引擎
Elastic 中国社区官方博客2 天前
使用重新设计的 AutoOps 更快地进行 Elasticsearch 问题排查
大数据·运维·前端·人工智能·elasticsearch·搜索引擎·全文检索
阿拉雷️2 天前
【搜索实战】Spring Boot 3.3 + AI Agent × Elasticsearch:让AI自动优化搜索策略,查询速度从2秒压到50毫秒
人工智能·spring boot·elasticsearch