Skip to content

Commit f8a13e5

Browse files
authored
feat(bigquery): add HTTP response attribute tracing (#12109)
This adds generic HTTP response attribute capture via open telemetry to BigQuery. Error attributes are not included in this PR. [example success POST](https://screenshot.googleplex.com/9BARUi5tNbLBrNL.png) [example failed GET](https://screenshot.googleplex.com/4GUwJFKoVkEVvKD.png)
1 parent 73c0bd3 commit f8a13e5

File tree

2 files changed

+253
-17
lines changed

2 files changed

+253
-17
lines changed

java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/telemetry/HttpTracingRequestInitializer.java

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,25 @@ public void initialize(HttpRequest request) throws IOException {
7777
String host = request.getUrl().getHost();
7878
int port = request.getUrl().getPort();
7979
addInitialHttpAttributesToSpan(span, host, port);
80+
81+
HttpResponseInterceptor originalInterceptor = request.getResponseInterceptor();
82+
request.setResponseInterceptor(
83+
response -> {
84+
addCommonResponseAttributesToSpan(request, response, span);
85+
if (originalInterceptor != null) {
86+
originalInterceptor.interceptResponse(response);
87+
}
88+
});
89+
90+
HttpUnsuccessfulResponseHandler originalHandler = request.getUnsuccessfulResponseHandler();
91+
request.setUnsuccessfulResponseHandler(
92+
(request1, response, supportsRetry) -> {
93+
addCommonResponseAttributesToSpan(request1, response, span);
94+
if (originalHandler != null) {
95+
return originalHandler.handleResponse(request1, response, supportsRetry);
96+
}
97+
return false;
98+
});
8099
}
81100

82101
/** Add initial HTTP attributes to the existing active span */
@@ -89,4 +108,32 @@ private void addInitialHttpAttributesToSpan(Span span, String host, Integer port
89108
}
90109
// TODO add full sanitized url, url domain, request method
91110
}
111+
112+
private static void addCommonResponseAttributesToSpan(
113+
HttpRequest request, HttpResponse response, Span span) {
114+
// We add request body size and update request method after we receive response as
115+
// the data is not always available until after the http request execution
116+
addRequestBodySizeToSpan(request, span);
117+
span.setAttribute(HTTP_REQUEST_METHOD, request.getRequestMethod());
118+
addResponseBodySizeToSpan(response, span);
119+
span.setAttribute(HTTP_RESPONSE_STATUS_CODE, response.getStatusCode());
120+
}
121+
122+
static void addRequestBodySizeToSpan(HttpRequest request, Span span) {
123+
try {
124+
if (request.getContent() != null && request.getEncoding() == null) {
125+
span.setAttribute(HTTP_REQUEST_BODY_SIZE, request.getContent().getLength());
126+
}
127+
} catch (IOException e) {
128+
// Ignore - body size not available
129+
}
130+
}
131+
132+
static void addResponseBodySizeToSpan(HttpResponse response, Span span) {
133+
Long contentLength = response.getHeaders().getContentLength();
134+
if (contentLength != null && contentLength > 0) {
135+
span.setAttribute(HTTP_RESPONSE_BODY_SIZE, contentLength);
136+
}
137+
// TODO handle chunked responses
138+
}
92139
}

java-bigquery/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/telemetry/HttpTracingRequestInitializerTest.java

Lines changed: 206 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,24 @@
1919
import static org.junit.jupiter.api.Assertions.assertEquals;
2020
import static org.junit.jupiter.api.Assertions.assertNull;
2121
import static org.mockito.ArgumentMatchers.any;
22+
import static org.mockito.ArgumentMatchers.anyBoolean;
2223
import static org.mockito.Mockito.mock;
2324
import static org.mockito.Mockito.times;
2425
import static org.mockito.Mockito.verify;
26+
import static org.mockito.Mockito.when;
2527

