使用 OTel SDK 增强 Go 应用程序¶
Golang 无侵入式接入链路请参考 通过 Operator 实现应用程序无侵入增强 文档,通过注解实现自动接入链路。
OpenTelemetry 也简称为 OTel,是一个开源的可观测性框架,可以帮助在 Go 应用程序中生成和收集遥测数据:链路、指标和日志。
本文主要讲解如何在 Go 应用程序中通过 OpenTelemetry Go SDK 增强并接入链路监控。
使用 OTel SDK 增强 Go 应用¶
安装相关依赖¶
必须先安装与 OpenTelemetry exporter 和 SDK 相关的依赖项。如果您正在使用其他请求路由器,请参考请求路由。 切换/进入到应用程序源文件夹后运行以下命令:
go get go.opentelemetry.io/otel@v1.19.0 \
go.opentelemetry.io/otel/trace@v1.19.0 \
go.opentelemetry.io/otel/sdk@v1.19.0 \
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin@v0.46.1 \
go.opentelemetry.io/otel/exporters/otlp/otlptrace@v1.19.0 \
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc@v1.19.0
使用 OTel SDK 创建初始化函数¶
为了让应用程序能够发送数据,需要一个函数来初始化 OpenTelemetry。在 main.go 文件中添加以下代码片段:
import (
"context"
"os"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.7.0"
"go.uber.org/zap"
"google.golang.org/grpc"
)
var tracerExp *otlptrace.Exporter
func retryInitTracer() func() {
var shutdown func()
go func() {
for {
// otel will reconnected and re-send spans when otel col recover. so, we don't need to re-init tracer exporter.
if tracerExp == nil {
shutdown = initTracer()
} else {
break
}
time.Sleep(time.Minute * 5)
}
}()
return shutdown
}
func initTracer() func() {
// temporarily set timeout to 10s
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
serviceName, ok := os.LookupEnv("OTEL_SERVICE_NAME")
if !ok {
serviceName = "server_name"
os.Setenv("OTEL_SERVICE_NAME", serviceName)
}
otelAgentAddr, ok := os.LookupEnv("OTEL_EXPORTER_OTLP_ENDPOINT")
if !ok {
otelAgentAddr = "http://localhost:4317"
os.Setenv("OTEL_EXPORTER_OTLP_ENDPOINT", otelAgentAddr)
}
zap.S().Infof("OTLP Trace connect to: %s with service name: %s", otelAgentAddr, serviceName)
traceExporter, err := otlptracegrpc.New(ctx, otlptracegrpc.WithInsecure(), otlptracegrpc.WithDialOption(grpc.WithBlock()))
if err != nil {
handleErr(err, "OTLP Trace gRPC Creation")
return nil
}
tracerProvider := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(traceExporter),
sdktrace.WithSampler(sdktrace.AlwaysSample()),
sdktrace.WithResource(resource.NewWithAttributes(semconv.SchemaURL)))
otel.SetTracerProvider(tracerProvider)
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))
tracerExp = traceExporter
return func() {
// Shutdown will flush any remaining spans and shut down the exporter.
handleErr(tracerProvider.Shutdown(ctx), "failed to shutdown TracerProvider")
}
}
func handleErr(err error, message string) {
if err != nil {
zap.S().Errorf("%s: %v", message, err)
}
}
在 main.go 中初始化跟踪器¶
修改 main 函数以在 main.go 中初始化跟踪器。另外当您的服务关闭时,应该调用 TracerProvider.Shutdown() 确保导出所有 Span。该服务将该调用作为主函数中的延迟函数:
func main() {
// start otel tracing
if shutdown := retryInitTracer(); shutdown != nil {
defer shutdown()
}
......
}
为应用添加 OTel Gin 中间件¶
通过在 main.go 中添加以下行来配置 Gin 以使用中间件:
import (
....
"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
)
func main() {
......
r := gin.Default()
r.Use(otelgin.Middleware("my-app"))
......
}
运行应用程序¶
-
本地调试运行
注意: 此步骤仅用于本地开发调试,生产环境中 Operator 会自动完成以下环境变量的注入。
以上步骤已经完成了初始化 SDK 的工作,现在如果需要在本地开发进行调试,需要提前获取到 insight-system 命名空间下 insight-agent-opentelemerty-collector 的地址,假设为: insight-agent-opentelemetry-collector.insight-system.svc.cluster.local:4317 。
因此,可以在你本地启动应用程序的时候添加如下环境变量:
-
生产环境运行
请参考通过 Operator 实现应用程序无侵入增强 中 只注入环境变量注解 相关介绍,为 deployment yaml 添加注解:
instrumentation.opentelemetry.io/inject-sdk: "insight-system/insight-opentelemetry-autoinstrumentation"
如果无法使用注解的方式,您可以手动在 deployment yaml 添加如下环境变量:
······
env:
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: 'http://insight-agent-opentelemetry-collector.insight-system.svc.cluster.local:4317'
- name: OTEL_SERVICE_NAME
value: "your depolyment name" # (1)!
- name: OTEL_K8S_NAMESPACE
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.namespace
- name: OTEL_RESOURCE_ATTRIBUTES_NODE_NAME
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: spec.nodeName
- name: OTEL_RESOURCE_ATTRIBUTES_POD_NAME
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.name
- name: OTEL_RESOURCE_ATTRIBUTES
value: 'k8s.namespace.name=$(OTEL_K8S_NAMESPACE),k8s.node.name=$(OTEL_RESOURCE_ATTRIBUTES_NODE_NAME),k8s.pod.name=$(OTEL_RESOURCE_ATTRIBUTES_POD_NAME)'
······
- 修改此值
请求路由¶
OpenTelemetry gin/gonic 增强¶
# Add one line to your import() stanza depending upon your request router:
middleware "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
然后注入 OpenTelemetry 中间件:
OpenTelemetry gorillamux 增强¶
# Add one line to your import() stanza depending upon your request router:
middleware "go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux"
然后注入 OpenTelemetry 中间件:
gRPC 增强¶
同样,OpenTelemetry 也可以帮助您自动检测 gRPC 请求。要检测您拥有的任何 gRPC 服务器,请将拦截器添加到服务器的实例化中。
import (
grpcotel "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
)
func main() {
[...]
s := grpc.NewServer(
grpc.UnaryInterceptor(grpcotel.UnaryServerInterceptor()),
grpc.StreamInterceptor(grpcotel.StreamServerInterceptor()),
)
}
需要注意的是,如果你的程序里面使用到了 Grpc Client 调用第三方服务,你还需要对 Grpc Client 添加拦截器:
[...]
conn, err := grpc.Dial(addr, grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithUnaryInterceptor(otelgrpc.UnaryClientInterceptor()),
grpc.WithStreamInterceptor(otelgrpc.StreamClientInterceptor()),
)
如果不使用请求路由¶
在将 http.Handler 传递给 ServeMux 的每个地方,您都将包装处理程序函数。例如,将进行以下替换:
- mux.Handle("/path", h)
+ mux.Handle("/path", otelhttp.NewHandler(h, "description of path"))
---
- mux.Handle("/path", http.HandlerFunc(f))
+ mux.Handle("/path", otelhttp.NewHandler(http.HandlerFunc(f), "description of path"))
通过这种方式,您可以确保使用 othttp 包装的每个函数都会自动收集其元数据并启动相应的跟踪。
数据库访问增强¶
Golang Gorm¶
OpenTelemetry 社区也开发了数据库访问库的中间件,比如 Gorm:
import (
"github.com/uptrace/opentelemetry-go-extra/otelgorm"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
if err != nil {
panic(err)
}
otelPlugin := otelgorm.NewPlugin(otelgorm.WithDBName("mydb"), # 缺失会导致数据库相关拓扑展示不完整
otelgorm.WithAttributes(semconv.ServerAddress("memory"))) # 缺失会导致数据库相关拓扑展示不完整
if err := db.Use(otelPlugin); err != nil {
panic(err)
}
自定义 Span¶
很多时候,OpenTelemetry 提供的中间件不能帮助我们记录更多内部调用的函数,需要我们自定义 Span 来记录
······
_, span := otel.Tracer("GetServiceDetail").Start(ctx,
"spanMetricDao.GetServiceDetail",
trace.WithSpanKind(trace.SpanKindInternal))
defer span.End()
······
向 span 添加自定义属性和事件¶
也可以将自定义属性或标签设置为 Span。要添加自定义属性和事件,请按照以下步骤操作:
导入跟踪和属性库¶
从上下文中获取当前 Span¶
在当前 Span 中设置属性¶
为当前 Span 添加 Event¶
添加 span 事件是使用 span 对象上的 AddEvent 完成的。
记录错误和异常¶
import "go.opentelemetry.io/otel/codes"
// 获取当前 span
span := trace.SpanFromContext(ctx)
// RecordError 会自动将一个错误转换成 span even
span.RecordError(err)
// 标记这个 span 错误
span.SetStatus(codes.Error, "internal error")
参考¶
有关 Demo 演示请参考: - opentelemetry-demo/productcatalogservice - opentelemetry-collector-contrib/demo