Skip to content

impl(o11y): introduce http.response.body_size#4157

Open
diegomarquezp wants to merge 27 commits intomainfrom
observability/tracing-attr/body-sze
Open

impl(o11y): introduce http.response.body_size#4157
diegomarquezp wants to merge 27 commits intomainfrom
observability/tracing-attr/body-sze

Conversation

@diegomarquezp
Copy link
Contributor

@diegomarquezp diegomarquezp commented Mar 19, 2026

Introduces the response size attribute.

Note that the sibling attribute http.request.body_size is not a straightforward implementation since the HTTP libraries don't offer an easy way to access the compressed body size.

@product-auto-label product-auto-label bot added the size: l Pull request size is large. label Mar 19, 2026
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly improves observability by adding a new http.response.body_size attribute. It integrates the measurement of HTTP response body sizes directly into the gax-httpjson library and extends the existing tracing infrastructure to capture and report this metric. This enhancement provides valuable insights into the data transfer volume for API calls, aiding in performance analysis and debugging. The changes are thoroughly tested to ensure correctness across different communication patterns.

Highlights

  • Observability: Introduced the http.response.body_size attribute to track the size of HTTP response bodies for enhanced observability.
  • HTTP JSON Client: Implemented logic within gax-httpjson to accurately measure and record response body sizes for both unary and server-streaming calls.
  • Tracing Infrastructure: Expanded the tracing framework (ApiTracer, OpenCensus, OpenTelemetry) to support and propagate the new http.response.body_size attribute.
  • Testing: Added new integration tests to validate the accurate recording of response body sizes in tracing spans.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces observability for HTTP response body size by adding a new attribute http.response.body.size. The changes span across the gax-httpjson and gax modules, introducing new APIs in ApiTracer and implementing the size recording in HttpJsonClientCallImpl. New tests are added to verify the functionality for both unary and server-streaming calls.

My review has identified a few areas for improvement:

  1. A potential logic error in HttpJsonClientCallImpl and OpencensusTracer regarding the incorrect invocation of responseReceived() for unary calls, leading to erroneous tracing data.
  2. A minor documentation issue in the ApiTracer interface.
  3. An incomplete implementation of the new setAttribute method in OpenTelemetryTraceManager, which could lead to non-conformance with OpenTelemetry Semantic Conventions for data types.

Details and suggestions are provided in the specific comments.

I am having trouble creating individual review comments. Click here to see my feedback.

gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonClientCallImpl.java (442-445)

high

Calling responseReceived() within responseReceived(long responseSize) in OpencensusTracer is problematic. The responseReceived() method is designed to record a message event for streaming RPCs. However, responseReceived(long) is also invoked for unary calls to record the response size. This implementation will incorrectly add a streaming message event to spans for unary calls.

This is also inconsistent with the SpanTracer (OpenTelemetry) implementation, which only sets the size attribute.

To fix this, responseReceived() should not be called in OpencensusTracer.responseReceived(long). Instead, HttpJsonClientCallImpl should be modified to explicitly call tracer.responseReceived() for server-streaming calls, in addition to tracer.responseReceived(responseSize).

    ApiTracer tracer = callOptions.getTracer();
    if (tracer != null) {
      long responseSize = responseBodySizeEnd - responseBodySizeStart;
      tracer.responseReceived(responseSize);
      if (methodDescriptor.getType() == MethodType.SERVER_STREAMING) {
        tracer.responseReceived();
      }
    }

gax-java/gax/src/main/java/com/google/api/gax/tracing/OpencensusTracer.java (411-416)

high

Calling responseReceived() within responseReceived(long responseSize) is problematic. The responseReceived() method is designed to record a message event for streaming RPCs. However, responseReceived(long) is also invoked for unary calls to record the response size. This implementation will incorrectly add a streaming message event to spans for unary calls.

This is also inconsistent with the SpanTracer (OpenTelemetry) implementation, which only sets the size attribute.