28+
import com.google.api.client.http.ByteArrayContent;
2629
import com.google.api.client.http.GenericUrl;
30+
import com.google.api.client.http.HttpContent;
31+
import com.google.api.client.http.HttpEncoding;
2732
import com.google.api.client.http.HttpRequest;
2833
import com.google.api.client.http.HttpRequestFactory;
2934
import com.google.api.client.http.HttpRequestInitializer;
3035
import com.google.api.client.http.HttpResponse;
36+
import com.google.api.client.http.HttpResponseException;
37+
import com.google.api.client.http.HttpResponseInterceptor;
3138
import com.google.api.client.http.HttpTransport;
39+
import com.google.api.client.http.HttpUnsuccessfulResponseHandler;
3240
import com.google.api.client.http.LowLevelHttpRequest;
3341
import com.google.api.client.http.LowLevelHttpResponse;
3442
import com.google.api.client.testing.http.MockHttpTransport;
@@ -77,22 +85,23 @@ public void setUp() {
7785
}
7886

7987
@Test
80-
public void testRequestAttributesAreSetIfSpanExists() throws IOException {
81-
HttpTransport transport = createTransport();
82-
HttpRequest request = buildGetRequest(transport, initializer, BASE_URL);
88+
public void testAllAttributesAreSetOnSuccessfulCall() throws IOException {
89+
HttpResponseInterceptor originalInterceptor = mock(HttpResponseInterceptor.class);
90+
HttpRequestInitializer delegateInitializer =
91+
request -> request.setResponseInterceptor(originalInterceptor);
92+
HttpTracingRequestInitializer tracingInitializer =
93+
new HttpTracingRequestInitializer(delegateInitializer, tracer);
94+
String responseContent = "{\"status\": \"ok\"}";
95+
HttpTransport transport = createTransport(200, responseContent);
96+
HttpContent requestContent =
97+
ByteArrayContent.fromString("application/json", "{\"test\": \"data\"}");
98+
HttpRequest request = buildPostRequest(transport, tracingInitializer, BASE_URL, requestContent);
8399

84100
HttpResponse response = request.execute();
85101
response.disconnect();
86102

87-
// End the span before verifying exported spans, so it appears in the exporter
88-
spanScope.close();
89-
parentSpan.end();
90-
91-
List<SpanData> spans = spanExporter.getFinishedSpanItems();
92-
assertEquals(1, spans.size());
93-
94-
SpanData span = spans.get(0);
95-
verifyGeneralSpanData(span);
103+
verify(originalInterceptor, times(1)).interceptResponse(any(HttpResponse.class));
104+
closeAndVerifySpanData(200, "POST", 16, requestContent.getLength());
96105
}
97106

98107
@Test
@@ -103,14 +112,11 @@ public void testExistingParentAttributesAreNotAffectedByRequestAttributes() thro
103112

104113
HttpResponse response = request.execute();
105114
response.disconnect();
106-
107-
// End the span before verifying exported spans, so it appears in the exporter
108115
spanScope.close();
109116
parentSpan.end();
110117

111118
List<SpanData> spans = spanExporter.getFinishedSpanItems();
112119
assertEquals(1, spans.size());
113-
114120
SpanData span = spans.get(0);
115121
assertEquals("value", span.getAttributes().get(AttributeKey.stringKey("parent_attribute")));
116122
}
@@ -152,14 +158,160 @@ public void testDelegateInitializerIsCalled() throws IOException {
152158
verify(delegateInitializer, times(1)).initialize(any(HttpRequest.class));
153159
}
154160

