Add query/time metric for SQL queries from router (#12867)

* Add query/time metric for SQL queries from router

* Fix query cancel bug when user has overriden native query-id in a SQL query
This commit is contained in:
Rohan Garg 2022-09-07 13:54:46 +05:30 committed by GitHub
parent ee22663dd3
commit 7aa8d7f987
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 348 additions and 44 deletions

View File

@ -39,6 +39,11 @@ Metrics may have additional dimensions beyond those listed above.
## Query metrics ## Query metrics
### Router
|Metric|Description|Dimensions|Normal Value|
|------|-----------|----------|------------|
|`query/time`|Milliseconds taken to complete a query.|Native Query: dataSource, type, interval, hasFilters, duration, context, remoteAddress, id.|< 1s|
### Broker ### Broker
|Metric|Description|Dimensions|Normal Value| |Metric|Description|Dimensions|Normal Value|

View File

@ -46,4 +46,9 @@ public class DefaultGenericQueryMetricsFactory implements GenericQueryMetricsFac
return queryMetrics; return queryMetrics;
} }
@Override
public QueryMetrics<Query<?>> makeMetrics()
{
return new DefaultQueryMetrics<>();
}
} }

View File

@ -132,6 +132,12 @@ public class DefaultQueryMetrics<QueryType extends Query<?>> implements QueryMet
setDimension(DruidMetrics.ID, StringUtils.nullToEmptyNonDruidDataString(query.getId())); setDimension(DruidMetrics.ID, StringUtils.nullToEmptyNonDruidDataString(query.getId()));
} }
@Override
public void queryId(String queryId)
{
setDimension(DruidMetrics.ID, StringUtils.nullToEmptyNonDruidDataString(queryId));
}
@Override @Override
public void subQueryId(QueryType query) public void subQueryId(QueryType query)
{ {
@ -144,6 +150,12 @@ public class DefaultQueryMetrics<QueryType extends Query<?>> implements QueryMet
// Emit nothing by default. // Emit nothing by default.
} }
@Override
public void sqlQueryId(String sqlQueryId)
{
// Emit nothing by default.
}
@Override @Override
public void context(QueryType query) public void context(QueryType query)
{ {

View File

@ -46,4 +46,11 @@ public interface GenericQueryMetricsFactory
* call {@link QueryMetrics#query(Query)} with the given query on the created QueryMetrics object before returning. * call {@link QueryMetrics#query(Query)} with the given query on the created QueryMetrics object before returning.
*/ */
QueryMetrics<Query<?>> makeMetrics(Query<?> query); QueryMetrics<Query<?>> makeMetrics(Query<?> query);
/**
* Creates a {@link QueryMetrics} which doesn't have predefined QueryMetrics subclass. This method is used
* by the router to build a {@link QueryMetrics} for SQL queries. It is needed since at router, there is no native
* query linked to a SQL query.
*/
QueryMetrics<Query<?>> makeMetrics();
} }

View File

@ -202,6 +202,12 @@ public interface QueryMetrics<QueryType extends Query<?>>
@PublicApi @PublicApi
void queryId(QueryType query); void queryId(QueryType query);
/**
* Sets id of the given query as dimension.
*/
@PublicApi
void queryId(@SuppressWarnings("UnusedParameters") String queryId);
/** /**
* Sets {@link Query#getSubQueryId()} of the given query as dimension. * Sets {@link Query#getSubQueryId()} of the given query as dimension.
*/ */
@ -214,6 +220,12 @@ public interface QueryMetrics<QueryType extends Query<?>>
@PublicApi @PublicApi
void sqlQueryId(QueryType query); void sqlQueryId(QueryType query);
/**
* Sets sqlQueryId as a dimension
*/
@PublicApi
void sqlQueryId(@SuppressWarnings("UnusedParameters") String sqlQueryId);
/** /**
* Sets {@link Query#getContext()} of the given query as dimension. * Sets {@link Query#getContext()} of the given query as dimension.
*/ */

View File

@ -87,6 +87,12 @@ public class DefaultSearchQueryMetrics implements SearchQueryMetrics
throw new ISE("Unsupported method in default query metrics implementation."); throw new ISE("Unsupported method in default query metrics implementation.");
} }
@Override
public void queryId(String queryId)
{
throw new ISE("Unsupported method in default query metrics implementation.");
}
@Override @Override
public void subQueryId(SearchQuery query) public void subQueryId(SearchQuery query)
{ {
@ -99,6 +105,12 @@ public class DefaultSearchQueryMetrics implements SearchQueryMetrics
throw new ISE("Unsupported method in default query metrics implementation."); throw new ISE("Unsupported method in default query metrics implementation.");
} }
@Override
public void sqlQueryId(String sqlQueryId)
{
throw new ISE("Unsupported method in default query metrics implementation.");
}
@Override @Override
public void granularity(SearchQuery query) public void granularity(SearchQuery query)
{ {

View File

@ -67,6 +67,10 @@ public class DefaultQueryMetricsTest
.build(); .build();
queryMetrics.query(query); queryMetrics.query(query);
queryMetrics.reportQueryTime(0).emit(serviceEmitter); queryMetrics.reportQueryTime(0).emit(serviceEmitter);
// No way to verify this right now since DefaultQueryMetrics implements a no-op for sqlQueryId(String) and queryId(String)
// This change is done to keep the code coverage tool happy by exercising the implementation
queryMetrics.sqlQueryId("dummy");
queryMetrics.queryId("dummy");
Map<String, Object> actualEvent = cachingEmitter.getLastEmittedEvent().toMap(); Map<String, Object> actualEvent = cachingEmitter.getLastEmittedEvent().toMap();
Assert.assertEquals(13, actualEvent.size()); Assert.assertEquals(13, actualEvent.size());
Assert.assertTrue(actualEvent.containsKey("feed")); Assert.assertTrue(actualEvent.containsKey("feed"));
@ -81,7 +85,7 @@ public class DefaultQueryMetricsTest
Assert.assertEquals(expectedStringIntervals, actualEvent.get(DruidMetrics.INTERVAL)); Assert.assertEquals(expectedStringIntervals, actualEvent.get(DruidMetrics.INTERVAL));
Assert.assertEquals("true", actualEvent.get("hasFilters")); Assert.assertEquals("true", actualEvent.get("hasFilters"));
Assert.assertEquals(expectedIntervals.get(0).toDuration().toString(), actualEvent.get("duration")); Assert.assertEquals(expectedIntervals.get(0).toDuration().toString(), actualEvent.get("duration"));
Assert.assertEquals("", actualEvent.get(DruidMetrics.ID)); Assert.assertEquals("dummy", actualEvent.get(DruidMetrics.ID));
Assert.assertEquals("query/time", actualEvent.get("metric")); Assert.assertEquals("query/time", actualEvent.get("metric"));
Assert.assertEquals(0L, actualEvent.get("value")); Assert.assertEquals(0L, actualEvent.get("value"));
Assert.assertEquals(ImmutableMap.of("testKey", "testValue"), actualEvent.get("context")); Assert.assertEquals(ImmutableMap.of("testKey", "testValue"), actualEvent.get("context"));

View File

@ -21,6 +21,7 @@ package org.apache.druid.query.search;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import org.apache.druid.java.util.common.ISE;
import org.apache.druid.java.util.emitter.service.ServiceEmitter; import org.apache.druid.java.util.emitter.service.ServiceEmitter;
import org.apache.druid.query.CachingEmitter; import org.apache.druid.query.CachingEmitter;
import org.apache.druid.query.DefaultQueryMetricsTest; import org.apache.druid.query.DefaultQueryMetricsTest;
@ -86,6 +87,8 @@ public class DefaultSearchQueryMetricsTest
// Metric // Metric
Assert.assertEquals("query/time", actualEvent.get("metric")); Assert.assertEquals("query/time", actualEvent.get("metric"));
Assert.assertEquals(0L, actualEvent.get("value")); Assert.assertEquals(0L, actualEvent.get("value"));
Assert.assertThrows(ISE.class, () -> queryMetrics.sqlQueryId("dummy"));
} }
@Test @Test

