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

相关推荐
蘑菇蘑菇不会开花~4 小时前
MySQL | 实战 | 4 种将数据同步到ES方案
数据库·mysql·elasticsearch
菜到极致就是渣5 小时前
Elasticsearch集群
大数据·elasticsearch·搜索引擎
搬砖天才、6 小时前
ELK-01-elasticsearch-8.15.1安装
elk·elasticsearch
不平衡的叉叉树20 小时前
ES数据的删除与备份
大数据·elasticsearch·搜索引擎
铭毅天下20 小时前
Elasticsearch 完整格式的 URL 进行分词,有什么好的解决方案吗?
大数据·elasticsearch·搜索引擎·全文检索
golove66620 小时前
elasticsearch实战应用
elasticsearch
唤醒手腕1 天前
2024 年最新前端ES-Module模块化、webpack打包工具详细教程(更新中)
前端·elasticsearch·webpack
杰克逊的日记1 天前
1 elasticsearch安装
大数据·elasticsearch·搜索引擎·安装
菜到极致就是渣1 天前
Java如何操作Elasticsearch
java·开发语言·elasticsearch
衣舞晨风1 天前
Elasticsearch不停机切换(上云)方案
大数据·elasticsearch·上云·迁移·不停机