161+
@Test
162+
public void testUnsuccessfulResponseHandlerSetsAttributesAndCallsOriginal() throws IOException {
163+
HttpUnsuccessfulResponseHandler originalHandler = mock(HttpUnsuccessfulResponseHandler.class);
164+
when(originalHandler.handleResponse(
165+
any(HttpRequest.class), any(HttpResponse.class), anyBoolean()))
166+
.thenReturn(false);
167+
HttpRequestInitializer delegateInitializer =
168+
request -> request.setUnsuccessfulResponseHandler(originalHandler);
169+
HttpTracingRequestInitializer tracingInitializer =
170+
new HttpTracingRequestInitializer(delegateInitializer, tracer);
171+
String responseContent = "{\"error\": \"not found\"}";
172+
HttpTransport transport = createTransport(404, responseContent);
173+
HttpContent requestContent =
174+
ByteArrayContent.fromString("application/json", "{\"test\": \"data\"}");
175+
HttpRequest request = buildPostRequest(transport, tracingInitializer, BASE_URL, requestContent);
176+
177+
try {
178+
request.execute();
179+
} catch (HttpResponseException e) {
180+
// Expected
181+
}
182+
183+
verify(originalHandler, times(1))
184+
.handleResponse(any(HttpRequest.class), any(HttpResponse.class), anyBoolean());
185+
closeAndVerifySpanData(404, "POST", 16, responseContent.length());
186+
}
187+
188+
@Test
189+
public void testUnsuccessfulResponseHandlerSetsErrorIfNoOriginal() throws IOException {
190+
HttpTracingRequestInitializer tracingInitializer =
191+
new HttpTracingRequestInitializer(null, tracer);
192+
HttpTransport transport = createTransport(401);
193+
HttpRequest request = buildGetRequest(transport, tracingInitializer, BASE_URL);
194+
195+
try {
196+
request.execute();
197+
} catch (HttpResponseException e) {
198+
// Expected
199+
}
200+
201+
closeAndVerifySpanData(401, "GET", -1, -1);
202+
}
203+
204+
@Test
205+
public void testAddRequestBodySizeToSpan_ExceptionHandled() throws IOException {
206+
HttpContent content = mock(HttpContent.class);
207+
when(content.getLength()).thenThrow(new IOException("test"));
208+
HttpTransport transport = createTransport();
209+
HttpRequest request = buildPostRequest(transport, null, BASE_URL, content);
210+
211+
HttpTracingRequestInitializer.addRequestBodySizeToSpan(request, parentSpan);
212+
213+
spanScope.close();
214+
parentSpan.end();
215+
216+
List<SpanData> spans = spanExporter.getFinishedSpanItems();
217+
assertEquals(1, spans.size());
218+
SpanData span = spans.get(0);
219+
assertNull(span.getAttributes().get(HttpTracingRequestInitializer.HTTP_REQUEST_BODY_SIZE));
220+
}
221+
222+
@Test
223+
public void testAddRequestBodySizeToSpan_NullBody() throws IOException {
224+
HttpTransport transport = createTransport();
225+
HttpRequest request = buildPostRequest(transport, null, BASE_URL, null);
226+
227+
HttpTracingRequestInitializer.addRequestBodySizeToSpan(request, parentSpan);
228+
229+
spanScope.close();
230+
parentSpan.end();
231+
232+
List<SpanData> spans = spanExporter.getFinishedSpanItems();
233+
assertEquals(1, spans.size());
234+
SpanData span = spans.get(0);
235+
assertNull(span.getAttributes().get(HttpTracingRequestInitializer.HTTP_REQUEST_BODY_SIZE));
236+
}
237+
238+
@Test
239+
public void testAddRequestBodySizeToSpan_WithEncoding() throws IOException {
240+
HttpTransport transport = createTransport();
241+
HttpContent content = ByteArrayContent.fromString("application/json", "{\"test\": \"data\"}");
242+
HttpRequest request = buildPostRequest(transport, null, BASE_URL, content);
243+
request.setEncoding(mock(HttpEncoding.class));
244+
245+
HttpTracingRequestInitializer.addRequestBodySizeToSpan(request, parentSpan);
246+
247+
spanScope.close();
248+
parentSpan.end();
249+
250+
List<SpanData> spans = spanExporter.getFinishedSpanItems();
251+
assertEquals(1, spans.size());
252+
SpanData span = spans.get(0);
253+
assertNull(span.getAttributes().get(HttpTracingRequestInitializer.HTTP_REQUEST_BODY_SIZE));
254+
}
255+
256+
@Test
257+
public void testAddRequestBodySizeToSpan() throws IOException {
258+
HttpTransport transport = createTransport();
259+
HttpContent content = ByteArrayContent.fromString("application/json", "{\"test\": \"data\"}");
260+
HttpRequest request = buildPostRequest(transport, null, BASE_URL, content);
261+
262+
HttpTracingRequestInitializer.addRequestBodySizeToSpan(request, parentSpan);
263+
264+
spanScope.close();
265+
parentSpan.end();
266+
267+
List<SpanData> spans = spanExporter.getFinishedSpanItems();
268+
assertEquals(1, spans.size());
269+
SpanData span = spans.get(0);
270+
assertEquals(
271+
content.getLength(),
272+
span.getAttributes().get(HttpTracingRequestInitializer.HTTP_REQUEST_BODY_SIZE));
273+
}
274+
275+
@Test
276+
public void testAddResponseBodySizeToSpan_NullLength() throws IOException {
277+
HttpTransport transport =
278+
createTransport(200, null); // Null response content means null Content-Length header
279+
HttpRequest request = buildGetRequest(transport, null, BASE_URL);
280+
HttpResponse response = request.execute();
281+
282+
HttpTracingRequestInitializer.addResponseBodySizeToSpan(response, parentSpan);
283+
284+
spanScope.close();
285+
parentSpan.end();
286+
287+
List<SpanData> spans = spanExporter.getFinishedSpanItems();
288+
assertEquals(1, spans.size());
289+
SpanData span = spans.get(0);
290+
assertNull(span.getAttributes().get(HttpTracingRequestInitializer.HTTP_RESPONSE_BODY_SIZE));
291+
}
292+
155293
private static HttpTransport createTransport() {
294+
return createTransport(200, null);
295+
}
296+
297+
private static HttpTransport createTransport(int statusCode) {
298+
return createTransport(statusCode, null);
299+
}
300+
301+
private static HttpTransport createTransport(int statusCode, String responseContent) {
156302
return new MockHttpTransport() {
157303
@Override
158304
public LowLevelHttpRequest buildRequest(String method, String url) {
159305
return new MockLowLevelHttpRequest() {
160306
@Override
161307
public LowLevelHttpResponse execute() {
162-
return new MockLowLevelHttpResponse();
308+
MockLowLevelHttpResponse response = new MockLowLevelHttpResponse();
309+
response.setStatusCode(statusCode);
310+
if (responseContent != null) {
311+
response.setContent(responseContent);
312+
response.addHeader("Content-Length", String.valueOf(responseContent.length()));
313+
}
314+
return response;
163315
}
164316
};
165317
}
@@ -173,7 +325,25 @@ private static HttpRequest buildGetRequest(
173325
return requestFactory.buildGetRequest(new GenericUrl(url));
174326
}
175327

176-
private void verifyGeneralSpanData(SpanData span) {
328+
private static HttpRequest buildPostRequest(
329+
HttpTransport transport,
330+
HttpRequestInitializer requestInitializer,
331+
String url,
332+
HttpContent content)
333+
throws IOException {
334+
HttpRequestFactory requestFactory = transport.createRequestFactory(requestInitializer);
335+
return requestFactory.buildPostRequest(new GenericUrl(url), content);
336+
}
337+
338+
private void closeAndVerifySpanData(
339+
long responseCode, String method, long requestBodySize, long responseBodySize) {
340+
// close span so it is available for verification
341+
spanScope.close();
342+
parentSpan.end();
343+
List<SpanData> spans = spanExporter.getFinishedSpanItems();
344+
345+
assertEquals(1, spans.size());
346+
SpanData span = spans.get(0);
177347
assertEquals(SPAN_NAME, span.getName());
178348
assertEquals(BIGQUERY_DOMAIN, span.getAttributes().get(BigQueryTelemetryTracer.SERVER_ADDRESS));
179349
assertEquals(443, span.getAttributes().get(BigQueryTelemetryTracer.SERVER_PORT));
@@ -192,5 +362,24 @@ private void verifyGeneralSpanData(SpanData span) {
192362
assertEquals(
193363
HttpTracingRequestInitializer.HTTP_RPC_SYSTEM_NAME,
194364
span.getAttributes().get(BigQueryTelemetryTracer.RPC_SYSTEM_NAME));
365+
assertEquals(
366+
responseCode,
367+
span.getAttributes().get(HttpTracingRequestInitializer.HTTP_RESPONSE_STATUS_CODE));
368+
assertEquals(
369+
method, span.getAttributes().get(HttpTracingRequestInitializer.HTTP_REQUEST_METHOD));
370+
if (requestBodySize >= 0) {
371+
assertEquals(
372+
requestBodySize,
373+
span.getAttributes().get(HttpTracingRequestInitializer.HTTP_REQUEST_BODY_SIZE));
374+
} else {
375+
assertNull(span.getAttributes().get(HttpTracingRequestInitializer.HTTP_REQUEST_BODY_SIZE));
376+
}
377+
if (responseBodySize >= 0) {
378+
assertEquals(
379+
responseBodySize,
380+
span.getAttributes().get(HttpTracingRequestInitializer.HTTP_RESPONSE_BODY_SIZE));
381+
} else {
382+
assertNull(span.getAttributes().get(HttpTracingRequestInitializer.HTTP_RESPONSE_BODY_SIZE));
383+
}
195384
}
196385
}

0 commit comments

Comments
 (0)