Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
4ef3d9f
impl(o11y): introduce body size attributes
diegomarquezp Mar 19, 2026
c00e49a
impl(httpjson): record response and request sizes
diegomarquezp Mar 19, 2026
c6da324
test: expand tests
diegomarquezp Mar 19, 2026
72be770
fix revert request size attribute
diegomarquezp Mar 19, 2026
7865574
Merge remote-tracking branch 'origin/main' into observability/tracing…
diegomarquezp Mar 19, 2026
1265da3
chore: cleanup
diegomarquezp Mar 19, 2026
94e55db
fix: use separate logic for unary resposne body size
diegomarquezp Mar 19, 2026
2cb0479
test: improvements
diegomarquezp Mar 19, 2026
dc84757
test: improve coverage
diegomarquezp Mar 19, 2026
cba2159
fix: revert changes in opencensus
diegomarquezp Mar 19, 2026
9435824
test: cover baseapitracer
diegomarquezp Mar 19, 2026
0ae245d
test: test setAttribute
diegomarquezp Mar 19, 2026
9f1bcd8
fix: address code quality flags
diegomarquezp Mar 19, 2026
eb959bb
refactor: use content lenght header
diegomarquezp Mar 20, 2026
7bc8aa0
Merge origin/main into observability/tracing-attr/body-sze (accept ma…
diegomarquezp Mar 20, 2026
11950af
fix(tracing): add body_size tracking to OpenTelemetry SpanTracer
diegomarquezp Mar 20, 2026
6e8b311
chore(tracing): revert HttpJson tracing and merge test files
diegomarquezp Mar 20, 2026
6035f29
chore: format
diegomarquezp Mar 20, 2026
960ddd1
refactor(o11y): remove redundant responseHeadersReceived from BaseApi…
diegomarquezp Mar 20, 2026
097e5ee
refactor: update responseHeadersReceived to use Map<String, Object> a…
diegomarquezp Mar 20, 2026
ab61cbb
test: add responseHeadersReceived tests to SpanTracerTest
diegomarquezp Mar 20, 2026
869bd2a
refactor: extract CONTENT_LENGTH_KEY to static final string
diegomarquezp Mar 20, 2026
c721960
refactor: remove NumberFormatException handling in tracers
diegomarquezp Mar 20, 2026
53117a3
fix: simplify logic of header getter
diegomarquezp Mar 23, 2026
247f871
fix: use number format exception
diegomarquezp Mar 23, 2026
fad817a
merge: resolve conflicts with origin/main
diegomarquezp Mar 23, 2026
c2fdd2a
chore: format code
diegomarquezp Mar 23, 2026
e2df169
fix: handle array content length
diegomarquezp Mar 24, 2026
2bf6326
Merge remote-tracking branch 'origin/main' into observability/tracing…
diegomarquezp Mar 24, 2026
43a12ab
test: fix test
diegomarquezp Mar 24, 2026
c0427b3
fix(tracing): gracefully unwrap Content-Length tracing attribute
diegomarquezp Mar 24, 2026
c93eb4d
fix: remove unintended files
diegomarquezp Mar 24, 2026
bf13631
test: Implement exact expected magnitude calculation for HTTP payload…
diegomarquezp Mar 24, 2026
f92c66d
chore: generate libraries at Tue Mar 24 20:46:46 UTC 2026
cloud-java-bot Mar 24, 2026
a882735
test: relax expected body size
diegomarquezp Mar 24, 2026
4a02b34
Merge branch 'observability/tracing-attr/body-sze' of https://github.…
diegomarquezp Mar 24, 2026
03d905a
chore: generate libraries at Tue Mar 24 21:09:57 UTC 2026
cloud-java-bot Mar 24, 2026
6e88a5a
Merge branch 'observability/tracing-attr/body-sze' of https://github.…
diegomarquezp Mar 24, 2026
cdb3f84
fix: address comments
diegomarquezp Mar 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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();
}

Expand All @@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -118,6 +120,9 @@ final class HttpJsonClientCallImpl<RequestT, ResponseT>
@GuardedBy("lock")
private ProtoMessageJsonStreamIterator responseStreamIterator;

@GuardedBy("lock")
private CountingInputStream responseCountingStream;

@GuardedBy("lock")
private volatile boolean closed;

Expand Down Expand Up @@ -400,14 +405,19 @@ private boolean consumeMessageFromStream() throws IOException {
return true;
}

