-
Notifications
You must be signed in to change notification settings - Fork 96
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support passing X-Request-ID to Druid #68
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -26,6 +26,7 @@ | |
import java.util.List; | ||
import java.util.Map.Entry; | ||
import java.util.Set; | ||
import java.util.regex.Pattern; | ||
|
||
import javax.annotation.Priority; | ||
import javax.inject.Singleton; | ||
|
@@ -68,10 +69,12 @@ public class BardLoggingFilter implements ContainerRequestFilter, ContainerRespo | |
private static final String PROPERTY_NANOS = LoggingFilter.class.getName() + ".nanos"; | ||
private static final String PROPERTY_REQ_LEN = LoggingFilter.class.getName() + ".reqlen"; | ||
private static final String PROPERTY_OUTPUT_STREAM = LoggingFilter.class.getName() + ".ostream"; | ||
private static final Pattern VALID_REQUEST_ID = Pattern.compile("[a-zA-Z0-9+/=-]+"); | ||
|
||
public static final double MILLISECONDS_PER_NANOSECOND = 1000000.0; | ||
public static final String TOTAL_TIMER = "TotalTime"; | ||
public static final String CLIENT_TOTAL_TIMER = "ClientRequestTotalTime"; | ||
public static final String X_REQUEST_ID_HEADER = "x-request-id"; | ||
|
||
/** | ||
* Intercept the Container request to add length of request and a start timestamp. | ||
|
@@ -83,6 +86,7 @@ public class BardLoggingFilter implements ContainerRequestFilter, ContainerRespo | |
@Override | ||
public void filter(ContainerRequestContext request) throws IOException { | ||
|
||
appendRequestId(request.getHeaders().getFirst(X_REQUEST_ID_HEADER)); | ||
RequestLog.startTiming(TOTAL_TIMER); | ||
RequestLog.startTiming(this); | ||
RequestLog.record(new Preface(request)); | ||
|
@@ -108,6 +112,7 @@ public void filter(ContainerRequestContext request) throws IOException { | |
@Override | ||
public void filter(ContainerRequestContext request, ContainerResponseContext response) | ||
throws IOException { | ||
appendRequestId(request.getHeaders().getFirst(X_REQUEST_ID_HEADER)); | ||
RequestLog.startTiming(this); | ||
StringBuilder debugMsgBuilder = new StringBuilder(); | ||
|
||
|
@@ -168,6 +173,7 @@ public void filter(ContainerRequestContext request, ContainerResponseContext res | |
*/ | ||
@Override | ||
public void filter(ClientRequestContext request) throws IOException { | ||
appendRequestId(request.getStringHeaders().getFirst(X_REQUEST_ID_HEADER)); | ||
RequestLog.startTiming(CLIENT_TOTAL_TIMER); | ||
request.setProperty(PROPERTY_NANOS, System.nanoTime()); | ||
} | ||
|
@@ -198,6 +204,30 @@ public void filter(ClientRequestContext request, ClientResponseContext response) | |
RequestLog.stopTiming(CLIENT_TOTAL_TIMER); | ||
} | ||
|
||
/** | ||
* Add a request id to pass to druid. Only added if x-request-id follows the format specified at: | ||
* https://devcenter.heroku.com/articles/http-request-id | ||
* | ||
* @param requestId Request id to add as queryId prefix to druid | ||
*/ | ||
private void appendRequestId(String requestId) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be good to have a test for this, validating the rules. |
||
if (isValidRequestId(requestId)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't understand this line. We are ignoring a requestId if it is a valid request id? Wouldn't we want to ignore the request id if it isn't valid? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch. I think the behavior is correct (yay testing) the name is just wrong. @DeathByTape, could you open a quick PR to correct this? |
||
return; // Ignore according to https://devcenter.heroku.com/articles/http-request-id | ||
} | ||
RequestLog.addIdPrefix(requestId); | ||
} | ||
|
||
/** | ||
* Validate whether or not a string is acceptable as an x-request-id header argument. | ||
* | ||
* @param requestId Request id to validate | ||
* @return True if valid, false otherwise | ||
*/ | ||
private boolean isValidRequestId(String requestId) { | ||
return requestId == null || requestId.isEmpty() || requestId.length() > 200 | ||
|| !VALID_REQUEST_ID.matcher(requestId).matches(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks like this method is actually returning the opposite of what it claims. It's returning |
||
} | ||
|
||
/** | ||
* Render the URI as a string. | ||
* | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
// Copyright 2016 Yahoo Inc. | ||
// Licensed under the terms of the Apache license. Please see LICENSE.md file distributed with this work for terms. | ||
package com.yahoo.bard.webservice.web.endpoints | ||
|
||
import com.yahoo.bard.webservice.data.dimension.BardDimensionField | ||
import com.yahoo.bard.webservice.data.dimension.DimensionDictionary | ||
import com.yahoo.bard.webservice.util.JsonSlurper | ||
import com.yahoo.bard.webservice.util.JsonSortStrategy | ||
import com.yahoo.bard.webservice.web.filters.BardLoggingFilter | ||
import org.apache.commons.lang.StringUtils | ||
|
||
import javax.ws.rs.core.MultivaluedHashMap | ||
|
||
|
||
class RequestIdPrefixesDruidQueryIdSpec extends BaseDataServletComponentSpec { | ||
def prefixId = UUID.randomUUID().toString(); | ||
|
||
@Override | ||
def setup() { | ||
DimensionDictionary dimensionStore = jtb.configurationLoader.dimensionDictionary | ||
dimensionStore.findByApiName("color").with { | ||
addDimensionRow(BardDimensionField.makeDimensionRow(it, "Foo", "FooDesc")) | ||
addDimensionRow(BardDimensionField.makeDimensionRow(it, "Bar", "BarDesc")) | ||
addDimensionRow(BardDimensionField.makeDimensionRow(it, "Baz", "BazDesc")) | ||
} | ||
} | ||
|
||
@Override | ||
Class<?>[] getResourceClasses() { | ||
[ DataServlet.class, BardLoggingFilter.class ] | ||
} | ||
|
||
@Override | ||
String getTarget() { | ||
return "data/shapes/week/color" | ||
} | ||
|
||
@Override | ||
Map<String, List<String>> getQueryParams() { | ||
[ | ||
"metrics": ["width","depth"], | ||
"dateTime": ["2014-06-02/2014-06-30"], | ||
"sort": ["width|desc","depth|asc"] | ||
] | ||
} | ||
|
||
@Override | ||
boolean compareResult(String result, String expectedResult, JsonSortStrategy sortStrategy = JsonSortStrategy.SORT_MAPS) { | ||
def parsedJson = new JsonSlurper(sortStrategy).parseText(result) | ||
if (parsedJson.context != null) { | ||
return parsedJson.context.queryId.startsWith(prefixId) | ||
} | ||
// Default to true-- the base spec runs an extra test which we consider a noop. | ||
return true | ||
} | ||
|
||
@Override | ||
String getExpectedDruidQuery() { | ||
// Noop | ||
"""{}""" | ||
} | ||
|
||
@Override | ||
String getFakeDruidResponse() { | ||
// Noop | ||
"""[]""" | ||
} | ||
|
||
@Override | ||
String getExpectedApiResponse() { | ||
// Noop | ||
"""{}""" | ||
} | ||
|
||
@Override | ||
MultivaluedHashMap<String, String> getAdditionalApiRequestHeaders() { | ||
return ["x-request-id": prefixId] | ||
} | ||
|
||
def "verify invalid x-request-id values"() { | ||
BardLoggingFilter filter = new BardLoggingFilter() | ||
assert !filter.isValidRequestId('abcd$') // Invalid char | ||
assert !filter.isValidRequestId(StringUtils.leftPad('a', 200, 'a')) // Too long | ||
assert !filter.isValidRequestId('') // empty string | ||
assert !filter.isValidRequestId(null) // null | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While this will update the
current.logId
field (and the MDC value), I don't think it will update the UUID that is part of theRequestLog
payload. I think that theLogInfo.uuid
field (on the object stored incurrent.info
) needs to be updated as well in order for that to happen.I think the reason this was missed is because we're holding that UUID in 2 places
current.logId
(we read from that all over), and in theLogInfo.uuid
field (current.info
). It would be great if we could get rid of thecurrent.logId
field and only use the value from theLogInfo
object. This refactor isn't required for this PR, but it would be great if we could pull it in sooner, rather than later.As part of the change around where the logId is accessed from, we should update
getId
to pull from the single source of truth as well.Also, since this is the closest I can get to it, it would be cool (if we do this refactoring) to move the
MDC.put
call in theinit
method up much higher, so that there's less of a gap between getting a UUID and setting it into MCD.