我刚刚开始使用OpenTelemeter,并为此创建了两个(微)服务:StandardGeoMap.

终端用户向Standard服务发送请求,后者又向GeoMap发送请求以在将结果返回给终端用户之前获取信息.我正在使用GRPC进行所有通信.

我将我的职能作为工具如下:

对于Standard人:

type standardService struct {
    pb.UnimplementedStandardServiceServer
}

func (s *standardService) GetStandard(ctx context.Context, in *pb.GetStandardRequest) (*pb.GetStandardResponse, error) {

    conn, _:= createClient(ctx, geomapSvcAddr)
    defer conn1.Close()

    newCtx, span1 := otel.Tracer(name).Start(ctx, "GetStandard")
    defer span1.End()

    countryInfo, err := pb.NewGeoMapServiceClient(conn).GetCountry(newCtx,
        &pb.GetCountryRequest{
            Name: in.Name,
        })

    //...

    return &pb.GetStandardResponse{
        Standard: standard,
    }, nil

}

func createClient(ctx context.Context, svcAddr string) (*grpc.ClientConn, error) {
    return grpc.DialContext(ctx, svcAddr,
        grpc.WithTransportCredentials(insecure.NewCredentials()),
        grpc.WithUnaryInterceptor(otelgrpc.UnaryClientInterceptor()),
    )
}

对于GeoMap人:

type geomapService struct {
    pb.UnimplementedGeoMapServiceServer
}

func (s *geomapService) GetCountry(ctx context.Context, in *pb.GetCountryRequest) (*pb.GetCountryResponse, error) {

    _, span := otel.Tracer(name).Start(ctx, "GetCountry")
    defer span.End()

    span.SetAttributes(attribute.String("country", in.Name))

    span.AddEvent("Retrieving country info")

    //...
    
    span.AddEvent("Country info retrieved")

    return &pb.GetCountryResponse{
        Country: &country,
    }, nil

}

这两个服务都被配置为将它们的跨度发送到Jaeger后端,并共享几乎相同的Main函数(注释中指出了微小的差异):

const (
    name        = "mapedia"
    service     = "geomap" //or standard
    environment = "production"
    id          = 1
)

func tracerProvider(url string) (*tracesdk.TracerProvider, error) {
    // Create the Jaeger exporter
    exp, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(url)))
    if err != nil {
        return nil, err
    }
    tp := tracesdk.NewTracerProvider(
        // Always be sure to batch in production.
        tracesdk.WithBatcher(exp),
        // Record information about this application in a Resource.
        tracesdk.WithResource(resource.NewWithAttributes(
            semconv.SchemaURL,
            semconv.ServiceName(service),
            attribute.String("environment", environment),
            attribute.Int64("ID", id),
        )),
    )
    return tp, nil
}

func main() {

    tp, err := tracerProvider("http://localhost:14268/api/traces")
    if err != nil {
        log.Fatal(err)
    }

    defer func() {
        if err := tp.Shutdown(context.Background()); err != nil {
            log.Fatal(err)
        }
    }()
    otel.SetTracerProvider(tp)

    listener, err := net.Listen("tcp", ":"+port)
    if err != nil {
        panic(err)
    }

    s := grpc.NewServer(
        grpc.UnaryInterceptor(otelgrpc.UnaryServerInterceptor()),
    )
    reflection.Register(s)
    pb.RegisterGeoMapServiceServer(s, &geomapService{}) // or pb.RegisterStandardServiceServer(s, &standardService{})
    if err := s.Serve(listener); err != nil {
        log.Fatalf("Failed to serve: %v", err)
    }
}

当我查看由终端用户对Standard服务的请求生成的跟踪时,我可以看到,正如预期的那样,它正在调用其GeoMap服务:

Standard trace

但是,我没有看到我添加到子范围中的任何属性或事件(在检测GeoMapGetCountry函数时,我添加了一个属性和两个事件).

然而,我注意到,这些属性在另一个单独的跟踪中可用(在Jaeger中的"geomap"服务下可用),其SPAN ID与Standard服务中的子跨距完全无关:

geomap trace

现在,我期望的是只有一个跟踪,并在Standard范围内看到与子范围中的GeoMap相关的所有属性/事件.从这里怎样才能达到预期的效果?

推荐答案

应将范围上下文(包含跟踪ID和范围ID,如"Service Instrumentation & Terminology"中所述)从父范围传播到子范围,以便它们成为同一跟踪的一部分.