To fix this, responseReceived() should not be called here. Instead, HttpJsonClientCallImpl should be modified to explicitly call tracer.responseReceived() for server-streaming calls, in addition to tracer.responseReceived(responseSize).

  public void responseReceived(long responseSize) {
    span.putAttribute(
        ObservabilityAttributes.HTTP_RESPONSE_BODY_SIZE,
        AttributeValue.longAttributeValue(responseSize));
  }

gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracer.java (182-184)

medium

The Javadoc for responseReceived(long) states it's for a "streaming response". This is a bit misleading as the method is also used for unary calls to record the response body size. To avoid confusion, could you please make the documentation more generic?

  /** Adds an annotation for the size of a received response body in bytes. */
  default void responseReceived(long responseSize) {}
  ;

gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryTraceManager.java (80-90)

medium

The new setAttribute method only handles Long, Integer, and String explicitly, falling back to value.toString() for all other types. This could lead to incorrect attribute types for other common types like Double or Boolean, which would be converted to strings instead of being stored as their native types. To ensure conformance with OpenTelemetry Semantic Conventions for data types, please consider handling more primitive wrapper types directly.

    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 Double) {
        span.setAttribute(key, (Double) value);
      } else if (value instanceof Boolean) {
        span.setAttribute(key, (Boolean) value);
      } else if (value instanceof String) {
        span.setAttribute(key, (String) value);
      } else {
        span.setAttribute(key, value.toString());
      }
    }
References
  1. When defining OpenTelemetry attributes, conform to the Semantic Conventions (SemConv) for data types.

@diegomarquezp
Copy link
Contributor Author

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a new observability attribute http.response.body_size to track the size of HTTP response bodies. The implementation correctly uses a CountingInputStream to measure the bytes read from the response stream for both unary and server-streaming calls. The changes span across the tracing interfaces and implementations (OpenCensus, OpenTelemetry) to support this new attribute. New unit and integration tests are added to verify the functionality. The changes are well-implemented and improve the observability of the HTTP client. The suggestion to enhance the setAttribute method for better OpenTelemetry type conformity is noted.

@diegomarquezp diegomarquezp marked this pull request as ready for review March 19, 2026 18:06
@diegomarquezp diegomarquezp marked this pull request as draft March 19, 2026 18:13
@diegomarquezp
Copy link
Contributor Author

/gcbrun

@diegomarquezp diegomarquezp marked this pull request as ready for review March 19, 2026 20:10
}

if (responseCountingStream == null) {
responseCountingStream = new CountingInputStream(runnableResult.getResponseContent());
Copy link
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
Contributor Author

Choose a reason for hiding this comment

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

Done

;

/** Adds an annotation that a streaming response has been received with size. */
default void recordResponseSize(long responseSize) {}
Copy link
Contributor

Choose a reason for hiding this comment

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

If we go with the header approach, I think we can make this new method more generic and pass all headers to the tracer. Maybe something like "headersReceived".

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

@sonarqubecloud
Copy link

Quality Gate Failed Quality Gate failed for 'gapic-generator-java-root'

Failed conditions
27.1% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

@sonarqubecloud
Copy link

Quality Gate Failed Quality Gate failed for 'java_showcase_integration_tests'

Failed conditions
68.8% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

try {
String contentLengthStr =
value instanceof java.util.List
? ((java.util.List<?>) value).get(0).toString()
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this an edge case? I'm not sure we want to consider this edge case even if it could happen. I think it's OK to parseLong directly and consider other scenarios invalid.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

Copy link
Contributor

Choose a reason for hiding this comment

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

Actually I meant that we don't have to check if it is a list, but it's still better to do a try catch.

* @return the content length in bytes, or -1 if the header is missing or malformed.
*/
private long extractContentLength(java.util.Map<String, Object> headers) {
if (headers == null) {
Copy link
Contributor

@blakeli0 blakeli0 Mar 21, 2026

Choose a reason for hiding this comment

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

I think the logic in this method and the next method can be simplified to

if (headers == null || headers.get(CONTENT_LENGTH_KEY) == null) { 
  return -1;
}
return Long.parseLong(String.valueOf(headers.get(CONTENT_LENGTH_KEY)));

Add try catch as I mentioned in another comment though.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size: l Pull request size is large.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants