Go 语言 OpenTelemetry 接入指南
本文档提供 Go 语言应用接入 OpenTelemetry 的完整指南,包括零代码接入和 SDK 手动接入两种方式。
目录
概述
Go 语言特性
Go 语言的 OpenTelemetry 支持具有以下特点:
- 成熟稳定: API 和 SDK 已达到 1.0 稳定版本
- 高性能: 极低的性能开销(< 5% CPU, 10-20MB 内存)
- 丰富的埋点库: 支持 50+ 主流框架和库
- Context 传播: 完美契合 Go 的 context.Context 机制
- 并发安全: 所有 API 都是线程安全的
版本要求
- Go 版本: 1.20+ (推荐 1.21+)
- OpenTelemetry Go: v1.20.0+
- 支持架构: amd64, arm64, 386, arm
支持的信号
- ✅ Traces: 完全支持
- ✅ Metrics: 完全支持
- ✅ Logs: 支持(通过日志库集成)
适用场景
- Web 服务(HTTP/gRPC)
- 微服务架构
- 数据库应用
- 消息队列应用
- 批处理作业
零代码接入(推荐)
零代码接入无需修改应用代码,通过环境变量配置即可自动完成埋点。
方式 1: 使用官方 Auto-Instrumentation(实验性)
Go 官方的自动埋点目前处于实验阶段,支持有限。对于生产环境,建议使用 loongsuite-go-agent 或 SDK 手动接入。
安装
go install go.opentelemetry.io/auto@latest
配置环境变量
# 服务信息export OTEL_SERVICE_NAME=my-go-serviceexport OTEL_RESOURCE_ATTRIBUTES=service.version=1.0.0,deployment.environment=production# OTLP Exporterexport OTEL_EXPORTER_OTLP_ENDPOINT=uptrace-grpc.yodao.cn:4317export OTEL_EXPORTER_OTLP_PROTOCOL=grpcexport OTEL_EXPORTER_OTLP_HEADERS=uptrace-dsn=http://token@uptrace-grpc.yodao.cn/project# Traces 配置export OTEL_TRACES_EXPORTER=otlpexport OTEL_TRACES_SAMPLER=parentbased_traceidratioexport OTEL_TRACES_SAMPLER_ARG=0.1# Metrics 配置export OTEL_METRICS_EXPORTER=otlpexport OTEL_METRIC_EXPORT_INTERVAL=60000# Logs 配置export OTEL_LOGS_EXPORTER=otlp
启动应用
# 使用 auto-instrumentation 启动go run go.opentelemetry.io/auto -service.name=my-service ./main.go# 或编译后运行go build -o myappgo run go.opentelemetry.io/auto -service.name=my-service ./myapp
方式 2: 使用 loongsuite-go-agent(推荐)
loongsuite-go-agent 是阿里巴巴开源的 Go 自动埋点工具,支持编译时注入,功能更强大。
详细文档: Go loongsuite-go-agent 接入指南
快速开始
# 1. 安装 otel 工具curl -sSf https://raw.githubusercontent.com/alibaba/loongsuite-go-agent/main/install.sh | sh# 2. 初始化项目cd your-projectotel init# 3. 配置环境变量export OTEL_SERVICE_NAME=my-serviceexport OTEL_EXPORTER_OTLP_ENDPOINT=uptrace-grpc.yodao.cn:4317export OTEL_EXPORTER_OTLP_HEADERS=uptrace-dsn=http://token@uptrace-grpc.yodao.cn/project# 4. 使用 otel 工具编译otel build -o myapp# 5. 运行./myapp
支持的框架(部分)
- Web: Gin, Echo, Fiber, Chi, Gorilla Mux, Hertz, Kratos
- 数据库: database/sql, GORM, MongoDB, Redis, Elasticsearch
- RPC: gRPC, Kitex, Dubbo-go, RPCX
- 消息队列: Kafka, RabbitMQ, RocketMQ, NATS
- HTTP 客户端: net/http, fasthttp
- 日志: zap, logrus, zerolog, slog
完整列表: https://github.com/alibaba/loongsuite-go-agent#supported-libraries
方式 3: 使用 Kubernetes Operator
如果您的应用运行在 Kubernetes 中,可以使用 OpenTelemetry Operator 自动注入。
安装 Operator
kubectl apply -f https://github.com/open-telemetry/opentelemetry-operator/releases/latest/download/opentelemetry-operator.yaml
创建 Instrumentation 资源
apiVersion: opentelemetry.io/v1alpha1kind: Instrumentationmetadata:name: my-instrumentationnamespace: defaultspec:exporter:endpoint: uptrace-grpc.yodao.cn:4317env:- name: OTEL_EXPORTER_OTLP_HEADERSvalue: uptrace-dsn=http://token@uptrace-grpc.yodao.cn/projectpropagators:- tracecontext- baggagesampler:type: parentbased_traceidratioargument: "0.1"
部署应用
在 Pod 上添加 annotation:
apiVersion: apps/v1kind: Deploymentmetadata:name: my-appspec:template:metadata:annotations:instrumentation.opentelemetry.io/inject-go: "true"spec:containers:- name: appimage: my-go-app:latest
验证零代码接入
启动应用后,访问您的服务端点,然后在 APM 平台查看数据:
- Traces: 查看调用链路
- Metrics: 查看请求量、延迟等指标
- Service Map: 查看服务拓扑图
快速验证脚本
#!/bin/bash# verify-instrumentation.shecho "Checking service..."curl -s http://localhost:8080/health > /dev/nullecho "Waiting for traces to be exported..."sleep 5echo "Check your APM platform for service: $OTEL_SERVICE_NAME"echo "Endpoint: $OTEL_EXPORTER_OTLP_ENDPOINT"
SDK 手动接入
SDK 手动接入提供更精细的控制和自定义能力。
安装依赖
# 核心 SDKgo get go.opentelemetry.io/otel@latestgo get go.opentelemetry.io/otel/sdk@latest# OTLP Exportergo get go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc@latestgo get go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc@latest# 埋点库(可选)go get go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp@latestgo get go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc@latest
基础初始化
1. 创建 OpenTelemetry 初始化函数
// otel.gopackage mainimport ("context""fmt""time""go.opentelemetry.io/otel""go.opentelemetry.io/otel/attribute""go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc""go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc""go.opentelemetry.io/otel/propagation""go.opentelemetry.io/otel/sdk/metric""go.opentelemetry.io/otel/sdk/resource"sdktrace "go.opentelemetry.io/otel/sdk/trace"semconv "go.opentelemetry.io/otel/semconv/v1.24.0""google.golang.org/grpc""google.golang.org/grpc/credentials")// InitOpenTelemetry 初始化 OpenTelemetryfunc InitOpenTelemetry(ctx context.Context) (func(context.Context) error, error) {// 1. 创建 Resourceres, err := resource.New(ctx,resource.WithAttributes(semconv.ServiceName("my-go-service"),semconv.ServiceVersion("1.0.0"),attribute.String("deployment.environment", "production"),),resource.WithProcess(),resource.WithOS(),resource.WithContainer(),resource.WithHost(),)if err != nil {return nil, fmt.Errorf("failed to create resource: %w", err)}// 2. 创建 Trace ExportertraceExporter, err := otlptracegrpc.New(ctx,otlptracegrpc.WithEndpoint("api.uptrace.dev:4317"),otlptracegrpc.WithTLSCredentials(credentials.NewClientTLSFromCert(nil, "")),otlptracegrpc.WithHeaders(map[string]string{"uptrace-dsn": "http://token@uptrace-grpc.yodao.cn/project",}),)if err != nil {return nil, fmt.Errorf("failed to create trace exporter: %w", err)}// 3. 创建 Trace ProvidertracerProvider := sdktrace.NewTracerProvider(sdktrace.WithResource(res),sdktrace.WithBatcher(traceExporter,sdktrace.WithBatchTimeout(5*time.Second),sdktrace.WithMaxExportBatchSize(512),),sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.TraceIDRatioBased(0.1), // 10% 采样)),)otel.SetTracerProvider(tracerProvider)// 4. 创建 Metric ExportermetricExporter, err := otlpmetricgrpc.New(ctx,otlpmetricgrpc.WithEndpoint("api.uptrace.dev:4317"),otlpmetricgrpc.WithTLSCredentials(credentials.NewClientTLSFromCert(nil, "")),otlpmetricgrpc.WithHeaders(map[string]string{"uptrace-dsn": "http://token@uptrace-grpc.yodao.cn/project",}),)if err != nil {return nil, fmt.Errorf("failed to create metric exporter: %w", err)}// 5. 创建 Metric ProvidermeterProvider := metric.NewMeterProvider(metric.WithResource(res),metric.WithReader(metric.NewPeriodicReader(metricExporter,metric.WithInterval(60*time.Second),),),)otel.SetMeterProvider(meterProvider)// 6. 设置 Propagatorotel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{},propagation.Baggage{},),)// 7. 返回 Shutdown 函数return func(ctx context.Context) error {var err errorif e := tracerProvider.Shutdown(ctx); e != nil {err = e}if e := meterProvider.Shutdown(ctx); e != nil {if err != nil {err = fmt.Errorf("%v; %w", err, e)} else {err = e}}return err}, nil}
2. 在 main 函数中初始化
// main.gopackage mainimport ("context""log""os""os/signal""syscall""time")func main() {ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)defer cancel()// 初始化 OpenTelemetryshutdown, err := InitOpenTelemetry(ctx)if err != nil {log.Fatalf("Failed to initialize OpenTelemetry: %v", err)}// 确保在程序退出时调用 shutdowndefer func() {ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)defer cancel()if err := shutdown(ctx); err != nil {log.Printf("Error shutting down OpenTelemetry: %v", err)}}()// 优雅关闭sigCh := make(chan os.Signal, 1)signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)// 启动应用go startServer()<-sigChlog.Println("Shutting down gracefully...")}func startServer() {// 应用逻辑}
Traces - 手动埋点
创建和使用 Span
package mainimport ("context""go.opentelemetry.io/otel""go.opentelemetry.io/otel/attribute""go.opentelemetry.io/otel/codes""go.opentelemetry.io/otel/trace")var tracer = otel.Tracer("my-service")// ProcessOrder 处理订单func ProcessOrder(ctx context.Context, orderID string) error {// 创建 Spanctx, span := tracer.Start(ctx, "process_order",trace.WithAttributes(attribute.String("order.id", orderID),attribute.String("operation", "process"),),trace.WithSpanKind(trace.SpanKindInternal),)defer span.End()// 添加事件span.AddEvent("order_validation_started")// 验证订单if err := validateOrder(ctx, orderID); err != nil {// 记录错误span.RecordError(err)span.SetStatus(codes.Error, err.Error())return err}span.AddEvent("order_validated")// 处理支付if err := processPayment(ctx, orderID); err != nil {span.RecordError(err)span.SetStatus(codes.Error, "payment failed")return err}span.AddEvent("payment_processed")// 设置成功状态span.SetStatus(codes.Ok, "order processed successfully")span.SetAttributes(attribute.Bool("order.success", true))return nil}func validateOrder(ctx context.Context, orderID string) error {ctx, span := tracer.Start(ctx, "validate_order")defer span.End()// 业务逻辑span.SetAttributes(attribute.String("order.id", orderID))return nil}func processPayment(ctx context.Context, orderID string) error {ctx, span := tracer.Start(ctx, "process_payment",trace.WithAttributes(attribute.String("order.id", orderID),attribute.String("payment.method", "credit_card"),),)defer span.End()// 支付逻辑return nil}
Span 属性和事件
func ExampleSpanAttributes(ctx context.Context) {ctx, span := tracer.Start(ctx, "example_operation")defer span.End()// 设置属性span.SetAttributes(attribute.String("user.id", "12345"),attribute.String("user.email", "user@example.com"),attribute.Int("user.age", 30),attribute.Bool("user.premium", true),attribute.StringSlice("user.roles", []string{"admin", "user"}),)// 添加事件span.AddEvent("cache_miss",trace.WithAttributes(attribute.String("cache.key", "user:12345"),attribute.String("cache.type", "redis"),),)// 添加带时间戳的事件span.AddEvent("retry_attempt",trace.WithTimestamp(time.Now()),trace.WithAttributes(attribute.Int("retry.count", 2),attribute.String("retry.reason", "timeout"),),)}
Context 传播
// HTTP 客户端传播func MakeHTTPRequest(ctx context.Context, url string) (*http.Response, error) {ctx, span := tracer.Start(ctx, "http_request",trace.WithAttributes(attribute.String("http.url", url),attribute.String("http.method", "GET"),),)defer span.End()req, err := http.NewRequestWithContext(ctx, "GET", url, nil)if err != nil {return nil, err}// 注入 trace context 到 HTTP headersotel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(req.Header))resp, err := http.DefaultClient.Do(req)if err != nil {span.RecordError(err)return nil, err}span.SetAttributes(attribute.Int("http.status_code", resp.StatusCode))return resp, nil}// HTTP 服务端提取func ExtractContext(r *http.Request) context.Context {// 从 HTTP headers 提取 trace contextctx := otel.GetTextMapPropagator().Extract(r.Context(),propagation.HeaderCarrier(r.Header),)return ctx}
Metrics - 指标采集
Counter(计数器)
package mainimport ("context""go.opentelemetry.io/otel""go.opentelemetry.io/otel/attribute""go.opentelemetry.io/otel/metric")var meter = otel.Meter("my-service")// 创建 Countervar (requestCounter metric.Int64Counter)func init() {var err errorrequestCounter, err = meter.Int64Counter("http.server.requests",metric.WithDescription("Total number of HTTP requests"),metric.WithUnit("1"),)if err != nil {panic(err)}}func HandleRequest(ctx context.Context, method, route string, statusCode int) {// 增加计数requestCounter.Add(ctx, 1,metric.WithAttributes(attribute.String("http.method", method),attribute.String("http.route", route),attribute.Int("http.status_code", statusCode),),)}
Histogram(直方图)
var (requestDuration metric.Float64Histogram)func init() {var err errorrequestDuration, err = meter.Float64Histogram("http.server.duration",metric.WithDescription("HTTP request duration"),metric.WithUnit("ms"),metric.WithExplicitBucketBoundaries(5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000,),)if err != nil {panic(err)}}func RecordDuration(ctx context.Context, duration float64, method, route string) {requestDuration.Record(ctx, duration,metric.WithAttributes(attribute.String("http.method", method),attribute.String("http.route", route),),)}
UpDownCounter(升降计数器)
var (activeConnections metric.Int64UpDownCounter)func init() {var err erroractiveConnections, err = meter.Int64UpDownCounter("http.server.active_connections",metric.WithDescription("Number of active HTTP connections"),metric.WithUnit("1"),)if err != nil {panic(err)}}func ConnectionOpened(ctx context.Context) {activeConnections.Add(ctx, 1)}func ConnectionClosed(ctx context.Context) {activeConnections.Add(ctx, -1)}
Gauge(仪表)
var (memoryUsage metric.Int64ObservableGauge)func init() {var err errormemoryUsage, err = meter.Int64ObservableGauge("runtime.memory.heap_alloc",metric.WithDescription("Heap memory allocated"),metric.WithUnit("bytes"),)if err != nil {panic(err)}// 注册回调函数_, err = meter.RegisterCallback(func(_ context.Context, o metric.Observer) error {var m runtime.MemStatsruntime.ReadMemStats(&m)o.ObserveInt64(memoryUsage, int64(m.HeapAlloc))return nil},memoryUsage,)if err != nil {panic(err)}}
Logs - 日志集成
集成 Zap
package mainimport ("go.opentelemetry.io/otel/trace""go.uber.org/zap""go.uber.org/zap/zapcore")// NewLogger 创建带 trace 信息的 loggerfunc NewLogger() *zap.Logger {config := zap.NewProductionConfig()config.EncoderConfig.TimeKey = "timestamp"config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoderlogger, _ := config.Build(zap.AddCallerSkip(1),zap.AddStacktrace(zapcore.ErrorLevel),)return logger}// LogWithTrace 记录带 trace 信息的日志func LogWithTrace(ctx context.Context, logger *zap.Logger, msg string, fields ...zap.Field) {span := trace.SpanFromContext(ctx)if span.SpanContext().IsValid() {fields = append(fields,zap.String("trace_id", span.SpanContext().TraceID().String()),zap.String("span_id", span.SpanContext().SpanID().String()),)}logger.Info(msg, fields...)}// 使用示例func Example(ctx context.Context) {logger := NewLogger()defer logger.Sync()ctx, span := tracer.Start(ctx, "example_operation")defer span.End()LogWithTrace(ctx, logger, "Processing request",zap.String("user_id", "12345"),zap.String("action", "create"),)}
集成 Logrus
package mainimport ("context""github.com/sirupsen/logrus""go.opentelemetry.io/otel/trace")// TraceHook 添加 trace 信息到日志type TraceHook struct{}func (hook *TraceHook) Levels() []logrus.Level {return logrus.AllLevels}func (hook *TraceHook) Fire(entry *logrus.Entry) error {if ctx, ok := entry.Data["ctx"].(context.Context); ok {span := trace.SpanFromContext(ctx)if span.SpanContext().IsValid() {entry.Data["trace_id"] = span.SpanContext().TraceID().String()entry.Data["span_id"] = span.SpanContext().SpanID().String()}delete(entry.Data, "ctx")}return nil}// NewLogrusLogger 创建 loggerfunc NewLogrusLogger() *logrus.Logger {logger := logrus.New()logger.SetFormatter(&logrus.JSONFormatter{TimestampFormat: "2006-01-02T15:04:05.000Z07:00",})logger.AddHook(&TraceHook{})return logger}// 使用示例func ExampleLogrus(ctx context.Context) {logger := NewLogrusLogger()ctx, span := tracer.Start(ctx, "example_operation")defer span.End()logger.WithFields(logrus.Fields{"ctx": ctx,"user_id": "12345","action": "create",}).Info("Processing request")}
集成 slog (Go 1.21+)
package mainimport ("context""log/slog""os""go.opentelemetry.io/otel/trace")// TraceHandler 包装 slog.Handler 添加 trace 信息type TraceHandler struct {slog.Handler}func (h *TraceHandler) Handle(ctx context.Context, r slog.Record) error {span := trace.SpanFromContext(ctx)if span.SpanContext().IsValid() {r.AddAttrs(slog.String("trace_id", span.SpanContext().TraceID().String()),slog.String("span_id", span.SpanContext().SpanID().String()),)}return h.Handler.Handle(ctx, r)}// NewSlogLogger 创建 loggerfunc NewSlogLogger() *slog.Logger {handler := &TraceHandler{Handler: slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo,}),}return slog.New(handler)}// 使用示例func ExampleSlog(ctx context.Context) {logger := NewSlogLogger()ctx, span := tracer.Start(ctx, "example_operation")defer span.End()logger.InfoContext(ctx, "Processing request",slog.String("user_id", "12345"),slog.String("action", "create"),)}
框架集成示例
Gin Framework
package mainimport ("github.com/gin-gonic/gin""go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin""go.opentelemetry.io/otel""go.opentelemetry.io/otel/attribute")func main() {// 初始化 OpenTelemetryshutdown, err := InitOpenTelemetry(context.Background())if err != nil {log.Fatal(err)}defer shutdown(context.Background())// 创建 Gin 引擎r := gin.Default()// 添加 OpenTelemetry 中间件r.Use(otelgin.Middleware("my-service"))// 定义路由r.GET("/users/:id", GetUser)r.POST("/users", CreateUser)// 启动服务器r.Run(":8080")}var tracer = otel.Tracer("my-service")func GetUser(c *gin.Context) {ctx := c.Request.Context()// 手动创建子 Spanctx, span := tracer.Start(ctx, "get_user_handler")defer span.End()userID := c.Param("id")span.SetAttributes(attribute.String("user.id", userID))// 查询用户user, err := queryUser(ctx, userID)if err != nil {span.RecordError(err)c.JSON(500, gin.H{"error": err.Error()})return}c.JSON(200, user)}func CreateUser(c *gin.Context) {ctx := c.Request.Context()ctx, span := tracer.Start(ctx, "create_user_handler")defer span.End()var user Userif err := c.ShouldBindJSON(&user); err != nil {span.RecordError(err)c.JSON(400, gin.H{"error": err.Error()})return}if err := saveUser(ctx, &user); err != nil {span.RecordError(err)c.JSON(500, gin.H{"error": err.Error()})return}c.JSON(201, user)}type User struct {ID string `json:"id"`Name string `json:"name"`Email string `json:"email"`}func queryUser(ctx context.Context, id string) (*User, error) {ctx, span := tracer.Start(ctx, "query_user",trace.WithAttributes(attribute.String("db.operation", "SELECT")),)defer span.End()// 数据库查询逻辑return &User{ID: id, Name: "John Doe"}, nil}func saveUser(ctx context.Context, user *User) error {ctx, span := tracer.Start(ctx, "save_user",trace.WithAttributes(attribute.String("db.operation", "INSERT")),)defer span.End()// 数据库插入逻辑return nil}
Echo Framework
package mainimport ("github.com/labstack/echo/v4""go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho")func main() {// 初始化 OpenTelemetryshutdown, err := InitOpenTelemetry(context.Background())if err != nil {log.Fatal(err)}defer shutdown(context.Background())// 创建 Echo 实例e := echo.New()// 添加 OpenTelemetry 中间件e.Use(otelecho.Middleware("my-service"))// 定义路由e.GET("/users/:id", func(c echo.Context) error {ctx := c.Request().Context()ctx, span := tracer.Start(ctx, "get_user")defer span.End()userID := c.Param("id")span.SetAttributes(attribute.String("user.id", userID))return c.JSON(200, map[string]string{"id": userID})})// 启动服务器e.Start(":8080")}
gRPC Server
package mainimport ("context""log""net""google.golang.org/grpc""go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"pb "your-package/proto")type server struct {pb.UnimplementedUserServiceServer}func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.GetUserResponse, error) {// ctx 已经包含了 trace 信息ctx, span := tracer.Start(ctx, "GetUser")defer span.End()span.SetAttributes(attribute.String("user.id", req.GetId()))// 业务逻辑return &pb.GetUserResponse{Id: req.GetId(),Name: "John Doe",}, nil}func main() {// 初始化 OpenTelemetryshutdown, err := InitOpenTelemetry(context.Background())if err != nil {log.Fatal(err)}defer shutdown(context.Background())// 创建 gRPC 服务器s := grpc.NewServer(grpc.StatsHandler(otelgrpc.NewServerHandler()),)pb.RegisterUserServiceServer(s, &server{})lis, err := net.Listen("tcp", ":50051")if err != nil {log.Fatal(err)}log.Println("gRPC server listening on :50051")if err := s.Serve(lis); err != nil {log.Fatal(err)}}
gRPC Client
package mainimport ("context""google.golang.org/grpc""google.golang.org/grpc/credentials/insecure""go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"pb "your-package/proto")func main() {// 创建 gRPC 连接conn, err := grpc.NewClient("localhost:50051",grpc.WithTransportCredentials(insecure.NewCredentials()),grpc.WithStatsHandler(otelgrpc.NewClientHandler()),)if err != nil {log.Fatal(err)}defer conn.Close()client := pb.NewUserServiceClient(conn)// 调用服务ctx, span := tracer.Start(context.Background(), "call_get_user")defer span.End()resp, err := client.GetUser(ctx, &pb.GetUserRequest{Id: "123"})if err != nil {span.RecordError(err)log.Fatal(err)}log.Printf("User: %+v", resp)}
Database - database/sql
package mainimport ("context""database/sql"_ "github.com/go-sql-driver/mysql""go.opentelemetry.io/contrib/instrumentation/database/sql/otelsql"semconv "go.opentelemetry.io/otel/semconv/v1.24.0")func main() {// 注册带埋点的驱动driverName, err := otelsql.Register("mysql",otelsql.WithAttributes(semconv.DBSystemMySQL,),)if err != nil {log.Fatal(err)}// 打开数据库连接db, err := sql.Open(driverName, "user:password@tcp(localhost:3306)/dbname")if err != nil {log.Fatal(err)}defer db.Close()// 记录统计信息if err := otelsql.RecordStats(db); err != nil {log.Fatal(err)}// 使用数据库ctx, span := tracer.Start(context.Background(), "database_query")defer span.End()rows, err := db.QueryContext(ctx, "SELECT id, name FROM users WHERE id = ?", 123)if err != nil {span.RecordError(err)log.Fatal(err)}defer rows.Close()}
Database - GORM
package mainimport ("gorm.io/driver/mysql""gorm.io/gorm""go.opentelemetry.io/contrib/instrumentation/gorm.io/gorm/otelgorm")func main() {dsn := "user:password@tcp(localhost:3306)/dbname?charset=utf8mb4&parseTime=True"db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})if err != nil {log.Fatal(err)}// 添加 OpenTelemetry 插件if err := db.Use(otelgorm.NewPlugin()); err != nil {log.Fatal(err)}// 使用 GORMctx, span := tracer.Start(context.Background(), "gorm_query")defer span.End()var users []Userresult := db.WithContext(ctx).Find(&users)if result.Error != nil {span.RecordError(result.Error)}}
Redis - go-redis
package mainimport ("context""github.com/redis/go-redis/v9""go.opentelemetry.io/contrib/instrumentation/github.com/redis/go-redis/v9/otelredis")func main() {// 创建 Redis 客户端rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379",})// 添加 OpenTelemetry Hookif err := otelredis.InstrumentTracing(rdb); err != nil {log.Fatal(err)}if err := otelredis.InstrumentMetrics(rdb); err != nil {log.Fatal(err)}// 使用 Redisctx, span := tracer.Start(context.Background(), "redis_operation")defer span.End()err := rdb.Set(ctx, "key", "value", 0).Err()if err != nil {span.RecordError(err)}val, err := rdb.Get(ctx, "key").Result()if err != nil {span.RecordError(err)}log.Println(val)}
Kafka - sarama
package mainimport ("github.com/IBM/sarama""go.opentelemetry.io/contrib/instrumentation/github.com/IBM/sarama/otelsarama")func main() {config := sarama.NewConfig()config.Version = sarama.V2_8_0_0config.Producer.Return.Successes = true// 创建生产者producer, err := sarama.NewSyncProducer([]string{"localhost:9092"}, config)if err != nil {log.Fatal(err)}defer producer.Close()// 包装生产者producer = otelsarama.WrapSyncProducer(config, producer)// 发送消息ctx, span := tracer.Start(context.Background(), "kafka_produce")defer span.End()// 注入 trace contextmsg := &sarama.ProducerMessage{Topic: "test-topic",Value: sarama.StringEncoder("test message"),}// 传播 contextotel.GetTextMapPropagator().Inject(ctx, otelsarama.NewProducerMessageCarrier(msg))partition, offset, err := producer.SendMessage(msg)if err != nil {span.RecordError(err)}log.Printf("Message sent to partition %d at offset %d\n", partition, offset)}
高级配置
自定义采样策略
基于 Span 名称的采样
package mainimport ("strings"sdktrace "go.opentelemetry.io/otel/sdk/trace")// CustomSampler 自定义采样器type CustomSampler struct {defaultSampler sdktrace.SampleralwaysSample []string // 始终采样的 Span 名称neverSample []string // 从不采样的 Span 名称}func NewCustomSampler(defaultRate float64, alwaysSample, neverSample []string) sdktrace.Sampler {return &CustomSampler{defaultSampler: sdktrace.TraceIDRatioBased(defaultRate),alwaysSample: alwaysSample,neverSample: neverSample,}}func (s *CustomSampler) ShouldSample(p sdktrace.SamplingParameters) sdktrace.SamplingResult {spanName := p.Name// 检查是否需要总是采样for _, name := range s.alwaysSample {if strings.Contains(spanName, name) {return sdktrace.SamplingResult{Decision: sdktrace.RecordAndSample,Tracestate: p.ParentContext.TraceState(),}}}// 检查是否从不采样for _, name := range s.neverSample {if strings.Contains(spanName, name) {return sdktrace.SamplingResult{Decision: sdktrace.Drop,Tracestate: p.ParentContext.TraceState(),}}}// 使用默认采样策略return s.defaultSampler.ShouldSample(p)}func (s *CustomSampler) Description() string {return "CustomSampler"}// 使用示例func InitWithCustomSampler() *sdktrace.TracerProvider {sampler := NewCustomSampler(0.1, // 默认 10% 采样[]string{"/checkout", "/payment"}, // 始终采样[]string{"/health", "/metrics"}, // 从不采样)return sdktrace.NewTracerProvider(sdktrace.WithSampler(sdktrace.ParentBased(sampler)),)}
基于属性的采样
// AttributeBasedSampler 基于属性的采样器type AttributeBasedSampler struct {defaultSampler sdktrace.Sampler}func NewAttributeBasedSampler(defaultRate float64) sdktrace.Sampler {return &AttributeBasedSampler{defaultSampler: sdktrace.TraceIDRatioBased(defaultRate),}}func (s *AttributeBasedSampler) ShouldSample(p sdktrace.SamplingParameters) sdktrace.SamplingResult {// 检查特定属性for _, attr := range p.Attributes {if string(attr.Key) == "important" && attr.Value.AsBool() {// 重要请求总是采样return sdktrace.SamplingResult{Decision: sdktrace.RecordAndSample,Tracestate: p.ParentContext.TraceState(),}}// 错误请求总是采样if string(attr.Key) == "error" && attr.Value.AsBool() {return sdktrace.SamplingResult{Decision: sdktrace.RecordAndSample,Tracestate: p.ParentContext.TraceState(),}}}return s.defaultSampler.ShouldSample(p)}func (s *AttributeBasedSampler) Description() string {return "AttributeBasedSampler"}
Span Processor
自定义属性添加
// AttributeAdder 添加自定义属性到所有 Spantype AttributeAdder struct {sdktrace.SpanProcessorattributes []attribute.KeyValue}func NewAttributeAdder(processor sdktrace.SpanProcessor, attrs []attribute.KeyValue) sdktrace.SpanProcessor {return &AttributeAdder{SpanProcessor: processor,attributes: attrs,}}func (a *AttributeAdder) OnStart(parent context.Context, s sdktrace.ReadWriteSpan) {// 添加自定义属性s.SetAttributes(a.attributes...)a.SpanProcessor.OnStart(parent, s)}func (a *AttributeAdder) OnEnd(s sdktrace.ReadOnlySpan) {a.SpanProcessor.OnEnd(s)}func (a *AttributeAdder) Shutdown(ctx context.Context) error {return a.SpanProcessor.Shutdown(ctx)}func (a *AttributeAdder) ForceFlush(ctx context.Context) error {return a.SpanProcessor.ForceFlush(ctx)}// 使用示例func InitWithAttributeAdder() *sdktrace.TracerProvider {exporter, _ := otlptracegrpc.New(context.Background())bsp := sdktrace.NewBatchSpanProcessor(exporter)// 包装处理器processor := NewAttributeAdder(bsp, []attribute.KeyValue{attribute.String("environment", "production"),attribute.String("region", "us-east-1"),attribute.String("version", "1.0.0"),})return sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(processor),)}
过滤 Span
// FilteringProcessor 过滤特定 Spantype FilteringProcessor struct {sdktrace.SpanProcessorfilter func(sdktrace.ReadOnlySpan) bool}func NewFilteringProcessor(processor sdktrace.SpanProcessor, filter func(sdktrace.ReadOnlySpan) bool) sdktrace.SpanProcessor {return &FilteringProcessor{SpanProcessor: processor,filter: filter,}}func (f *FilteringProcessor) OnEnd(s sdktrace.ReadOnlySpan) {// 只处理符合条件的 Spanif f.filter(s) {f.SpanProcessor.OnEnd(s)}}// 使用示例:过滤掉健康检查请求func InitWithFiltering() *sdktrace.TracerProvider {exporter, _ := otlptracegrpc.New(context.Background())bsp := sdktrace.NewBatchSpanProcessor(exporter)processor := NewFilteringProcessor(bsp, func(s sdktrace.ReadOnlySpan) bool {// 过滤掉健康检查return !strings.Contains(s.Name(), "/health")})return sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(processor),)}
多 Exporter 配置
func InitWithMultipleExporters(ctx context.Context) (*sdktrace.TracerProvider, error) {res, _ := resource.New(ctx, resource.WithAttributes(semconv.ServiceName("my-service"),))// OTLP Exporter (主要后端)otlpExporter, err := otlptracegrpc.New(ctx,otlptracegrpc.WithEndpoint("api.uptrace.dev:4317"),otlptracegrpc.WithTLSCredentials(credentials.NewClientTLSFromCert(nil, "")),otlptracegrpc.WithHeaders(map[string]string{"uptrace-dsn": "http://token@uptrace-grpc.yodao.cn/project",}),)if err != nil {return nil, err}// Jaeger Exporter (备份)jaegerExporter, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint("http://localhost:14268/api/traces"),))if err != nil {return nil, err}// Console Exporter (本地调试)consoleExporter, err := stdouttrace.New(stdouttrace.WithPrettyPrint(),)if err != nil {return nil, err}tp := sdktrace.NewTracerProvider(sdktrace.WithResource(res),// 添加多个处理器sdktrace.WithBatcher(otlpExporter),sdktrace.WithBatcher(jaegerExporter),sdktrace.WithBatcher(consoleExporter),)return tp, nil}
资源检测
import ("go.opentelemetry.io/otel/sdk/resource"semconv "go.opentelemetry.io/otel/semconv/v1.24.0")func CreateResourceWithDetectors(ctx context.Context) (*resource.Resource, error) {return resource.New(ctx,// 手动设置的资源属性resource.WithAttributes(semconv.ServiceName("my-service"),semconv.ServiceVersion("1.0.0"),),// 自动检测进程信息resource.WithProcess(),// 自动检测主机信息resource.WithHost(),// 自动检测操作系统信息resource.WithOS(),// 自动检测容器信息resource.WithContainer(),// 自动检测环境变量中的资源属性resource.WithFromEnv(),// 自动检测 TelemetrySDK 信息resource.WithTelemetrySDK(),)}
性能优化配置
func InitOptimizedProvider(ctx context.Context) (*sdktrace.TracerProvider, error) {exporter, err := otlptracegrpc.New(ctx,otlptracegrpc.WithEndpoint("api.uptrace.dev:4317"),otlptracegrpc.WithTLSCredentials(credentials.NewClientTLSFromCert(nil, "")),// 启用压缩otlptracegrpc.WithCompressor("gzip"),// 设置超时otlptracegrpc.WithTimeout(10*time.Second),// 设置重试otlptracegrpc.WithRetry(otlptracegrpc.RetryConfig{Enabled: true,InitialInterval: 1 * time.Second,MaxInterval: 30 * time.Second,MaxElapsedTime: 5 * time.Minute,}),)if err != nil {return nil, err}tp := sdktrace.NewTracerProvider(sdktrace.WithBatcher(exporter,// 批量大小sdktrace.WithMaxExportBatchSize(512),// 批量超时sdktrace.WithBatchTimeout(5*time.Second),// 队列大小sdktrace.WithMaxQueueSize(2048),// 导出超时sdktrace.WithExportTimeout(30*time.Second),),// 采样率sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.TraceIDRatioBased(0.1),)),// Span 限制sdktrace.WithSpanLimits(sdktrace.SpanLimits{AttributeValueLengthLimit: 1024,AttributeCountLimit: 128,EventCountLimit: 128,LinkCountLimit: 128,AttributePerEventCountLimit: 128,AttributePerLinkCountLimit: 128,}),)return tp, nil}
最佳实践
1. Context 传递
正确做法: 始终传递 context
// ✅ 正确func ProcessOrder(ctx context.Context, orderID string) error {ctx, span := tracer.Start(ctx, "process_order")defer span.End()// 传递 context 给下游函数if err := validateOrder(ctx, orderID); err != nil {return err}if err := chargePayment(ctx, orderID); err != nil {return err}return nil}
错误做法: 创建新的 context
// ❌ 错误func ProcessOrder(ctx context.Context, orderID string) error {ctx, span := tracer.Start(ctx, "process_order")defer span.End()// 不要创建新的 context.Background()if err := validateOrder(context.Background(), orderID); err != nil {return err}return nil}
2. Span 命名
正确做法: 使用描述性名称
// ✅ 正确ctx, span := tracer.Start(ctx, "database.query.users.by_id")ctx, span := tracer.Start(ctx, "http.client.post.payment_gateway")ctx, span := tracer.Start(ctx, "cache.get.user_profile")
错误做法: 使用模糊或过长的名称
// ❌ 错误ctx, span := tracer.Start(ctx, "do_something")ctx, span := tracer.Start(ctx, "query")ctx, span := tracer.Start(ctx, fmt.Sprintf("get_user_%s_from_database_with_params_%v", id, params))
3. 错误处理
正确做法: 记录错误并设置状态
// ✅ 正确func QueryDatabase(ctx context.Context) error {ctx, span := tracer.Start(ctx, "query_database")defer span.End()err := db.Query(ctx, "SELECT * FROM users")if err != nil {span.RecordError(err)span.SetStatus(codes.Error, err.Error())return err}span.SetStatus(codes.Ok, "")return nil}
错误做法: 忽略错误或不设置状态
// ❌ 错误func QueryDatabase(ctx context.Context) error {ctx, span := tracer.Start(ctx, "query_database")defer span.End()err := db.Query(ctx, "SELECT * FROM users")// 没有记录错误return err}
4. 资源管理
正确做法: 始终调用 defer span.End()
// ✅ 正确func Operation(ctx context.Context) error {ctx, span := tracer.Start(ctx, "operation")defer span.End()// 业务逻辑return nil}
错误做法: 忘记调用 span.End()
// ❌ 错误func Operation(ctx context.Context) error {ctx, span := tracer.Start(ctx, "operation")// 忘记 defer span.End()return nil}
5. 属性使用
正确做法: 使用语义约定
// ✅ 正确span.SetAttributes(semconv.HTTPMethod("GET"),semconv.HTTPURL("https://api.example.com/users"),semconv.HTTPStatusCode(200),semconv.DBSystem("mysql"),semconv.DBName("users_db"),semconv.DBStatement("SELECT * FROM users WHERE id = ?"),)
错误做法: 使用自定义键名
// ❌ 错误(除非必要)span.SetAttributes(attribute.String("method", "GET"),attribute.String("url", "https://api.example.com/users"),attribute.Int("status", 200),)
6. 避免过度埋点
正确做法: 只在关键路径埋点
// ✅ 正确func ProcessOrder(ctx context.Context) error {ctx, span := tracer.Start(ctx, "process_order")defer span.End()// 关键操作validateOrder(ctx)chargePayment(ctx)sendEmail(ctx)return nil}
错误做法: 对每个小函数都埋点
// ❌ 错误(过度埋点)func ProcessOrder(ctx context.Context) error {ctx, span := tracer.Start(ctx, "process_order")defer span.End()validateID(ctx) // 太细粒度checkFormat(ctx) // 太细粒度validateAge(ctx) // 太细粒度validateEmail(ctx) // 太细粒度return nil}
7. Metrics 最佳实践
var (// ✅ 正确:在包级别创建仪表requestCounter metric.Int64CounterrequestDuration metric.Float64Histogram)func init() {meter := otel.Meter("my-service")requestCounter, _ = meter.Int64Counter("requests")requestDuration, _ = meter.Float64Histogram("request_duration")}func HandleRequest(ctx context.Context) {start := time.Now()// 业务逻辑duration := time.Since(start).Milliseconds()// 记录指标requestCounter.Add(ctx, 1)requestDuration.Record(ctx, float64(duration))}
8. 优雅关闭
func main() {ctx := context.Background()shutdown, err := InitOpenTelemetry(ctx)if err != nil {log.Fatal(err)}// ✅ 正确:确保调用 shutdowndefer func() {ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)defer cancel()if err := shutdown(ctx); err != nil {log.Printf("Error shutting down: %v", err)}}()// 应用逻辑}
故障排查
常见问题
1. 数据未发送到后端
症状: APM 平台看不到数据
排查步骤:
- 检查环境变量配置:
echo $OTEL_SERVICE_NAMEecho $OTEL_EXPORTER_OTLP_ENDPOINTecho $OTEL_EXPORTER_OTLP_HEADERS
- 检查网络连接:
curl -v uptrace-grpc.yodao.cn:4317
- 启用调试日志:
import "go.opentelemetry.io/otel"import "go.opentelemetry.io/otel/exporters/stdout/stdouttrace"// 临时添加 console exporterconsoleExporter, _ := stdouttrace.New(stdouttrace.WithPrettyPrint())tp := sdktrace.NewTracerProvider(sdktrace.WithBatcher(consoleExporter),)
- 检查采样率:
export OTEL_TRACES_SAMPLER_ARG=1.0 # 100% 采样用于调试
2. Span 未关联
症状: Span 之间没有父子关系
原因: Context 传递不正确
解决方案:
// 确保传递正确的 contextfunc ParentFunction(ctx context.Context) {ctx, span := tracer.Start(ctx, "parent")defer span.End()// ✅ 传递 ctxChildFunction(ctx)}func ChildFunction(ctx context.Context) {// ✅ 使用传入的 ctxctx, span := tracer.Start(ctx, "child")defer span.End()}
3. 内存泄漏
症状: 内存持续增长
原因:
- 没有调用
span.End() - 队列积压
解决方案:
// 1. 始终使用 deferfunc Operation(ctx context.Context) {ctx, span := tracer.Start(ctx, "operation")defer span.End() // ✅ 使用 defer}// 2. 调整队列大小tp := sdktrace.NewTracerProvider(sdktrace.WithBatcher(exporter,sdktrace.WithMaxQueueSize(1024), // 减小队列sdktrace.WithBatchTimeout(2*time.Second), // 更频繁导出),)
4. 高 CPU 使用率
原因: 采样率过高或批量配置不当
解决方案:
# 降低采样率export OTEL_TRACES_SAMPLER_ARG=0.01 # 1%# 调整批量配置export OTEL_BSP_MAX_EXPORT_BATCH_SIZE=256export OTEL_BSP_SCHEDULE_DELAY=10000 # 10 秒
调试技巧
1. 启用详细日志
import ("go.opentelemetry.io/otel""go.opentelemetry.io/otel/sdk/trace")// 设置全局错误处理器otel.SetErrorHandler(otel.ErrorHandlerFunc(func(err error) {log.Printf("OpenTelemetry error: %v", err)}))
2. 使用 Console Exporter
import "go.opentelemetry.io/otel/exporters/stdout/stdouttrace"exporter, _ := stdouttrace.New(stdouttrace.WithPrettyPrint(),stdouttrace.WithWriter(os.Stderr),)
3. 检查 Span 内容
// 自定义 Processor 打印 Spantype DebugProcessor struct{}func (p *DebugProcessor) OnStart(parent context.Context, s sdktrace.ReadWriteSpan) {log.Printf("Span started: %s", s.Name())}func (p *DebugProcessor) OnEnd(s sdktrace.ReadOnlySpan) {log.Printf("Span ended: %s, Duration: %v", s.Name(), s.EndTime().Sub(s.StartTime()))}func (p *DebugProcessor) Shutdown(ctx context.Context) error { return nil }func (p *DebugProcessor) ForceFlush(ctx context.Context) error { return nil }// 添加到 Providertp := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(&DebugProcessor{}),)
4. 性能分析
import _ "net/http/pprof"// 启动 pprof 服务器go func() {log.Println(http.ListenAndServe("localhost:6060", nil))}()// 访问 http://localhost:6060/debug/pprof/// 查看内存和 CPU 使用情况
环境变量完整示例
# 开发环境 - 完整调试export OTEL_LOG_LEVEL=debugexport OTEL_SERVICE_NAME=my-service-devexport OTEL_TRACES_SAMPLER=always_onexport OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317export OTEL_TRACES_EXPORTER=otlp,console# 生产环境 - 优化性能export OTEL_LOG_LEVEL=warnexport OTEL_SERVICE_NAME=my-serviceexport OTEL_RESOURCE_ATTRIBUTES=service.version=1.0.0,deployment.environment=productionexport OTEL_EXPORTER_OTLP_ENDPOINT=uptrace-grpc.yodao.cn:4317export OTEL_EXPORTER_OTLP_PROTOCOL=grpcexport OTEL_EXPORTER_OTLP_HEADERS=uptrace-dsn=http://token@uptrace-grpc.yodao.cn/projectexport OTEL_EXPORTER_OTLP_COMPRESSION=gzipexport OTEL_TRACES_SAMPLER=parentbased_traceidratioexport OTEL_TRACES_SAMPLER_ARG=0.1export OTEL_BSP_MAX_QUEUE_SIZE=2048export OTEL_BSP_MAX_EXPORT_BATCH_SIZE=512export OTEL_BSP_SCHEDULE_DELAY=5000
下一步
- Go loongsuite-go-agent 接入 - 更强大的零代码接入
- 采样策略 - 深入了解采样配置
- 自定义埋点 - 高级埋点技巧
- 性能优化 - 降低监控开销
- Kubernetes 部署(如果使用 Kubernetes):
- OpenTelemetry Operator 概述 - 了解如何在 Kubernetes 中使用 Operator
- 自动注入 - 零代码自动注入 instrumentation
参考资源
- OpenTelemetry Go Documentation
- OpenTelemetry Go SDK
- OpenTelemetry Go Contrib
- Go Semantic Conventions
- Example Applications