From 8c4149c8f300ef3d49176dc4f42f857f115f4df7 Mon Sep 17 00:00:00 2001 From: Wes Tarle Date: Mon, 20 Apr 2026 15:49:42 +0000 Subject: [PATCH 1/3] impl: add client request spans to gax.Invoke --- go.work.sum | 6 ++ v2/invoke.go | 8 +++ v2/invoke_tracing_test.go | 118 ++++++++++++++++++++++++++++++++++++++ v2/telemetry.go | 80 ++++++++++++++++++++++++++ 4 files changed, 212 insertions(+) create mode 100644 v2/invoke_tracing_test.go diff --git a/go.work.sum b/go.work.sum index b57a805e..309e7daf 100644 --- a/go.work.sum +++ b/go.work.sum @@ -113,6 +113,7 @@ cloud.google.com/go/auth v0.16.2/go.mod h1:sRBas2Y1fB1vZTdurouM0AzuYQBMZinrUYL8E cloud.google.com/go/auth v0.16.5/go.mod h1:utzRfHMP+Vv0mpOkTRQoWD2q3BatTOoWbA7gCc2dUhQ= cloud.google.com/go/auth v0.18.1/go.mod h1:GfTYoS9G3CWpRA3Va9doKN9mjPGRS+v41jmZAhBzbrA= cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M= +cloud.google.com/go/auth v0.20.0/go.mod h1:942/yi/itH1SsmpyrbnTMDgGfdy2BUqIKyd0cyYLc5Q= cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q= cloud.google.com/go/auth/oauth2adapt v0.2.3/go.mod h1:tMQXOfZzFuNuUxOypHlQEXgdfX5cuhwU+ffUuXRJE8I= cloud.google.com/go/auth/oauth2adapt v0.2.5/go.mod h1:AlmsELtlEBnaNTL7jCj8VQFLy6mbZv0s4Q7NGBeQ5E8= @@ -980,6 +981,7 @@ github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEP github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w= github.com/googleapis/gax-go/v2 v2.16.0/go.mod h1:o1vfQjjNZn4+dPnRdl/4ZD7S9414Y4xA+a/6Icj6l14= github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY= +github.com/googleapis/gax-go/v2 v2.21.0/go.mod h1:But/NJU6TnZsrLai/xBAQLLz+Hc7fHZJt/hsCz3Fih4= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w= github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= @@ -1035,12 +1037,14 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.5 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg= go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= @@ -1344,6 +1348,7 @@ google.golang.org/genproto/googleapis/bytestream v0.0.0-20250818200422-3122310a4 google.golang.org/genproto/googleapis/bytestream v0.0.0-20260122232226-8e98ce8d340d/go.mod h1:Tej9lWiwVvQJP+b43pjJIsr/3mZycXWCIyoiXmbFf40= google.golang.org/genproto/googleapis/bytestream v0.0.0-20260226221140-a57be14db171/go.mod h1:9amqk/8LQWEC4RjyUxMx1DebyQ7hZB9gvl67bHmgZ2E= google.golang.org/genproto/googleapis/bytestream v0.0.0-20260311181403-84a4fc48630c/go.mod h1:9amqk/8LQWEC4RjyUxMx1DebyQ7hZB9gvl67bHmgZ2E= +google.golang.org/genproto/googleapis/bytestream v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:6TABGosqSqU2l1+fJ3jdvOYPPVryeKybxYF0cCZkTBE= google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= google.golang.org/genproto/googleapis/rpc v0.0.0-20230629202037-9506855d4529/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= google.golang.org/genproto/googleapis/rpc v0.0.0-20230706204954-ccb25ca9f130/go.mod h1:8mL13HKkDa+IuJ8yruA3ci0q+0vsUz4m//+ottjwS5o= @@ -1379,6 +1384,7 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go. google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= diff --git a/v2/invoke.go b/v2/invoke.go index ea16c3c5..a7ef7474 100644 --- a/v2/invoke.go +++ b/v2/invoke.go @@ -97,6 +97,14 @@ func invoke(ctx context.Context, call APICall, settings CallSettings, sp sleeper }() } + var endSpan func(error) + if IsFeatureEnabled("TRACING") { + ctx, endSpan = StartClientRequestSpan(ctx) + defer func() { + endSpan(err) + }() + } + retryCount := 0 // Feature gate: GOOGLE_SDK_GO_EXPERIMENTAL_TRACING=true tracingEnabled := IsFeatureEnabled("TRACING") diff --git a/v2/invoke_tracing_test.go b/v2/invoke_tracing_test.go new file mode 100644 index 00000000..10d31cf8 --- /dev/null +++ b/v2/invoke_tracing_test.go @@ -0,0 +1,118 @@ +package gax + +import ( + "context" + "testing" + + "github.com/googleapis/gax-go/v2/callctx" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/codes" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/sdk/trace/tracetest" + grpccodes "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func TestInvokeClientRequestSpan(t *testing.T) { + t.Setenv("GOOGLE_SDK_GO_EXPERIMENTAL_TRACING", "true") + TestOnlyResetIsFeatureEnabled() + defer TestOnlyResetIsFeatureEnabled() + + tests := []struct { + name string + setupCtx func() context.Context + callFunc func(context.Context, CallSettings) error + wantSpanName string + wantAttrs map[string]string + wantStatus codes.Code + wantErr bool + }{ + { + name: "success_with_client_span_name", + setupCtx: func() context.Context { + return callctx.WithTelemetryContext(context.Background(), "client_span_name", "MyClient.MyMethod") + }, + callFunc: func(ctx context.Context, settings CallSettings) error { + return nil + }, + wantSpanName: "MyClient.MyMethod", + wantAttrs: map[string]string{}, + wantStatus: codes.Ok, + wantErr: false, + }, + { + name: "success_with_fallback_rpc_method", + setupCtx: func() context.Context { + return callctx.WithTelemetryContext(context.Background(), "rpc_method", "my.pkg.Service/Method") + }, + callFunc: func(ctx context.Context, settings CallSettings) error { + return nil + }, + wantSpanName: "my.pkg.Service/Method", + wantAttrs: map[string]string{ + "rpc.method": "my.pkg.Service/Method", + }, + wantStatus: codes.Ok, + wantErr: false, + }, + { + name: "failure_records_error_and_attributes", + setupCtx: func() context.Context { + return callctx.WithTelemetryContext(context.Background(), "client_span_name", "MyMethod") + }, + callFunc: func(ctx context.Context, settings CallSettings) error { + return status.Error(grpccodes.Unavailable, "service unavailable") + }, + wantSpanName: "MyMethod", + wantAttrs: map[string]string{ + "error.type": "UNAVAILABLE", + "rpc.response.status_code": "UNAVAILABLE", + "status.message": "service unavailable", + }, + wantStatus: codes.Error, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := tt.setupCtx() + + sr := tracetest.NewSpanRecorder() + tp := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(sr)) + + oldProvider := otel.GetTracerProvider() + otel.SetTracerProvider(tp) + defer otel.SetTracerProvider(oldProvider) + + err := Invoke(ctx, tt.callFunc) + if (err != nil) != tt.wantErr { + t.Fatalf("Invoke() error = %v, wantErr %v", err, tt.wantErr) + } + + spans := sr.Ended() + if len(spans) == 0 { + t.Fatalf("expected at least 1 span recorded") + } + + span := spans[0] + if span.Name() != tt.wantSpanName { + t.Errorf("expected span name %q, got %q", tt.wantSpanName, span.Name()) + } + if span.Status().Code != tt.wantStatus { + t.Errorf("expected span status %v, got %v", tt.wantStatus, span.Status().Code) + } + + gotAttrs := make(map[string]string) + for _, a := range span.Attributes() { + gotAttrs[string(a.Key)] = a.Value.AsString() + } + + for k, v := range tt.wantAttrs { + if gotAttrs[k] != v { + t.Errorf("expected attribute %q=%q, got %q", k, v, gotAttrs[k]) + } + } + }) + } +} diff --git a/v2/telemetry.go b/v2/telemetry.go index b849b369..ef6c76f9 100644 --- a/v2/telemetry.go +++ b/v2/telemetry.go @@ -42,7 +42,9 @@ import ( "github.com/googleapis/gax-go/v2/callctx" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" + otelcodes "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/trace" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) @@ -467,3 +469,81 @@ func recordMetric(ctx context.Context, settings CallSettings, d time.Duration, e settings.clientMetrics.durationHistogram().Record(recordCtx, d.Seconds(), metric.WithAttributes(attrs...)) } + +// StartClientRequestSpan starts a client request span if tracing is enabled. +// It reads the span name from the context (key: client_span_name) and falls back to rpc_method. +// It returns the updated context and a closure to end the span, which avoids leaking OTel types to callers. +func StartClientRequestSpan(ctx context.Context) (context.Context, func(error)) { + if !IsFeatureEnabled("TRACING") { + return ctx, func(error) {} + } + spanName, _ := callctx.TelemetryFromContext(ctx, "client_span_name") + if spanName == "" { + spanName, _ = callctx.TelemetryFromContext(ctx, "rpc_method") + if spanName == "" { + spanName = "ClientRequest" + } + } + + tp := otel.GetTracerProvider() + tracer := tp.Tracer("github.com/googleapis/gax-go/v2") + + ctx, span := tracer.Start(ctx, spanName, trace.WithSpanKind(trace.SpanKindInternal)) + + // Add static attributes from context if available + if rpcMethod, ok := callctx.TelemetryFromContext(ctx, "rpc_method"); ok && rpcMethod != "" { + span.SetAttributes(attribute.String("rpc.method", rpcMethod)) + } + if urlTemplate, ok := callctx.TelemetryFromContext(ctx, "url_template"); ok && urlTemplate != "" { + span.SetAttributes(attribute.String("url.template", urlTemplate)) + } + if resName, ok := callctx.TelemetryFromContext(ctx, "resource_name"); ok && resName != "" { + span.SetAttributes(attribute.String("gcp.resource.destination.id", resName)) + } + + return ctx, func(err error) { + EndClientRequestSpan(ctx, span, err) + } +} + +// EndClientRequestSpan ends the client request span and records error details if present. +func EndClientRequestSpan(ctx context.Context, span trace.Span, err error) { + if span == nil { + return + } + if err != nil { + span.RecordError(err) + span.SetStatus(otelcodes.Error, err.Error()) + + // Extract structured error info + errInfo := ExtractTelemetryErrorInfo(ctx, err) + if errInfo.ErrorType != "" { + span.SetAttributes(attribute.String("error.type", errInfo.ErrorType)) + } + span.SetAttributes(attribute.String("rpc.response.status_code", errInfo.StatusCode)) + if errInfo.StatusMessage != "" { + span.SetAttributes(attribute.String("status.message", errInfo.StatusMessage)) + } + if errInfo.Domain != "" { + span.SetAttributes(attribute.String("gcp.errors.domain", errInfo.Domain)) + } + for k, v := range errInfo.Metadata { + span.SetAttributes(attribute.String("gcp.errors.metadata."+k, v)) + } + } else { + span.SetStatus(otelcodes.Ok, "OK") + span.SetAttributes(attribute.String("rpc.response.status_code", "OK")) + } + + // Extract transport telemetry if available + if td := ExtractTransportTelemetry(ctx); td != nil { + if td.ServerAddress() != "" { + span.SetAttributes(attribute.String("server.address", td.ServerAddress())) + } + if td.ServerPort() != 0 { + span.SetAttributes(attribute.Int("server.port", td.ServerPort())) + } + } + + span.End() +} From 04ddc3deb0b6297fd69bdc98769a88946cb02704 Mon Sep 17 00:00:00 2001 From: Baha Aiman Date: Tue, 23 Jun 2026 06:54:16 +0000 Subject: [PATCH 2/3] add copyright header --- v2/go.mod | 4 ++-- v2/go.sum | 2 ++ v2/invoke_tracing_test.go | 29 +++++++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/v2/go.mod b/v2/go.mod index e06e51d0..71df6df2 100644 --- a/v2/go.mod +++ b/v2/go.mod @@ -6,7 +6,9 @@ require ( github.com/google/go-cmp v0.7.0 go.opentelemetry.io/otel v1.44.0 go.opentelemetry.io/otel/metric v1.44.0 + go.opentelemetry.io/otel/sdk v1.44.0 go.opentelemetry.io/otel/sdk/metric v1.44.0 + go.opentelemetry.io/otel/trace v1.44.0 google.golang.org/api v0.284.0 google.golang.org/genproto v0.0.0-20260618152121-87f3d3e198d3 google.golang.org/genproto/googleapis/api v0.0.0-20260618152121-87f3d3e198d3 @@ -21,8 +23,6 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/google/uuid v1.6.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/otel/sdk v1.44.0 // indirect - go.opentelemetry.io/otel/trace v1.44.0 // indirect golang.org/x/net v0.55.0 // indirect golang.org/x/sys v0.45.0 // indirect golang.org/x/text v0.37.0 // indirect diff --git a/v2/go.sum b/v2/go.sum index f5b4d400..6ecb36d1 100644 --- a/v2/go.sum +++ b/v2/go.sum @@ -31,6 +31,8 @@ go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRk go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= diff --git a/v2/invoke_tracing_test.go b/v2/invoke_tracing_test.go index 10d31cf8..b818ba50 100644 --- a/v2/invoke_tracing_test.go +++ b/v2/invoke_tracing_test.go @@ -1,3 +1,32 @@ +// Copyright 2016, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + package gax import ( From a1b982bec6b763270d470085fb54506d50b664b5 Mon Sep 17 00:00:00 2001 From: Baha Aiman Date: Tue, 23 Jun 2026 12:45:43 +0000 Subject: [PATCH 3/3] perf(telemetry): remove redundant context.WithoutCancel from metrics path Eliminates an unnecessary heap allocation on the critical hot path of every API call. Synchronous, in-memory metric recording (`histogram.Record()`) does not perform I/O and is completely unaffected by context cancellation. Furthermore, trace context values (active spans) remain fully extractable via `trace.SpanContextFromContext(ctx)` even if the context's `Done()` channel has been closed. Thus, wrapping the context with `WithoutCancel` is redundant. --- v2/telemetry.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/v2/telemetry.go b/v2/telemetry.go index ef6c76f9..62f84d43 100644 --- a/v2/telemetry.go +++ b/v2/telemetry.go @@ -435,10 +435,6 @@ func recordMetric(ctx context.Context, settings CallSettings, d time.Duration, e return } - // Use context.WithoutCancel to ensure metric records even if context is canceled - // preserving any trace context that might be required for exemplars. - recordCtx := context.WithoutCancel(ctx) - // Pre-allocate to avoid repeated appends (5 is the max number of dynamic attributes added here) attrs := make([]attribute.KeyValue, 0, len(settings.clientMetrics.attributes())+5) attrs = append(attrs, settings.clientMetrics.attributes()...) @@ -467,7 +463,7 @@ func recordMetric(ctx context.Context, settings CallSettings, d time.Duration, e attrs = append(attrs, attribute.String("url.template", urlTemplate)) } - settings.clientMetrics.durationHistogram().Record(recordCtx, d.Seconds(), metric.WithAttributes(attrs...)) + settings.clientMetrics.durationHistogram().Record(ctx, d.Seconds(), metric.WithAttributes(attrs...)) } // StartClientRequestSpan starts a client request span if tracing is enabled.