Skip to content

Commit 9705937

Browse files
committed
#621 - add the logging of requestId (port to 2.35.*)
Added 'RequestIdInterceptor' on the apache http client to generate 'requestId' for the client requests. It wasn't added to Spring 'RestTemplate' because the Sardine client doesn't use it. Added 'ResponseMissingRequestIdInterceptor' on the apache http client to add missing 'X-GDC-REQUEST' header in responses coming from WebDav server. Added the 'requestId' information to exceptions to easier tracking the problem.
1 parent f8f61e1 commit 9705937

8 files changed

Lines changed: 223 additions & 11 deletions

File tree

src/main/java/com/gooddata/GoodData.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,8 @@ private HttpClientBuilder createHttpClientBuilder(final GoodDataSettings setting
288288

289289
return HttpClientBuilder.create()
290290
.setUserAgent(StringUtils.isNotBlank(settings.getUserAgent()) ? String.format("%s %s", settings.getUserAgent(), getUserAgent()) : getUserAgent())
291+
.addInterceptorFirst(new RequestIdInterceptor())
292+
.addInterceptorFirst(new ResponseMissingRequestIdInterceptor())
291293
.setConnectionManager(connectionManager)
292294
.setDefaultRequestConfig(requestConfig.build());
293295
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright (C) 2004-2021, GoodData(R) Corporation. All rights reserved.
3+
* This source code is licensed under the BSD-style license found in the
4+
* LICENSE.txt file in the root directory of this source tree.
5+
*/
6+
package com.gooddata;
7+
8+
import org.apache.commons.lang3.RandomStringUtils;
9+
import org.apache.http.Header;
10+
import org.apache.http.HttpException;
11+
import org.apache.http.HttpRequest;
12+
import org.apache.http.HttpRequestInterceptor;
13+
import org.apache.http.annotation.Contract;
14+
import org.apache.http.annotation.ThreadingBehavior;
15+
import org.apache.http.protocol.HttpContext;
16+
17+
import java.io.IOException;
18+
19+
import static com.gooddata.gdc.Header.GDC_REQUEST_ID;
20+
21+
/**
22+
* Intercepts the client-side requests on low-level in order to be able to catch requests also from the Sardine,
23+
* that is working independently from Spring {@link org.springframework.web.client.RestTemplate} to set
24+
* the X-GDC-REQUEST header to them.
25+
*/
26+
@Contract(threading = ThreadingBehavior.IMMUTABLE)
27+
public class RequestIdInterceptor implements HttpRequestInterceptor {
28+
29+
@Override
30+
public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
31+
final StringBuilder requestIdBuilder = new StringBuilder();
32+
final Header requestIdHeader = request.getFirstHeader(GDC_REQUEST_ID);
33+
if (requestIdHeader != null) {
34+
requestIdBuilder.append(requestIdHeader.getValue()).append(":");
35+
}
36+
final String requestId = requestIdBuilder.append(RandomStringUtils.randomAlphanumeric(16)).toString();
37+
request.setHeader(GDC_REQUEST_ID, requestId);
38+
}
39+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright (C) 2004-2021, GoodData(R) Corporation. All rights reserved.
3+
* This source code is licensed under the BSD-style license found in the
4+
* LICENSE.txt file in the root directory of this source tree.
5+
*/
6+
package com.gooddata;
7+
8+
import org.apache.http.Header;
9+
import org.apache.http.HttpException;
10+
import org.apache.http.HttpResponse;
11+
import org.apache.http.HttpResponseInterceptor;
12+
import org.apache.http.annotation.Contract;
13+
import org.apache.http.annotation.ThreadingBehavior;
14+
import org.apache.http.protocol.HttpContext;
15+
import org.apache.http.protocol.HttpCoreContext;
16+
17+
import java.io.IOException;
18+
19+
import static com.gooddata.gdc.Header.GDC_REQUEST_ID;
20+
21+
22+
/**
23+
* Intercepts responses to check if they have set the X-GDC-REQUEST header for easier debugging.
24+
* If not, it takes this header from the request sent.
25+
*/
26+
@Contract(threading = ThreadingBehavior.IMMUTABLE)
27+
public class ResponseMissingRequestIdInterceptor implements HttpResponseInterceptor {
28+
29+
@Override
30+
public void process(final HttpResponse response, final HttpContext context) throws HttpException, IOException {
31+
32+
if (response.getFirstHeader(GDC_REQUEST_ID) == null) {
33+
final HttpCoreContext coreContext = HttpCoreContext.adapt(context);
34+
final Header requestIdHeader = coreContext.getRequest().getFirstHeader(GDC_REQUEST_ID);
35+
response.setHeader(GDC_REQUEST_ID, requestIdHeader.getValue());
36+
}
37+
}
38+
}

src/main/java/com/gooddata/gdc/DataStoreService.java

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
*/
66
package com.gooddata.gdc;
77

8-
import com.github.sardine.Sardine;
98
import com.github.sardine.impl.SardineException;
9+
import com.gooddata.GoodDataRestException;
1010
import com.gooddata.UriPrefixer;
1111
import org.apache.http.Header;
1212
import org.apache.http.HeaderIterator;
@@ -24,19 +24,23 @@
2424
import org.apache.http.client.methods.CloseableHttpResponse;
2525
import org.apache.http.client.methods.HttpUriRequest;
2626
import org.apache.http.conn.ClientConnectionManager;
27+
import org.apache.http.entity.InputStreamEntity;
2728
import org.apache.http.impl.client.CloseableHttpClient;
2829
import org.apache.http.impl.client.HttpClientBuilder;
30+
import org.apache.http.message.BasicHeader;
2931
import org.apache.http.params.HttpParams;
32+
import org.apache.http.protocol.HTTP;
3033
import org.apache.http.protocol.HttpContext;
3134
import org.springframework.http.HttpMethod;
3235
import org.springframework.http.HttpStatus;
3336
import org.springframework.http.ResponseEntity;
34-
import org.springframework.web.client.RestClientException;
3537
import org.springframework.web.client.RestTemplate;
3638

3739
import java.io.IOException;
3840
import java.io.InputStream;
3941
import java.net.URI;
42+
import java.util.Collections;
43+
import java.util.List;
4044
import java.util.Locale;
4145

4246
import static com.gooddata.util.Validate.notEmpty;
@@ -47,7 +51,7 @@
4751
*/
4852
public class DataStoreService {
4953

50-
private final Sardine sardine;
54+
private final GdcSardine sardine;
5155
private final GdcService gdcService;
5256
private final URI gdcUri;
5357
private final RestTemplate restTemplate;
@@ -102,7 +106,10 @@ public void upload(String path, InputStream stream) {
102106

103107
private void upload(URI url, InputStream stream) {
104108
try {
105-
sardine.put(url.toString(), stream);
109+
// We need to use it this way, if we want to track request_id in the stacktrace.
110+
InputStreamEntity entity = new InputStreamEntity(stream);
111+
List<Header> headers = Collections.singletonList(new BasicHeader(HTTP.EXPECT_DIRECTIVE, HTTP.EXPECT_CONTINUE));
112+
sardine.put(url.toString(), entity, headers, new GdcSardineResponseHandler());
106113
} catch (SardineException e) {
107114
if (HttpStatus.INTERNAL_SERVER_ERROR.value() == e.getStatusCode()) {
108115
// this error may occur when user issues request to WebDAV before SST and TT were obtained
@@ -144,7 +151,7 @@ public InputStream download(String path) {
144151
notEmpty(path, "path");
145152
final URI uri = getUri(path);
146153
try {
147-
return sardine.get(uri.toString());
154+
return sardine.get(uri.toString(), Collections.emptyList(), new GdcSardineResponseHandler());
148155
} catch (IOException e) {
149156
throw new DataStoreException("Unable to download from " + uri, e);
150157
}
@@ -165,7 +172,7 @@ public void delete(String path) {
165172
if (HttpStatus.MOVED_PERMANENTLY.equals(result.getStatusCode())) {
166173
restTemplate.exchange(result.getHeaders().getLocation(), HttpMethod.DELETE, org.springframework.http.HttpEntity.EMPTY, Void.class);
167174
}
168-
} catch (RestClientException e) {
175+
} catch (GoodDataRestException e) {
169176
throw new DataStoreException("Unable to delete " + uri, e);
170177
}
171178
}

src/main/java/com/gooddata/gdc/GdcSardine.java

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,18 @@
88
import static com.gooddata.util.Validate.notNull;
99

1010
import com.github.sardine.impl.SardineImpl;
11+
import com.github.sardine.impl.io.ContentLengthInputStream;
12+
import com.github.sardine.impl.io.HttpMethodReleaseInputStream;
13+
import org.apache.http.Header;
14+
import org.apache.http.HttpResponse;
1115
import org.apache.http.client.ResponseHandler;
16+
import org.apache.http.client.methods.HttpGet;
1217
import org.apache.http.client.methods.HttpRequestBase;
1318
import org.apache.http.impl.client.HttpClientBuilder;
1419

1520
import java.io.IOException;
21+
import java.util.List;
22+
import java.util.Map;
1623

1724
/**
1825
* This class extends SardineImpl, connections were not correctly closed by parent
@@ -28,11 +35,41 @@ public GdcSardine(HttpClientBuilder builder) {
2835
*/
2936
@Override
3037
protected <T> T execute(HttpRequestBase request, ResponseHandler<T> responseHandler) throws IOException {
31-
notNull(request,"request");
38+
notNull(request, "request");
3239
try {
3340
return super.execute(request, responseHandler);
3441
} finally {
3542
request.releaseConnection();
3643
}
3744
}
45+
46+
/**
47+
* The method body is retrieved from {@link SardineImpl#get(String, Map)} and extended about the response handler
48+
* to be able to handle responses arbitrarily.
49+
*
50+
* @param url Path to the resource including protocol and hostname
51+
* @param headers Additional HTTP headers to add to the request
52+
* @param responseHandler Arbitrary response handler to manipulate with responses
53+
* @return Data stream to read from
54+
* @throws IOException I/O error or HTTP response validation failure
55+
*/
56+
public <T> ContentLengthInputStream get(final String url, final List<Header> headers, final
57+
ResponseHandler<T> responseHandler) throws IOException {
58+
59+
final HttpGet get = new HttpGet(url);
60+
for (Header header : headers) {
61+
get.addHeader(header);
62+
}
63+
// Must use #execute without handler, otherwise the entity is consumed
64+
// already after the handler exits.
65+
final HttpResponse response = this.execute(get);
66+
try {
67+
responseHandler.handleResponse(response);
68+
// Will abort the read when closed before EOF.
69+
return new ContentLengthInputStream(new HttpMethodReleaseInputStream(response), response.getEntity().getContentLength());
70+
} catch (IOException ex) {
71+
get.abort();
72+
throw ex;
73+
}
74+
}
3875
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright (C) 2007-2021, GoodData(R) Corporation. All rights reserved.
3+
*/
4+
package com.gooddata.gdc;
5+
6+
import com.github.sardine.impl.SardineException;
7+
8+
/**
9+
* Extended Sardine exception about X-GDC-REQUEST header value.
10+
*/
11+
public class GdcSardineException extends SardineException {
12+
13+
private final String requestId;
14+
15+
/**
16+
* @param msg Custom description of failure
17+
* @param statusCode Error code returned by server
18+
* @param responsePhrase Response phrase following the error code
19+
* @param requestId The X-GDC-REQUEST identifier.
20+
*/
21+
public GdcSardineException(String requestId, String msg, int statusCode, String responsePhrase) {
22+
super(msg, statusCode, responsePhrase);
23+
this.requestId = requestId;
24+
}
25+
26+
@Override
27+
public String getMessage() {
28+
return String.format("[request_id=%s]: %s (%d %s)", this.requestId, super.getMessage(), this.getStatusCode(),
29+
this.getResponsePhrase()
30+
);
31+
}
32+
33+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright (C) 2004-2021, GoodData(R) Corporation. All rights reserved.
3+
* This source code is licensed under the BSD-style license found in the
4+
* LICENSE.txt file in the root directory of this source tree.
5+
*/
6+
package com.gooddata.gdc;
7+
8+
import com.github.sardine.impl.SardineException;
9+
import org.apache.http.Header;
10+
import org.apache.http.HttpResponse;
11+
import org.apache.http.HttpStatus;
12+
import org.apache.http.StatusLine;
13+
import org.apache.http.client.ResponseHandler;
14+
15+
import java.io.IOException;
16+
17+
import static com.gooddata.gdc.Header.GDC_REQUEST_ID;
18+
19+
20+
/**
21+
* A basic validation response handler that extends the functionality of {@link com.github.sardine.impl.handler.ValidatingResponseHandler}
22+
* about the addition of X-GDC-REQUEST header to exception.
23+
*/
24+
class GdcSardineResponseHandler implements ResponseHandler<Void> {
25+
26+
@Override
27+
public Void handleResponse(final HttpResponse response) throws IOException {
28+
final StatusLine statusLine = response.getStatusLine();
29+
final int statusCode = statusLine.getStatusCode();
30+
if (statusCode >= HttpStatus.SC_OK && statusCode < HttpStatus.SC_MULTIPLE_CHOICES) {
31+
return null;
32+
}
33+
final Header requestIdHeader = response.getFirstHeader(GDC_REQUEST_ID);
34+
if (requestIdHeader != null) {
35+
throw new GdcSardineException(requestIdHeader.getValue(), "Unexpected response", statusLine.getStatusCode(),
36+
statusLine.getReasonPhrase()
37+
);
38+
} else {
39+
throw new SardineException("Unexpected response", statusLine.getStatusCode(), statusLine.getReasonPhrase());
40+
}
41+
}
42+
}

src/test/java/com/gooddata/gdc/DatastoreServiceAT.java

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@
1818
import java.util.UUID;
1919

2020
import static com.gooddata.util.ResourceUtils.readFromResource;
21-
import static org.hamcrest.CoreMatchers.equalTo;
21+
import static org.hamcrest.CoreMatchers.containsString;
22+
import static org.hamcrest.CoreMatchers.instanceOf;
2223
import static org.hamcrest.CoreMatchers.is;
24+
import static org.hamcrest.CoreMatchers.equalTo;
2325
import static org.hamcrest.MatcherAssert.assertThat;
24-
import static org.testng.Assert.assertEquals;
2526
import static org.testng.Assert.fail;
2627

2728
/**
@@ -60,6 +61,17 @@ public void datastoreDownload() throws Exception {
6061
}
6162

6263
@Test(groups = "datastore", dependsOnMethods = "datastoreDownload")
64+
public void verifyRequestIdInException() throws Exception {
65+
DataStoreService dataStoreService = AbstractGoodDataAT.gd.getDataStoreService();
66+
67+
try (InputStream ignored = dataStoreService.download(this.directory + "/none.txt")) {
68+
fail("The exception should contain the request_id in its stacktrace.");
69+
} catch (Exception e){
70+
assertThat(e.getCause().toString(), containsString("request_id"));
71+
}
72+
}
73+
74+
@Test(groups = "datastore", dependsOnMethods = "verifyRequestIdInException")
6375
public void datastoreDelete() throws Exception {
6476
DataStoreService dataStoreService = gd.getDataStoreService();
6577
dataStoreService.delete(this.file);
@@ -68,8 +80,10 @@ public void datastoreDelete() throws Exception {
6880
try {
6981
dataStoreService.delete(this.directory);
7082
fail("Exception was expected, as there is nothing to delete");
71-
} catch (GoodDataRestException e) {
72-
assertEquals(404, e.getStatusCode());
83+
} catch (DataStoreException e) {
84+
assertThat(e.getCause().toString(), containsString("request_id"));
85+
assertThat(e.getCause().toString(), containsString("404"));
86+
assertThat(e.getCause(), instanceOf(GoodDataRestException.class));
7387
}
7488
}
7589

0 commit comments

Comments
 (0)