View File

@ -98,6 +98,7 @@ public class QueryResource implements QueryCountStatsProvider
*/ */
public static final String HEADER_RESPONSE_CONTEXT = "X-Druid-Response-Context"; public static final String HEADER_RESPONSE_CONTEXT = "X-Druid-Response-Context";
public static final String HEADER_IF_NONE_MATCH = "If-None-Match"; public static final String HEADER_IF_NONE_MATCH = "If-None-Match";
public static final String QUERY_ID_RESPONSE_HEADER = "X-Druid-Query-Id";
public static final String HEADER_ETAG = "ETag"; public static final String HEADER_ETAG = "ETag";
protected final QueryLifecycleFactory queryLifecycleFactory; protected final QueryLifecycleFactory queryLifecycleFactory;
@ -252,7 +253,7 @@ public class QueryResource implements QueryCountStatsProvider
}, },
ioReaderWriter.getResponseWriter().getResponseType() ioReaderWriter.getResponseWriter().getResponseType()
) )
.header("X-Druid-Query-Id", queryId); .header(QUERY_ID_RESPONSE_HEADER, queryId);
transferEntityTag(responseContext, responseBuilder); transferEntityTag(responseContext, responseBuilder);

View File

@ -39,6 +39,7 @@ import org.apache.druid.java.util.common.IAE;
import org.apache.druid.java.util.common.jackson.JacksonUtils; import org.apache.druid.java.util.common.jackson.JacksonUtils;
import org.apache.druid.java.util.emitter.EmittingLogger; import org.apache.druid.java.util.emitter.EmittingLogger;
import org.apache.druid.java.util.emitter.service.ServiceEmitter; import org.apache.druid.java.util.emitter.service.ServiceEmitter;
import org.apache.druid.query.BaseQuery;
import org.apache.druid.query.DruidMetrics; import org.apache.druid.query.DruidMetrics;
import org.apache.druid.query.GenericQueryMetricsFactory; import org.apache.druid.query.GenericQueryMetricsFactory;
import org.apache.druid.query.Query; import org.apache.druid.query.Query;
@ -56,6 +57,7 @@ import org.apache.druid.server.security.AuthenticationResult;
import org.apache.druid.server.security.Authenticator; import org.apache.druid.server.security.Authenticator;
import org.apache.druid.server.security.AuthenticatorMapper; import org.apache.druid.server.security.AuthenticatorMapper;
import org.apache.druid.sql.http.SqlQuery; import org.apache.druid.sql.http.SqlQuery;
import org.apache.druid.sql.http.SqlResource;
import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.api.Response; import org.eclipse.jetty.client.api.Response;
@ -65,12 +67,14 @@ import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.proxy.AsyncProxyServlet; import org.eclipse.jetty.proxy.AsyncProxyServlet;
import javax.annotation.Nullable;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.Response.Status;
import java.io.IOException; import java.io.IOException;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Properties; import java.util.Properties;
import java.util.UUID; import java.util.UUID;
@ -266,11 +270,16 @@ public class AsyncQueryForwardingServlet extends AsyncProxyServlet implements Qu
handleException(response, objectMapper, e); handleException(response, objectMapper, e);
return; return;
} }
} else if (routeSqlByStrategy && isSqlQueryEndpoint && HttpMethod.POST.is(method)) { } else if (isSqlQueryEndpoint && HttpMethod.POST.is(method)) {
try { try {
SqlQuery inputSqlQuery = objectMapper.readValue(request.getInputStream(), SqlQuery.class); SqlQuery inputSqlQuery = objectMapper.readValue(request.getInputStream(), SqlQuery.class);
inputSqlQuery = buildSqlQueryWithId(inputSqlQuery);
request.setAttribute(SQL_QUERY_ATTRIBUTE, inputSqlQuery); request.setAttribute(SQL_QUERY_ATTRIBUTE, inputSqlQuery);
targetServer = hostFinder.findServerSql(inputSqlQuery); if (routeSqlByStrategy) {
targetServer = hostFinder.findServerSql(inputSqlQuery);
} else {
targetServer = hostFinder.pickDefaultServer();
}
LOG.debug("Forwarding SQL query to broker [%s]", targetServer.getHost()); LOG.debug("Forwarding SQL query to broker [%s]", targetServer.getHost());
} }
catch (IOException e) { catch (IOException e) {
@ -292,6 +301,22 @@ public class AsyncQueryForwardingServlet extends AsyncProxyServlet implements Qu
doService(request, response); doService(request, response);
} }
/**
* Rebuilds the {@link SqlQuery} object with sqlQueryId and queryId context parameters if not present
* @param sqlQuery the original SqlQuery
* @return an updated sqlQuery object with sqlQueryId and queryId context parameters
*/
private SqlQuery buildSqlQueryWithId(SqlQuery sqlQuery)
{
Map<String, Object> context = new HashMap<>(sqlQuery.getContext());
String sqlQueryId = (String) context.getOrDefault(BaseQuery.SQL_QUERY_ID, UUID.randomUUID().toString());
// set queryId to sqlQueryId if not overridden
String queryId = (String) context.getOrDefault(BaseQuery.QUERY_ID, sqlQueryId);
context.put(BaseQuery.SQL_QUERY_ID, sqlQueryId);
context.put(BaseQuery.QUERY_ID, queryId);
return sqlQuery.withOverridenContext(context);
}
/** /**
* Issues async query cancellation requests to all Brokers (except the given * Issues async query cancellation requests to all Brokers (except the given
* targetServer). Query cancellation on the targetServer is handled by the * targetServer). Query cancellation on the targetServer is handled by the
@ -449,12 +474,15 @@ public class AsyncQueryForwardingServlet extends AsyncProxyServlet implements Qu
@Override @Override
protected Response.Listener newProxyResponseListener(HttpServletRequest request, HttpServletResponse response) protected Response.Listener newProxyResponseListener(HttpServletRequest request, HttpServletResponse response)
{ {
final Query query = (Query) request.getAttribute(QUERY_ATTRIBUTE); boolean isJDBC = request.getAttribute(AVATICA_QUERY_ATTRIBUTE) != null;
if (query != null) { return newMetricsEmittingProxyResponseListener(
return newMetricsEmittingProxyResponseListener(request, response, query, System.nanoTime()); request,
} else { response,
return super.newProxyResponseListener(request, response); (Query) request.getAttribute(QUERY_ATTRIBUTE),
} (SqlQuery) request.getAttribute(SQL_QUERY_ATTRIBUTE),
isJDBC,
System.nanoTime()
);
} }
@Override @Override
@ -500,11 +528,13 @@ public class AsyncQueryForwardingServlet extends AsyncProxyServlet implements Qu
private Response.Listener newMetricsEmittingProxyResponseListener( private Response.Listener newMetricsEmittingProxyResponseListener(
HttpServletRequest request, HttpServletRequest request,
HttpServletResponse response, HttpServletResponse response,
Query query, @Nullable Query query,
@Nullable SqlQuery sqlQuery,
boolean isJDBC,
long startNs long startNs
) )
{ {
return new MetricsEmittingProxyResponseListener(request, response, query, startNs); return new MetricsEmittingProxyResponseListener(request, response, query, sqlQuery, isJDBC, startNs);
} }
@Override @Override
@ -660,22 +690,28 @@ public class AsyncQueryForwardingServlet extends AsyncProxyServlet implements Qu
private class MetricsEmittingProxyResponseListener<T> extends ProxyResponseListener private class MetricsEmittingProxyResponseListener<T> extends ProxyResponseListener
{ {
private final HttpServletRequest req; private final HttpServletRequest req;
private final HttpServletResponse res; @Nullable
private final Query<T> query; private final Query<T> query;
@Nullable
private final SqlQuery sqlQuery;
private final boolean isJDBC;
private final long startNs; private final long startNs;
public MetricsEmittingProxyResponseListener( public MetricsEmittingProxyResponseListener(
HttpServletRequest request, HttpServletRequest request,
HttpServletResponse response, HttpServletResponse response,
Query<T> query, @Nullable Query<T> query,
@Nullable SqlQuery sqlQuery,
boolean isJDBC,
long startNs long startNs
) )
{ {
super(request, response); super(request, response);
this.req = request; this.req = request;
this.res = response;
this.query = query; this.query = query;
this.sqlQuery = sqlQuery;
this.isJDBC = isJDBC;
this.startNs = startNs; this.startNs = startNs;
} }
@ -683,14 +719,63 @@ public class AsyncQueryForwardingServlet extends AsyncProxyServlet implements Qu
public void onComplete(Result result) public void onComplete(Result result)
{ {
final long requestTimeNs = System.nanoTime() - startNs; final long requestTimeNs = System.nanoTime() - startNs;
try { String queryId = null;
boolean success = result.isSucceeded(); String sqlQueryId = null;
if (success) { if (isJDBC) {
successfulQueryCount.incrementAndGet(); sqlQueryId = result.getResponse().getHeaders().get(SqlResource.SQL_QUERY_ID_RESPONSE_HEADER);
} else { } else if (sqlQuery != null) {
failedQueryCount.incrementAndGet(); sqlQueryId = (String) sqlQuery.getContext().getOrDefault(BaseQuery.SQL_QUERY_ID, null);
queryId = (String) sqlQuery.getContext().getOrDefault(BaseQuery.QUERY_ID, null);
} else if (query != null) {
queryId = query.getId();
}
// not a native or SQL query, no need to emit metrics and logs
if (queryId == null && sqlQueryId == null) {
super.onComplete(result);
return;
}
boolean success = result.isSucceeded();
if (success) {
successfulQueryCount.incrementAndGet();
} else {
failedQueryCount.incrementAndGet();
}
emitQueryTime(requestTimeNs, success, sqlQueryId, queryId);
//noinspection VariableNotUsedInsideIf
if (sqlQueryId != null) {
// SQL query doesn't have a native query translation in router. Hence, not logging the native query.
if (sqlQuery != null) {
try {
requestLogger.logSqlQuery(
RequestLogLine.forSql(
sqlQuery.getQuery(),
sqlQuery.getContext(),
DateTimes.nowUtc(),
req.getRemoteAddr(),
new QueryStats(
ImmutableMap.of(
"query/time",
TimeUnit.NANOSECONDS.toMillis(requestTimeNs),
"success",
success
&& result.getResponse().getStatus() == Status.OK.getStatusCode()
)
)
)
);
}
catch (IOException e) {
LOG.error(e, "Unable to log SQL query [%s]!", sqlQuery);
}
} }
emitQueryTime(requestTimeNs, success); super.onComplete(result);
return;
}
try {
requestLogger.logNativeQuery( requestLogger.logNativeQuery(
RequestLogLine.forNative( RequestLogLine.forNative(
query, query,
@ -718,10 +803,64 @@ public class AsyncQueryForwardingServlet extends AsyncProxyServlet implements Qu
@Override @Override
public void onFailure(Response response, Throwable failure) public void onFailure(Response response, Throwable failure)
{ {
final long requestTimeNs = System.nanoTime() - startNs;
final String errorMessage = failure.getMessage();
String queryId = null;
String sqlQueryId = null;
if (isJDBC) {
sqlQueryId = response.getHeaders().get(SqlResource.SQL_QUERY_ID_RESPONSE_HEADER);
} else if (sqlQuery != null) {
sqlQueryId = (String) sqlQuery.getContext().getOrDefault(BaseQuery.SQL_QUERY_ID, null);
queryId = (String) sqlQuery.getContext().getOrDefault(BaseQuery.QUERY_ID, null);
} else if (query != null) {
queryId = query.getId();
}
// not a native or SQL query, no need to emit metrics and logs
if (queryId == null && sqlQueryId == null) {
super.onFailure(response, failure);
return;
}
failedQueryCount.incrementAndGet();
emitQueryTime(requestTimeNs, false, sqlQueryId, queryId);
//noinspection VariableNotUsedInsideIf
if (sqlQueryId != null) {
// SQL query doesn't have a native query translation in router. Hence, not logging the native query.
if (sqlQuery != null) {
try {
requestLogger.logSqlQuery(
RequestLogLine.forSql(
sqlQuery.getQuery(),
sqlQuery.getContext(),
DateTimes.nowUtc(),
req.getRemoteAddr(),
new QueryStats(
ImmutableMap.of(
"success",
false,
"exception",
errorMessage == null ? "no message" : errorMessage
)
)
)
);
}
catch (IOException e) {
LOG.error(e, "Unable to log SQL query [%s]!", sqlQuery);
}
LOG.makeAlert(failure, "Exception handling request")
.addData("exception", failure.toString())
.addData("sqlQuery", sqlQuery)
.addData("peer", req.getRemoteAddr())
.emit();
}
super.onFailure(response, failure);
return;
}
try { try {
final String errorMessage = failure.getMessage();
failedQueryCount.incrementAndGet();
emitQueryTime(System.nanoTime() - startNs, false);
requestLogger.logNativeQuery( requestLogger.logNativeQuery(
RequestLogLine.forNative( RequestLogLine.forNative(
query, query,
@ -751,14 +890,25 @@ public class AsyncQueryForwardingServlet extends AsyncProxyServlet implements Qu
super.onFailure(response, failure); super.onFailure(response, failure);
} }
private void emitQueryTime(long requestTimeNs, boolean success) private void emitQueryTime(long requestTimeNs, boolean success, @Nullable String sqlQueryId, @Nullable String queryId)
{ {
QueryMetrics queryMetrics = DruidMetrics.makeRequestMetrics( QueryMetrics queryMetrics;
queryMetricsFactory, if (sqlQueryId != null) {
warehouse.getToolChest(query), queryMetrics = queryMetricsFactory.makeMetrics();
query, queryMetrics.remoteAddress(req.getRemoteAddr());
req.getRemoteAddr() // Setting sqlQueryId and queryId dimensions to the metric
); queryMetrics.sqlQueryId(sqlQueryId);
if (queryId != null) { // query id is null for JDBC SQL
queryMetrics.queryId(queryId);
}
} else {
queryMetrics = DruidMetrics.makeRequestMetrics(
queryMetricsFactory,
warehouse.getToolChest(query),
query,
req.getRemoteAddr()
);
}
queryMetrics.success(success); queryMetrics.success(success);
queryMetrics.reportQueryTime(requestTimeNs).emit(emitter); queryMetrics.reportQueryTime(requestTimeNs).emit(emitter);
} }

View File

@ -48,6 +48,7 @@ import org.apache.druid.java.util.common.StringUtils;
import org.apache.druid.java.util.common.granularity.Granularities; import org.apache.druid.java.util.common.granularity.Granularities;
import org.apache.druid.java.util.common.jackson.JacksonUtils; import org.apache.druid.java.util.common.jackson.JacksonUtils;
import org.apache.druid.java.util.common.lifecycle.Lifecycle; import org.apache.druid.java.util.common.lifecycle.Lifecycle;
import org.apache.druid.java.util.metrics.StubServiceEmitter;
import org.apache.druid.query.DefaultGenericQueryMetricsFactory; import org.apache.druid.query.DefaultGenericQueryMetricsFactory;
import org.apache.druid.query.Druids; import org.apache.druid.query.Druids;
import org.apache.druid.query.MapQueryToolChestWarehouse; import org.apache.druid.query.MapQueryToolChestWarehouse;
@ -72,6 +73,11 @@ import org.apache.druid.sql.http.ResultFormat;
import org.apache.druid.sql.http.SqlQuery; import org.apache.druid.sql.http.SqlQuery;
import org.easymock.EasyMock; import org.easymock.EasyMock;
import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.HttpResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.api.Result;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.HandlerList; import org.eclipse.jetty.server.handler.HandlerList;
@ -205,15 +211,24 @@ public class AsyncQueryForwardingServletTest extends BaseJettyTest
@Test @Test
public void testSqlQueryProxy() throws Exception public void testSqlQueryProxy() throws Exception
{ {
final SqlQuery query = new SqlQuery("SELECT * FROM foo", ResultFormat.ARRAY, false, false, false, null, null); final SqlQuery query = new SqlQuery(
"SELECT * FROM foo",
ResultFormat.ARRAY,
false,
false,
false,
ImmutableMap.of("sqlQueryId", "dummy"),
null
);
final QueryHostFinder hostFinder = EasyMock.createMock(QueryHostFinder.class); final QueryHostFinder hostFinder = EasyMock.createMock(QueryHostFinder.class);
EasyMock.expect(hostFinder.findServerSql(query)) EasyMock.expect(hostFinder.findServerSql(
.andReturn(new TestServer("http", "1.2.3.4", 9999)).once(); query.withOverridenContext(ImmutableMap.of("sqlQueryId", "dummy", "queryId", "dummy")))
).andReturn(new TestServer("http", "1.2.3.4", 9999)).once();
EasyMock.replay(hostFinder); EasyMock.replay(hostFinder);
Properties properties = new Properties(); Properties properties = new Properties();
properties.setProperty("druid.router.sql.enable", "true"); properties.setProperty("druid.router.sql.enable", "true");
verifyServletCallsForQuery(query, true, hostFinder, properties); verifyServletCallsForQuery(query, true, false, hostFinder, properties);
} }
@Test @Test
@ -230,7 +245,21 @@ public class AsyncQueryForwardingServletTest extends BaseJettyTest
EasyMock.expect(hostFinder.pickServer(query)).andReturn(new TestServer("http", "1.2.3.4", 9999)).once(); EasyMock.expect(hostFinder.pickServer(query)).andReturn(new TestServer("http", "1.2.3.4", 9999)).once();
EasyMock.replay(hostFinder); EasyMock.replay(hostFinder);
verifyServletCallsForQuery(query, false, hostFinder, new Properties()); verifyServletCallsForQuery(query, false, false, hostFinder, new Properties());
}
@Test
public void testJDBCSqlProxy() throws Exception
{
final ImmutableMap<String, Object> jdbcRequest = ImmutableMap.of("connectionId", "dummy");
final QueryHostFinder hostFinder = EasyMock.createMock(QueryHostFinder.class);
EasyMock.expect(hostFinder.findServerAvatica("dummy"))
.andReturn(new TestServer("http", "1.2.3.4", 9999))
.once();
EasyMock.replay(hostFinder);
verifyServletCallsForQuery(jdbcRequest, false, true, hostFinder, new Properties());
} }
@Test @Test
@ -485,13 +514,13 @@ public class AsyncQueryForwardingServletTest extends BaseJettyTest
*/ */
private void verifyServletCallsForQuery( private void verifyServletCallsForQuery(
Object query, Object query,
boolean isSql, boolean isNativeSql,
boolean isJDBCSql,
QueryHostFinder hostFinder, QueryHostFinder hostFinder,
Properties properties Properties properties
) throws Exception ) throws Exception
{ {
final ObjectMapper jsonMapper = TestHelper.makeJsonMapper(); final ObjectMapper jsonMapper = TestHelper.makeJsonMapper();
final HttpServletRequest requestMock = EasyMock.createMock(HttpServletRequest.class);
final ByteArrayInputStream inputStream = new ByteArrayInputStream(jsonMapper.writeValueAsBytes(query)); final ByteArrayInputStream inputStream = new ByteArrayInputStream(jsonMapper.writeValueAsBytes(query));
final ServletInputStream servletInputStream = new ServletInputStream() final ServletInputStream servletInputStream = new ServletInputStream()
{ {
@ -525,22 +554,58 @@ public class AsyncQueryForwardingServletTest extends BaseJettyTest
return b; return b;
} }
}; };
final HttpServletRequest requestMock = EasyMock.createMock(HttpServletRequest.class);
EasyMock.expect(requestMock.getContentType()).andReturn("application/json").times(2); EasyMock.expect(requestMock.getContentType()).andReturn("application/json").times(2);
requestMock.setAttribute("org.apache.druid.proxy.objectMapper", jsonMapper); requestMock.setAttribute("org.apache.druid.proxy.objectMapper", jsonMapper);
EasyMock.expectLastCall(); EasyMock.expectLastCall();
EasyMock.expect(requestMock.getRequestURI()).andReturn(isSql ? "/druid/v2/sql" : "/druid/v2/"); EasyMock.expect(requestMock.getRequestURI())
.andReturn(isNativeSql ? "/druid/v2/sql" : (isJDBCSql ? "/druid/v2/sql/avatica" : "/druid/v2/"));
EasyMock.expect(requestMock.getMethod()).andReturn("POST"); EasyMock.expect(requestMock.getMethod()).andReturn("POST");
EasyMock.expect(requestMock.getInputStream()).andReturn(servletInputStream); if (isNativeSql) {
SqlQuery sqlQuery = (SqlQuery) query;
query = sqlQuery.withOverridenContext(ImmutableMap.of("sqlQueryId", "dummy", "queryId", "dummy"));
}
requestMock.setAttribute( requestMock.setAttribute(
isSql ? "org.apache.druid.proxy.sqlQuery" : "org.apache.druid.proxy.query", "org.apache.druid.proxy." + (isNativeSql ? "sqlQuery" : (isJDBCSql ? "avaticaQuery" : "query")),
query isJDBCSql ? jsonMapper.writeValueAsBytes(query) : query
); );
EasyMock.expectLastCall();
EasyMock.expect(requestMock.getInputStream()).andReturn(servletInputStream);
// metrics related mocking
EasyMock.expect(requestMock.getAttribute("org.apache.druid.proxy.avaticaQuery"))
.andReturn(isJDBCSql ? query : null);
EasyMock.expect(requestMock.getAttribute("org.apache.druid.proxy.query"))
.andReturn(isJDBCSql ? null : (isNativeSql ? null : query));
EasyMock.expect(requestMock.getAttribute("org.apache.druid.proxy.sqlQuery"))
.andReturn(isJDBCSql ? null : (isNativeSql ? query : null));
EasyMock.expect(requestMock.getRemoteAddr()).andReturn("0.0.0.0:0").times(isJDBCSql ? 1 : 2);
requestMock.setAttribute("org.apache.druid.proxy.to.host", "1.2.3.4:9999"); requestMock.setAttribute("org.apache.druid.proxy.to.host", "1.2.3.4:9999");
EasyMock.expectLastCall();
requestMock.setAttribute("org.apache.druid.proxy.to.host.scheme", "http"); requestMock.setAttribute("org.apache.druid.proxy.to.host.scheme", "http");
EasyMock.expectLastCall(); EasyMock.expectLastCall();
EasyMock.replay(requestMock); EasyMock.replay(requestMock);
final AtomicLong didService = new AtomicLong(); final AtomicLong didService = new AtomicLong();
final Request proxyRequestMock = Mockito.spy(Request.class);
final Result result = new Result(
proxyRequestMock,
new HttpResponse(proxyRequestMock, ImmutableList.of())
{
@Override
public HttpFields getHeaders()
{
HttpFields httpFields = new HttpFields();
if (isJDBCSql) {
httpFields.add(new HttpField("X-Druid-SQL-Query-Id", "jdbcDummy"));
} else if (isNativeSql) {
httpFields.add(new HttpField("X-Druid-SQL-Query-Id", "dummy"));
}
return httpFields;
}
}
);
final StubServiceEmitter stubServiceEmitter = new StubServiceEmitter("", "");
final AsyncQueryForwardingServlet servlet = new AsyncQueryForwardingServlet( final AsyncQueryForwardingServlet servlet = new AsyncQueryForwardingServlet(
new MapQueryToolChestWarehouse(ImmutableMap.of()), new MapQueryToolChestWarehouse(ImmutableMap.of()),
jsonMapper, jsonMapper,
@ -548,7 +613,7 @@ public class AsyncQueryForwardingServletTest extends BaseJettyTest
hostFinder, hostFinder,
null, null,
null, null,
new NoopServiceEmitter(), stubServiceEmitter,
new NoopRequestLogger(), new NoopRequestLogger(),
new DefaultGenericQueryMetricsFactory(), new DefaultGenericQueryMetricsFactory(),
new AuthenticatorMapper(ImmutableMap.of()), new AuthenticatorMapper(ImmutableMap.of()),
@ -568,6 +633,19 @@ public class AsyncQueryForwardingServletTest extends BaseJettyTest
servlet.service(requestMock, null); servlet.service(requestMock, null);
// NPE is expected since the listener's onComplete calls the parent class' onComplete which fails due to
// partial state of the servlet. Hence, only catching the exact exception to avoid possible errors.
// Further, the metric assertions are also done to ensure that the metrics have emitted.
try {
servlet.newProxyResponseListener(requestMock, null).onComplete(result);
}
catch (NullPointerException ignored) {
}
Assert.assertEquals("query/time", stubServiceEmitter.getEvents().get(0).toMap().get("metric"));
if (!isJDBCSql) {
Assert.assertEquals("dummy", stubServiceEmitter.getEvents().get(0).toMap().get("id"));
}
// This test is mostly about verifying that the servlet calls the right methods the right number of times. // This test is mostly about verifying that the servlet calls the right methods the right number of times.
EasyMock.verify(hostFinder, requestMock); EasyMock.verify(hostFinder, requestMock);
Assert.assertEquals(1, didService.get()); Assert.assertEquals(1, didService.get());

View File

@ -180,6 +180,8 @@ public class NativeQueryMaker implements QueryMaker
final String queryId = UUID.randomUUID().toString(); final String queryId = UUID.randomUUID().toString();
plannerContext.addNativeQueryId(queryId); plannerContext.addNativeQueryId(queryId);
query = query.withId(queryId); query = query.withId(queryId);
} else {
plannerContext.addNativeQueryId(query.getId());
} }
query = query.withSqlQueryId(plannerContext.getSqlQueryId()); query = query.withSqlQueryId(plannerContext.getSqlQueryId());

View File

@ -81,6 +81,19 @@ public class SqlQuery
} }
} }
public SqlQuery withOverridenContext(Map<String, Object> overridenContext)
{
return new SqlQuery(
getQuery(),
getResultFormat(),
includeHeader(),
includeTypesHeader(),
includeSqlTypesHeader(),
overridenContext,
getParameters()
);
}
@JsonProperty @JsonProperty
public String getQuery() public String getQuery()
{ {