With OpenTelemetry, this is often done automatically by instrumenting your code with the provided plugins for various libraries, including gRPC.
However, the propagation does not seem to be working correctly in your case.

In your code, you are starting a new span in the GetStandard function, and then using that context (newCtx) when making the GetCountry request. That is correct, as the new context should contain the span context of the parent span (GetStandard).
But the issue might be related to your createClient function:

func createClient(ctx context.Context, svcAddr string) (*grpc.ClientConn, error) {
    return grpc.DialContext(ctx, svcAddr,
        grpc.WithTransportCredentials(insecure.NewCredentials()),
        grpc.WithUnaryInterceptor(otelgrpc.UnaryClientInterceptor()),
    )
}

您在这里正确地使用了otelgrpc.UnaryClientInterceptor,它应该确保正确地传播上下文,但是不清楚何时调用该函数.如果它正被调用before,则调用GetStandard函数,则用于创建客户端的上下文not将包括来自GetStandard的跨度上下文.

为了进行测试,请try 并确保创建了客户端after调用了GetStandard函数,并且在整个请求中使用了相同的上下文.

您可以通过将newCtx直接传递给GetCountry函数来实现这一点,如您的GetStandard函数的修改版本所示:

func (s *standardService) GetStandard(ctx context.Context, in *pb.GetStandardRequest) (*pb.GetStandardResponse, error) {
    newCtx, span1 := otel.Tracer(name).Start(ctx, "GetStandard")
    defer span1.End()

    conn, _:= createClient(newCtx, geomapSvcAddr)
    defer conn.Close()

    countryInfo, err := pb.NewGeoMapServiceClient(conn).GetCountry(newCtx,
        &pb.GetCountryRequest{
            Name: in.Name,
        })

    //...

    return &pb.GetStandardResponse{
        Standard: standard,
    }, nil
}

现在,用于创建客户端和发出GetCountry请求的上下文将包括GetStandard中的SPAN上下文,并且它们应该显示为Jaeger中相同跟踪的一部分.

(像往常一样,一定要判断从createClientGetCountry等函数返回的错误,为简洁起见,这里没有显示).


此外,还有:

  • 还要判断您的传播者:确保在两个服务中使用相同的context propagator,最好是W3C TraceContextPropagator,这是OpenTelemeter中的默认设置.

    您可以按如下方式显式设置传播方:

    otel.SetTextMapPropagator(propagation.TraceContext{})
    

    将以上行添加到两个服务中的main函数的开头.

  • 确保传递元数据:GRPC拦截器应该自动从请求的元数据中插入/提取跟踪上下文,但要仔细判断以确保它正常工作.

    GetCountry函数中启动SPAN后,您可以记录跟踪ID和SPAN ID:

    ctx, span := otel.Tracer(name).Start(ctx, "GetCountry")
    sc := trace.SpanContextFromContext(ctx)
    log.Printf("Trace ID: %s, Span ID: %s", sc.TraceID(), sc.SpanID())
    defer span.End()
    

    并在您的GetStandard函数中执行相同的操作:

    newCtx, span1 := otel.Tracer(name).Start(ctx, "GetStandard")
    sc := trace.SpanContextFromContext(newCtx)
    log.Printf("Trace ID: %s, Span ID: %s", sc.TraceID(), sc.SpanID())
    defer span1.End()
    

    如果上下文被正确传播,则两个服务中的跟踪ID应该匹配.

Go相关问答推荐

追加一个字节数组的分配比2个字节数组的分配要少得多

读取JSON数据并在网页上显示

在Golang中,@LATEST和@UPGRADE特殊查询有什么不同?

错误.如果它包含切片,则返回FALSE

如何在Golang中获取mp3文件的持续时间?

如何模拟go的Elastic search SDK?

Golang校验器包:重命名字段错误处理

如何将验证器标记添加到嵌套字段

从 eBPF LRU 哈希映射中错误驱逐的元素

在两个单独的速率受限端点之间同步请求

将 struct 转换为 CSV 字符串

在密钥不存在时处理 PATCH 部分更新

如何使用泛型将接口转换为指定类型

枚举的 Golang 验证器自定义验证规则

如何使用 fyne Go 使用 canvas.NewText() 使文本可滚动

为什么 reflect.TypeOf(new(Encoder)).Elem() != reflect.TypeOf(interfaceVariable)?

未定义 protoc protoc-gen-go 时间戳

Golang:每个键具有多个值的映射

try 执行`go test ./... -v`时,Golang中有没有办法设置标志

Beego - 我需要context.Context而不是 Beego 上下文