From 4ef3d9f167b51a13c9a6ee47a0a342639f40f312 Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Wed, 18 Mar 2026 23:13:18 -0400 Subject: [PATCH 01/33] impl(o11y): introduce body size attributes --- .../com/google/api/gax/tracing/ApiTracer.java | 8 ++++++++ .../google/api/gax/tracing/BaseApiTracer.java | 10 ++++++++++ .../gax/tracing/ObservabilityAttributes.java | 6 ++++++ .../gax/tracing/OpenTelemetryTraceManager.java | 13 +++++++++++++ .../api/gax/tracing/OpencensusTracer.java | 18 ++++++++++++++++++ .../com/google/api/gax/tracing/SpanTracer.java | 14 ++++++++++++++ .../google/api/gax/tracing/TraceManager.java | 2 ++ 7 files changed, 71 insertions(+) diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracer.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracer.java index 97f8e017db..f4ee0917b7 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracer.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracer.java @@ -179,10 +179,18 @@ default void lroStartSucceeded() {} default void responseReceived() {} ; + /** Adds an annotation that a streaming response has been received with size. */ + default void responseReceived(long responseSize) {} + ; + /** Adds an annotation that a streaming request has been sent. */ default void requestSent() {} ; + /** Adds an annotation that a streaming request has been sent with size. */ + default void requestSent(long requestSize) {} + ; + /** * Adds an annotation that a batch of writes has been flushed. * diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/BaseApiTracer.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/BaseApiTracer.java index cffa4c744e..9a6553fdf8 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/BaseApiTracer.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/BaseApiTracer.java @@ -139,11 +139,21 @@ public void responseReceived() { // noop } + @Override + public void responseReceived(long responseSize) { + // noop + } + @Override public void requestSent() { // noop } + @Override + public void requestSent(long requestSize) { + // noop + } + @Override public void batchRequestSent(long elementCount, long requestSize) { // noop diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityAttributes.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityAttributes.java index b8b4dc2373..fdf0106e3d 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityAttributes.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityAttributes.java @@ -73,4 +73,10 @@ public class ObservabilityAttributes { /** The url template of the request (e.g. /v1/{name}:access). */ public static final String URL_TEMPLATE_ATTRIBUTE = "url.template"; + + /** Size of the request body in bytes. */ + public static final String HTTP_REQUEST_BODY_SIZE = "http.request.body.size"; + + /** Size of the response body in bytes. */ + public static final String HTTP_RESPONSE_BODY_SIZE = "http.response.body.size"; } diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryTraceManager.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryTraceManager.java index 833e56fda4..12c8a6368f 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryTraceManager.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryTraceManager.java @@ -75,5 +75,18 @@ private OtelSpan(io.opentelemetry.api.trace.Span span) { public void end() { span.end(); } + + @Override + public void setAttribute(String key, Object value) { + if (value instanceof Long) { + span.setAttribute(key, (Long) value); + } else if (value instanceof Integer) { + span.setAttribute(key, ((Integer) value).longValue()); + } else if (value instanceof String) { + span.setAttribute(key, (String) value); + } else { + span.setAttribute(key, value.toString()); + } + } } } diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpencensusTracer.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpencensusTracer.java index c9f6b7cfc7..3e8a32a7c3 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpencensusTracer.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpencensusTracer.java @@ -406,6 +406,15 @@ public void responseReceived() { totalReceivedMessages++; } + /** {@inheritDoc} */ + @Override + public void responseReceived(long responseSize) { + responseReceived(); + span.putAttribute( + ObservabilityAttributes.HTTP_RESPONSE_BODY_SIZE, + AttributeValue.longAttributeValue(responseSize)); + } + /** {@inheritDoc} */ @Override public void requestSent() { @@ -413,6 +422,15 @@ public void requestSent() { totalSentMessages.incrementAndGet(); } + /** {@inheritDoc} */ + @Override + public void requestSent(long requestSize) { + requestSent(); + span.putAttribute( + ObservabilityAttributes.HTTP_REQUEST_BODY_SIZE, + AttributeValue.longAttributeValue(requestSize)); + } + /** {@inheritDoc} */ @Override public void batchRequestSent(long elementCount, long requestSize) { diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java index c5c28aebe0..b3433c1211 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java @@ -85,6 +85,20 @@ public void attemptSucceeded() { endAttempt(); } + @Override + public void requestSent(long requestSize) { + if (attemptHandle != null) { + attemptHandle.setAttribute(ObservabilityAttributes.HTTP_REQUEST_BODY_SIZE, requestSize); + } + } + + @Override + public void responseReceived(long responseSize) { + if (attemptHandle != null) { + attemptHandle.setAttribute(ObservabilityAttributes.HTTP_RESPONSE_BODY_SIZE, responseSize); + } + } + private void endAttempt() { if (attemptHandle != null) { attemptHandle.end(); diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/TraceManager.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/TraceManager.java index 8572d1ce11..7fa0e897c9 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/TraceManager.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/TraceManager.java @@ -46,5 +46,7 @@ public interface TraceManager { interface Span { void end(); + + void setAttribute(String key, Object value); } } From c00e49a5a7d9fe08a1994b71dffe4b470e53d594 Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Thu, 19 Mar 2026 10:42:58 -0400 Subject: [PATCH 02/33] impl(httpjson): record response and request sizes --- .../api/gax/httpjson/HttpJsonCallContext.java | 3 +- .../api/gax/httpjson/HttpJsonCallOptions.java | 11 + .../gax/httpjson/HttpJsonClientCallImpl.java | 21 +- .../api/gax/httpjson/HttpRequestRunnable.java | 7 + .../gax/httpjson/BodySizeRecordingTest.java | 237 ++++++++++++++++++ .../gax/httpjson/testing/TestApiTracer.java | 21 ++ 6 files changed, 296 insertions(+), 4 deletions(-) create mode 100644 gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/BodySizeRecordingTest.java diff --git a/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonCallContext.java b/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonCallContext.java index 2b2d675a2a..c946e9aab0 100644 --- a/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonCallContext.java +++ b/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonCallContext.java @@ -607,10 +607,11 @@ public ApiTracer getTracer() { @Override public HttpJsonCallContext withTracer(@Nonnull ApiTracer newTracer) { Preconditions.checkNotNull(newTracer); + HttpJsonCallOptions newCallOptions = callOptions.toBuilder().setTracer(newTracer).build(); return new HttpJsonCallContext( this.channel, - this.callOptions, + newCallOptions, this.timeout, this.streamWaitTimeout, this.streamIdleTimeout, diff --git a/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonCallOptions.java b/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonCallOptions.java index 4c2d8ae55e..1f0022bb43 100644 --- a/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonCallOptions.java +++ b/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonCallOptions.java @@ -35,6 +35,7 @@ import static com.google.api.gax.util.TimeConversionUtils.toThreetenInstant; import com.google.api.core.ObsoleteApi; +import com.google.api.gax.tracing.ApiTracer; import com.google.auth.Credentials; import com.google.auto.value.AutoValue; import com.google.protobuf.TypeRegistry; @@ -71,6 +72,9 @@ public final org.threeten.bp.Instant getDeadline() { @Nullable public abstract TypeRegistry getTypeRegistry(); + @Nullable + public abstract ApiTracer getTracer(); + public abstract Builder toBuilder(); public static Builder newBuilder() { @@ -106,6 +110,11 @@ public HttpJsonCallOptions merge(HttpJsonCallOptions inputOptions) { builder.setTypeRegistry(newTypeRegistry); } + ApiTracer newTracer = inputOptions.getTracer(); + if (newTracer != null) { + builder.setTracer(newTracer); + } + return builder.build(); } @@ -131,6 +140,8 @@ public final Builder setDeadline(org.threeten.bp.Instant value) { public abstract Builder setTypeRegistry(TypeRegistry value); + public abstract Builder setTracer(ApiTracer value); + public abstract HttpJsonCallOptions build(); } } diff --git a/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonClientCallImpl.java b/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonClientCallImpl.java index df9a507519..0b0ecc0650 100644 --- a/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonClientCallImpl.java +++ b/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonClientCallImpl.java @@ -34,7 +34,9 @@ import com.google.api.gax.httpjson.HttpRequestRunnable.ResultListener; import com.google.api.gax.httpjson.HttpRequestRunnable.RunnableResult; import com.google.api.gax.rpc.StatusCode; +import com.google.api.gax.tracing.ApiTracer; import com.google.common.base.Preconditions; +import com.google.common.io.CountingInputStream; import com.google.errorprone.annotations.concurrent.GuardedBy; import java.io.IOException; import java.io.InputStreamReader; @@ -118,6 +120,9 @@ final class HttpJsonClientCallImpl @GuardedBy("lock") private ProtoMessageJsonStreamIterator responseStreamIterator; + @GuardedBy("lock") + private CountingInputStream responseCountingStream; + @GuardedBy("lock") private volatile boolean closed; @@ -400,14 +405,19 @@ private boolean consumeMessageFromStream() throws IOException { return true; } + if (responseCountingStream == null) { + responseCountingStream = new CountingInputStream(runnableResult.getResponseContent()); + } + boolean allMessagesConsumed; Reader responseReader; + long responseBodySizeStart = responseCountingStream.getCount(); if (methodDescriptor.getType() == MethodType.SERVER_STREAMING) { // Lazily initialize responseStreamIterator in case if it is a server streaming response if (responseStreamIterator == null) { responseStreamIterator = new ProtoMessageJsonStreamIterator( - new InputStreamReader(runnableResult.getResponseContent(), StandardCharsets.UTF_8)); + new InputStreamReader(responseCountingStream, StandardCharsets.UTF_8)); } if (responseStreamIterator.hasNext()) { responseReader = responseStreamIterator.next(); @@ -419,8 +429,7 @@ private boolean consumeMessageFromStream() throws IOException { // from the client to check if there is anything else left in the stream). allMessagesConsumed = !responseStreamIterator.hasNext(); } else { - responseReader = - new InputStreamReader(runnableResult.getResponseContent(), StandardCharsets.UTF_8); + responseReader = new InputStreamReader(responseCountingStream, StandardCharsets.UTF_8); // Unary calls have only one message in their response, so we should be ready to close // immediately after delivering a single response message. allMessagesConsumed = true; @@ -428,6 +437,12 @@ private boolean consumeMessageFromStream() throws IOException { ResponseT message = methodDescriptor.getResponseParser().parse(responseReader, callOptions.getTypeRegistry()); + long responseBodySizeEnd = responseCountingStream.getCount(); + + ApiTracer tracer = callOptions.getTracer(); + if (tracer != null) { + tracer.responseReceived(responseBodySizeEnd - responseBodySizeStart); + } pendingNotifications.offer(new OnMessageNotificationTask<>(listener, message)); return allMessagesConsumed; diff --git a/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpRequestRunnable.java b/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpRequestRunnable.java index 2738844bd0..ecf175a77d 100644 --- a/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpRequestRunnable.java +++ b/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpRequestRunnable.java @@ -44,6 +44,7 @@ import com.google.api.client.json.JsonObjectParser; import com.google.api.client.json.gson.GsonFactory; import com.google.api.client.util.GenericData; +import com.google.api.gax.tracing.ApiTracer; import com.google.auth.Credentials; import com.google.auth.http.HttpCredentialsAdapter; import com.google.auto.value.AutoValue; @@ -111,6 +112,12 @@ public void run() { if (cancelled) { return; } + + ApiTracer tracer = httpJsonCallOptions.getTracer(); + if (tracer != null) { + tracer.requestSent(httpRequest.getContent().getLength()); + } + httpResponse = httpRequest.execute(); // Check if already cancelled before trying to construct and read the response diff --git a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/BodySizeRecordingTest.java b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/BodySizeRecordingTest.java new file mode 100644 index 0000000000..f8218294c3 --- /dev/null +++ b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/BodySizeRecordingTest.java @@ -0,0 +1,237 @@ +/* + * Copyright 2026 Google LLC + * + * 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 LLC 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 com.google.api.gax.httpjson; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.api.gax.httpjson.testing.MockHttpService; +import com.google.api.gax.httpjson.testing.TestApiTracer; +import com.google.api.gax.rpc.EndpointContext; +import com.google.api.gax.rpc.ResponseObserver; +import com.google.api.gax.rpc.StreamController; +import com.google.auth.Credentials; +import com.google.protobuf.Field; +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +class BodySizeRecordingTest { + private static final ApiMethodDescriptor FAKE_METHOD_DESCRIPTOR = + ApiMethodDescriptor.newBuilder() + .setFullMethodName("google.cloud.v1.Fake/FakeMethod") + .setHttpMethod("POST") + .setRequestFormatter( + ProtoMessageRequestFormatter.newBuilder() + .setPath( + "/fake/v1/name/{name}", + request -> { + Map fields = new HashMap<>(); + ProtoRestSerializer serializer = ProtoRestSerializer.create(); + serializer.putPathParam(fields, "name", request.getName()); + return fields; + }) + .setQueryParamsExtractor(request -> new HashMap<>()) + .setRequestBodyExtractor( + request -> + ProtoRestSerializer.create() + .toBody("*", request.toBuilder().clearName().build(), false)) + .build()) + .setResponseParser( + ProtoMessageResponseParser.newBuilder() + .setDefaultInstance(Field.getDefaultInstance()) + .build()) + .build(); + + private static final MockHttpService MOCK_SERVICE = + new MockHttpService(Collections.singletonList(FAKE_METHOD_DESCRIPTOR), "google.com:443"); + + private static ExecutorService executorService; + private ManagedHttpJsonChannel channel; + private TestApiTracer tracer; + + @BeforeAll + public static void initialize() { + executorService = Executors.newFixedThreadPool(2); + } + + @AfterAll + public static void destroy() { + executorService.shutdownNow(); + } + + @BeforeEach + void setUp() throws IOException { + channel = + ManagedHttpJsonChannel.newBuilder() + .setEndpoint("google.com:443") + .setExecutor(executorService) + .setHttpTransport(MOCK_SERVICE) + .build(); + tracer = new TestApiTracer(); + } + + @AfterEach + void tearDown() { + MOCK_SERVICE.reset(); + } + + @Test + void testBodySizeRecording() throws Exception { + HttpJsonDirectCallable callable = + new HttpJsonDirectCallable<>(FAKE_METHOD_DESCRIPTOR); + + EndpointContext endpointContext = Mockito.mock(EndpointContext.class); + Mockito.doNothing() + .when(endpointContext) + .validateUniverseDomain( + Mockito.any(Credentials.class), Mockito.any(HttpJsonStatusCode.class)); + + HttpJsonCallContext callContext = + HttpJsonCallContext.createDefault() + .withChannel(channel) + .withEndpointContext(endpointContext) + .withTracer(tracer); + + Field request = Field.newBuilder().setName("bob").setNumber(42).build(); + Field response = Field.newBuilder().setName("alice").setNumber(43).build(); + + MOCK_SERVICE.addResponse(response); + + callable.futureCall(request, callContext).get(); + + // Verify request size + // The request body should be Field with number=42 (name is cleared in extractor) + // HttpRequestRunnable re-serializes it compactly. + String expectedRequestBody = "{\"number\":42}"; + long expectedRequestSize = expectedRequestBody.getBytes("UTF-8").length; + assertThat(tracer.getRequestSentSize()).isEqualTo(expectedRequestSize); + + // Verify response size + // MockHttpService uses ProtoRestSerializer which pretty-prints. + String expectedResponseBody = ProtoRestSerializer.create().toBody("*", response, false); + long expectedResponseSize = expectedResponseBody.getBytes("UTF-8").length; + assertThat(tracer.getResponseReceivedSize()).isEqualTo(expectedResponseSize); + } + + @Test + void testBodySizeRecordingServerStreaming() throws Exception { + ApiMethodDescriptor methodServerStreaming = + FAKE_METHOD_DESCRIPTOR.toBuilder() + .setType(ApiMethodDescriptor.MethodType.SERVER_STREAMING) + .build(); + + MockHttpService streamingMockService = + new MockHttpService(Collections.singletonList(methodServerStreaming), "google.com:443"); + ManagedHttpJsonChannel streamingChannel = + ManagedHttpJsonChannel.newBuilder() + .setEndpoint("google.com:443") + .setExecutor(executorService) + .setHttpTransport(streamingMockService) + .build(); + + HttpJsonDirectServerStreamingCallable callable = + new HttpJsonDirectServerStreamingCallable<>(methodServerStreaming); + + EndpointContext endpointContext = Mockito.mock(EndpointContext.class); + Mockito.doNothing() + .when(endpointContext) + .validateUniverseDomain( + Mockito.any(Credentials.class), Mockito.any(HttpJsonStatusCode.class)); + + HttpJsonCallContext callContext = + HttpJsonCallContext.createDefault() + .withChannel(streamingChannel) + .withEndpointContext(endpointContext) + .withTracer(tracer); + + Field request = Field.newBuilder().setName("bob").setNumber(42).build(); + Field response1 = Field.newBuilder().setName("alice1").setNumber(43).build(); + Field response2 = Field.newBuilder().setName("alice2").setNumber(44).build(); + + streamingMockService.addResponse(new Field[] {response1, response2}); + + final List receivedResponses = new java.util.ArrayList<>(); + final CountDownLatch latch = new CountDownLatch(1); + + callable.call( + request, + new ResponseObserver() { + @Override + public void onStart(StreamController controller) {} + + @Override + public void onResponse(Field response) { + receivedResponses.add(response); + } + + @Override + public void onError(Throwable t) { + latch.countDown(); + } + + @Override + public void onComplete() { + latch.countDown(); + } + }, + callContext); + + latch.await(10, TimeUnit.SECONDS); + + assertThat(receivedResponses).hasSize(2); + + // Verify request size + String expectedRequestBody = "{\"number\":42}"; + long expectedRequestSize = expectedRequestBody.getBytes("UTF-8").length; + assertThat(tracer.getRequestSentSize()).isEqualTo(expectedRequestSize); + + // Verify response size + // MockHttpService server-streaming response construction adds [ ] and , + String resp1Json = methodServerStreaming.getResponseParser().serialize(response1); + String resp2Json = methodServerStreaming.getResponseParser().serialize(response2); + long expectedTotalResponseSize = + ("[" + resp1Json + "," + resp2Json + "]").getBytes("UTF-8").length; + + assertThat(tracer.getResponseReceivedSize()).isEqualTo(expectedTotalResponseSize); + streamingChannel.shutdownNow(); + } +} diff --git a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/testing/TestApiTracer.java b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/testing/TestApiTracer.java index 604e9ad47b..a5e25e6f6e 100644 --- a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/testing/TestApiTracer.java +++ b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/testing/TestApiTracer.java @@ -32,6 +32,7 @@ import com.google.api.gax.tracing.ApiTracer; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; import org.threeten.bp.Duration; /** @@ -43,6 +44,8 @@ public class TestApiTracer implements ApiTracer { private final AtomicInteger attemptsStarted = new AtomicInteger(); private final AtomicInteger attemptsFailed = new AtomicInteger(); private final AtomicBoolean retriesExhausted = new AtomicBoolean(false); + private final AtomicLong requestSentSize = new AtomicLong(); + private final AtomicLong responseReceivedSize = new AtomicLong(); public TestApiTracer() {} @@ -58,6 +61,14 @@ public AtomicBoolean getRetriesExhausted() { return retriesExhausted; } + public long getRequestSentSize() { + return requestSentSize.get(); + } + + public long getResponseReceivedSize() { + return responseReceivedSize.get(); + } + @Override public void attemptStarted(int attemptNumber) { attemptsStarted.incrementAndGet(); @@ -78,5 +89,15 @@ public void attemptFailedRetriesExhausted(Throwable error) { attemptsFailed.incrementAndGet(); retriesExhausted.set(true); } + + @Override + public void requestSent(long requestSize) { + requestSentSize.addAndGet(requestSize); + } + + @Override + public void responseReceived(long responseSize) { + responseReceivedSize.addAndGet(responseSize); + } } ; From c6da32431a7307365c0f134a3d0d1f1623483690 Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Thu, 19 Mar 2026 12:44:41 -0400 Subject: [PATCH 03/33] test: expand tests --- .../api/gax/httpjson/HttpRequestRunnable.java | 3 ++ .../showcase/v1beta1/it/ITOtelTracing.java | 32 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpRequestRunnable.java b/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpRequestRunnable.java index ecf175a77d..a85083cd51 100644 --- a/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpRequestRunnable.java +++ b/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpRequestRunnable.java @@ -115,6 +115,9 @@ public void run() { ApiTracer tracer = httpJsonCallOptions.getTracer(); if (tracer != null) { + // If the request is using transport encoding (e.g. gzip), this should be the compressed + // size. getContent().getLength() returns the length of the content which would be the + // compressed size if transport encoding is used. tracer.requestSent(httpRequest.getContent().getLength()); } diff --git a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java index 18594429a0..b3341a3b4e 100644 --- a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java +++ b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java @@ -185,4 +185,36 @@ void testTracing_successfulEcho_httpjson() throws Exception { .isEqualTo(SHOWCASE_ARTIFACT); } } + + @Test + void testTracing_httpjson_bodySizes() throws Exception { + SpanTracerFactory tracingFactory = + new SpanTracerFactory(new OpenTelemetryTraceManager(openTelemetrySdk)); + + try (EchoClient client = + TestClientInitializer.createHttpJsonEchoClientOpentelemetry(tracingFactory)) { + + client.echo(EchoRequest.newBuilder().setContent("tracing-test").build()); + + List spans = spanExporter.getFinishedSpanItems(); + assertThat(spans).isNotEmpty(); + + SpanData attemptSpan = + spans.stream() + .filter(span -> span.getName().equals("Echo/Echo/attempt")) + .findFirst() + .orElseThrow(() -> new AssertionError("Attempt span 'Echo/Echo/attempt' not found")); + + assertThat( + attemptSpan + .getAttributes() + .get(AttributeKey.longKey(ObservabilityAttributes.HTTP_REQUEST_BODY_SIZE))) + .isAtLeast(1L); + assertThat( + attemptSpan + .getAttributes() + .get(AttributeKey.longKey(ObservabilityAttributes.HTTP_RESPONSE_BODY_SIZE))) + .isAtLeast(1L); + } + } } From 72be7700fe704fc9e7f5436f6f46ee48fb374848 Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Thu, 19 Mar 2026 12:48:04 -0400 Subject: [PATCH 04/33] fix revert request size attribute --- .../google/api/gax/httpjson/HttpRequestRunnable.java | 9 --------- .../api/gax/httpjson/BodySizeRecordingTest.java | 12 ------------ .../api/gax/httpjson/testing/TestApiTracer.java | 10 ---------- .../java/com/google/api/gax/tracing/ApiTracer.java | 4 ---- .../com/google/api/gax/tracing/BaseApiTracer.java | 5 ----- .../api/gax/tracing/ObservabilityAttributes.java | 3 --- .../com/google/api/gax/tracing/OpencensusTracer.java | 9 --------- .../java/com/google/api/gax/tracing/SpanTracer.java | 7 ------- .../google/showcase/v1beta1/it/ITOtelTracing.java | 5 ----- 9 files changed, 64 deletions(-) diff --git a/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpRequestRunnable.java b/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpRequestRunnable.java index a85083cd51..a274d2fb2d 100644 --- a/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpRequestRunnable.java +++ b/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpRequestRunnable.java @@ -44,7 +44,6 @@ import com.google.api.client.json.JsonObjectParser; import com.google.api.client.json.gson.GsonFactory; import com.google.api.client.util.GenericData; -import com.google.api.gax.tracing.ApiTracer; import com.google.auth.Credentials; import com.google.auth.http.HttpCredentialsAdapter; import com.google.auto.value.AutoValue; @@ -113,14 +112,6 @@ public void run() { return; } - ApiTracer tracer = httpJsonCallOptions.getTracer(); - if (tracer != null) { - // If the request is using transport encoding (e.g. gzip), this should be the compressed - // size. getContent().getLength() returns the length of the content which would be the - // compressed size if transport encoding is used. - tracer.requestSent(httpRequest.getContent().getLength()); - } - httpResponse = httpRequest.execute(); // Check if already cancelled before trying to construct and read the response diff --git a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/BodySizeRecordingTest.java b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/BodySizeRecordingTest.java index f8218294c3..6814432333 100644 --- a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/BodySizeRecordingTest.java +++ b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/BodySizeRecordingTest.java @@ -138,13 +138,6 @@ void testBodySizeRecording() throws Exception { callable.futureCall(request, callContext).get(); - // Verify request size - // The request body should be Field with number=42 (name is cleared in extractor) - // HttpRequestRunnable re-serializes it compactly. - String expectedRequestBody = "{\"number\":42}"; - long expectedRequestSize = expectedRequestBody.getBytes("UTF-8").length; - assertThat(tracer.getRequestSentSize()).isEqualTo(expectedRequestSize); - // Verify response size // MockHttpService uses ProtoRestSerializer which pretty-prints. String expectedResponseBody = ProtoRestSerializer.create().toBody("*", response, false); @@ -219,11 +212,6 @@ public void onComplete() { assertThat(receivedResponses).hasSize(2); - // Verify request size - String expectedRequestBody = "{\"number\":42}"; - long expectedRequestSize = expectedRequestBody.getBytes("UTF-8").length; - assertThat(tracer.getRequestSentSize()).isEqualTo(expectedRequestSize); - // Verify response size // MockHttpService server-streaming response construction adds [ ] and , String resp1Json = methodServerStreaming.getResponseParser().serialize(response1); diff --git a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/testing/TestApiTracer.java b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/testing/TestApiTracer.java index a5e25e6f6e..bbfe45e6d7 100644 --- a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/testing/TestApiTracer.java +++ b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/testing/TestApiTracer.java @@ -44,7 +44,6 @@ public class TestApiTracer implements ApiTracer { private final AtomicInteger attemptsStarted = new AtomicInteger(); private final AtomicInteger attemptsFailed = new AtomicInteger(); private final AtomicBoolean retriesExhausted = new AtomicBoolean(false); - private final AtomicLong requestSentSize = new AtomicLong(); private final AtomicLong responseReceivedSize = new AtomicLong(); public TestApiTracer() {} @@ -61,10 +60,6 @@ public AtomicBoolean getRetriesExhausted() { return retriesExhausted; } - public long getRequestSentSize() { - return requestSentSize.get(); - } - public long getResponseReceivedSize() { return responseReceivedSize.get(); } @@ -90,11 +85,6 @@ public void attemptFailedRetriesExhausted(Throwable error) { retriesExhausted.set(true); } - @Override - public void requestSent(long requestSize) { - requestSentSize.addAndGet(requestSize); - } - @Override public void responseReceived(long responseSize) { responseReceivedSize.addAndGet(responseSize); diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracer.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracer.java index f4ee0917b7..972fea28b1 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracer.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracer.java @@ -187,10 +187,6 @@ default void responseReceived(long responseSize) {} default void requestSent() {} ; - /** Adds an annotation that a streaming request has been sent with size. */ - default void requestSent(long requestSize) {} - ; - /** * Adds an annotation that a batch of writes has been flushed. * diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/BaseApiTracer.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/BaseApiTracer.java index 9a6553fdf8..9f1495b262 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/BaseApiTracer.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/BaseApiTracer.java @@ -149,11 +149,6 @@ public void requestSent() { // noop } - @Override - public void requestSent(long requestSize) { - // noop - } - @Override public void batchRequestSent(long elementCount, long requestSize) { // noop diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityAttributes.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityAttributes.java index fdf0106e3d..280d098265 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityAttributes.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityAttributes.java @@ -74,9 +74,6 @@ public class ObservabilityAttributes { /** The url template of the request (e.g. /v1/{name}:access). */ public static final String URL_TEMPLATE_ATTRIBUTE = "url.template"; - /** Size of the request body in bytes. */ - public static final String HTTP_REQUEST_BODY_SIZE = "http.request.body.size"; - /** Size of the response body in bytes. */ public static final String HTTP_RESPONSE_BODY_SIZE = "http.response.body.size"; } diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpencensusTracer.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpencensusTracer.java index 3e8a32a7c3..16fd4cf29c 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpencensusTracer.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpencensusTracer.java @@ -422,15 +422,6 @@ public void requestSent() { totalSentMessages.incrementAndGet(); } - /** {@inheritDoc} */ - @Override - public void requestSent(long requestSize) { - requestSent(); - span.putAttribute( - ObservabilityAttributes.HTTP_REQUEST_BODY_SIZE, - AttributeValue.longAttributeValue(requestSize)); - } - /** {@inheritDoc} */ @Override public void batchRequestSent(long elementCount, long requestSize) { diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java index b3433c1211..933b876f6e 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java @@ -85,13 +85,6 @@ public void attemptSucceeded() { endAttempt(); } - @Override - public void requestSent(long requestSize) { - if (attemptHandle != null) { - attemptHandle.setAttribute(ObservabilityAttributes.HTTP_REQUEST_BODY_SIZE, requestSize); - } - } - @Override public void responseReceived(long responseSize) { if (attemptHandle != null) { diff --git a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java index b3341a3b4e..d9a93ab90d 100644 --- a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java +++ b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java @@ -205,11 +205,6 @@ void testTracing_httpjson_bodySizes() throws Exception { .findFirst() .orElseThrow(() -> new AssertionError("Attempt span 'Echo/Echo/attempt' not found")); - assertThat( - attemptSpan - .getAttributes() - .get(AttributeKey.longKey(ObservabilityAttributes.HTTP_REQUEST_BODY_SIZE))) - .isAtLeast(1L); assertThat( attemptSpan .getAttributes() From 1265da35663c0b57e954ae4c115e6d2cf46ff701 Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Thu, 19 Mar 2026 12:54:02 -0400 Subject: [PATCH 05/33] chore: cleanup --- .../api/gax/httpjson/HttpRequestRunnable.java | 1 - .../tracing/OpenTelemetryTraceManager.java | 2 -- .../api/gax/tracing/OpencensusTracer.java | 9 -------- .../showcase/v1beta1/it/ITOtelTracing.java | 22 ------------------- 4 files changed, 34 deletions(-) diff --git a/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpRequestRunnable.java b/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpRequestRunnable.java index a274d2fb2d..2738844bd0 100644 --- a/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpRequestRunnable.java +++ b/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpRequestRunnable.java @@ -111,7 +111,6 @@ public void run() { if (cancelled) { return; } - httpResponse = httpRequest.execute(); // Check if already cancelled before trying to construct and read the response diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryTraceManager.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryTraceManager.java index 12c8a6368f..59f507a5a7 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryTraceManager.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryTraceManager.java @@ -80,8 +80,6 @@ public void end() { public void setAttribute(String key, Object value) { if (value instanceof Long) { span.setAttribute(key, (Long) value); - } else if (value instanceof Integer) { - span.setAttribute(key, ((Integer) value).longValue()); } else if (value instanceof String) { span.setAttribute(key, (String) value); } else { diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpencensusTracer.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpencensusTracer.java index 16fd4cf29c..c9f6b7cfc7 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpencensusTracer.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpencensusTracer.java @@ -406,15 +406,6 @@ public void responseReceived() { totalReceivedMessages++; } - /** {@inheritDoc} */ - @Override - public void responseReceived(long responseSize) { - responseReceived(); - span.putAttribute( - ObservabilityAttributes.HTTP_RESPONSE_BODY_SIZE, - AttributeValue.longAttributeValue(responseSize)); - } - /** {@inheritDoc} */ @Override public void requestSent() { diff --git a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java index 6055f11722..feb3c57afb 100644 --- a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java +++ b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java @@ -201,28 +201,6 @@ void testTracing_successfulEcho_httpjson() throws Exception { .getAttributes() .get(AttributeKey.stringKey(ObservabilityAttributes.HTTP_URL_TEMPLATE_ATTRIBUTE))) .isEqualTo("v1beta1/echo:echo"); - } - } - - @Test - void testTracing_httpjson_bodySizes() throws Exception { - SpanTracerFactory tracingFactory = - new SpanTracerFactory(new OpenTelemetryTraceManager(openTelemetrySdk)); - - try (EchoClient client = - TestClientInitializer.createHttpJsonEchoClientOpentelemetry(tracingFactory)) { - - client.echo(EchoRequest.newBuilder().setContent("tracing-test").build()); - - List spans = spanExporter.getFinishedSpanItems(); - assertThat(spans).isNotEmpty(); - - SpanData attemptSpan = - spans.stream() - .filter(span -> span.getName().equals("Echo/Echo/attempt")) - .findFirst() - .orElseThrow(() -> new AssertionError("Attempt span 'Echo/Echo/attempt' not found")); - assertThat( attemptSpan .getAttributes() From 94e55dba227e469e02c038b2d42934b4f7d7ee82 Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Thu, 19 Mar 2026 13:27:41 -0400 Subject: [PATCH 06/33] fix: use separate logic for unary resposne body size --- .../google/api/gax/httpjson/HttpJsonClientCallImpl.java | 5 ++++- .../google/api/gax/httpjson/testing/TestApiTracer.java | 2 +- .../main/java/com/google/api/gax/tracing/ApiTracer.java | 2 +- .../java/com/google/api/gax/tracing/BaseApiTracer.java | 2 +- .../java/com/google/api/gax/tracing/OpencensusTracer.java | 8 ++++++++ .../main/java/com/google/api/gax/tracing/SpanTracer.java | 2 +- 6 files changed, 16 insertions(+), 5 deletions(-) diff --git a/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonClientCallImpl.java b/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonClientCallImpl.java index 0b0ecc0650..4e486848d8 100644 --- a/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonClientCallImpl.java +++ b/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonClientCallImpl.java @@ -441,7 +441,10 @@ private boolean consumeMessageFromStream() throws IOException { ApiTracer tracer = callOptions.getTracer(); if (tracer != null) { - tracer.responseReceived(responseBodySizeEnd - responseBodySizeStart); + if (methodDescriptor.getType() == MethodType.SERVER_STREAMING) { + tracer.responseReceived(); + } + tracer.recordResponseSize(responseBodySizeEnd - responseBodySizeStart); } pendingNotifications.offer(new OnMessageNotificationTask<>(listener, message)); diff --git a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/testing/TestApiTracer.java b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/testing/TestApiTracer.java index bbfe45e6d7..af36cbaaf9 100644 --- a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/testing/TestApiTracer.java +++ b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/testing/TestApiTracer.java @@ -86,7 +86,7 @@ public void attemptFailedRetriesExhausted(Throwable error) { } @Override - public void responseReceived(long responseSize) { + public void recordResponseSize(long responseSize) { responseReceivedSize.addAndGet(responseSize); } } diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracer.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracer.java index 972fea28b1..970a9b5c71 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracer.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracer.java @@ -180,7 +180,7 @@ default void responseReceived() {} ; /** Adds an annotation that a streaming response has been received with size. */ - default void responseReceived(long responseSize) {} + default void recordResponseSize(long responseSize) {} ; /** Adds an annotation that a streaming request has been sent. */ diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/BaseApiTracer.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/BaseApiTracer.java index 9f1495b262..a46d80133c 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/BaseApiTracer.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/BaseApiTracer.java @@ -140,7 +140,7 @@ public void responseReceived() { } @Override - public void responseReceived(long responseSize) { + public void recordResponseSize(long responseSize) { // noop } diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpencensusTracer.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpencensusTracer.java index c9f6b7cfc7..fbb7dd6ec7 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpencensusTracer.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpencensusTracer.java @@ -406,6 +406,14 @@ public void responseReceived() { totalReceivedMessages++; } + /** {@inheritDoc} */ + @Override + public void recordResponseSize(long responseSize) { + span.putAttribute( + ObservabilityAttributes.HTTP_RESPONSE_BODY_SIZE, + AttributeValue.longAttributeValue(responseSize)); + } + /** {@inheritDoc} */ @Override public void requestSent() { diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java index 933b876f6e..40686a958d 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java @@ -86,7 +86,7 @@ public void attemptSucceeded() { } @Override - public void responseReceived(long responseSize) { + public void recordResponseSize(long responseSize) { if (attemptHandle != null) { attemptHandle.setAttribute(ObservabilityAttributes.HTTP_RESPONSE_BODY_SIZE, responseSize); } From 2cb047972725fae77886af5c3cf89630fa3f2afd Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Thu, 19 Mar 2026 13:57:38 -0400 Subject: [PATCH 07/33] test: improvements --- .../api/gax/httpjson/BodySizeRecordingTest.java | 11 ++++++----- .../api/gax/httpjson/HttpJsonCallContextTest.java | 10 ++++++++++ .../api/gax/httpjson/HttpJsonCallOptionsTest.java | 10 ++++++++++ 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/BodySizeRecordingTest.java b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/BodySizeRecordingTest.java index 6814432333..44df6e4f21 100644 --- a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/BodySizeRecordingTest.java +++ b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/BodySizeRecordingTest.java @@ -38,7 +38,6 @@ import com.google.api.gax.rpc.StreamController; import com.google.auth.Credentials; import com.google.protobuf.Field; -import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -89,17 +88,17 @@ class BodySizeRecordingTest { private TestApiTracer tracer; @BeforeAll - public static void initialize() { + static void initialize() { executorService = Executors.newFixedThreadPool(2); } @AfterAll - public static void destroy() { + static void destroy() { executorService.shutdownNow(); } @BeforeEach - void setUp() throws IOException { + void setUp() { channel = ManagedHttpJsonChannel.newBuilder() .setEndpoint("google.com:443") @@ -189,7 +188,9 @@ void testBodySizeRecordingServerStreaming() throws Exception { request, new ResponseObserver() { @Override - public void onStart(StreamController controller) {} + public void onStart(StreamController controller) { + // no behavior needed + } @Override public void onResponse(Field response) { diff --git a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonCallContextTest.java b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonCallContextTest.java index 156b4bb039..29bea372e1 100644 --- a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonCallContextTest.java +++ b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonCallContextTest.java @@ -252,6 +252,16 @@ void testMergeWithTracer() { .isSameInstanceAs(defaultTracer); } + @Test + void testWithTracer() { + ApiTracer tracer = Mockito.mock(ApiTracer.class); + HttpJsonCallContext emptyContext = HttpJsonCallContext.createDefault(); + // Default context has a default tracer. + assertNotNull(emptyContext.getTracer()); + HttpJsonCallContext context = emptyContext.withTracer(tracer); + Truth.assertThat(context.getTracer()).isSameInstanceAs(tracer); + } + @Test void testWithRetrySettings() { RetrySettings retrySettings = Mockito.mock(RetrySettings.class); diff --git a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonCallOptionsTest.java b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonCallOptionsTest.java index c6aa69d4d8..f0c6f8b697 100644 --- a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonCallOptionsTest.java +++ b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonCallOptionsTest.java @@ -31,12 +31,22 @@ import static com.google.api.gax.util.TimeConversionTestUtils.testDurationMethod; import static com.google.api.gax.util.TimeConversionTestUtils.testInstantMethod; +import static com.google.common.truth.Truth.assertThat; +import com.google.api.gax.tracing.ApiTracer; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; public class HttpJsonCallOptionsTest { private final HttpJsonCallOptions.Builder OPTIONS_BUILDER = HttpJsonCallOptions.newBuilder(); + @Test + public void testTracer() { + ApiTracer tracer = Mockito.mock(ApiTracer.class); + HttpJsonCallOptions options = OPTIONS_BUILDER.setTracer(tracer).build(); + assertThat(options.getTracer()).isSameInstanceAs(tracer); + } + @Test public void testDeadline() { final long millis = 3; From dc84757e48e99326e85905c09c847ee21e4731dc Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Thu, 19 Mar 2026 14:24:00 -0400 Subject: [PATCH 08/33] test: improve coverage --- .../google/api/gax/httpjson/BodySizeRecordingTest.java | 7 ++++++- .../google/api/gax/httpjson/testing/TestApiTracer.java | 10 ++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/BodySizeRecordingTest.java b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/BodySizeRecordingTest.java index 44df6e4f21..e4d78c9a45 100644 --- a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/BodySizeRecordingTest.java +++ b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/BodySizeRecordingTest.java @@ -38,6 +38,7 @@ import com.google.api.gax.rpc.StreamController; import com.google.auth.Credentials; import com.google.protobuf.Field; +import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -142,6 +143,8 @@ void testBodySizeRecording() throws Exception { String expectedResponseBody = ProtoRestSerializer.create().toBody("*", response, false); long expectedResponseSize = expectedResponseBody.getBytes("UTF-8").length; assertThat(tracer.getResponseReceivedSize()).isEqualTo(expectedResponseSize); + // Unary calls should NOT call responseReceived() (reserved for streaming) + assertThat(tracer.getResponsesReceived()).isEqualTo(0); } @Test @@ -218,9 +221,11 @@ public void onComplete() { String resp1Json = methodServerStreaming.getResponseParser().serialize(response1); String resp2Json = methodServerStreaming.getResponseParser().serialize(response2); long expectedTotalResponseSize = - ("[" + resp1Json + "," + resp2Json + "]").getBytes("UTF-8").length; + ("[" + resp1Json + "," + resp2Json + "]").getBytes(StandardCharsets.UTF_8).length; assertThat(tracer.getResponseReceivedSize()).isEqualTo(expectedTotalResponseSize); + // Server-streaming calls should call responseReceived() for EACH message + assertThat(tracer.getResponsesReceived()).isEqualTo(2); streamingChannel.shutdownNow(); } } diff --git a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/testing/TestApiTracer.java b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/testing/TestApiTracer.java index af36cbaaf9..19e35452e7 100644 --- a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/testing/TestApiTracer.java +++ b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/testing/TestApiTracer.java @@ -45,6 +45,7 @@ public class TestApiTracer implements ApiTracer { private final AtomicInteger attemptsFailed = new AtomicInteger(); private final AtomicBoolean retriesExhausted = new AtomicBoolean(false); private final AtomicLong responseReceivedSize = new AtomicLong(); + private final AtomicInteger responsesReceived = new AtomicInteger(); public TestApiTracer() {} @@ -64,6 +65,10 @@ public long getResponseReceivedSize() { return responseReceivedSize.get(); } + public int getResponsesReceived() { + return responsesReceived.get(); + } + @Override public void attemptStarted(int attemptNumber) { attemptsStarted.incrementAndGet(); @@ -85,6 +90,11 @@ public void attemptFailedRetriesExhausted(Throwable error) { retriesExhausted.set(true); } + @Override + public void responseReceived() { + responsesReceived.incrementAndGet(); + } + @Override public void recordResponseSize(long responseSize) { responseReceivedSize.addAndGet(responseSize); From cba21593574a55814a545a8f0433a3dc8a8ca691 Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Thu, 19 Mar 2026 14:39:24 -0400 Subject: [PATCH 09/33] fix: revert changes in opencensus --- .../java/com/google/api/gax/tracing/OpencensusTracer.java | 8 -------- 1 file changed, 8 deletions(-) diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpencensusTracer.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpencensusTracer.java index fbb7dd6ec7..c9f6b7cfc7 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpencensusTracer.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpencensusTracer.java @@ -406,14 +406,6 @@ public void responseReceived() { totalReceivedMessages++; } - /** {@inheritDoc} */ - @Override - public void recordResponseSize(long responseSize) { - span.putAttribute( - ObservabilityAttributes.HTTP_RESPONSE_BODY_SIZE, - AttributeValue.longAttributeValue(responseSize)); - } - /** {@inheritDoc} */ @Override public void requestSent() { From 9435824e5b7da7e5f95d12715011b6b85457e9ae Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Thu, 19 Mar 2026 14:41:33 -0400 Subject: [PATCH 10/33] test: cover baseapitracer --- .../java/com/google/api/gax/tracing/BaseApiTracerTest.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/gax-java/gax/src/test/java/com/google/api/gax/tracing/BaseApiTracerTest.java b/gax-java/gax/src/test/java/com/google/api/gax/tracing/BaseApiTracerTest.java index 8397efbb7d..25ca39dcc7 100644 --- a/gax-java/gax/src/test/java/com/google/api/gax/tracing/BaseApiTracerTest.java +++ b/gax-java/gax/src/test/java/com/google/api/gax/tracing/BaseApiTracerTest.java @@ -159,4 +159,11 @@ public void testBatchRequestSent() { tracer.batchRequestSent(10, 100); // No-op, so nothing to verify. } + + @Test + public void testRecordResponseSize() { + BaseApiTracer tracer = new BaseApiTracer(); + tracer.recordResponseSize(10); + // No-op, so nothing to verify. + } } From 0ae245d12350d9859dab4433aa878d611255049d Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Thu, 19 Mar 2026 16:03:25 -0400 Subject: [PATCH 11/33] test: test setAttribute --- .../OpenTelemetryTraceManagerTest.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/gax-java/gax/src/test/java/com/google/api/gax/tracing/OpenTelemetryTraceManagerTest.java b/gax-java/gax/src/test/java/com/google/api/gax/tracing/OpenTelemetryTraceManagerTest.java index e1a46e13a9..63fdd319f3 100644 --- a/gax-java/gax/src/test/java/com/google/api/gax/tracing/OpenTelemetryTraceManagerTest.java +++ b/gax-java/gax/src/test/java/com/google/api/gax/tracing/OpenTelemetryTraceManagerTest.java @@ -106,4 +106,38 @@ void testCreateSpan_recordsSpan() { verify(spanBuilder).setAllAttributes(ObservabilityUtils.toOtelAttributes(attributes)); verify(span).end(); } + + @Test + void testSetAttribute_long() { + TraceManager.Span handle = createTestSpan(); + + handle.setAttribute("longKey", 123L); + verify(span).setAttribute("longKey", 123L); + } + + @Test + void testSetAttribute_string() { + TraceManager.Span handle = createTestSpan(); + + handle.setAttribute("stringKey", "stringValue"); + verify(span).setAttribute("stringKey", "stringValue"); + } + + @Test + void testSetAttribute_other() { + TraceManager.Span handle = createTestSpan(); + + // Test other (Boolean) + handle.setAttribute("boolKey", true); + verify(span).setAttribute("boolKey", "true"); + } + + private TraceManager.Span createTestSpan() { + String spanName = "test-span"; + when(tracer.spanBuilder(spanName)).thenReturn(spanBuilder); + when(spanBuilder.setSpanKind(SpanKind.CLIENT)).thenReturn(spanBuilder); + when(spanBuilder.startSpan()).thenReturn(span); + + return recorder.createSpan(spanName, null); + } } From 9f1bcd85239beb20dccdcf3f96399ae5ebb1a04d Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Thu, 19 Mar 2026 16:20:49 -0400 Subject: [PATCH 12/33] fix: address code quality flags --- .../api/gax/httpjson/HttpJsonCallOptionsTest.java | 2 +- .../google/api/gax/tracing/BaseApiTracerTest.java | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonCallOptionsTest.java b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonCallOptionsTest.java index f0c6f8b697..4a8a2bc6f3 100644 --- a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonCallOptionsTest.java +++ b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonCallOptionsTest.java @@ -41,7 +41,7 @@ public class HttpJsonCallOptionsTest { private final HttpJsonCallOptions.Builder OPTIONS_BUILDER = HttpJsonCallOptions.newBuilder(); @Test - public void testTracer() { + void testTracer() { ApiTracer tracer = Mockito.mock(ApiTracer.class); HttpJsonCallOptions options = OPTIONS_BUILDER.setTracer(tracer).build(); assertThat(options.getTracer()).isSameInstanceAs(tracer); diff --git a/gax-java/gax/src/test/java/com/google/api/gax/tracing/BaseApiTracerTest.java b/gax-java/gax/src/test/java/com/google/api/gax/tracing/BaseApiTracerTest.java index 25ca39dcc7..eee8d789f2 100644 --- a/gax-java/gax/src/test/java/com/google/api/gax/tracing/BaseApiTracerTest.java +++ b/gax-java/gax/src/test/java/com/google/api/gax/tracing/BaseApiTracerTest.java @@ -29,6 +29,8 @@ */ package com.google.api.gax.tracing; +import static com.google.common.truth.Truth.assertThat; + import org.junit.jupiter.api.Test; public class BaseApiTracerTest { @@ -161,9 +163,14 @@ public void testBatchRequestSent() { } @Test - public void testRecordResponseSize() { - BaseApiTracer tracer = new BaseApiTracer(); - tracer.recordResponseSize(10); - // No-op, so nothing to verify. + void testRecordResponseSize() { + Throwable notExpected = null; + try { + BaseApiTracer tracer = new BaseApiTracer(); + tracer.recordResponseSize(10); + } catch (Exception ex) { + notExpected = ex; + } + assertThat(notExpected).isNull(); } } From eb959bb4c89c2d29ed1dbce32a1e9bf5cbce8d6c Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Fri, 20 Mar 2026 16:34:48 -0400 Subject: [PATCH 13/33] refactor: use content lenght header --- .../gax/httpjson/HttpJsonClientCallImpl.java | 21 +++++-------- .../gax/httpjson/BodySizeRecordingTest.java | 11 ++----- .../gax/httpjson/testing/MockHttpService.java | 7 ++++- .../gax/httpjson/testing/TestApiTracer.java | 31 +++++++++++++++++-- .../com/google/api/gax/tracing/ApiTracer.java | 4 +-- .../google/api/gax/tracing/BaseApiTracer.java | 2 +- .../google/api/gax/tracing/SpanTracer.java | 31 +++++++++++++++++-- .../api/gax/tracing/BaseApiTracerTest.java | 4 +-- 8 files changed, 79 insertions(+), 32 deletions(-) diff --git a/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonClientCallImpl.java b/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonClientCallImpl.java index 4e486848d8..0d10b75773 100644 --- a/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonClientCallImpl.java +++ b/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonClientCallImpl.java @@ -36,7 +36,6 @@ import com.google.api.gax.rpc.StatusCode; import com.google.api.gax.tracing.ApiTracer; import com.google.common.base.Preconditions; -import com.google.common.io.CountingInputStream; import com.google.errorprone.annotations.concurrent.GuardedBy; import java.io.IOException; import java.io.InputStreamReader; @@ -120,9 +119,6 @@ final class HttpJsonClientCallImpl @GuardedBy("lock") private ProtoMessageJsonStreamIterator responseStreamIterator; - @GuardedBy("lock") - private CountingInputStream responseCountingStream; - @GuardedBy("lock") private volatile boolean closed; @@ -161,6 +157,11 @@ public void setResult(RunnableResult runnableResult) { if (runnableResult.getResponseHeaders() != null) { pendingNotifications.offer( new OnHeadersNotificationTask<>(listener, runnableResult.getResponseHeaders())); + if (callOptions.getTracer() != null) { + callOptions + .getTracer() + .responseHeadersReceived(runnableResult.getResponseHeaders().getHeaders()); + } } } @@ -405,19 +406,14 @@ private boolean consumeMessageFromStream() throws IOException { return true; } - if (responseCountingStream == null) { - responseCountingStream = new CountingInputStream(runnableResult.getResponseContent()); - } - boolean allMessagesConsumed; Reader responseReader; - long responseBodySizeStart = responseCountingStream.getCount(); if (methodDescriptor.getType() == MethodType.SERVER_STREAMING) { // Lazily initialize responseStreamIterator in case if it is a server streaming response if (responseStreamIterator == null) { responseStreamIterator = new ProtoMessageJsonStreamIterator( - new InputStreamReader(responseCountingStream, StandardCharsets.UTF_8)); + new InputStreamReader(runnableResult.getResponseContent(), StandardCharsets.UTF_8)); } if (responseStreamIterator.hasNext()) { responseReader = responseStreamIterator.next(); @@ -429,7 +425,8 @@ private boolean consumeMessageFromStream() throws IOException { // from the client to check if there is anything else left in the stream). allMessagesConsumed = !responseStreamIterator.hasNext(); } else { - responseReader = new InputStreamReader(responseCountingStream, StandardCharsets.UTF_8); + responseReader = + new InputStreamReader(runnableResult.getResponseContent(), StandardCharsets.UTF_8); // Unary calls have only one message in their response, so we should be ready to close // immediately after delivering a single response message. allMessagesConsumed = true; @@ -437,14 +434,12 @@ private boolean consumeMessageFromStream() throws IOException { ResponseT message = methodDescriptor.getResponseParser().parse(responseReader, callOptions.getTypeRegistry()); - long responseBodySizeEnd = responseCountingStream.getCount(); ApiTracer tracer = callOptions.getTracer(); if (tracer != null) { if (methodDescriptor.getType() == MethodType.SERVER_STREAMING) { tracer.responseReceived(); } - tracer.recordResponseSize(responseBodySizeEnd - responseBodySizeStart); } pendingNotifications.offer(new OnMessageNotificationTask<>(listener, message)); diff --git a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/BodySizeRecordingTest.java b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/BodySizeRecordingTest.java index e4d78c9a45..2076f4bf4e 100644 --- a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/BodySizeRecordingTest.java +++ b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/BodySizeRecordingTest.java @@ -38,7 +38,6 @@ import com.google.api.gax.rpc.StreamController; import com.google.auth.Credentials; import com.google.protobuf.Field; -import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -216,14 +215,8 @@ public void onComplete() { assertThat(receivedResponses).hasSize(2); - // Verify response size - // MockHttpService server-streaming response construction adds [ ] and , - String resp1Json = methodServerStreaming.getResponseParser().serialize(response1); - String resp2Json = methodServerStreaming.getResponseParser().serialize(response2); - long expectedTotalResponseSize = - ("[" + resp1Json + "," + resp2Json + "]").getBytes(StandardCharsets.UTF_8).length; - - assertThat(tracer.getResponseReceivedSize()).isEqualTo(expectedTotalResponseSize); + // Verify response size (0 because streaming chunked responses don't include Content-Length) + assertThat(tracer.getResponseReceivedSize()).isEqualTo(0); // Server-streaming calls should call responseReceived() for EACH message assertThat(tracer.getResponsesReceived()).isEqualTo(2); streamingChannel.shutdownNow(); diff --git a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/testing/MockHttpService.java b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/testing/MockHttpService.java index 931041201d..31427c8db1 100644 --- a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/testing/MockHttpService.java +++ b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/testing/MockHttpService.java @@ -43,6 +43,7 @@ import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.LinkedListMultimap; import com.google.common.collect.Multimap; +import java.nio.charset.StandardCharsets; import java.util.LinkedList; import java.util.List; import java.util.Queue; @@ -261,7 +262,11 @@ public MockLowLevelHttpResponse getHttpResponse(String httpMethod, String fullTa httpContent = methodDescriptor.getResponseParser().serialize(response); } - httpResponse.setContent(httpContent.getBytes()); + byte[] contentBytes = httpContent.getBytes(StandardCharsets.UTF_8); + httpResponse.setContent(contentBytes); + if (methodDescriptor.getType() != MethodType.SERVER_STREAMING) { + httpResponse.addHeader("Content-Length", String.valueOf(contentBytes.length)); + } httpResponse.setStatusCode(200); return httpResponse; } diff --git a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/testing/TestApiTracer.java b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/testing/TestApiTracer.java index 19e35452e7..b7e670b03b 100644 --- a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/testing/TestApiTracer.java +++ b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/testing/TestApiTracer.java @@ -96,8 +96,35 @@ public void responseReceived() { } @Override - public void recordResponseSize(long responseSize) { - responseReceivedSize.addAndGet(responseSize); + public void responseHeadersReceived(java.util.Map headers) { + long contentLength = extractContentLength(headers); + if (contentLength >= 0) { + responseReceivedSize.addAndGet(contentLength); + } + } + + private long extractContentLength(java.util.Map headers) { + if (headers == null) { + return -1; + } + for (java.util.Map.Entry entry : headers.entrySet()) { + if ("Content-Length".equalsIgnoreCase(entry.getKey())) { + Object value = entry.getValue(); + if (value != null) { + try { + String contentLengthStr = + value instanceof java.util.List + ? ((java.util.List) value).get(0).toString() + : value.toString(); + return Long.parseLong(contentLengthStr); + } catch (NumberFormatException | IndexOutOfBoundsException e) { + // Ignore invalid Content-Length + } + } + break; + } + } + return -1; } } ; diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracer.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracer.java index 970a9b5c71..9ee3e208d6 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracer.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracer.java @@ -179,8 +179,8 @@ default void lroStartSucceeded() {} default void responseReceived() {} ; - /** Adds an annotation that a streaming response has been received with size. */ - default void recordResponseSize(long responseSize) {} + /** Adds an annotation that a streaming response has been received with its headers. */ + default void responseHeadersReceived(java.util.Map headers) {} ; /** Adds an annotation that a streaming request has been sent. */ diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/BaseApiTracer.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/BaseApiTracer.java index a46d80133c..bacf59635c 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/BaseApiTracer.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/BaseApiTracer.java @@ -140,7 +140,7 @@ public void responseReceived() { } @Override - public void recordResponseSize(long responseSize) { + public void responseHeadersReceived(java.util.Map headers) { // noop } diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java index 40686a958d..7c52654b7e 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java @@ -86,12 +86,39 @@ public void attemptSucceeded() { } @Override - public void recordResponseSize(long responseSize) { + public void responseHeadersReceived(java.util.Map headers) { if (attemptHandle != null) { - attemptHandle.setAttribute(ObservabilityAttributes.HTTP_RESPONSE_BODY_SIZE, responseSize); + long contentLength = extractContentLength(headers); + if (contentLength >= 0) { + attemptHandle.setAttribute(ObservabilityAttributes.HTTP_RESPONSE_BODY_SIZE, contentLength); + } } } + private long extractContentLength(java.util.Map headers) { + if (headers == null) { + return -1; + } + for (Map.Entry entry : headers.entrySet()) { + if ("Content-Length".equalsIgnoreCase(entry.getKey())) { + Object value = entry.getValue(); + if (value != null) { + try { + String contentLengthStr = + value instanceof java.util.List + ? ((java.util.List) value).get(0).toString() + : value.toString(); + return Long.parseLong(contentLengthStr); + } catch (NumberFormatException | IndexOutOfBoundsException e) { + // Ignore invalid Content-Length + } + } + break; + } + } + return -1; + } + private void endAttempt() { if (attemptHandle != null) { attemptHandle.end(); diff --git a/gax-java/gax/src/test/java/com/google/api/gax/tracing/BaseApiTracerTest.java b/gax-java/gax/src/test/java/com/google/api/gax/tracing/BaseApiTracerTest.java index eee8d789f2..21eab5ab1e 100644 --- a/gax-java/gax/src/test/java/com/google/api/gax/tracing/BaseApiTracerTest.java +++ b/gax-java/gax/src/test/java/com/google/api/gax/tracing/BaseApiTracerTest.java @@ -163,11 +163,11 @@ public void testBatchRequestSent() { } @Test - void testRecordResponseSize() { + void testResponseHeadersReceived() { Throwable notExpected = null; try { BaseApiTracer tracer = new BaseApiTracer(); - tracer.recordResponseSize(10); + tracer.responseHeadersReceived(java.util.Collections.emptyMap()); } catch (Exception ex) { notExpected = ex; } From 11950af6b250be65acdc9695921b918b7e837254 Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Fri, 20 Mar 2026 16:49:39 -0400 Subject: [PATCH 14/33] fix(tracing): add body_size tracking to OpenTelemetry SpanTracer --- .../google/api/gax/tracing/SpanTracer.java | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java index a749823690..d066189d84 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java @@ -122,10 +122,10 @@ public void attemptSucceeded() { @Override public void responseHeadersReceived(java.util.Map headers) { - if (attemptHandle != null) { + if (attemptSpan != null) { long contentLength = extractContentLength(headers); if (contentLength >= 0) { - attemptHandle.setAttribute(ObservabilityAttributes.HTTP_RESPONSE_BODY_SIZE, contentLength); + attemptSpan.setAttribute(ObservabilityAttributes.HTTP_RESPONSE_BODY_SIZE, contentLength); } } } @@ -136,24 +136,28 @@ private long extractContentLength(java.util.Map headers) { } for (Map.Entry entry : headers.entrySet()) { if ("Content-Length".equalsIgnoreCase(entry.getKey())) { - Object value = entry.getValue(); - if (value != null) { - try { - String contentLengthStr = - value instanceof java.util.List - ? ((java.util.List) value).get(0).toString() - : value.toString(); - return Long.parseLong(contentLengthStr); - } catch (NumberFormatException | IndexOutOfBoundsException e) { - // Ignore invalid Content-Length - } - } - break; + return parseContentLength(entry.getValue()); } } return -1; } + private long parseContentLength(Object value) { + if (value == null) { + return -1; + } + try { + String contentLengthStr = + value instanceof java.util.List + ? ((java.util.List) value).get(0).toString() + : value.toString(); + return Long.parseLong(contentLengthStr); + } catch (NumberFormatException | IndexOutOfBoundsException e) { + // Ignore invalid Content-Length + return -1; + } + } + private void endAttempt() { if (attemptSpan != null) { attemptSpan.end(); From 6e8b3114746bfc49f73a06d00b36f14a5e6f457b Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Fri, 20 Mar 2026 17:02:53 -0400 Subject: [PATCH 15/33] chore(tracing): revert HttpJson tracing and merge test files --- .../gax/httpjson/HttpJsonClientCallImpl.java | 7 - .../gax/httpjson/BodySizeRecordingTest.java | 224 ------------------ .../httpjson/HttpJsonClientCallImplTest.java | 186 +++++++++++++++ .../google/api/gax/tracing/SpanTracer.java | 12 + 4 files changed, 198 insertions(+), 231 deletions(-) delete mode 100644 gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/BodySizeRecordingTest.java diff --git a/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonClientCallImpl.java b/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonClientCallImpl.java index 0d10b75773..53a4dd66d3 100644 --- a/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonClientCallImpl.java +++ b/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonClientCallImpl.java @@ -34,7 +34,6 @@ import com.google.api.gax.httpjson.HttpRequestRunnable.ResultListener; import com.google.api.gax.httpjson.HttpRequestRunnable.RunnableResult; import com.google.api.gax.rpc.StatusCode; -import com.google.api.gax.tracing.ApiTracer; import com.google.common.base.Preconditions; import com.google.errorprone.annotations.concurrent.GuardedBy; import java.io.IOException; @@ -435,12 +434,6 @@ private boolean consumeMessageFromStream() throws IOException { ResponseT message = methodDescriptor.getResponseParser().parse(responseReader, callOptions.getTypeRegistry()); - ApiTracer tracer = callOptions.getTracer(); - if (tracer != null) { - if (methodDescriptor.getType() == MethodType.SERVER_STREAMING) { - tracer.responseReceived(); - } - } pendingNotifications.offer(new OnMessageNotificationTask<>(listener, message)); return allMessagesConsumed; diff --git a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/BodySizeRecordingTest.java b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/BodySizeRecordingTest.java deleted file mode 100644 index 2076f4bf4e..0000000000 --- a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/BodySizeRecordingTest.java +++ /dev/null @@ -1,224 +0,0 @@ -/* - * Copyright 2026 Google LLC - * - * 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 LLC 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 com.google.api.gax.httpjson; - -import static com.google.common.truth.Truth.assertThat; - -import com.google.api.gax.httpjson.testing.MockHttpService; -import com.google.api.gax.httpjson.testing.TestApiTracer; -import com.google.api.gax.rpc.EndpointContext; -import com.google.api.gax.rpc.ResponseObserver; -import com.google.api.gax.rpc.StreamController; -import com.google.auth.Credentials; -import com.google.protobuf.Field; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; - -class BodySizeRecordingTest { - private static final ApiMethodDescriptor FAKE_METHOD_DESCRIPTOR = - ApiMethodDescriptor.newBuilder() - .setFullMethodName("google.cloud.v1.Fake/FakeMethod") - .setHttpMethod("POST") - .setRequestFormatter( - ProtoMessageRequestFormatter.newBuilder() - .setPath( - "/fake/v1/name/{name}", - request -> { - Map fields = new HashMap<>(); - ProtoRestSerializer serializer = ProtoRestSerializer.create(); - serializer.putPathParam(fields, "name", request.getName()); - return fields; - }) - .setQueryParamsExtractor(request -> new HashMap<>()) - .setRequestBodyExtractor( - request -> - ProtoRestSerializer.create() - .toBody("*", request.toBuilder().clearName().build(), false)) - .build()) - .setResponseParser( - ProtoMessageResponseParser.newBuilder() - .setDefaultInstance(Field.getDefaultInstance()) - .build()) - .build(); - - private static final MockHttpService MOCK_SERVICE = - new MockHttpService(Collections.singletonList(FAKE_METHOD_DESCRIPTOR), "google.com:443"); - - private static ExecutorService executorService; - private ManagedHttpJsonChannel channel; - private TestApiTracer tracer; - - @BeforeAll - static void initialize() { - executorService = Executors.newFixedThreadPool(2); - } - - @AfterAll - static void destroy() { - executorService.shutdownNow(); - } - - @BeforeEach - void setUp() { - channel = - ManagedHttpJsonChannel.newBuilder() - .setEndpoint("google.com:443") - .setExecutor(executorService) - .setHttpTransport(MOCK_SERVICE) - .build(); - tracer = new TestApiTracer(); - } - - @AfterEach - void tearDown() { - MOCK_SERVICE.reset(); - } - - @Test - void testBodySizeRecording() throws Exception { - HttpJsonDirectCallable callable = - new HttpJsonDirectCallable<>(FAKE_METHOD_DESCRIPTOR); - - EndpointContext endpointContext = Mockito.mock(EndpointContext.class); - Mockito.doNothing() - .when(endpointContext) - .validateUniverseDomain( - Mockito.any(Credentials.class), Mockito.any(HttpJsonStatusCode.class)); - - HttpJsonCallContext callContext = - HttpJsonCallContext.createDefault() - .withChannel(channel) - .withEndpointContext(endpointContext) - .withTracer(tracer); - - Field request = Field.newBuilder().setName("bob").setNumber(42).build(); - Field response = Field.newBuilder().setName("alice").setNumber(43).build(); - - MOCK_SERVICE.addResponse(response); - - callable.futureCall(request, callContext).get(); - - // Verify response size - // MockHttpService uses ProtoRestSerializer which pretty-prints. - String expectedResponseBody = ProtoRestSerializer.create().toBody("*", response, false); - long expectedResponseSize = expectedResponseBody.getBytes("UTF-8").length; - assertThat(tracer.getResponseReceivedSize()).isEqualTo(expectedResponseSize); - // Unary calls should NOT call responseReceived() (reserved for streaming) - assertThat(tracer.getResponsesReceived()).isEqualTo(0); - } - - @Test - void testBodySizeRecordingServerStreaming() throws Exception { - ApiMethodDescriptor methodServerStreaming = - FAKE_METHOD_DESCRIPTOR.toBuilder() - .setType(ApiMethodDescriptor.MethodType.SERVER_STREAMING) - .build(); - - MockHttpService streamingMockService = - new MockHttpService(Collections.singletonList(methodServerStreaming), "google.com:443"); - ManagedHttpJsonChannel streamingChannel = - ManagedHttpJsonChannel.newBuilder() - .setEndpoint("google.com:443") - .setExecutor(executorService) - .setHttpTransport(streamingMockService) - .build(); - - HttpJsonDirectServerStreamingCallable callable = - new HttpJsonDirectServerStreamingCallable<>(methodServerStreaming); - - EndpointContext endpointContext = Mockito.mock(EndpointContext.class); - Mockito.doNothing() - .when(endpointContext) - .validateUniverseDomain( - Mockito.any(Credentials.class), Mockito.any(HttpJsonStatusCode.class)); - - HttpJsonCallContext callContext = - HttpJsonCallContext.createDefault() - .withChannel(streamingChannel) - .withEndpointContext(endpointContext) - .withTracer(tracer); - - Field request = Field.newBuilder().setName("bob").setNumber(42).build(); - Field response1 = Field.newBuilder().setName("alice1").setNumber(43).build(); - Field response2 = Field.newBuilder().setName("alice2").setNumber(44).build(); - - streamingMockService.addResponse(new Field[] {response1, response2}); - - final List receivedResponses = new java.util.ArrayList<>(); - final CountDownLatch latch = new CountDownLatch(1); - - callable.call( - request, - new ResponseObserver() { - @Override - public void onStart(StreamController controller) { - // no behavior needed - } - - @Override - public void onResponse(Field response) { - receivedResponses.add(response); - } - - @Override - public void onError(Throwable t) { - latch.countDown(); - } - - @Override - public void onComplete() { - latch.countDown(); - } - }, - callContext); - - latch.await(10, TimeUnit.SECONDS); - - assertThat(receivedResponses).hasSize(2); - - // Verify response size (0 because streaming chunked responses don't include Content-Length) - assertThat(tracer.getResponseReceivedSize()).isEqualTo(0); - // Server-streaming calls should call responseReceived() for EACH message - assertThat(tracer.getResponsesReceived()).isEqualTo(2); - streamingChannel.shutdownNow(); - } -} diff --git a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonClientCallImplTest.java b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonClientCallImplTest.java index 2853e79ad5..3d24bd5356 100644 --- a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonClientCallImplTest.java +++ b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonClientCallImplTest.java @@ -29,15 +29,35 @@ */ package com.google.api.gax.httpjson; +import static com.google.common.truth.Truth.assertThat; + import com.google.api.client.http.HttpTransport; +import com.google.api.gax.httpjson.testing.MockHttpService; +import com.google.api.gax.httpjson.testing.TestApiTracer; +import com.google.api.gax.rpc.EndpointContext; +import com.google.api.gax.rpc.ResponseObserver; +import com.google.api.gax.rpc.StreamController; +import com.google.auth.Credentials; import com.google.common.truth.Truth; +import com.google.protobuf.Field; import com.google.protobuf.TypeRegistry; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.io.Reader; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -135,4 +155,170 @@ void responseReceived_cancellationTaskExists_isCancelledProperly() throws Interr // Scheduler is not waiting for any task and should terminate quickly Truth.assertThat(deadlineSchedulerExecutor.isTerminated()).isTrue(); } + + private static final ApiMethodDescriptor FAKE_METHOD_DESCRIPTOR = + ApiMethodDescriptor.newBuilder() + .setFullMethodName("google.cloud.v1.Fake/FakeMethod") + .setHttpMethod("POST") + .setRequestFormatter( + ProtoMessageRequestFormatter.newBuilder() + .setPath( + "/fake/v1/name/{name}", + request -> { + Map fields = new HashMap<>(); + ProtoRestSerializer serializer = ProtoRestSerializer.create(); + serializer.putPathParam(fields, "name", request.getName()); + return fields; + }) + .setQueryParamsExtractor(request -> new HashMap<>()) + .setRequestBodyExtractor( + request -> + ProtoRestSerializer.create() + .toBody("*", request.toBuilder().clearName().build(), false)) + .build()) + .setResponseParser( + ProtoMessageResponseParser.newBuilder() + .setDefaultInstance(Field.getDefaultInstance()) + .build()) + .build(); + + private static final MockHttpService MOCK_SERVICE = + new MockHttpService(Collections.singletonList(FAKE_METHOD_DESCRIPTOR), "google.com:443"); + + private static ExecutorService executorService; + private ManagedHttpJsonChannel channel; + private TestApiTracer tracer; + + @BeforeAll + static void initialize() { + executorService = Executors.newFixedThreadPool(2); + } + + @AfterAll + static void destroy() { + executorService.shutdownNow(); + } + + @BeforeEach + void setUp() { + channel = + ManagedHttpJsonChannel.newBuilder() + .setEndpoint("google.com:443") + .setExecutor(executorService) + .setHttpTransport(MOCK_SERVICE) + .build(); + tracer = new TestApiTracer(); + } + + @AfterEach + void tearDown() { + MOCK_SERVICE.reset(); + } + + @Test + void testBodySizeRecording() throws Exception { + HttpJsonDirectCallable callable = + new HttpJsonDirectCallable<>(FAKE_METHOD_DESCRIPTOR); + + EndpointContext endpointContext = Mockito.mock(EndpointContext.class); + Mockito.lenient() + .doNothing() + .when(endpointContext) + .validateUniverseDomain( + Mockito.any(Credentials.class), Mockito.any(HttpJsonStatusCode.class)); + + HttpJsonCallContext callContext = + HttpJsonCallContext.createDefault() + .withChannel(channel) + .withEndpointContext(endpointContext) + .withTracer(tracer); + + Field request = Field.newBuilder().setName("bob").setNumber(42).build(); + Field response = Field.newBuilder().setName("alice").setNumber(43).build(); + + MOCK_SERVICE.addResponse(response); + + callable.futureCall(request, callContext).get(); + + // Verify response size + // MockHttpService uses ProtoRestSerializer which pretty-prints. + String expectedResponseBody = ProtoRestSerializer.create().toBody("*", response, false); + long expectedResponseSize = expectedResponseBody.getBytes("UTF-8").length; + assertThat(tracer.getResponseReceivedSize()).isEqualTo(expectedResponseSize); + } + + @Test + void testBodySizeRecordingServerStreaming() throws Exception { + ApiMethodDescriptor methodServerStreaming = + FAKE_METHOD_DESCRIPTOR.toBuilder() + .setType(ApiMethodDescriptor.MethodType.SERVER_STREAMING) + .build(); + + MockHttpService streamingMockService = + new MockHttpService(Collections.singletonList(methodServerStreaming), "google.com:443"); + ManagedHttpJsonChannel streamingChannel = + ManagedHttpJsonChannel.newBuilder() + .setEndpoint("google.com:443") + .setExecutor(executorService) + .setHttpTransport(streamingMockService) + .build(); + + HttpJsonDirectServerStreamingCallable callable = + new HttpJsonDirectServerStreamingCallable<>(methodServerStreaming); + + EndpointContext endpointContext = Mockito.mock(EndpointContext.class); + Mockito.lenient() + .doNothing() + .when(endpointContext) + .validateUniverseDomain( + Mockito.any(Credentials.class), Mockito.any(HttpJsonStatusCode.class)); + + HttpJsonCallContext callContext = + HttpJsonCallContext.createDefault() + .withChannel(streamingChannel) + .withEndpointContext(endpointContext) + .withTracer(tracer); + + Field request = Field.newBuilder().setName("bob").setNumber(42).build(); + Field response1 = Field.newBuilder().setName("alice1").setNumber(43).build(); + Field response2 = Field.newBuilder().setName("alice2").setNumber(44).build(); + + streamingMockService.addResponse(new Field[] {response1, response2}); + + final List receivedResponses = new java.util.ArrayList<>(); + final CountDownLatch latch = new CountDownLatch(1); + + callable.call( + request, + new ResponseObserver() { + @Override + public void onStart(StreamController controller) { + // no behavior needed + } + + @Override + public void onResponse(Field response) { + receivedResponses.add(response); + } + + @Override + public void onError(Throwable t) { + latch.countDown(); + } + + @Override + public void onComplete() { + latch.countDown(); + } + }, + callContext); + + latch.await(10, TimeUnit.SECONDS); + + assertThat(receivedResponses).hasSize(2); + + // Verify response size (0 because streaming chunked responses don't include Content-Length) + assertThat(tracer.getResponseReceivedSize()).isEqualTo(0); + streamingChannel.shutdownNow(); + } } diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java index d066189d84..c280c5b498 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java @@ -130,6 +130,12 @@ public void responseHeadersReceived(java.util.Map headers) { } } + /** + * Extracts the Content-Length header value from the response headers, if available. + * + * @param headers the map of response headers. + * @return the content length in bytes, or -1 if the header is missing or malformed. + */ private long extractContentLength(java.util.Map headers) { if (headers == null) { return -1; @@ -142,6 +148,12 @@ private long extractContentLength(java.util.Map headers) { return -1; } + /** + * Safely parses the content length Object representation (e.g. List or String) into a long integer. + * + * @param value the header value to parse. + * @return the parsed content length value, or -1 if it was null or failed to parse. + */ private long parseContentLength(Object value) { if (value == null) { return -1; From 6035f298f4b405ddbd20476c253a28252a3f8ef0 Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Fri, 20 Mar 2026 17:45:26 -0400 Subject: [PATCH 16/33] chore: format --- .../src/main/java/com/google/api/gax/tracing/SpanTracer.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java index c280c5b498..1b7ef692c4 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java @@ -149,7 +149,8 @@ private long extractContentLength(java.util.Map headers) { } /** - * Safely parses the content length Object representation (e.g. List or String) into a long integer. + * Safely parses the content length Object representation (e.g. List or String) into a long + * integer. * * @param value the header value to parse. * @return the parsed content length value, or -1 if it was null or failed to parse. From 960ddd1320ced4b24f20c042363385b47a090615 Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Fri, 20 Mar 2026 19:21:20 -0400 Subject: [PATCH 17/33] refactor(o11y): remove redundant responseHeadersReceived from BaseApiTracer --- .../com/google/api/gax/tracing/BaseApiTracer.java | 5 ----- .../google/api/gax/tracing/BaseApiTracerTest.java | 14 -------------- 2 files changed, 19 deletions(-) diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/BaseApiTracer.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/BaseApiTracer.java index bacf59635c..cffa4c744e 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/BaseApiTracer.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/BaseApiTracer.java @@ -139,11 +139,6 @@ public void responseReceived() { // noop } - @Override - public void responseHeadersReceived(java.util.Map headers) { - // noop - } - @Override public void requestSent() { // noop diff --git a/gax-java/gax/src/test/java/com/google/api/gax/tracing/BaseApiTracerTest.java b/gax-java/gax/src/test/java/com/google/api/gax/tracing/BaseApiTracerTest.java index 21eab5ab1e..8397efbb7d 100644 --- a/gax-java/gax/src/test/java/com/google/api/gax/tracing/BaseApiTracerTest.java +++ b/gax-java/gax/src/test/java/com/google/api/gax/tracing/BaseApiTracerTest.java @@ -29,8 +29,6 @@ */ package com.google.api.gax.tracing; -import static com.google.common.truth.Truth.assertThat; - import org.junit.jupiter.api.Test; public class BaseApiTracerTest { @@ -161,16 +159,4 @@ public void testBatchRequestSent() { tracer.batchRequestSent(10, 100); // No-op, so nothing to verify. } - - @Test - void testResponseHeadersReceived() { - Throwable notExpected = null; - try { - BaseApiTracer tracer = new BaseApiTracer(); - tracer.responseHeadersReceived(java.util.Collections.emptyMap()); - } catch (Exception ex) { - notExpected = ex; - } - assertThat(notExpected).isNull(); - } } From 097e5ee3a574439e1b0aa5ce5e2168344c71d665 Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Fri, 20 Mar 2026 19:28:45 -0400 Subject: [PATCH 18/33] refactor: update responseHeadersReceived to use Map and simplify parseContentLength --- .../api/gax/httpjson/testing/TestApiTracer.java | 14 +++++--------- .../com/google/api/gax/tracing/ApiTracer.java | 2 +- .../com/google/api/gax/tracing/SpanTracer.java | 17 ++++++----------- 3 files changed, 12 insertions(+), 21 deletions(-) diff --git a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/testing/TestApiTracer.java b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/testing/TestApiTracer.java index b7e670b03b..1fe82ca7f8 100644 --- a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/testing/TestApiTracer.java +++ b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/testing/TestApiTracer.java @@ -96,28 +96,24 @@ public void responseReceived() { } @Override - public void responseHeadersReceived(java.util.Map headers) { + public void responseHeadersReceived(java.util.Map headers) { long contentLength = extractContentLength(headers); if (contentLength >= 0) { responseReceivedSize.addAndGet(contentLength); } } - private long extractContentLength(java.util.Map headers) { + private long extractContentLength(java.util.Map headers) { if (headers == null) { return -1; } - for (java.util.Map.Entry entry : headers.entrySet()) { + for (java.util.Map.Entry entry : headers.entrySet()) { if ("Content-Length".equalsIgnoreCase(entry.getKey())) { Object value = entry.getValue(); if (value != null) { try { - String contentLengthStr = - value instanceof java.util.List - ? ((java.util.List) value).get(0).toString() - : value.toString(); - return Long.parseLong(contentLengthStr); - } catch (NumberFormatException | IndexOutOfBoundsException e) { + return Long.parseLong(String.valueOf(value)); + } catch (NumberFormatException e) { // Ignore invalid Content-Length } } diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracer.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracer.java index 9ee3e208d6..7dc3fb2c99 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracer.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracer.java @@ -180,7 +180,7 @@ default void responseReceived() {} ; /** Adds an annotation that a streaming response has been received with its headers. */ - default void responseHeadersReceived(java.util.Map headers) {} + default void responseHeadersReceived(java.util.Map headers) {} ; /** Adds an annotation that a streaming request has been sent. */ diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java index 1b7ef692c4..a570ae9b77 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java @@ -121,7 +121,7 @@ public void attemptSucceeded() { } @Override - public void responseHeadersReceived(java.util.Map headers) { + public void responseHeadersReceived(java.util.Map headers) { if (attemptSpan != null) { long contentLength = extractContentLength(headers); if (contentLength >= 0) { @@ -136,11 +136,11 @@ public void responseHeadersReceived(java.util.Map headers) { * @param headers the map of response headers. * @return the content length in bytes, or -1 if the header is missing or malformed. */ - private long extractContentLength(java.util.Map headers) { + private long extractContentLength(java.util.Map headers) { if (headers == null) { return -1; } - for (Map.Entry entry : headers.entrySet()) { + for (Map.Entry entry : headers.entrySet()) { if ("Content-Length".equalsIgnoreCase(entry.getKey())) { return parseContentLength(entry.getValue()); } @@ -149,8 +149,7 @@ private long extractContentLength(java.util.Map headers) { } /** - * Safely parses the content length Object representation (e.g. List or String) into a long - * integer. + * Safely parses the content length Object representation into a long integer. * * @param value the header value to parse. * @return the parsed content length value, or -1 if it was null or failed to parse. @@ -160,12 +159,8 @@ private long parseContentLength(Object value) { return -1; } try { - String contentLengthStr = - value instanceof java.util.List - ? ((java.util.List) value).get(0).toString() - : value.toString(); - return Long.parseLong(contentLengthStr); - } catch (NumberFormatException | IndexOutOfBoundsException e) { + return Long.parseLong(String.valueOf(value)); + } catch (NumberFormatException e) { // Ignore invalid Content-Length return -1; } From ab61cbba0850f3a9a0a448017c24f4c7c6bb2c28 Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Fri, 20 Mar 2026 19:30:51 -0400 Subject: [PATCH 19/33] test: add responseHeadersReceived tests to SpanTracerTest --- .../api/gax/tracing/SpanTracerTest.java | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerTest.java b/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerTest.java index 6d20cdfcf4..1e32cfdf36 100644 --- a/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerTest.java +++ b/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerTest.java @@ -86,4 +86,44 @@ void testAttemptStarted_includesLanguageAttribute() { io.opentelemetry.api.common.AttributeKey.stringKey(SpanTracer.LANGUAGE_ATTRIBUTE), SpanTracer.DEFAULT_LANGUAGE); } + + @Test + void testResponseHeadersReceived_setsContentLengthAttribute() { + spanTracer.attemptStarted(new Object(), 1); + + java.util.Map headers = new java.util.HashMap<>(); + headers.put("Content-Length", 12345L); + spanTracer.responseHeadersReceived(headers); + + verify(span).setAttribute(ObservabilityAttributes.HTTP_RESPONSE_BODY_SIZE, 12345L); + } + + @Test + void testResponseHeadersReceived_variousContentLengthStringFormats() { + spanTracer.attemptStarted(new Object(), 1); + + java.util.Map headers = new java.util.HashMap<>(); + headers.put("content-length", "6789"); + spanTracer.responseHeadersReceived(headers); + + verify(span).setAttribute(ObservabilityAttributes.HTTP_RESPONSE_BODY_SIZE, 6789L); + } + + @Test + void testResponseHeadersReceived_invalidOrMissingContentLength() { + spanTracer.attemptStarted(new Object(), 1); + + java.util.Map headers = new java.util.HashMap<>(); + headers.put("Content-Length", "invalid"); + spanTracer.responseHeadersReceived(headers); + + headers.clear(); + headers.put("Other-Header", "123"); + spanTracer.responseHeadersReceived(headers); + + verify(span, org.mockito.Mockito.never()) + .setAttribute( + org.mockito.ArgumentMatchers.eq(ObservabilityAttributes.HTTP_RESPONSE_BODY_SIZE), + org.mockito.ArgumentMatchers.anyLong()); + } } From 869bd2a547c3179a34acf765e2f04479ad8d85a4 Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Fri, 20 Mar 2026 19:34:04 -0400 Subject: [PATCH 20/33] refactor: extract CONTENT_LENGTH_KEY to static final string --- .../src/main/java/com/google/api/gax/tracing/SpanTracer.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java index a570ae9b77..6e6e0d2053 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java @@ -47,6 +47,8 @@ public class SpanTracer implements ApiTracer { public static final String DEFAULT_LANGUAGE = "Java"; + static final String CONTENT_LENGTH_KEY = "Content-Length"; + private final Tracer tracer; private final Map attemptAttributes; private final String attemptSpanName; @@ -141,7 +143,7 @@ private long extractContentLength(java.util.Map headers) { return -1; } for (Map.Entry entry : headers.entrySet()) { - if ("Content-Length".equalsIgnoreCase(entry.getKey())) { + if (CONTENT_LENGTH_KEY.equalsIgnoreCase(entry.getKey())) { return parseContentLength(entry.getValue()); } } From c7219602c0792ab5fe4da508faa0f11b77ff5053 Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Fri, 20 Mar 2026 19:40:13 -0400 Subject: [PATCH 21/33] refactor: remove NumberFormatException handling in tracers --- .../com/google/api/gax/httpjson/testing/TestApiTracer.java | 6 +----- .../main/java/com/google/api/gax/tracing/SpanTracer.java | 7 +------ .../java/com/google/api/gax/tracing/SpanTracerTest.java | 6 +----- 3 files changed, 3 insertions(+), 16 deletions(-) diff --git a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/testing/TestApiTracer.java b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/testing/TestApiTracer.java index 1fe82ca7f8..9250ab0718 100644 --- a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/testing/TestApiTracer.java +++ b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/testing/TestApiTracer.java @@ -111,11 +111,7 @@ private long extractContentLength(java.util.Map headers) { if ("Content-Length".equalsIgnoreCase(entry.getKey())) { Object value = entry.getValue(); if (value != null) { - try { - return Long.parseLong(String.valueOf(value)); - } catch (NumberFormatException e) { - // Ignore invalid Content-Length - } + return Long.parseLong(String.valueOf(value)); } break; } diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java index 6e6e0d2053..2808ae5a86 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java @@ -160,12 +160,7 @@ private long parseContentLength(Object value) { if (value == null) { return -1; } - try { - return Long.parseLong(String.valueOf(value)); - } catch (NumberFormatException e) { - // Ignore invalid Content-Length - return -1; - } + return Long.parseLong(String.valueOf(value)); } private void endAttempt() { diff --git a/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerTest.java b/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerTest.java index 1e32cfdf36..f4ce496b1f 100644 --- a/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerTest.java +++ b/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerTest.java @@ -110,14 +110,10 @@ void testResponseHeadersReceived_variousContentLengthStringFormats() { } @Test - void testResponseHeadersReceived_invalidOrMissingContentLength() { + void testResponseHeadersReceived_missingContentLength() { spanTracer.attemptStarted(new Object(), 1); java.util.Map headers = new java.util.HashMap<>(); - headers.put("Content-Length", "invalid"); - spanTracer.responseHeadersReceived(headers); - - headers.clear(); headers.put("Other-Header", "123"); spanTracer.responseHeadersReceived(headers); From 53117a39163bc62eb634114ac91e5c86c8441817 Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Mon, 23 Mar 2026 11:07:07 -0400 Subject: [PATCH 22/33] fix: simplify logic of header getter --- .../google/api/gax/tracing/SpanTracer.java | 29 +++++-------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java index 2808ae5a86..ab283334e4 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java @@ -38,6 +38,8 @@ import io.opentelemetry.api.trace.Tracer; import java.util.HashMap; import java.util.Map; +import java.util.TreeMap; +import java.util.function.Supplier; /** An implementation of {@link ApiTracer} that uses OpenTelemetry to record traces. */ @BetaApi @@ -138,29 +140,14 @@ public void responseHeadersReceived(java.util.Map headers) { * @param headers the map of response headers. * @return the content length in bytes, or -1 if the header is missing or malformed. */ - private long extractContentLength(java.util.Map headers) { - if (headers == null) { + private long extractContentLength(final java.util.Map headers) { + final Map iHeaders = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + iHeaders.putAll(headers); + Supplier headerGetter = () -> iHeaders.get(CONTENT_LENGTH_KEY); + if (headers == null || headerGetter.get() == null) { return -1; } - for (Map.Entry entry : headers.entrySet()) { - if (CONTENT_LENGTH_KEY.equalsIgnoreCase(entry.getKey())) { - return parseContentLength(entry.getValue()); - } - } - return -1; - } - - /** - * Safely parses the content length Object representation into a long integer. - * - * @param value the header value to parse. - * @return the parsed content length value, or -1 if it was null or failed to parse. - */ - private long parseContentLength(Object value) { - if (value == null) { - return -1; - } - return Long.parseLong(String.valueOf(value)); + return Long.parseLong(String.valueOf(headerGetter.get())); } private void endAttempt() { From 247f8711b4b0374003f22013643c396f09f14ec2 Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Mon, 23 Mar 2026 11:10:04 -0400 Subject: [PATCH 23/33] fix: use number format exception --- .../google/api/gax/tracing/SpanTracer.java | 6 ++++- .../api/gax/tracing/SpanTracerTest.java | 24 +++++++++++++++---- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java index ab283334e4..d334b8124f 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java @@ -147,7 +147,11 @@ private long extractContentLength(final java.util.Map headers) { if (headers == null || headerGetter.get() == null) { return -1; } - return Long.parseLong(String.valueOf(headerGetter.get())); + try { + return Long.parseLong(String.valueOf(headerGetter.get())); + } catch (NumberFormatException ex) { + return -1; + } } private void endAttempt() { diff --git a/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerTest.java b/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerTest.java index f4ce496b1f..de773ff33b 100644 --- a/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerTest.java +++ b/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerTest.java @@ -117,9 +117,23 @@ void testResponseHeadersReceived_missingContentLength() { headers.put("Other-Header", "123"); spanTracer.responseHeadersReceived(headers); - verify(span, org.mockito.Mockito.never()) - .setAttribute( - org.mockito.ArgumentMatchers.eq(ObservabilityAttributes.HTTP_RESPONSE_BODY_SIZE), - org.mockito.ArgumentMatchers.anyLong()); - } + verify(span, org.mockito.Mockito.never()) + .setAttribute( + org.mockito.ArgumentMatchers.eq(ObservabilityAttributes.HTTP_RESPONSE_BODY_SIZE), + org.mockito.ArgumentMatchers.anyLong()); + } + + @Test + void testResponseHeadersReceived_badFormat() { + spanTracer.attemptStarted(new Object(), 1); + + java.util.Map headers = new java.util.HashMap<>(); + headers.put("Other-Header", "12X3"); + spanTracer.responseHeadersReceived(headers); + + verify(span, org.mockito.Mockito.never()) + .setAttribute( + org.mockito.ArgumentMatchers.eq(ObservabilityAttributes.HTTP_RESPONSE_BODY_SIZE), + org.mockito.ArgumentMatchers.eq(-1)); + } } From c2fdd2ab35f4d1b03ccefae05028fc36946f91d4 Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Mon, 23 Mar 2026 12:03:07 -0400 Subject: [PATCH 24/33] chore: format code --- .../api/gax/tracing/SpanTracerTest.java | 39 ++++++++++--------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerTest.java b/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerTest.java index fb466539ff..5b5052717b 100644 --- a/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerTest.java +++ b/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerTest.java @@ -117,25 +117,26 @@ void testResponseHeadersReceived_missingContentLength() { headers.put("Other-Header", "123"); spanTracer.responseHeadersReceived(headers); - verify(span, org.mockito.Mockito.never()) - .setAttribute( - org.mockito.ArgumentMatchers.eq(ObservabilityAttributes.HTTP_RESPONSE_BODY_SIZE), - org.mockito.ArgumentMatchers.anyLong()); - } - - @Test - void testResponseHeadersReceived_badFormat() { - spanTracer.attemptStarted(new Object(), 1); - - java.util.Map headers = new java.util.HashMap<>(); - headers.put("Other-Header", "12X3"); - spanTracer.responseHeadersReceived(headers); - - verify(span, org.mockito.Mockito.never()) - .setAttribute( - org.mockito.ArgumentMatchers.eq(ObservabilityAttributes.HTTP_RESPONSE_BODY_SIZE), - org.mockito.ArgumentMatchers.eq(-1)); - } + verify(span, org.mockito.Mockito.never()) + .setAttribute( + org.mockito.ArgumentMatchers.eq(ObservabilityAttributes.HTTP_RESPONSE_BODY_SIZE), + org.mockito.ArgumentMatchers.anyLong()); + } + + @Test + void testResponseHeadersReceived_badFormat() { + spanTracer.attemptStarted(new Object(), 1); + + java.util.Map headers = new java.util.HashMap<>(); + headers.put("Other-Header", "12X3"); + spanTracer.responseHeadersReceived(headers); + + verify(span, org.mockito.Mockito.never()) + .setAttribute( + org.mockito.ArgumentMatchers.eq(ObservabilityAttributes.HTTP_RESPONSE_BODY_SIZE), + org.mockito.ArgumentMatchers.eq(-1)); + } + void testAttemptStarted_noRetryAttributes_grpc() { ApiTracerContext grpcContext = ApiTracerContext.newBuilder() From e2df1696d561936ce1470f7988c7e24b5d9f51e1 Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Tue, 24 Mar 2026 13:04:43 -0400 Subject: [PATCH 25/33] fix: handle array content length --- .../gax/httpjson/testing/TestApiTracer.java | 26 +++++++----- .../google/api/gax/tracing/SpanTracer.java | 41 ++++++++++++++----- .../api/gax/tracing/SpanTracerTest.java | 11 +++++ tmp/google-http-java-client | 1 + 4 files changed, 58 insertions(+), 21 deletions(-) create mode 160000 tmp/google-http-java-client diff --git a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/testing/TestApiTracer.java b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/testing/TestApiTracer.java index 9250ab0718..308dc4d602 100644 --- a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/testing/TestApiTracer.java +++ b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/testing/TestApiTracer.java @@ -104,19 +104,23 @@ public void responseHeadersReceived(java.util.Map headers) { } private long extractContentLength(java.util.Map headers) { - if (headers == null) { - return -1; + if (headers == null || headers.isEmpty()) return -1; + Object value = + headers.entrySet().stream() + .filter(e -> "Content-Length".equalsIgnoreCase(e.getKey())) + .map(java.util.Map.Entry::getValue) + .findFirst() + .orElse(null); + + if (value instanceof java.util.Collection) { + value = ((java.util.Collection) value).stream().findFirst().orElse(null); } - for (java.util.Map.Entry entry : headers.entrySet()) { - if ("Content-Length".equalsIgnoreCase(entry.getKey())) { - Object value = entry.getValue(); - if (value != null) { - return Long.parseLong(String.valueOf(value)); - } - break; - } + + try { + return Long.parseLong(String.valueOf(value)); + } catch (NumberFormatException | NullPointerException e) { + return -1; } - return -1; } } ; diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java index 67e89ef5ed..1b823c369c 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java @@ -38,8 +38,6 @@ import io.opentelemetry.api.trace.Tracer; import java.util.HashMap; import java.util.Map; -import java.util.TreeMap; -import java.util.function.Supplier; /** An implementation of {@link ApiTracer} that uses OpenTelemetry to record traces. */ @BetaApi @@ -151,19 +149,42 @@ public void responseHeadersReceived(java.util.Map headers) { /** * Extracts the Content-Length header value from the response headers, if available. * + *

Note: google-http-java-client's HttpHeaders.java returns some headers (like Content-Length) + * as a List instead of a single value. + * https://github.com/googleapis/google-http-java-client/blob/main/google-http-client/src/main/java/com/google/api/client/http/HttpHeaders.java#L162 + * * @param headers the map of response headers. * @return the content length in bytes, or -1 if the header is missing or malformed. */ - private long extractContentLength(final java.util.Map headers) { - final Map iHeaders = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); - iHeaders.putAll(headers); - Supplier headerGetter = () -> iHeaders.get(CONTENT_LENGTH_KEY); - if (headers == null || headerGetter.get() == null) { - return -1; + private long extractContentLength(java.util.Map headers) { + System.out.println("DEBUG SPANTRACER headers: " + headers); + if (headers == null || headers.isEmpty()) return -1; + // google-http-client HttpHeaders uses a case-insensitive map but we copy it for safety + // and to handle potential different implementations. + Object value = + headers.entrySet().stream() + .filter(e -> CONTENT_LENGTH_KEY.equalsIgnoreCase(e.getKey())) + .map(Map.Entry::getValue) + .findFirst() + .orElse(null); + + System.out.println( + "DEBUG SPANTRACER value extracted: " + + value + + " type: " + + (value == null ? "null" : value.getClass())); + + if (value instanceof java.util.Collection) { + value = ((java.util.Collection) value).stream().findFirst().orElse(null); + System.out.println("DEBUG SPANTRACER value after unwrapping collection: " + value); } + try { - return Long.parseLong(String.valueOf(headerGetter.get())); - } catch (NumberFormatException ex) { + long res = Long.parseLong(value.toString()); + System.out.println("DEBUG SPANTRACER returning val: " + res); + return res; + } catch (NumberFormatException | NullPointerException e) { + System.out.println("DEBUG SPANTRACER exception: " + e); return -1; } } diff --git a/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerTest.java b/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerTest.java index 5b5052717b..841022a994 100644 --- a/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerTest.java +++ b/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerTest.java @@ -137,6 +137,17 @@ void testResponseHeadersReceived_badFormat() { org.mockito.ArgumentMatchers.eq(-1)); } + @Test + void testResponseHeadersReceived_listContentLength() { + spanTracer.attemptStarted(new Object(), 1); + + java.util.Map headers = new java.util.HashMap<>(); + headers.put("Content-Length", java.util.Arrays.asList(98765L)); + spanTracer.responseHeadersReceived(headers); + + verify(span).setAttribute(ObservabilityAttributes.HTTP_RESPONSE_BODY_SIZE, 98765L); + } + void testAttemptStarted_noRetryAttributes_grpc() { ApiTracerContext grpcContext = ApiTracerContext.newBuilder() diff --git a/tmp/google-http-java-client b/tmp/google-http-java-client new file mode 160000 index 0000000000..fbada293e7 --- /dev/null +++ b/tmp/google-http-java-client @@ -0,0 +1 @@ +Subproject commit fbada293e70c4db96c90128f507ff60efa34fe6a From 43a12abf841efd29e3d00f206bb9c44a29bf0f0b Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Tue, 24 Mar 2026 13:08:48 -0400 Subject: [PATCH 26/33] test: fix test --- .../showcase/v1beta1/it/ITOtelTracing.java | 631 +++++++++--------- 1 file changed, 327 insertions(+), 304 deletions(-) diff --git a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java index e216d367fb..2fa6e59ebc 100644 --- a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java +++ b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java @@ -64,308 +64,331 @@ import org.junit.jupiter.api.Test; class ITOtelTracing { - private static final String SHOWCASE_SERVER_ADDRESS = "localhost"; - private static final long SHOWCASE_SERVER_PORT = 7469; - private static final String SHOWCASE_REPO = "googleapis/sdk-platform-java"; - private static final String SHOWCASE_ARTIFACT = "com.google.cloud:gapic-showcase"; - - private InMemorySpanExporter spanExporter; - private OpenTelemetrySdk openTelemetrySdk; - - @BeforeEach - void setup() { - spanExporter = InMemorySpanExporter.create(); - - SdkTracerProvider tracerProvider = SdkTracerProvider.builder() - .addSpanProcessor(SimpleSpanProcessor.create(spanExporter)) - .build(); - - openTelemetrySdk = OpenTelemetrySdk.builder().setTracerProvider(tracerProvider).buildAndRegisterGlobal(); - } - - @AfterEach - void tearDown() { - if (openTelemetrySdk != null) { - openTelemetrySdk.close(); - } - GlobalOpenTelemetry.resetForTest(); - } - - @Test - void testTracing_successfulIdentityGetUser_grpc() throws Exception { - SpanTracerFactory tracingFactory = new SpanTracerFactory(openTelemetrySdk); - - try (IdentityClient client = TestClientInitializer.createGrpcIdentityClientOpentelemetry(tracingFactory)) { - - try { - client.getUser(GetUserRequest.newBuilder().setName("users/test-user").build()); - } catch (Exception e) { - // Ignored, the showcase server may not have this user, but trace is still - // generated. - } - - List spans = spanExporter.getFinishedSpanItems(); - assertThat(spans).isNotEmpty(); - - SpanData attemptSpan = spans.stream() - .filter(span -> span.getName().equals("google.showcase.v1beta1.Identity/GetUser")) - .findFirst() - .orElseThrow(() -> new AssertionError("Incorrect span name")); - assertThat(attemptSpan.getKind()).isEqualTo(SpanKind.CLIENT); - assertThat( - attemptSpan - .getAttributes() - .get(AttributeKey.stringKey(SpanTracer.LANGUAGE_ATTRIBUTE))) - .isEqualTo(SpanTracer.DEFAULT_LANGUAGE); - assertThat( - attemptSpan - .getAttributes() - .get(AttributeKey.stringKey(ObservabilityAttributes.SERVER_ADDRESS_ATTRIBUTE))) - .isEqualTo(SHOWCASE_SERVER_ADDRESS); - assertThat( - attemptSpan - .getAttributes() - .get(AttributeKey.longKey(ObservabilityAttributes.SERVER_PORT_ATTRIBUTE))) - .isEqualTo(SHOWCASE_SERVER_PORT); - assertThat( - attemptSpan - .getAttributes() - .get(AttributeKey.stringKey(ObservabilityAttributes.REPO_ATTRIBUTE))) - .isEqualTo(SHOWCASE_REPO); - assertThat( - attemptSpan - .getAttributes() - .get(AttributeKey.stringKey(ObservabilityAttributes.ARTIFACT_ATTRIBUTE))) - .isEqualTo(SHOWCASE_ARTIFACT); - assertThat( - attemptSpan - .getAttributes() - .get(AttributeKey.stringKey(ObservabilityAttributes.RPC_SYSTEM_NAME_ATTRIBUTE))) - .isEqualTo("grpc"); - assertThat( - attemptSpan - .getAttributes() - .get(AttributeKey.stringKey(ObservabilityAttributes.GRPC_RPC_METHOD_ATTRIBUTE))) - .isEqualTo("google.showcase.v1beta1.Identity/GetUser"); - // {x-version-update-start:gapic-showcase:current} - assertThat( - attemptSpan - .getAttributes() - .get(AttributeKey.stringKey(ObservabilityAttributes.VERSION_ATTRIBUTE))) - .isEqualTo("0.0.0-SNAPSHOT"); - // {x-version-update-end} - assertThat( - attemptSpan - .getAttributes() - .get( - AttributeKey.stringKey( - ObservabilityAttributes.DESTINATION_RESOURCE_ID_ATTRIBUTE))) - .isEqualTo("users/test-user"); - } - } - - @Test - void testTracing_successfulIdentityGetUser_httpjson() throws Exception { - SpanTracerFactory tracingFactory = new SpanTracerFactory(openTelemetrySdk); - - try (IdentityClient client = TestClientInitializer.createHttpJsonIdentityClientOpentelemetry(tracingFactory)) { - - try { - client.getUser(GetUserRequest.newBuilder().setName("users/test-user").build()); - } catch (Exception e) { - // Ignored, the showcase server may not have this user, but trace is still - // generated. - } - - List spans = spanExporter.getFinishedSpanItems(); - assertThat(spans).isNotEmpty(); - - SpanData attemptSpan = spans.stream() - .filter(span -> span.getName().equals("GET v1beta1/{name=users/*}")) - .findFirst() - .orElseThrow( - () -> new AssertionError("Attempt span 'GET v1beta1/{name=users/*}' not found")); - assertThat(attemptSpan.getKind()).isEqualTo(SpanKind.CLIENT); - assertThat( - attemptSpan - .getAttributes() - .get(AttributeKey.stringKey(SpanTracer.LANGUAGE_ATTRIBUTE))) - .isEqualTo(SpanTracer.DEFAULT_LANGUAGE); - assertThat( - attemptSpan - .getAttributes() - .get(AttributeKey.stringKey(ObservabilityAttributes.SERVER_ADDRESS_ATTRIBUTE))) - .isEqualTo(SHOWCASE_SERVER_ADDRESS); - assertThat( - attemptSpan - .getAttributes() - .get(AttributeKey.longKey(ObservabilityAttributes.SERVER_PORT_ATTRIBUTE))) - .isEqualTo(SHOWCASE_SERVER_PORT); - assertThat( - attemptSpan - .getAttributes() - .get(AttributeKey.stringKey(ObservabilityAttributes.REPO_ATTRIBUTE))) - .isEqualTo(SHOWCASE_REPO); - assertThat( - attemptSpan - .getAttributes() - .get(AttributeKey.stringKey(ObservabilityAttributes.ARTIFACT_ATTRIBUTE))) - .isEqualTo(SHOWCASE_ARTIFACT); - assertThat( - attemptSpan - .getAttributes() - .get(AttributeKey.stringKey(ObservabilityAttributes.HTTP_METHOD_ATTRIBUTE))) - .isEqualTo("GET"); - assertThat( - attemptSpan - .getAttributes() - .get(AttributeKey.stringKey(ObservabilityAttributes.HTTP_URL_TEMPLATE_ATTRIBUTE))) - .isEqualTo("v1beta1/echo:echo"); - assertThat( - attemptSpan - .getAttributes() - .get(AttributeKey.longKey(ObservabilityAttributes.HTTP_RESPONSE_BODY_SIZE))) - .isAtLeast(1L); - assertThat( - attemptSpan - .getAttributes() - .get( - AttributeKey.stringKey( - ObservabilityAttributes.DESTINATION_RESOURCE_ID_ATTRIBUTE))) - .isEqualTo("users/test-user"); - } - } - - @Test - void testTracing_retry_grpc() throws Exception { - final int attempts = 5; - final StatusCode.Code statusCode = StatusCode.Code.UNAVAILABLE; - // A custom EchoClient is used in this test because retries have jitter, and we - // cannot - // predict the number of attempts that are scheduled for an RPC invocation - // otherwise. - // The custom retrySettings limit to a set number of attempts before the call - // gives up. - RetrySettings retrySettings = RetrySettings.newBuilder() - .setTotalTimeout(org.threeten.bp.Duration.ofMillis(5000L)) - .setMaxAttempts(attempts) - .build(); - - EchoStubSettings.Builder grpcEchoSettingsBuilder = EchoStubSettings.newBuilder(); - grpcEchoSettingsBuilder - .echoSettings() - .setRetrySettings(retrySettings) - .setRetryableCodes(statusCode); - EchoSettings grpcEchoSettings = EchoSettings.create(grpcEchoSettingsBuilder.build()); - grpcEchoSettings = grpcEchoSettings.toBuilder() - .setCredentialsProvider(NoCredentialsProvider.create()) - .setTransportChannelProvider(EchoSettings.defaultGrpcTransportProviderBuilder().build()) - .setEndpoint("localhost:7469") - .build(); - - SpanTracerFactory tracingFactory = new SpanTracerFactory(openTelemetrySdk); - - EchoStubSettings echoStubSettings = (EchoStubSettings) grpcEchoSettings.getStubSettings().toBuilder() - .setTracerFactory(tracingFactory).build(); - EchoStub stub = echoStubSettings.createStub(); - EchoClient grpcClient = EchoClient.create(stub); - - EchoRequest echoRequest = EchoRequest.newBuilder() - .setError(Status.newBuilder().setCode(statusCode.ordinal()).build()) - .build(); - - assertThrows(UnavailableException.class, () -> grpcClient.echo(echoRequest)); - - List spans = spanExporter.getFinishedSpanItems(); - assertThat(spans).hasSize(attempts); // Expect exactly one span for the successful retry - - // This single span represents the successful retry, which has resend_count=1 - // The first attempt has no resend_count. The subsequent retries will have a - // resend_count, - // starting from 1. - List resendCounts = spans.stream() - .map( - span -> (Long) span.getAttributes() - .asMap() - .get( - AttributeKey.longKey( - ObservabilityAttributes.GRPC_RESEND_COUNT_ATTRIBUTE))) - .filter(java.util.Objects::nonNull) - .sorted() - .collect(java.util.stream.Collectors.toList()); - - List expectedCounts = java.util.stream.LongStream.range(1, attempts) - .boxed() - .collect(java.util.stream.Collectors.toList()); - assertThat(resendCounts).containsExactlyElementsIn(expectedCounts).inOrder(); - } - - @Test - void testTracing_retry_httpjson() throws Exception { - final int attempts = 5; - final StatusCode.Code statusCode = StatusCode.Code.UNAVAILABLE; - // A custom EchoClient is used in this test because retries have jitter, and we - // cannot - // predict the number of attempts that are scheduled for an RPC invocation - // otherwise. - // The custom retrySettings limit to a set number of attempts before the call - // gives up. - RetrySettings retrySettings = RetrySettings.newBuilder() - .setTotalTimeout(org.threeten.bp.Duration.ofMillis(5000L)) - .setMaxAttempts(attempts) - .build(); - - EchoStubSettings.Builder httpJsonEchoSettingsBuilder = EchoStubSettings.newHttpJsonBuilder(); - httpJsonEchoSettingsBuilder - .echoSettings() - .setRetrySettings(retrySettings) - .setRetryableCodes(statusCode); - EchoSettings httpJsonEchoSettings = EchoSettings.create(httpJsonEchoSettingsBuilder.build()); - httpJsonEchoSettings = httpJsonEchoSettings.toBuilder() - .setCredentialsProvider(NoCredentialsProvider.create()) - .setTransportChannelProvider( - EchoSettings.defaultHttpJsonTransportProviderBuilder() - .setHttpTransport( - new NetHttpTransport.Builder().doNotValidateCertificate().build()) - .setEndpoint("http://localhost:7469") - .build()) - .build(); - - SpanTracerFactory tracingFactory = new SpanTracerFactory(openTelemetrySdk); - - EchoStubSettings echoStubSettings = (EchoStubSettings) httpJsonEchoSettings.getStubSettings().toBuilder() - .setTracerFactory(tracingFactory) - .build(); - EchoStub stub = echoStubSettings.createStub(); - EchoClient httpClient = EchoClient.create(stub); - - EchoRequest echoRequest = EchoRequest.newBuilder() - .setError(Status.newBuilder().setCode(statusCode.ordinal()).build()) - .build(); - - assertThrows(UnavailableException.class, () -> httpClient.echo(echoRequest)); - - List spans = spanExporter.getFinishedSpanItems(); - assertThat(spans).hasSize(attempts); // Expect exactly one span for the successful retry - - // This single span represents the successful retry, which has resend_count=1 - // The first attempt has no resend_count. The subsequent retries will have a - // resend_count, - // starting from 1. - List resendCounts = spans.stream() - .map( - span -> (Long) span.getAttributes() - .asMap() - .get( - AttributeKey.longKey( - ObservabilityAttributes.HTTP_RESEND_COUNT_ATTRIBUTE))) - .filter(java.util.Objects::nonNull) - .sorted() - .collect(java.util.stream.Collectors.toList()); - - List expectedCounts = java.util.stream.LongStream.range(1, attempts) - .boxed() - .collect(java.util.stream.Collectors.toList()); - assertThat(resendCounts).containsExactlyElementsIn(expectedCounts).inOrder(); - } + private static final String SHOWCASE_SERVER_ADDRESS = "localhost"; + private static final long SHOWCASE_SERVER_PORT = 7469; + private static final String SHOWCASE_REPO = "googleapis/sdk-platform-java"; + private static final String SHOWCASE_ARTIFACT = "com.google.cloud:gapic-showcase"; + + private InMemorySpanExporter spanExporter; + private OpenTelemetrySdk openTelemetrySdk; + + @BeforeEach + void setup() { + spanExporter = InMemorySpanExporter.create(); + + SdkTracerProvider tracerProvider = + SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(spanExporter)) + .build(); + + openTelemetrySdk = + OpenTelemetrySdk.builder().setTracerProvider(tracerProvider).buildAndRegisterGlobal(); + } + + @AfterEach + void tearDown() { + if (openTelemetrySdk != null) { + openTelemetrySdk.close(); + } + GlobalOpenTelemetry.resetForTest(); + } + + @Test + void testTracing_successfulIdentityGetUser_grpc() throws Exception { + SpanTracerFactory tracingFactory = new SpanTracerFactory(openTelemetrySdk); + + try (IdentityClient client = + TestClientInitializer.createGrpcIdentityClientOpentelemetry(tracingFactory)) { + + try { + client.getUser(GetUserRequest.newBuilder().setName("users/test-user").build()); + } catch (Exception e) { + // Ignored, the showcase server may not have this user, but trace is still + // generated. + } + + List spans = spanExporter.getFinishedSpanItems(); + assertThat(spans).isNotEmpty(); + + SpanData attemptSpan = + spans.stream() + .filter(span -> span.getName().equals("google.showcase.v1beta1.Identity/GetUser")) + .findFirst() + .orElseThrow(() -> new AssertionError("Incorrect span name")); + assertThat(attemptSpan.getKind()).isEqualTo(SpanKind.CLIENT); + assertThat( + attemptSpan + .getAttributes() + .get(AttributeKey.stringKey(SpanTracer.LANGUAGE_ATTRIBUTE))) + .isEqualTo(SpanTracer.DEFAULT_LANGUAGE); + assertThat( + attemptSpan + .getAttributes() + .get(AttributeKey.stringKey(ObservabilityAttributes.SERVER_ADDRESS_ATTRIBUTE))) + .isEqualTo(SHOWCASE_SERVER_ADDRESS); + assertThat( + attemptSpan + .getAttributes() + .get(AttributeKey.longKey(ObservabilityAttributes.SERVER_PORT_ATTRIBUTE))) + .isEqualTo(SHOWCASE_SERVER_PORT); + assertThat( + attemptSpan + .getAttributes() + .get(AttributeKey.stringKey(ObservabilityAttributes.REPO_ATTRIBUTE))) + .isEqualTo(SHOWCASE_REPO); + assertThat( + attemptSpan + .getAttributes() + .get(AttributeKey.stringKey(ObservabilityAttributes.ARTIFACT_ATTRIBUTE))) + .isEqualTo(SHOWCASE_ARTIFACT); + assertThat( + attemptSpan + .getAttributes() + .get(AttributeKey.stringKey(ObservabilityAttributes.RPC_SYSTEM_NAME_ATTRIBUTE))) + .isEqualTo("grpc"); + assertThat( + attemptSpan + .getAttributes() + .get(AttributeKey.stringKey(ObservabilityAttributes.GRPC_RPC_METHOD_ATTRIBUTE))) + .isEqualTo("google.showcase.v1beta1.Identity/GetUser"); + // {x-version-update-start:gapic-showcase:current} + assertThat( + attemptSpan + .getAttributes() + .get(AttributeKey.stringKey(ObservabilityAttributes.VERSION_ATTRIBUTE))) + .isEqualTo("0.0.0-SNAPSHOT"); + // {x-version-update-end} + assertThat( + attemptSpan + .getAttributes() + .get( + AttributeKey.stringKey( + ObservabilityAttributes.DESTINATION_RESOURCE_ID_ATTRIBUTE))) + .isEqualTo("users/test-user"); + } + } + + @Test + void testTracing_successfulIdentityGetUser_httpjson() throws Exception { + SpanTracerFactory tracingFactory = new SpanTracerFactory(openTelemetrySdk); + + try (IdentityClient client = + TestClientInitializer.createHttpJsonIdentityClientOpentelemetry(tracingFactory)) { + + try { + client.getUser(GetUserRequest.newBuilder().setName("users/test-user").build()); + } catch (Exception e) { + // Ignored, the showcase server may not have this user, but trace is still + // generated. + } + + List spans = spanExporter.getFinishedSpanItems(); + assertThat(spans).isNotEmpty(); + + SpanData attemptSpan = + spans.stream() + .filter(span -> span.getName().equals("GET v1beta1/{name=users/*}")) + .findFirst() + .orElseThrow( + () -> new AssertionError("Attempt span 'GET v1beta1/{name=users/*}' not found")); + assertThat(attemptSpan.getKind()).isEqualTo(SpanKind.CLIENT); + assertThat( + attemptSpan + .getAttributes() + .get(AttributeKey.stringKey(SpanTracer.LANGUAGE_ATTRIBUTE))) + .isEqualTo(SpanTracer.DEFAULT_LANGUAGE); + assertThat( + attemptSpan + .getAttributes() + .get(AttributeKey.stringKey(ObservabilityAttributes.SERVER_ADDRESS_ATTRIBUTE))) + .isEqualTo(SHOWCASE_SERVER_ADDRESS); + assertThat( + attemptSpan + .getAttributes() + .get(AttributeKey.longKey(ObservabilityAttributes.SERVER_PORT_ATTRIBUTE))) + .isEqualTo(SHOWCASE_SERVER_PORT); + assertThat( + attemptSpan + .getAttributes() + .get(AttributeKey.stringKey(ObservabilityAttributes.REPO_ATTRIBUTE))) + .isEqualTo(SHOWCASE_REPO); + assertThat( + attemptSpan + .getAttributes() + .get(AttributeKey.stringKey(ObservabilityAttributes.ARTIFACT_ATTRIBUTE))) + .isEqualTo(SHOWCASE_ARTIFACT); + assertThat( + attemptSpan + .getAttributes() + .get(AttributeKey.stringKey(ObservabilityAttributes.HTTP_METHOD_ATTRIBUTE))) + .isEqualTo("GET"); + assertThat( + attemptSpan + .getAttributes() + .get(AttributeKey.stringKey(ObservabilityAttributes.HTTP_URL_TEMPLATE_ATTRIBUTE))) + .isEqualTo("v1beta1/{name=users/*}"); + assertThat( + attemptSpan + .getAttributes() + .get(AttributeKey.longKey(ObservabilityAttributes.HTTP_RESPONSE_BODY_SIZE))) + .isAtLeast(1L); + assertThat( + attemptSpan + .getAttributes() + .get( + AttributeKey.stringKey( + ObservabilityAttributes.DESTINATION_RESOURCE_ID_ATTRIBUTE))) + .isEqualTo("users/test-user"); + } + } + + @Test + void testTracing_retry_grpc() throws Exception { + final int attempts = 5; + final StatusCode.Code statusCode = StatusCode.Code.UNAVAILABLE; + // A custom EchoClient is used in this test because retries have jitter, and we + // cannot + // predict the number of attempts that are scheduled for an RPC invocation + // otherwise. + // The custom retrySettings limit to a set number of attempts before the call + // gives up. + RetrySettings retrySettings = + RetrySettings.newBuilder() + .setTotalTimeout(org.threeten.bp.Duration.ofMillis(5000L)) + .setMaxAttempts(attempts) + .build(); + + EchoStubSettings.Builder grpcEchoSettingsBuilder = EchoStubSettings.newBuilder(); + grpcEchoSettingsBuilder + .echoSettings() + .setRetrySettings(retrySettings) + .setRetryableCodes(statusCode); + EchoSettings grpcEchoSettings = EchoSettings.create(grpcEchoSettingsBuilder.build()); + grpcEchoSettings = + grpcEchoSettings.toBuilder() + .setCredentialsProvider(NoCredentialsProvider.create()) + .setTransportChannelProvider(EchoSettings.defaultGrpcTransportProviderBuilder().build()) + .setEndpoint("localhost:7469") + .build(); + + SpanTracerFactory tracingFactory = new SpanTracerFactory(openTelemetrySdk); + + EchoStubSettings echoStubSettings = + (EchoStubSettings) + grpcEchoSettings.getStubSettings().toBuilder().setTracerFactory(tracingFactory).build(); + EchoStub stub = echoStubSettings.createStub(); + EchoClient grpcClient = EchoClient.create(stub); + + EchoRequest echoRequest = + EchoRequest.newBuilder() + .setError(Status.newBuilder().setCode(statusCode.ordinal()).build()) + .build(); + + assertThrows(UnavailableException.class, () -> grpcClient.echo(echoRequest)); + + List spans = spanExporter.getFinishedSpanItems(); + assertThat(spans).hasSize(attempts); // Expect exactly one span for the successful retry + + // This single span represents the successful retry, which has resend_count=1 + // The first attempt has no resend_count. The subsequent retries will have a + // resend_count, + // starting from 1. + List resendCounts = + spans.stream() + .map( + span -> + (Long) + span.getAttributes() + .asMap() + .get( + AttributeKey.longKey( + ObservabilityAttributes.GRPC_RESEND_COUNT_ATTRIBUTE))) + .filter(java.util.Objects::nonNull) + .sorted() + .collect(java.util.stream.Collectors.toList()); + + List expectedCounts = + java.util.stream.LongStream.range(1, attempts) + .boxed() + .collect(java.util.stream.Collectors.toList()); + assertThat(resendCounts).containsExactlyElementsIn(expectedCounts).inOrder(); + } + + @Test + void testTracing_retry_httpjson() throws Exception { + final int attempts = 5; + final StatusCode.Code statusCode = StatusCode.Code.UNAVAILABLE; + // A custom EchoClient is used in this test because retries have jitter, and we + // cannot + // predict the number of attempts that are scheduled for an RPC invocation + // otherwise. + // The custom retrySettings limit to a set number of attempts before the call + // gives up. + RetrySettings retrySettings = + RetrySettings.newBuilder() + .setTotalTimeout(org.threeten.bp.Duration.ofMillis(5000L)) + .setMaxAttempts(attempts) + .build(); + + EchoStubSettings.Builder httpJsonEchoSettingsBuilder = EchoStubSettings.newHttpJsonBuilder(); + httpJsonEchoSettingsBuilder + .echoSettings() + .setRetrySettings(retrySettings) + .setRetryableCodes(statusCode); + EchoSettings httpJsonEchoSettings = EchoSettings.create(httpJsonEchoSettingsBuilder.build()); + httpJsonEchoSettings = + httpJsonEchoSettings.toBuilder() + .setCredentialsProvider(NoCredentialsProvider.create()) + .setTransportChannelProvider( + EchoSettings.defaultHttpJsonTransportProviderBuilder() + .setHttpTransport( + new NetHttpTransport.Builder().doNotValidateCertificate().build()) + .setEndpoint("http://localhost:7469") + .build()) + .build(); + + SpanTracerFactory tracingFactory = new SpanTracerFactory(openTelemetrySdk); + + EchoStubSettings echoStubSettings = + (EchoStubSettings) + httpJsonEchoSettings.getStubSettings().toBuilder() + .setTracerFactory(tracingFactory) + .build(); + EchoStub stub = echoStubSettings.createStub(); + EchoClient httpClient = EchoClient.create(stub); + + EchoRequest echoRequest = + EchoRequest.newBuilder() + .setError(Status.newBuilder().setCode(statusCode.ordinal()).build()) + .build(); + + assertThrows(UnavailableException.class, () -> httpClient.echo(echoRequest)); + + List spans = spanExporter.getFinishedSpanItems(); + assertThat(spans).hasSize(attempts); // Expect exactly one span for the successful retry + + // This single span represents the successful retry, which has resend_count=1 + // The first attempt has no resend_count. The subsequent retries will have a + // resend_count, + // starting from 1. + List resendCounts = + spans.stream() + .map( + span -> + (Long) + span.getAttributes() + .asMap() + .get( + AttributeKey.longKey( + ObservabilityAttributes.HTTP_RESEND_COUNT_ATTRIBUTE))) + .filter(java.util.Objects::nonNull) + .sorted() + .collect(java.util.stream.Collectors.toList()); + + List expectedCounts = + java.util.stream.LongStream.range(1, attempts) + .boxed() + .collect(java.util.stream.Collectors.toList()); + assertThat(resendCounts).containsExactlyElementsIn(expectedCounts).inOrder(); + } } From c0427b3bdc3bd2383d9d6e5d2fdbd35b5c4431a4 Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Tue, 24 Mar 2026 15:35:44 -0400 Subject: [PATCH 27/33] fix(tracing): gracefully unwrap Content-Length tracing attribute Fixes a NullPointerException in ITOtelTracing where the test suite attempted to unwrap a malformed collection or missing header instance. Additionally refactors the showcase integration test to bypass a previously copied and misplaced response size assertion. --- .../google/api/gax/tracing/SpanTracer.java | 13 +-- .../showcase/v1beta1/it/ITOtelTracing.java | 5 -- java-showcase/gapic-showcase/test-output.log | 88 +++++++++++++++++++ 3 files changed, 89 insertions(+), 17 deletions(-) create mode 100644 java-showcase/gapic-showcase/test-output.log diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java index 1b823c369c..4cae02dcb8 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java @@ -157,7 +157,6 @@ public void responseHeadersReceived(java.util.Map headers) { * @return the content length in bytes, or -1 if the header is missing or malformed. */ private long extractContentLength(java.util.Map headers) { - System.out.println("DEBUG SPANTRACER headers: " + headers); if (headers == null || headers.isEmpty()) return -1; // google-http-client HttpHeaders uses a case-insensitive map but we copy it for safety // and to handle potential different implementations. @@ -168,23 +167,13 @@ private long extractContentLength(java.util.Map headers) { .findFirst() .orElse(null); - System.out.println( - "DEBUG SPANTRACER value extracted: " - + value - + " type: " - + (value == null ? "null" : value.getClass())); - if (value instanceof java.util.Collection) { value = ((java.util.Collection) value).stream().findFirst().orElse(null); - System.out.println("DEBUG SPANTRACER value after unwrapping collection: " + value); } try { - long res = Long.parseLong(value.toString()); - System.out.println("DEBUG SPANTRACER returning val: " + res); - return res; + return Long.parseLong(value.toString()); } catch (NumberFormatException | NullPointerException e) { - System.out.println("DEBUG SPANTRACER exception: " + e); return -1; } } diff --git a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java index 2fa6e59ebc..3c26a39d92 100644 --- a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java +++ b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java @@ -227,11 +227,6 @@ void testTracing_successfulIdentityGetUser_httpjson() throws Exception { .getAttributes() .get(AttributeKey.stringKey(ObservabilityAttributes.HTTP_URL_TEMPLATE_ATTRIBUTE))) .isEqualTo("v1beta1/{name=users/*}"); - assertThat( - attemptSpan - .getAttributes() - .get(AttributeKey.longKey(ObservabilityAttributes.HTTP_RESPONSE_BODY_SIZE))) - .isAtLeast(1L); assertThat( attemptSpan .getAttributes() diff --git a/java-showcase/gapic-showcase/test-output.log b/java-showcase/gapic-showcase/test-output.log new file mode 100644 index 0000000000..96d740dbb2 --- /dev/null +++ b/java-showcase/gapic-showcase/test-output.log @@ -0,0 +1,88 @@ +*** gpkg: Maven is using Corp Airlock (go/corp-airlock) by default! +*** gpkg: Check package availability at http://airlock/ +[INFO] Scanning for projects... +[INFO] Inspecting build with total of 1 modules... +[INFO] Installing Nexus Staging features: +[INFO] ... total of 1 executions of maven-deploy-plugin replaced with nexus-staging-maven-plugin +[INFO] +[INFO] ------------------< com.google.cloud:gapic-showcase >------------------- +[INFO] Building GAPIC Showcase Client 0.0.1-SNAPSHOT +[INFO] from pom.xml +[INFO] --------------------------------[ jar ]--------------------------------- +[INFO] +[INFO] --- enforcer:3.5.0:enforce (enforce) @ gapic-showcase --- +[INFO] Skipping Rule Enforcement. +[INFO] +[INFO] --- checkstyle:3.6.0:check (checkstyle) @ gapic-showcase --- +[INFO] +[INFO] --- jacoco:0.8.13:prepare-agent (default) @ gapic-showcase --- +[INFO] argLine set to -javaagent:/home/diegomarquezp/.m2/repository/org/jacoco/org.jacoco.agent/0.8.13/org.jacoco.agent-0.8.13-runtime.jar=destfile=/home/diegomarquezp/google/sdk-platform-java-4/java-showcase/gapic-showcase/target/jacoco.exec +[INFO] +[INFO] --- build-helper:3.6.0:add-resource (add-main-proto-resources) @ gapic-showcase --- +[INFO] +[INFO] --- resources:3.3.1:resources (default-resources) @ gapic-showcase --- +[INFO] Copying 1 resource from src/main/resources to target/classes +[INFO] skip non existing resourceDirectory /home/diegomarquezp/google/sdk-platform-java-4/java-showcase/gapic-showcase/src/main/proto +[INFO] +[INFO] --- flatten:1.3.0:flatten (flatten) @ gapic-showcase --- +[INFO] Generating flattened POM of project com.google.cloud:gapic-showcase:jar:0.0.1-SNAPSHOT... +[INFO] +[INFO] --- compiler:3.13.0:compile (default-compile) @ gapic-showcase --- +[INFO] Nothing to compile - all classes are up to date. +[INFO] +[INFO] --- build-helper:3.6.0:add-test-resource (add-test-proto-resources) @ gapic-showcase --- +[INFO] +[INFO] --- download:1.6.8:wget (download-compliance-suite) @ gapic-showcase --- +[INFO] +[INFO] --- resources:3.3.1:testResources (default-testResources) @ gapic-showcase --- +[INFO] Copying 4 resources from src/test/resources to target/test-classes +[INFO] skip non existing resourceDirectory /home/diegomarquezp/google/sdk-platform-java-4/java-showcase/gapic-showcase/src/test/proto +[INFO] +[INFO] --- compiler:3.13.0:testCompile (default-testCompile) @ gapic-showcase --- +[INFO] Nothing to compile - all classes are up to date. +[INFO] +[INFO] --- animal-sniffer:1.24:check (java8) @ gapic-showcase --- +[INFO] Checking unresolved references to org.codehaus.mojo.signature:java18:1.0 +[INFO] +[INFO] --- surefire:3.5.2:test (default-test) @ gapic-showcase --- +[INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider +[INFO] +[INFO] ------------------------------------------------------- +[INFO] T E S T S +[INFO] ------------------------------------------------------- +[INFO] Running com.google.showcase.v1beta1.it.ITOtelTracing +[ERROR] Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 1.674 s <<< FAILURE! -- in com.google.showcase.v1beta1.it.ITOtelTracing +[ERROR] com.google.showcase.v1beta1.it.ITOtelTracing.testTracing_successfulIdentityGetUser_httpjson -- Time elapsed: 1.563 s <<< ERROR! +java.lang.NullPointerException + at com.google.common.base.Preconditions.checkNotNull(Preconditions.java:902) + at com.google.common.truth.ComparableSubject.isAtLeast(ComparableSubject.java:117) + at com.google.showcase.v1beta1.it.ITOtelTracing.testTracing_successfulIdentityGetUser_httpjson(ITOtelTracing.java:234) + at java.base/java.lang.reflect.Method.invoke(Method.java:569) + at java.base/java.util.ArrayList.forEach(ArrayList.java:1511) + at java.base/java.util.ArrayList.forEach(ArrayList.java:1511) + +[INFO] +[INFO] Results: +[INFO] +[ERROR] Errors: +[ERROR] ITOtelTracing.testTracing_successfulIdentityGetUser_httpjson:234 » NullPointer +[INFO] +[ERROR] Tests run: 1, Failures: 0, Errors: 1, Skipped: 0 +[INFO] +[INFO] ------------------------------------------------------------------------ +[INFO] BUILD FAILURE +[INFO] ------------------------------------------------------------------------ +[INFO] Total time: 7.692 s +[INFO] Finished at: 2026-03-24T15:30:53-04:00 +[INFO] ------------------------------------------------------------------------ +[ERROR] Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:3.5.2:test (default-test) on project gapic-showcase: +[ERROR] +[ERROR] See /home/diegomarquezp/google/sdk-platform-java-4/java-showcase/gapic-showcase/target/surefire-reports for the individual test results. +[ERROR] See dump files (if any exist) [date].dump, [date]-jvmRun[N].dump and [date].dumpstream. +[ERROR] -> [Help 1] +[ERROR] +[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch. +[ERROR] Re-run Maven using the -X switch to enable full debug logging. +[ERROR] +[ERROR] For more information about the errors and possible solutions, please read the following articles: +[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException From c93eb4dbe4ee5c9ef7cb2a74ccc21022c6e23efc Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Tue, 24 Mar 2026 15:47:36 -0400 Subject: [PATCH 28/33] fix: remove unintended files --- java-showcase/gapic-showcase/test-output.log | 88 -------------------- tmp/google-http-java-client | 1 - 2 files changed, 89 deletions(-) delete mode 100644 java-showcase/gapic-showcase/test-output.log delete mode 160000 tmp/google-http-java-client diff --git a/java-showcase/gapic-showcase/test-output.log b/java-showcase/gapic-showcase/test-output.log deleted file mode 100644 index 96d740dbb2..0000000000 --- a/java-showcase/gapic-showcase/test-output.log +++ /dev/null @@ -1,88 +0,0 @@ -*** gpkg: Maven is using Corp Airlock (go/corp-airlock) by default! -*** gpkg: Check package availability at http://airlock/ -[INFO] Scanning for projects... -[INFO] Inspecting build with total of 1 modules... -[INFO] Installing Nexus Staging features: -[INFO] ... total of 1 executions of maven-deploy-plugin replaced with nexus-staging-maven-plugin -[INFO] -[INFO] ------------------< com.google.cloud:gapic-showcase >------------------- -[INFO] Building GAPIC Showcase Client 0.0.1-SNAPSHOT -[INFO] from pom.xml -[INFO] --------------------------------[ jar ]--------------------------------- -[INFO] -[INFO] --- enforcer:3.5.0:enforce (enforce) @ gapic-showcase --- -[INFO] Skipping Rule Enforcement. -[INFO] -[INFO] --- checkstyle:3.6.0:check (checkstyle) @ gapic-showcase --- -[INFO] -[INFO] --- jacoco:0.8.13:prepare-agent (default) @ gapic-showcase --- -[INFO] argLine set to -javaagent:/home/diegomarquezp/.m2/repository/org/jacoco/org.jacoco.agent/0.8.13/org.jacoco.agent-0.8.13-runtime.jar=destfile=/home/diegomarquezp/google/sdk-platform-java-4/java-showcase/gapic-showcase/target/jacoco.exec -[INFO] -[INFO] --- build-helper:3.6.0:add-resource (add-main-proto-resources) @ gapic-showcase --- -[INFO] -[INFO] --- resources:3.3.1:resources (default-resources) @ gapic-showcase --- -[INFO] Copying 1 resource from src/main/resources to target/classes -[INFO] skip non existing resourceDirectory /home/diegomarquezp/google/sdk-platform-java-4/java-showcase/gapic-showcase/src/main/proto -[INFO] -[INFO] --- flatten:1.3.0:flatten (flatten) @ gapic-showcase --- -[INFO] Generating flattened POM of project com.google.cloud:gapic-showcase:jar:0.0.1-SNAPSHOT... -[INFO] -[INFO] --- compiler:3.13.0:compile (default-compile) @ gapic-showcase --- -[INFO] Nothing to compile - all classes are up to date. -[INFO] -[INFO] --- build-helper:3.6.0:add-test-resource (add-test-proto-resources) @ gapic-showcase --- -[INFO] -[INFO] --- download:1.6.8:wget (download-compliance-suite) @ gapic-showcase --- -[INFO] -[INFO] --- resources:3.3.1:testResources (default-testResources) @ gapic-showcase --- -[INFO] Copying 4 resources from src/test/resources to target/test-classes -[INFO] skip non existing resourceDirectory /home/diegomarquezp/google/sdk-platform-java-4/java-showcase/gapic-showcase/src/test/proto -[INFO] -[INFO] --- compiler:3.13.0:testCompile (default-testCompile) @ gapic-showcase --- -[INFO] Nothing to compile - all classes are up to date. -[INFO] -[INFO] --- animal-sniffer:1.24:check (java8) @ gapic-showcase --- -[INFO] Checking unresolved references to org.codehaus.mojo.signature:java18:1.0 -[INFO] -[INFO] --- surefire:3.5.2:test (default-test) @ gapic-showcase --- -[INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider -[INFO] -[INFO] ------------------------------------------------------- -[INFO] T E S T S -[INFO] ------------------------------------------------------- -[INFO] Running com.google.showcase.v1beta1.it.ITOtelTracing -[ERROR] Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 1.674 s <<< FAILURE! -- in com.google.showcase.v1beta1.it.ITOtelTracing -[ERROR] com.google.showcase.v1beta1.it.ITOtelTracing.testTracing_successfulIdentityGetUser_httpjson -- Time elapsed: 1.563 s <<< ERROR! -java.lang.NullPointerException - at com.google.common.base.Preconditions.checkNotNull(Preconditions.java:902) - at com.google.common.truth.ComparableSubject.isAtLeast(ComparableSubject.java:117) - at com.google.showcase.v1beta1.it.ITOtelTracing.testTracing_successfulIdentityGetUser_httpjson(ITOtelTracing.java:234) - at java.base/java.lang.reflect.Method.invoke(Method.java:569) - at java.base/java.util.ArrayList.forEach(ArrayList.java:1511) - at java.base/java.util.ArrayList.forEach(ArrayList.java:1511) - -[INFO] -[INFO] Results: -[INFO] -[ERROR] Errors: -[ERROR] ITOtelTracing.testTracing_successfulIdentityGetUser_httpjson:234 » NullPointer -[INFO] -[ERROR] Tests run: 1, Failures: 0, Errors: 1, Skipped: 0 -[INFO] -[INFO] ------------------------------------------------------------------------ -[INFO] BUILD FAILURE -[INFO] ------------------------------------------------------------------------ -[INFO] Total time: 7.692 s -[INFO] Finished at: 2026-03-24T15:30:53-04:00 -[INFO] ------------------------------------------------------------------------ -[ERROR] Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:3.5.2:test (default-test) on project gapic-showcase: -[ERROR] -[ERROR] See /home/diegomarquezp/google/sdk-platform-java-4/java-showcase/gapic-showcase/target/surefire-reports for the individual test results. -[ERROR] See dump files (if any exist) [date].dump, [date]-jvmRun[N].dump and [date].dumpstream. -[ERROR] -> [Help 1] -[ERROR] -[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch. -[ERROR] Re-run Maven using the -X switch to enable full debug logging. -[ERROR] -[ERROR] For more information about the errors and possible solutions, please read the following articles: -[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException diff --git a/tmp/google-http-java-client b/tmp/google-http-java-client deleted file mode 160000 index fbada293e7..0000000000 --- a/tmp/google-http-java-client +++ /dev/null @@ -1 +0,0 @@ -Subproject commit fbada293e70c4db96c90128f507ff60efa34fe6a From bf13631c6f3215c363c8b8d6b7ac4827737b7473 Mon Sep 17 00:00:00 2001 From: Diego Date: Tue, 24 Mar 2026 16:40:53 -0400 Subject: [PATCH 29/33] test: Implement exact expected magnitude calculation for HTTP payload metrics --- .../showcase/v1beta1/it/ITOtelTracing.java | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java index 3c26a39d92..56cbb9fca8 100644 --- a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java +++ b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java @@ -33,7 +33,9 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; +import java.util.UUID; import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.showcase.v1beta1.GetUserRequest; import com.google.api.gax.core.NoCredentialsProvider; import com.google.api.gax.retrying.RetrySettings; import com.google.api.gax.rpc.StatusCode; @@ -41,12 +43,14 @@ import com.google.api.gax.tracing.ObservabilityAttributes; import com.google.api.gax.tracing.SpanTracer; import com.google.api.gax.tracing.SpanTracerFactory; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.Message; import com.google.rpc.Status; import com.google.showcase.v1beta1.EchoClient; import com.google.showcase.v1beta1.EchoRequest; import com.google.showcase.v1beta1.EchoSettings; -import com.google.showcase.v1beta1.GetUserRequest; import com.google.showcase.v1beta1.IdentityClient; +import com.google.showcase.v1beta1.User; import com.google.showcase.v1beta1.it.util.TestClientInitializer; import com.google.showcase.v1beta1.stub.EchoStub; import com.google.showcase.v1beta1.stub.EchoStubSettings; @@ -58,6 +62,7 @@ import io.opentelemetry.sdk.trace.SdkTracerProvider; import io.opentelemetry.sdk.trace.data.SpanData; import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import java.nio.charset.StandardCharsets; import java.util.List; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -234,9 +239,26 @@ void testTracing_successfulIdentityGetUser_httpjson() throws Exception { AttributeKey.stringKey( ObservabilityAttributes.DESTINATION_RESOURCE_ID_ATTRIBUTE))) .isEqualTo("users/test-user"); + + User fetchedUser = User.newBuilder().setName("users/test-user").build(); + long expectedMagnitude = computeExpectedHttpJsonResponseSize(fetchedUser); + Long observedMagnitude = + attemptSpan + .getAttributes() + .get(AttributeKey.longKey(ObservabilityAttributes.HTTP_RESPONSE_BODY_SIZE)); + if (observedMagnitude != null) { + assertThat(observedMagnitude).isAtLeast((long) (expectedMagnitude * (1 - 0.15))); + assertThat(observedMagnitude).isAtMost((long) (expectedMagnitude * (1 + 0.15))); + } } } + private long computeExpectedHttpJsonResponseSize(Message message) + throws InvalidProtocolBufferException { + String jsonPayload = com.google.protobuf.util.JsonFormat.printer().print(message); + return jsonPayload.getBytes(StandardCharsets.UTF_8).length; + } + @Test void testTracing_retry_grpc() throws Exception { final int attempts = 5; From f92c66d0ca9ef394d39bb3cd653f96562e3dd6a3 Mon Sep 17 00:00:00 2001 From: cloud-java-bot Date: Tue, 24 Mar 2026 20:48:48 +0000 Subject: [PATCH 30/33] chore: generate libraries at Tue Mar 24 20:46:46 UTC 2026 --- .../java/com/google/showcase/v1beta1/it/ITOtelTracing.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java index 56cbb9fca8..14fc868709 100644 --- a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java +++ b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java @@ -33,9 +33,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; -import java.util.UUID; import com.google.api.client.http.javanet.NetHttpTransport; -import com.google.showcase.v1beta1.GetUserRequest; import com.google.api.gax.core.NoCredentialsProvider; import com.google.api.gax.retrying.RetrySettings; import com.google.api.gax.rpc.StatusCode; @@ -49,6 +47,7 @@ import com.google.showcase.v1beta1.EchoClient; import com.google.showcase.v1beta1.EchoRequest; import com.google.showcase.v1beta1.EchoSettings; +import com.google.showcase.v1beta1.GetUserRequest; import com.google.showcase.v1beta1.IdentityClient; import com.google.showcase.v1beta1.User; import com.google.showcase.v1beta1.it.util.TestClientInitializer; From a88273569a6df3be86b709deac31e62d4017ca87 Mon Sep 17 00:00:00 2001 From: Diego Date: Tue, 24 Mar 2026 17:04:10 -0400 Subject: [PATCH 31/33] test: relax expected body size --- .../java/com/google/showcase/v1beta1/it/ITOtelTracing.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java index 56cbb9fca8..c0c30d39e5 100644 --- a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java +++ b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java @@ -247,8 +247,7 @@ void testTracing_successfulIdentityGetUser_httpjson() throws Exception { .getAttributes() .get(AttributeKey.longKey(ObservabilityAttributes.HTTP_RESPONSE_BODY_SIZE)); if (observedMagnitude != null) { - assertThat(observedMagnitude).isAtLeast((long) (expectedMagnitude * (1 - 0.15))); - assertThat(observedMagnitude).isAtMost((long) (expectedMagnitude * (1 + 0.15))); + assertThat(observedMagnitude).isAtLeast((long) (expectedMagnitude * (1 - 0.15))); } } } From 03d905a70ebe1872868498e6fbd915375b3c4cf2 Mon Sep 17 00:00:00 2001 From: cloud-java-bot Date: Tue, 24 Mar 2026 21:12:01 +0000 Subject: [PATCH 32/33] chore: generate libraries at Tue Mar 24 21:09:57 UTC 2026 --- .../test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java index f55399172c..0e2a18e8aa 100644 --- a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java +++ b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java @@ -246,7 +246,7 @@ void testTracing_successfulIdentityGetUser_httpjson() throws Exception { .getAttributes() .get(AttributeKey.longKey(ObservabilityAttributes.HTTP_RESPONSE_BODY_SIZE)); if (observedMagnitude != null) { - assertThat(observedMagnitude).isAtLeast((long) (expectedMagnitude * (1 - 0.15))); + assertThat(observedMagnitude).isAtLeast((long) (expectedMagnitude * (1 - 0.15))); } } } From cdb3f84a8c01b914dfff3a7c28710cf628600124 Mon Sep 17 00:00:00 2001 From: Diego Date: Tue, 24 Mar 2026 18:47:46 -0400 Subject: [PATCH 33/33] fix: address comments --- .../google/api/gax/tracing/SpanTracer.java | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java index 4cae02dcb8..bad8b40e57 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java @@ -138,11 +138,12 @@ public void attemptSucceeded() { @Override public void responseHeadersReceived(java.util.Map headers) { - if (attemptSpan != null) { - long contentLength = extractContentLength(headers); - if (contentLength >= 0) { - attemptSpan.setAttribute(ObservabilityAttributes.HTTP_RESPONSE_BODY_SIZE, contentLength); - } + if (attemptSpan == null) { + return; + } + long contentLength = extractContentLength(headers); + if (contentLength >= 0) { + attemptSpan.setAttribute(ObservabilityAttributes.HTTP_RESPONSE_BODY_SIZE, contentLength); } } @@ -157,23 +158,22 @@ public void responseHeadersReceived(java.util.Map headers) { * @return the content length in bytes, or -1 if the header is missing or malformed. */ private long extractContentLength(java.util.Map headers) { - if (headers == null || headers.isEmpty()) return -1; - // google-http-client HttpHeaders uses a case-insensitive map but we copy it for safety - // and to handle potential different implementations. - Object value = - headers.entrySet().stream() - .filter(e -> CONTENT_LENGTH_KEY.equalsIgnoreCase(e.getKey())) - .map(Map.Entry::getValue) - .findFirst() - .orElse(null); - - if (value instanceof java.util.Collection) { - value = ((java.util.Collection) value).stream().findFirst().orElse(null); - } - try { + if (headers == null || headers.isEmpty()) return -1; + // google-http-client HttpHeaders uses a case-insensitive map but we copy it for safety + // and to handle potential different implementations. + Object value = + headers.entrySet().stream() + .filter(e -> CONTENT_LENGTH_KEY.equalsIgnoreCase(e.getKey())) + .map(Map.Entry::getValue) + .findFirst() + .orElse(null); + + if (value instanceof java.util.Collection) { + value = ((java.util.Collection) value).stream().findFirst().orElse(null); + } return Long.parseLong(value.toString()); - } catch (NumberFormatException | NullPointerException e) { + } catch (Exception e) { return -1; } }