// This file is part of OpenTSDB.
// Copyright (C) 2013 The OpenTSDB Authors.
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 2.1 of the License, or (at your
// option) any later version. This program is distributed in the hope that it
// will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
// General Public License for more details. You should have received a copy
// of the GNU Lesser General Public License along with this program. If not,
// see <http://www.gnu.org/licenses/>.
package net.opentsdb.tsd;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.when;
import static org.powermock.api.mockito.PowerMockito.mock;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import java.lang.reflect.Field;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import net.opentsdb.core.BaseTsdbTest;
import net.opentsdb.core.RowKey;
import net.opentsdb.core.TSDB;
import net.opentsdb.core.Tags;
import net.opentsdb.meta.Annotation;
import net.opentsdb.meta.TSMeta;
import net.opentsdb.meta.UIDMeta;
import net.opentsdb.search.SearchPlugin;
import net.opentsdb.search.SearchQuery;
import net.opentsdb.search.TestTimeSeriesLookup;
import net.opentsdb.search.TimeSeriesLookup;
import net.opentsdb.storage.MockBase;
import net.opentsdb.uid.NoSuchUniqueId;
import net.opentsdb.uid.UniqueId;
import net.opentsdb.uid.UniqueId.UniqueIdType;
import net.opentsdb.utils.Config;
import org.jboss.netty.handler.codec.http.DefaultHttpRequest;
import org.jboss.netty.handler.codec.http.HttpMethod;
import org.jboss.netty.handler.codec.http.HttpRequest;
import org.jboss.netty.handler.codec.http.HttpResponseStatus;
import org.jboss.netty.handler.codec.http.HttpVersion;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.powermock.reflect.Whitebox;
import com.stumbleupon.async.Deferred;
import com.sun.java_cup.internal.runtime.Scanner;
@RunWith(PowerMockRunner.class)
@PrepareForTest({ TSDB.class, Config.class, HttpQuery.class, UniqueId.class,
RowKey.class, Tags.class, TimeSeriesLookup.class, SearchRpc.class,
SearchPlugin.class, Scanner.class })
public final class TestSearchRpc extends BaseTsdbTest {
private SearchPlugin mock_plugin;
private SearchRpc rpc = new SearchRpc();
private SearchQuery search_query = null;
private static final Charset UTF = Charset.forName("UTF-8");
@Before
public void beforeLocal() throws Exception {
HttpQuery.initializeSerializerMaps(tsdb);
}
@Test
public void constructor() {
assertNotNull(new SearchRpc());
}
@Test
public void searchTSMeta() throws Exception {
setupAnswerSearchQuery();
final HttpQuery query = NettyMocks.getQuery(tsdb,
"/api/search/tsmeta?query=*");
rpc.execute(tsdb, query);
assertEquals(HttpResponseStatus.OK, query.response().getStatus());
final String result = query.response().getContent().toString(UTF);
assertTrue(result.contains("\"results\":[{\"tsuid\""));
assertEquals(1, search_query.getResults().size());
}
@Test
public void searchTSMeta_Summary() throws Exception {
setupAnswerSearchQuery();
final HttpQuery query = NettyMocks.getQuery(tsdb,
"/api/search/tsmeta_summary?query=*");
rpc.execute(tsdb, query);
assertEquals(HttpResponseStatus.OK, query.response().getStatus());
final String result = query.response().getContent().toString(UTF);
assertTrue(result.contains("\"host\":\"web01\""));
assertTrue(result.contains("\"metric\":\"sys.cpu.0\""));
assertEquals(1, search_query.getResults().size());
}
@Test
public void searchTSUIDs() throws Exception {
setupAnswerSearchQuery();
final HttpQuery query = NettyMocks.getQuery(tsdb,
"/api/search/tsuids?query=*");
rpc.execute(tsdb, query);
assertEquals(HttpResponseStatus.OK, query.response().getStatus());
final String result = query.response().getContent().toString(UTF);
assertTrue(result.contains("\"results\":[\"000001000001000001\""));
assertEquals(2, search_query.getResults().size());
}
@Test
public void searchUIDMeta() throws Exception {
setupAnswerSearchQuery();
final HttpQuery query = NettyMocks.getQuery(tsdb,
"/api/search/uidmeta?query=*");
rpc.execute(tsdb, query);
assertEquals(HttpResponseStatus.OK, query.response().getStatus());
final String result = query.response().getContent().toString(UTF);
assertTrue(result.contains("\"results\":[{\"uid\""));
assertEquals(2, search_query.getResults().size());
}
@Test
public void searchAnnotation() throws Exception {
setupAnswerSearchQuery();
final HttpQuery query = NettyMocks.getQuery(tsdb,
"/api/search/annotation?query=*");
rpc.execute(tsdb, query);
assertEquals(HttpResponseStatus.OK, query.response().getStatus());
final String result = query.response().getContent().toString(UTF);
assertTrue(result.contains("\"results\":[{\"tsuid\""));
assertEquals(1, search_query.getResults().size());
}
@Test
public void searchEmptyResultSet() throws Exception {
setupAnswerSearchQuery();
final HttpQuery query = NettyMocks.getQuery(tsdb,
"/api/search/annotation?query=EMTPY");
rpc.execute(tsdb, query);
assertEquals(HttpResponseStatus.OK, query.response().getStatus());
final String result = query.response().getContent().toString(UTF);
assertTrue(result.contains("\"results\":[]"));
assertEquals(0, search_query.getResults().size());
}
@Test
public void searchQSParseLimit() throws Exception {
setupAnswerSearchQuery();
final HttpQuery query = NettyMocks.getQuery(tsdb,
"/api/search/tsmeta?query=*&limit=42");
rpc.execute(tsdb, query);
assertEquals(HttpResponseStatus.OK, query.response().getStatus());
assertEquals(42, search_query.getLimit());
}
@Test
public void searchQSParseStartIndex() throws Exception {
setupAnswerSearchQuery();
final HttpQuery query = NettyMocks.getQuery(tsdb,
"/api/search/tsmeta?query=*&start_index=4");
rpc.execute(tsdb, query);
assertEquals(HttpResponseStatus.OK, query.response().getStatus());
assertEquals(4, search_query.getStartIndex());
}
@Test
public void searchPOST() throws Exception {
setupAnswerSearchQuery();
final HttpQuery query = NettyMocks.postQuery(tsdb,
"/api/search/tsmeta", "{\"query\":\"*\",\"limit\":42,\"startIndex\":2}");
rpc.execute(tsdb, query);
assertEquals(HttpResponseStatus.OK, query.response().getStatus());
final String result = query.response().getContent().toString(UTF);
assertTrue(result.contains("\"results\":[{\"tsuid\""));
assertEquals(1, search_query.getResults().size());
assertEquals(42, search_query.getLimit());
assertEquals(2, search_query.getStartIndex());
}
@Test (expected = BadRequestException.class)
public void searchBadMethod() throws Exception {
final HttpRequest req = new DefaultHttpRequest(HttpVersion.HTTP_1_1,
HttpMethod.PUT, "/api/search");
final HttpQuery query = new HttpQuery(tsdb, req, NettyMocks.fakeChannel());
rpc.execute(tsdb, query);
}
@Test (expected = BadRequestException.class)
public void searchMissingType() throws Exception {
final HttpQuery query = NettyMocks.getQuery(tsdb, "/api/search?query=*");
rpc.execute(tsdb, query);
}
@Test (expected = BadRequestException.class)
public void searchBadTypeType() throws Exception {
final HttpQuery query = NettyMocks.getQuery(tsdb,
"/api/search/badtype?query=*");
rpc.execute(tsdb, query);
}
@Test (expected = BadRequestException.class)
public void searchMissingQuery() throws Exception {
final HttpQuery query = NettyMocks.getQuery(tsdb, "/api/search/tsmeta");
rpc.execute(tsdb, query);
}
@Test (expected = BadRequestException.class)
public void searchPluginNotEnabled() throws Exception {
Whitebox.setInternalState(tsdb, "search", (SearchPlugin)null);
final HttpQuery query = NettyMocks.getQuery(tsdb,
"/api/search/tsmeta?query=*");
rpc.execute(tsdb, query);
}
@Test (expected = BadRequestException.class)
public void searchInvalidLimit() throws Exception {
final HttpQuery query = NettyMocks.getQuery(tsdb,
"/api/search/tsmeta?query=*&limit=nan");
rpc.execute(tsdb, query);
}
@Test (expected = BadRequestException.class)
public void searchInvalidStartIndex() throws Exception {
final HttpQuery query = NettyMocks.getQuery(tsdb,
"/api/search/tsmeta?query=*&start_index=nan");
rpc.execute(tsdb, query);
}
@Test
public void searchLookupTagkOnlyMeta() throws Exception {
setupLookup(true);
final HttpQuery query = NettyMocks.getQuery(tsdb,
"/api/search/lookup?m={host=}");
rpc.execute(tsdb, query);
assertEquals(HttpResponseStatus.OK, query.response().getStatus());
final String result = query.response().getContent().toString(UTF);
assertTrue(result.contains("\"host\":\"web01\""));
assertTrue(result.contains("\"totalResults\":5"));
assertTrue(result.contains("\"tsuid\":\"000001000001000001\""));
assertTrue(result.contains("\"tsuid\":\"000001000001000002\""));
assertTrue(result.contains("\"tsuid\":\"000002000001000001\""));
assertTrue(result.contains("\"tsuid\":\"000004000001000001000003000005\""));
assertTrue(result.contains("\"tsuid\":\"000004000001000002000003000005\""));
}
@Test
public void searchLookupPOSTTagkOnlyMeta() throws Exception {
setupLookup(true);
final HttpQuery query = NettyMocks.postQuery(tsdb,
"/api/search/lookup", "{\"tags\":[{\"key\":\"host\",\"value\":null}]}");
query.setSerializer();
rpc.execute(tsdb, query);
assertEquals(HttpResponseStatus.OK, query.response().getStatus());
final String result = query.response().getContent().toString(UTF);
assertTrue(result.contains("\"host\":\"web01\""));
assertTrue(result.contains("\"totalResults\":5"));
assertTrue(result.contains("\"tsuid\":\"000001000001000001\""));
assertTrue(result.contains("\"tsuid\":\"000001000001000002\""));
assertTrue(result.contains("\"tsuid\":\"000002000001000001\""));
assertTrue(result.contains("\"tsuid\":\"000004000001000001000003000005\""));
assertTrue(result.contains("\"tsuid\":\"000004000001000002000003000005\""));
}
@Test
public void searchLookupPOSTTagkOnlyData() throws Exception {
setupLookup(false);
final HttpQuery query = NettyMocks.postQuery(tsdb, "/api/search/lookup",
"{\"tags\":[{\"key\":\"host\",\"value\":null}],\"useMeta\":false}");
rpc.execute(tsdb, query);
assertEquals(HttpResponseStatus.OK, query.response().getStatus());
final String result = query.response().getContent().toString(UTF);
assertTrue(result.contains("\"host\":\"web01\""));
assertTrue(result.contains("\"totalResults\":5"));
assertTrue(result.contains("\"tsuid\":\"000001000001000001\""));
assertTrue(result.contains("\"tsuid\":\"000001000001000002\""));
assertTrue(result.contains("\"tsuid\":\"000002000001000001\""));
assertTrue(result.contains("\"tsuid\":\"000004000001000001000003000005\""));
assertTrue(result.contains("\"tsuid\":\"000004000001000002000003000005\""));
}
@Test
public void searchLookupNoMetaTable() throws Exception {
setupLookup(false);
final HttpQuery query = NettyMocks.postQuery(tsdb, "/api/search/lookup",
"{\"tags\":[{\"key\":\"host\",\"value\":null}]}");
rpc.execute(tsdb, query);
assertEquals(HttpResponseStatus.INTERNAL_SERVER_ERROR,
query.response().getStatus());
final String result = query.response().getContent().toString(UTF);
assertTrue(result.contains("\"code\":500"));
assertTrue(result.contains("\"message\":\"Unexpected exception\""));
}
@Test (expected = BadRequestException.class)
public void searchLookupMissingQuery() throws Exception {
setupLookup(true);
final HttpQuery query = NettyMocks.getQuery(tsdb, "/api/search/lookup");
rpc.execute(tsdb, query);
}
@Test (expected = BadRequestException.class)
public void searchLookupBadQuery() throws Exception {
setupLookup(true);
final HttpQuery query = NettyMocks.getQuery(tsdb, "/api/search/lookup?m={");
rpc.execute(tsdb, query);
}
@Test
public void searchLookupNSUN() throws Exception {
setupLookup(true);
final HttpQuery query = NettyMocks.getQuery(tsdb,
"/api/search/lookup?m=" + NSUN_METRIC);
rpc.execute(tsdb, query);
assertEquals(HttpResponseStatus.NOT_FOUND, query.response().getStatus());
final String result = query.response().getContent().toString(UTF);
assertTrue(result.contains("\"code\":404"));
assertTrue(result.contains("\"details\":\"No such name"));
}
@Test
public void searchLookupNSUI() throws Exception {
setupLookup(true);
when(metrics.getNameAsync(new byte[] { 0, 0, 4 }))
.thenAnswer(new Answer<Deferred<String>>() {
@Override
public Deferred<String> answer(InvocationOnMock invocation)
throws Throwable {
return Deferred.fromError(
new NoSuchUniqueId("metrics", new byte[] { 0, 0, 4 }));
}
});
final HttpQuery query = NettyMocks.postQuery(tsdb,
"/api/search/lookup", "{\"tags\":[{\"key\":\"host\",\"value\":null}]}");
query.setSerializer();
rpc.execute(tsdb, query);
assertEquals(HttpResponseStatus.NOT_FOUND, query.response().getStatus());
final String result = query.response().getContent().toString(UTF);
assertTrue(result.contains("\"code\":404"));
assertTrue(result.contains("\"details\":\"No such unique ID"));
}
/**
* Configures an Answer to respond with when the tests call
* tsdb.executeSearch(), responding to the type of query requested with valid
* responses for parsing tests.
*/
private void setupAnswerSearchQuery() {
mock_plugin = mock(SearchPlugin.class);
Whitebox.setInternalState(tsdb, "search", mock_plugin);
when(mock_plugin.executeQuery((SearchQuery)any())).thenAnswer(
new Answer<Deferred<SearchQuery>>() {
@Override
public Deferred<SearchQuery> answer(InvocationOnMock invocation)
throws Throwable {
final Object[] args = invocation.getArguments();
search_query = (SearchQuery)args[0];
List<Object> results = new ArrayList<Object>(1);
// if we want an empty response, return an empty response
if (search_query.getQuery().toUpperCase().equals("EMTPY")) {
search_query.setResults(results);
search_query.setTotalResults(0);
return Deferred.fromResult(search_query);
}
switch(search_query.getType()) {
case TSMETA:
final TSMeta meta = new TSMeta("000001000001000001");
meta.setCreated(1356998400);
meta.setDescription("System CPU metric");
UIDMeta uid = new UIDMeta(UniqueIdType.METRIC, "000001");
final Field uid_name = UIDMeta.class.getDeclaredField("name");
uid_name.setAccessible(true);
uid_name.set(uid, "sys.cpu.0");
final Field metric = TSMeta.class.getDeclaredField("metric");
metric.setAccessible(true);
metric.set(meta, uid);
final ArrayList<UIDMeta> tags = new ArrayList<UIDMeta>(2);
uid = new UIDMeta(UniqueIdType.TAGK, "000001");
uid_name.set(uid, "host");
tags.add(uid);
uid = new UIDMeta(UniqueIdType.TAGV, "000001");
uid_name.set(uid, "web01");
tags.add(uid);
final Field tags_field = TSMeta.class.getDeclaredField("tags");
tags_field.setAccessible(true);
tags_field.set(meta, tags);
results.add(meta);
break;
case LOOKUP:
case TSMETA_SUMMARY:
final HashMap<String, Object> ts = new HashMap<String, Object>(1);
ts.put("metric", "sys.cpu.0");
final HashMap<String, String> tag_map =
new HashMap<String, String>(2);
tag_map.put("host", "web01");
tag_map.put("owner", "ops");
ts.put("tags", tag_map);
ts.put("tsuid", "000001000001000001");
results.add(ts);
break;
case TSUIDS:
results.add("000001000001000001");
results.add("000002000002000002");
break;
case UIDMETA:
UIDMeta uid2 = new UIDMeta(UniqueIdType.METRIC, "000001");
final Field name_field = UIDMeta.class.getDeclaredField("name");
name_field.setAccessible(true);
name_field.set(uid2, "sys.cpu.0");
results.add(uid2);
uid2 = new UIDMeta(UniqueIdType.TAGK, "000001");
name_field.set(uid2, "host");
results.add(uid2);
break;
case ANNOTATION:
final Annotation note = new Annotation();
note.setStartTime(1356998400);
note.setEndTime(1356998460);
note.setDescription("Something went pear shaped");
note.setTSUID("000001000001000001");
results.add(note);
break;
}
search_query.setResults(results);
search_query.setTotalResults(results.size());
search_query.setTime(0.42F);
return Deferred.fromResult(search_query);
}
});
}
private void setupLookup(final boolean use_meta) {
storage = new MockBase(tsdb, client, true, true, true, true);
if (use_meta) {
TestTimeSeriesLookup.generateMeta(tsdb, storage);
} else {
TestTimeSeriesLookup.generateData(tsdb, storage);
}
when(metrics.getNameAsync(new byte[] { 0, 0, 4 }))
.thenAnswer(new Answer<Deferred<String>>() {
@Override
public Deferred<String> answer(InvocationOnMock invocation)
throws Throwable {
return Deferred.fromResult("filtered");
}
});
when(tag_names.getNameAsync(new byte[] { 0, 0, 6 }))
.thenAnswer(new Answer<Deferred<String>>() {
@Override
public Deferred<String> answer(InvocationOnMock invocation)
throws Throwable {
return Deferred.fromResult("6");
}
});
when(tag_names.getNameAsync(new byte[] { 0, 0, 8 }))
.thenAnswer(new Answer<Deferred<String>>() {
@Override
public Deferred<String> answer(InvocationOnMock invocation)
throws Throwable {
return Deferred.fromResult("8");
}
});
when(tag_names.getNameAsync(new byte[] { 0, 0, 9 }))
.thenAnswer(new Answer<Deferred<String>>() {
@Override
public Deferred<String> answer(InvocationOnMock invocation)
throws Throwable {
return Deferred.fromResult("9");
}
});
when(tag_values.getNameAsync(new byte[] { 0, 0, 7 }))
.thenAnswer(new Answer<Deferred<String>>() {
@Override
public Deferred<String> answer(InvocationOnMock invocation)
throws Throwable {
return Deferred.fromResult("7");
}
});
when(tag_values.getNameAsync(new byte[] { 0, 0, 5 }))
.thenAnswer(new Answer<Deferred<String>>() {
@Override
public Deferred<String> answer(InvocationOnMock invocation)
throws Throwable {
return Deferred.fromResult("5");
}
});
when(tag_values.getNameAsync(new byte[] { 0, 0, 10 }))
.thenAnswer(new Answer<Deferred<String>>() {
@Override
public Deferred<String> answer(InvocationOnMock invocation)
throws Throwable {
return Deferred.fromResult("10");
}
});
}
}