Skip to content

Commit da72954

Browse files
author
gdgate
authored
Merge pull request #1033 from daniel-rusnak/621-gh-issue-port-2.35
#621 - add the logging of requestId (port to 2.35.*) Reviewed-by: Jiri Mikulasek <jiri.mikulasek@gooddata.com> https://github.com/jimirocks
2 parents f8f61e1 + 9705937 commit da72954

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)