if (responseCountingStream == null) {
responseCountingStream = new CountingInputStream(runnableResult.getResponseContent());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not seem to be an intuitive approach, can we rely on the ContentLength header instead? See discussion.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

}

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();
Expand All @@ -419,15 +429,20 @@ 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;
}

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;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
/*
* 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<Field, Field> FAKE_METHOD_DESCRIPTOR =
ApiMethodDescriptor.<Field, Field>newBuilder()
.setFullMethodName("google.cloud.v1.Fake/FakeMethod")
.setHttpMethod("POST")
.setRequestFormatter(
ProtoMessageRequestFormatter.<Field>newBuilder()
.setPath(
"/fake/v1/name/{name}",
request -> {
Map<String, String> fields = new HashMap<>();
ProtoRestSerializer<Field> 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.<Field>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() {

Check warning on line 92 in gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/BodySizeRecordingTest.java

View check run for this annotation

SonarQubeCloud / [gapic-generator-java-root] SonarCloud Code Analysis

Remove this 'public' modifier.

See more on https://sonarcloud.io/project/issues?id=googleapis_gapic-generator-java&issues=AZ0HDaXDcKsPsk4GNO0p&open=AZ0HDaXDcKsPsk4GNO0p&pullRequest=4157

Check warning on line 92 in gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/BodySizeRecordingTest.java

View check run for this annotation

SonarQubeCloud / [java_showcase_integration_tests] SonarCloud Code Analysis

Remove this 'public' modifier.

See more on https://sonarcloud.io/project/issues?id=googleapis_gapic-generator-java_integration_tests&issues=AZ0HEYmZuLbE7AhQ5Y5g&open=AZ0HEYmZuLbE7AhQ5Y5g&pullRequest=4157
executorService = Executors.newFixedThreadPool(2);
}

@AfterAll
public static void destroy() {

Check warning on line 97 in gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/BodySizeRecordingTest.java

View check run for this annotation

SonarQubeCloud / [gapic-generator-java-root] SonarCloud Code Analysis

Remove this 'public' modifier.

See more on https://sonarcloud.io/project/issues?id=googleapis_gapic-generator-java&issues=AZ0HDaXDcKsPsk4GNO0q&open=AZ0HDaXDcKsPsk4GNO0q&pullRequest=4157

Check warning on line 97 in gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/BodySizeRecordingTest.java

View check run for this annotation

SonarQubeCloud / [java_showcase_integration_tests] SonarCloud Code Analysis

Remove this 'public' modifier.

See more on https://sonarcloud.io/project/issues?id=googleapis_gapic-generator-java_integration_tests&issues=AZ0HEYmZuLbE7AhQ5Y5h&open=AZ0HEYmZuLbE7AhQ5Y5h&pullRequest=4157
executorService.shutdownNow();
}

@BeforeEach
void setUp() throws IOException {

Check warning on line 102 in gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/BodySizeRecordingTest.java

View check run for this annotation

SonarQubeCloud / [gapic-generator-java-root] SonarCloud Code Analysis

Remove the declaration of thrown exception 'java.io.IOException', as it cannot be thrown from method's body.

See more on https://sonarcloud.io/project/issues?id=googleapis_gapic-generator-java&issues=AZ0HDaXDcKsPsk4GNO0r&open=AZ0HDaXDcKsPsk4GNO0r&pullRequest=4157

Check warning on line 102 in gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/BodySizeRecordingTest.java

View check run for this annotation

SonarQubeCloud / [java_showcase_integration_tests] SonarCloud Code Analysis

Remove the declaration of thrown exception 'java.io.IOException', as it cannot be thrown from method's body.

See more on https://sonarcloud.io/project/issues?id=googleapis_gapic-generator-java_integration_tests&issues=AZ0HEYmZuLbE7AhQ5Y5i&open=AZ0HEYmZuLbE7AhQ5Y5i&pullRequest=4157
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<Field, Field> 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);
}

@Test
void testBodySizeRecordingServerStreaming() throws Exception {
ApiMethodDescriptor<Field, Field> 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<Field, Field> 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<Field> receivedResponses = new java.util.ArrayList<>();
final CountDownLatch latch = new CountDownLatch(1);

callable.call(
request,
new ResponseObserver<Field>() {
@Override
public void onStart(StreamController controller) {}

Check failure on line 192 in gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/BodySizeRecordingTest.java

View check run for this annotation

SonarQubeCloud / [gapic-generator-java-root] SonarCloud Code Analysis

Add a nested comment explaining why this method is empty, throw an UnsupportedOperationException or complete the implementation.

See more on https://sonarcloud.io/project/issues?id=googleapis_gapic-generator-java&issues=AZ0HDaXDcKsPsk4GNO0s&open=AZ0HDaXDcKsPsk4GNO0s&pullRequest=4157

Check failure on line 192 in gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/BodySizeRecordingTest.java

View check run for this annotation

SonarQubeCloud / [java_showcase_integration_tests] SonarCloud Code Analysis

Add a nested comment explaining why this method is empty, throw an UnsupportedOperationException or complete the implementation.

See more on https://sonarcloud.io/project/issues?id=googleapis_gapic-generator-java_integration_tests&issues=AZ0HEYmZuLbE7AhQ5Y5j&open=AZ0HEYmZuLbE7AhQ5Y5j&pullRequest=4157

@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
// 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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -43,6 +44,7 @@ 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 responseReceivedSize = new AtomicLong();

public TestApiTracer() {}

Expand All @@ -58,6 +60,10 @@ public AtomicBoolean getRetriesExhausted() {
return retriesExhausted;
}

public long getResponseReceivedSize() {
return responseReceivedSize.get();
}

@Override
public void attemptStarted(int attemptNumber) {
attemptsStarted.incrementAndGet();
Expand All @@ -78,5 +84,10 @@ public void attemptFailedRetriesExhausted(Throwable error) {
attemptsFailed.incrementAndGet();
retriesExhausted.set(true);
}

@Override
public void responseReceived(long responseSize) {
responseReceivedSize.addAndGet(responseSize);
}
}
;
Original file line number Diff line number Diff line change
Expand Up @@ -179,9 +179,13 @@
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() {}
;

Check warning on line 188 in gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracer.java

View check run for this annotation

SonarQubeCloud / [gapic-generator-java-root] SonarCloud Code Analysis

Remove this empty statement.

See more on https://sonarcloud.io/project/issues?id=googleapis_gapic-generator-java&issues=AZ0HDaTfcKsPsk4GNO0o&open=AZ0HDaTfcKsPsk4GNO0o&pullRequest=4157

Check warning on line 188 in gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracer.java

View check run for this annotation

SonarQubeCloud / [java_showcase_integration_tests] SonarCloud Code Analysis

Remove this empty statement.

See more on https://sonarcloud.io/project/issues?id=googleapis_gapic-generator-java_integration_tests&issues=AZ0HEYiBuLbE7AhQ5Y5f&open=AZ0HEYiBuLbE7AhQ5Y5f&pullRequest=4157

/**
* Adds an annotation that a batch of writes has been flushed.
Expand Down
Loading
Loading