/* * Copyright 2016 KairosDB Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.kairosdb.core.http.rest; import ch.qos.logback.classic.Level; import com.google.common.base.Charsets; import com.google.common.collect.ImmutableSortedMap; import com.google.common.io.Resources; import com.google.inject.*; import com.google.inject.name.Names; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; import org.kairosdb.core.*; import org.kairosdb.core.aggregator.AggregatorFactory; import org.kairosdb.core.aggregator.TestAggregatorFactory; import org.kairosdb.core.datapoints.*; import org.kairosdb.core.datastore.*; import org.kairosdb.core.exception.DatastoreException; import org.kairosdb.core.groupby.GroupByFactory; import org.kairosdb.core.groupby.TestGroupByFactory; import org.kairosdb.core.http.WebServer; import org.kairosdb.core.http.WebServletModule; import org.kairosdb.core.http.rest.json.QueryParser; import org.kairosdb.core.http.rest.json.TestQueryPluginFactory; import org.kairosdb.testing.Client; import org.kairosdb.testing.JsonResponse; import org.kairosdb.util.LoggingUtils; import org.slf4j.bridge.SLF4JBridgeHandler; import java.io.IOException; import java.io.InputStream; import java.util.*; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.startsWith; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; public class MetricsResourceTest { private static final String ADD_METRIC_URL = "http://localhost:9001/api/v1/datapoints"; private static final String GET_METRIC_URL = "http://localhost:9001/api/v1/datapoints/query"; private static final String METRIC_NAMES_URL = "http://localhost:9001/api/v1/metricnames"; private static final String TAG_NAMES_URL = "http://localhost:9001/api/v1/tagnames"; private static final String TAG_VALUES_URL = "http://localhost:9001/api/v1/tagvalues"; private static TestDatastore datastore; private static QueryQueuingManager queuingManager; private static Client client; private static WebServer server; @BeforeClass public static void startup() throws Exception { //This sends jersey java util logging to logback SLF4JBridgeHandler.removeHandlersForRootLogger(); SLF4JBridgeHandler.install(); datastore = new TestDatastore(); queuingManager = new QueryQueuingManager(3, "localhost"); Injector injector = Guice.createInjector(new WebServletModule(new Properties()), new AbstractModule() { @Override protected void configure() { bind(String.class).annotatedWith(Names.named(WebServer.JETTY_ADDRESS_PROPERTY)).toInstance("0.0.0.0"); bind(Integer.class).annotatedWith(Names.named(WebServer.JETTY_PORT_PROPERTY)).toInstance(9001); bind(String.class).annotatedWith(Names.named(WebServer.JETTY_WEB_ROOT_PROPERTY)).toInstance("bogus"); bind(Datastore.class).toInstance(datastore); bind(KairosDatastore.class).in(Singleton.class); bind(AggregatorFactory.class).to(TestAggregatorFactory.class); bind(GroupByFactory.class).to(TestGroupByFactory.class); bind(QueryParser.class).in(Singleton.class); bind(new TypeLiteral<List<DataPointListener>>(){}).toProvider(DataPointListenerProvider.class); bind(QueryQueuingManager.class).toInstance(queuingManager); bindConstant().annotatedWith(Names.named("HOSTNAME")).to("HOST"); bindConstant().annotatedWith(Names.named("kairosdb.datastore.concurrentQueryThreads")).to(1); bindConstant().annotatedWith(Names.named("kairosdb.query_cache.keep_cache_files")).to(false); bind(KairosDataPointFactory.class).to(GuiceKairosDataPointFactory.class); bind(QueryPluginFactory.class).to(TestQueryPluginFactory.class); Properties props = new Properties(); InputStream is = getClass().getClassLoader().getResourceAsStream("kairosdb.properties"); try { props.load(is); is.close(); } catch (IOException e) { e.printStackTrace(); } //Names.bindProperties(binder(), props); bind(Properties.class).toInstance(props); bind(DoubleDataPointFactory.class) .to(DoubleDataPointFactoryImpl.class).in(Singleton.class); bind(DoubleDataPointFactoryImpl.class).in(Singleton.class); bind(LongDataPointFactory.class) .to(LongDataPointFactoryImpl.class).in(Singleton.class); bind(LongDataPointFactoryImpl.class).in(Singleton.class); bind(LegacyDataPointFactory.class).in(Singleton.class); bind(StringDataPointFactory.class).in(Singleton.class); } }); server = injector.getInstance(WebServer.class); server.start(); client = new Client(); } @AfterClass public static void tearDown() throws Exception { if (server != null) { server.stop(); } } @Test public void testAddEmptyBody() throws Exception { JsonResponse response = client.post("", ADD_METRIC_URL); assertResponse(response, 400, "{\"errors\":[\"Invalid json. No content due to end of input.\"]}"); } @Test public void testAddSingleMetricLongValueSuccess() throws Exception { String json = Resources.toString(Resources.getResource("single-metric-long.json"), Charsets.UTF_8); JsonResponse response = client.post(json, ADD_METRIC_URL); assertResponse(response, 204); } @Test public void testAddSingleMetricDoubleValueSuccess() throws Exception { String json = Resources.toString(Resources.getResource("single-metric-double.json"), Charsets.UTF_8); JsonResponse response = client.post(json, ADD_METRIC_URL); assertResponse(response, 204); } @Test public void testAddMutipleDatapointSuccess() throws Exception { String json = Resources.toString(Resources.getResource("multiple-datapoints-metric.json"), Charsets.UTF_8); JsonResponse response = client.post(json, ADD_METRIC_URL); assertResponse(response, 204); } @Test public void testAddMultipleMetricLongValueSuccess() throws Exception { String json = Resources.toString(Resources.getResource("multi-metric-long.json"), Charsets.UTF_8); JsonResponse response = client.post(json, ADD_METRIC_URL); assertThat(response.getStatusCode(), equalTo(204)); } @Test public void testAddMissingName() throws Exception { String json = Resources.toString(Resources.getResource("single-metric-missing-name.json"), Charsets.UTF_8); JsonResponse response = client.post(json, ADD_METRIC_URL); assertResponse(response, 400, "{\"errors\":[\"metric[0].name may not be empty.\"]}"); } @Test public void testAddTimestampZeroValid() throws Exception { String json = Resources.toString(Resources.getResource("multi-metric-timestamp-zero.json"), Charsets.UTF_8); JsonResponse response = client.post(json, ADD_METRIC_URL); assertResponse(response, 204); } @Test public void testQuery() throws IOException { String json = Resources.toString(Resources.getResource("query-metric-absolute-dates.json"), Charsets.UTF_8); JsonResponse response = client.post(json, GET_METRIC_URL); assertResponse(response, 200, "{\"queries\":" + "[{\"sample_size\":10,\"results\":" + "[{\"name\":\"abc.123\",\"group_by\":[{\"name\":\"type\",\"type\":\"number\"}],\"tags\":{\"server\":[\"server1\",\"server2\"]},\"values\":[[1,60.2],[2,30.200000000000003],[3,20.1]]}]}]}"); } @Test public void testQueryWithBeanValidationException() throws IOException { String json = Resources.toString(Resources.getResource("invalid-query-metric-relative-unit.json"), Charsets.UTF_8); JsonResponse response = client.post(json, GET_METRIC_URL); assertResponse(response, 400, "{\"errors\":[\"query.bogus is not a valid time unit, must be one of MILLISECONDS,SECONDS,MINUTES,HOURS,DAYS,WEEKS,MONTHS,YEARS\"]}"); } @Test public void testQueryWithJsonMapperParsingException() throws IOException { String json = Resources.toString(Resources.getResource("invalid-query-metric-json.json"), Charsets.UTF_8); JsonResponse response = client.post(json, GET_METRIC_URL); assertResponse(response, 400, "{\"errors\":[\"com.google.gson.stream.MalformedJsonException: Use JsonReader.setLenient(true) to accept malformed JSON at line 2 column 22\"]}"); } @Test public void testMetricNames() throws IOException { JsonResponse response = client.get(METRIC_NAMES_URL); assertResponse(response, 200, "{\"results\":[\"cpu\",\"memory\",\"disk\",\"network\"]}"); } @Test public void testTagNames() throws IOException { JsonResponse response = client.get(TAG_NAMES_URL); assertResponse(response, 200, "{\"results\":[\"server1\",\"server2\",\"server3\"]}"); } @Test public void testTagValues() throws IOException { JsonResponse response = client.get(TAG_VALUES_URL); assertResponse(response, 200, "{\"results\":[\"larry\",\"moe\",\"curly\"]}"); } @Test public void test_datastoreThrowsException() throws DatastoreException, IOException { Level previousLogLevel = LoggingUtils.setLogLevel(Level.OFF); try { datastore.throwQueryException(new DatastoreException("bogus")); String json = Resources.toString(Resources.getResource("query-metric-absolute-dates.json"), Charsets.UTF_8); JsonResponse response = client.post(json, GET_METRIC_URL); datastore.throwQueryException(null); assertThat(response.getStatusCode(), equalTo(500)); assertThat(response.getJson(), equalTo("{\"errors\":[\"org.kairosdb.core.exception.DatastoreException: bogus\"]}")); assertEquals(3, queuingManager.getAvailableThreads()); } finally { LoggingUtils.setLogLevel(previousLogLevel); } } private void assertResponse(JsonResponse response, int responseCode, String expectedContent) { assertThat(response.getStatusCode(), equalTo(responseCode)); assertThat(response.getHeader("Content-Type"), startsWith("application/json")); assertThat(response.getJson(), equalTo(expectedContent)); } private void assertResponse(JsonResponse response, int responseCode) { assertThat(response.getStatusCode(), equalTo(responseCode)); assertThat(response.getHeader("Content-Type"), startsWith("application/json")); assertThat(response.getStatusString(), equalTo("No Content")); } public static class TestDatastore implements Datastore { private DatastoreException m_toThrow = null; protected TestDatastore() throws DatastoreException { } public void throwQueryException(DatastoreException toThrow) { m_toThrow = toThrow; } @Override public void close() throws InterruptedException { } @Override public void putDataPoint(String metricName, ImmutableSortedMap<String, String> tags, DataPoint dataPoint, int ttl) throws DatastoreException { } @Override public Iterable<String> getMetricNames() { return Arrays.asList("cpu", "memory", "disk", "network"); } @Override public Iterable<String> getTagNames() { return Arrays.asList("server1", "server2", "server3"); } @Override public Iterable<String> getTagValues() { return Arrays.asList("larry", "moe", "curly"); } @Override public void queryDatabase(DatastoreMetricQuery query, QueryCallback queryCallback) throws DatastoreException { if (m_toThrow != null) throw m_toThrow; try { Map<String, String> tags = new TreeMap<String, String>(); tags.put("server", "server1"); queryCallback.startDataPointSet(LongDataPointFactoryImpl.DST_LONG, tags); queryCallback.addDataPoint(new LongDataPoint(1, 10)); queryCallback.addDataPoint(new LongDataPoint(1, 20)); queryCallback.addDataPoint(new LongDataPoint(2, 10)); queryCallback.addDataPoint(new LongDataPoint(2, 5)); queryCallback.addDataPoint(new LongDataPoint(3, 10)); tags = new TreeMap<String, String>(); tags.put("server", "server2"); queryCallback.startDataPointSet(DoubleDataPointFactoryImpl.DST_DOUBLE, tags); queryCallback.addDataPoint(new DoubleDataPoint(1, 10.1)); queryCallback.addDataPoint(new DoubleDataPoint(1, 20.1)); queryCallback.addDataPoint(new DoubleDataPoint(2, 10.1)); queryCallback.addDataPoint(new DoubleDataPoint(2, 5.1)); queryCallback.addDataPoint(new DoubleDataPoint(3, 10.1)); queryCallback.endDataPoints(); } catch (IOException e) { throw new DatastoreException(e); } } @Override public void deleteDataPoints(DatastoreMetricQuery deleteQuery) throws DatastoreException { } @Override public TagSet queryMetricTags(DatastoreMetricQuery query) throws DatastoreException { return null; } } }