2020import com .google .api .core .BetaApi ;
2121import com .google .api .core .InternalApi ;
2222import com .google .common .annotations .VisibleForTesting ;
23+ import com .google .common .io .CountingInputStream ;
2324import io .opentelemetry .api .common .AttributeKey ;
2425import io .opentelemetry .api .trace .Span ;
2526import io .opentelemetry .api .trace .Tracer ;
27+ import java .io .FilterInputStream ;
2628import java .io .IOException ;
29+ import java .io .InputStream ;
30+ import java .lang .reflect .Field ;
31+ import org .jspecify .annotations .NonNull ;
2732
2833/**
2934 * HttpRequestInitializer that wraps a delegate initializer, intercepts all HTTP requests, adds
@@ -50,6 +55,7 @@ public class HttpTracingRequestInitializer implements HttpRequestInitializer {
5055 AttributeKey .longKey ("http.response.body.size" );
5156
5257 @ VisibleForTesting static final String HTTP_RPC_SYSTEM_NAME = "http" ;
58+ @ VisibleForTesting static final String GZIP_ENCODING = "gzip" ;
5359
5460 private static final java .util .Set <String > REDACTED_QUERY_PARAMETERS =
5561 com .google .common .collect .ImmutableSet .of (
@@ -137,7 +143,42 @@ static void addResponseBodySizeToSpan(HttpResponse response, Span span) {
137143 if (contentLength != null && contentLength > 0 ) {
138144 span .setAttribute (HTTP_RESPONSE_BODY_SIZE , contentLength );
139145 }
140- // TODO handle chunked responses
146+ // For compressed responses without Content-Length, we need to wrap the response to get the
147+ // actual size
148+ if (GZIP_ENCODING .equals (response .getContentEncoding ())) {
149+ getResponseBodySizeForCompressedResponse (response , span );
150+ }
151+ }
152+
153+ /**
154+ * Wraps the response's input stream with a CountingInputStream to track bytes read. This handles
155+ * compressed transfer encoding where Content-Length is not available.
156+ */
157+ private static void getResponseBodySizeForCompressedResponse (HttpResponse response , Span span ) {
158+ try {
159+ InputStream content = response .getContent ();
160+ if (content == null ) {
161+ return ;
162+ }
163+
164+ InputStream wrappedStream = getWrappedInputStream (span , content );
165+ Field contentField = HttpResponse .class .getDeclaredField ("content" );
166+ contentField .setAccessible (true );
167+ contentField .set (response , wrappedStream );
168+ } catch (Exception e ) {
169+ // Ignore - stream wrapping failed, we will not track response size
170+ }
171+ }
172+
173+ private static @ NonNull InputStream getWrappedInputStream (Span span , InputStream content ) {
174+ CountingInputStream counter = new CountingInputStream (content );
175+ return new FilterInputStream (counter ) {
176+ @ Override
177+ public void close () throws IOException {
178+ super .close ();
179+ span .setAttribute (HTTP_RESPONSE_BODY_SIZE , counter .getCount ());
180+ }
181+ };
141182 }
142183
143184 /** Removes credentials from URL. */
0 commit comments