You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Golang otelgin中间件无法创建子Span,仅能观测中间件Span

问题描述

使用Golang的otelgin中间件为Go服务器的每个请求生成Span,但尝试创建子Span实现更细粒度链路追踪时失败,目前仅能观测到中间件生成的Span。

相关代码

main.go

package main

import (
    "context"
    "log"
    "net/http"
    "net/url"
    "os"
    "strconv"
    "time"

    "github.com/gin-gonic/gin"
    "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
    "go.opentelemetry.io/contrib/propagators/aws/xray"
    sampler "go.opentelemetry.io/contrib/samplers/aws/xray"
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/attribute"
    "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
    "go.opentelemetry.io/otel/sdk/trace"
    oteltrace "go.opentelemetry.io/otel/trace"
    "google.golang.org/grpc"
)

var svcName = os.Getenv("OTEL_SERVICE_NAME")

func main() {
    err := start_xray()
    if err != nil {
        log.Fatalf("Failed to start XRay: %v", err)
        return
    }

    r := gin.Default()
    r.Use(otelgin.Middleware(svcName))
    r.GET("/", func(ctx *gin.Context) {
        tracer := otel.Tracer(svcName)
        _, span := tracer.Start(ctx.Request.Context(), "sample-trace", oteltrace.WithSpanKind(oteltrace.SpanKindServer), oteltrace.WithAttributes([]attribute.KeyValue{
            {Key: "http.method", Value: attribute.StringValue(ctx.Request.Method)},
        }...))
        defer span.End()

        time.Sleep(1 * time.Second)

        ctx.JSON(http.StatusOK, gin.H{
            "healthy": true,
        })
        span.SetAttributes(attribute.KeyValue{Key: "http.status", Value: attribute.StringValue(strconv.Itoa(http.StatusOK))})

    })

    if err := r.Run(":8080"); err != nil {
        log.Fatal(err)
    }
}

func start_xray() error {
    ctx := context.Background()

    exporterEndpoint := os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT")
    if exporterEndpoint == "" {
        exporterEndpoint = "localhost:4317"
    }

    log.Println("Creating new OTLP trace exporter...")
    traceExporter, err := otlptracegrpc.New(ctx, otlptracegrpc.WithInsecure(), otlptracegrpc.WithEndpoint(exporterEndpoint), otlptracegrpc.WithDialOption(grpc.WithBlock()))
    if err != nil {
        log.Fatalf("Failed to create new OTLP trace exporter: %v", err)
        return err
    }

    idg := xray.NewIDGenerator()

    samplerEndpoint := os.Getenv("XRAY_ENDPOINT")
    if samplerEndpoint == "" {
        samplerEndpoint = "http://localhost:2000"
    }
    endpointUrl, err := url.Parse(samplerEndpoint)

    res, err := sampler.NewRemoteSampler(ctx, svcName, "ecs", sampler.WithEndpoint(*endpointUrl), sampler.WithSamplingRulesPollingInterval(10*time.Second))
    if err != nil {
        log.Fatalf("Failed to create new XRay Remote Sampler: %v", err)
        return err
    }

    // attach remote sampler to tracer provider
    tp := trace.NewTracerProvider(
        trace.WithSampler(res),
        trace.WithBatcher(traceExporter),
        trace.WithIDGenerator(idg),
    )

    otel.SetTracerProvider(tp)
    otel.SetTextMapPropagator(xray.Propagator{})
    return nil
}

docker-compose.yml

services:
  app:
    build:
      context: ./
    depends_on:
      - otel
    environment:
      - LISTEN_ADDRESS=0.0.0.0:8080
      - AWS_ACCESS_KEY_ID=<key>
      - AWS_SECRET_ACCESS_KEY=<secret>
      - OTEL_RESOURCE_ATTRIBUTES=service.name=sample-service
      - OTEL_SERVICE_NAME=sample-service
      - OTEL_EXPORTER_OTLP_ENDPOINT=otel:4317
      - XRAY_ENDPOINT=http://otel:2000
      - OTEL_JAVAAGENT_DEBUG=true
      - OTEL_METRICS_EXPORTER=otlp
    volumes:
      - /tmp/awscreds:/tmp/awscreds
    ports:
      - '8080:8080'

  otel:
    image: amazon/aws-otel-collector:latest
    command: --config /etc/ecs/ecs-default-config.yaml
    environment:
      - AWS_ACCESS_KEY_ID=<key>
      - AWS_SECRET_ACCESS_KEY=<secret>
      - AWS_REGION=us-east-1
    volumes:
      - .:/config
      - /tmp/awscreds:/tmp/awscreds
    ports:
      - '4317:4317'
      - '2000:2000'

Dockerfile

##
## STEP 1 - BUILD
##
FROM golang:1.21 AS build 
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY ./ ./
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /bootstrap ./cmd/server


##
## STEP 2 - DEPLOY
##
FROM alpine:latest

WORKDIR /

COPY --from=build /bootstrap /bootstrap

RUN apk update && apk add curl

EXPOSE 8080

ENTRYPOINT ["/bootstrap"]
解决方案

问题出在创建子Span时使用的上下文不对:otelgin中间件会将生成的请求Span存储在Gin的Context中,而你当前用的ctx.Request.Context()并没有携带这个父Span的上下文,导致新创建的Span无法关联为子Span。

修改方法是使用otelgin.GetSpan(ctx).Context()来获取包含父Span的上下文,替换原代码中的ctx.Request.Context():

r.GET("/", func(ctx *gin.Context) {
    tracer := otel.Tracer(svcName)
    // 替换上下文获取方式
    parentCtx := otelgin.GetSpan(ctx).Context()
    _, span := tracer.Start(parentCtx, "sample-trace", oteltrace.WithSpanKind(oteltrace.SpanKindInternal), // 子Span建议用Internal类型
        oteltrace.WithAttributes([]attribute.KeyValue{
            {Key: "http.method", Value: attribute.StringValue(ctx.Request.Method)},
        }...))
    defer span.End()

    time.Sleep(1 * time.Second)

    ctx.JSON(http.StatusOK, gin.H{
        "healthy": true,
    })
    span.SetAttributes(attribute.KeyValue{Key: "http.status", Value: attribute.StringValue(strconv.Itoa(http.StatusOK))})

})

另外,子Span的SpanKind建议设置为oteltrace.SpanKindInternal,因为这是服务内部的操作,而非对外的服务器端请求,这样链路追踪的语义更准确。

内容的提问来源于stack exchange,提问作者Javier Perez

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 02:50:57