package com.rackspacecloud.blueflood.inputs.handlers;
import com.codahale.metrics.Meter;
import com.google.common.util.concurrent.ListenableFuture;
import com.rackspacecloud.blueflood.inputs.formats.JSONMetric;
import com.rackspacecloud.blueflood.inputs.formats.JSONMetricsContainer;
import com.rackspacecloud.blueflood.io.Instrumentation;
import com.rackspacecloud.blueflood.outputs.formats.ErrorResponse;
import com.rackspacecloud.blueflood.outputs.handlers.HandlerTestsBase;
import com.rackspacecloud.blueflood.service.Configuration;
import com.rackspacecloud.blueflood.service.CoreConfig;
import com.rackspacecloud.blueflood.utils.DefaultClockImpl;
import com.rackspacecloud.blueflood.utils.TimeValue;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.*;
import org.codehaus.jackson.map.ObjectMapper;
import org.elasticsearch.common.lang3.StringUtils;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNull;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.*;
public class HttpMetricsIngestionHandlerTest extends HandlerTestsBase {
private HttpMetricsIngestionHandler handler;
private HttpMetricsIngestionServer.Processor processor;
private ChannelHandlerContext context;
private Channel channel;
private ChannelFuture channelFuture;
private Meter ingestedMetrics;
private Meter ingestedDelayedMetrics;
private static final String TENANT = "tenant";
@Before
public void setup() {
processor = mock(HttpMetricsIngestionServer.Processor.class);
handler = new HttpMetricsIngestionHandler(processor, new TimeValue(5, TimeUnit.SECONDS));
channel = mock(Channel.class);
context = mock(ChannelHandlerContext.class);
channelFuture = mock(ChannelFuture.class);
when(context.channel()).thenReturn(channel);
when(channel.write(anyString())).thenReturn(channelFuture);
ingestedMetrics = Instrumentation.getIngestedMetricsMeter(TENANT);
ingestedDelayedMetrics = Instrumentation.getIngestedDelayedMetricsMeter(TENANT);
}
@Test
public void emptyRequest_shouldGenerateErrorResponse() throws IOException {
String requestBody = "";
FullHttpRequest request = createIngestRequest(requestBody);
ArgumentCaptor<FullHttpResponse> argument = ArgumentCaptor.forClass(FullHttpResponse.class);
handler.handle(context, request);
verify(channel).write(argument.capture());
String errorResponseBody = argument.getValue().content().toString(Charset.defaultCharset());
ErrorResponse errorResponse = getErrorResponse(errorResponseBody);
assertEquals("Number of errors invalid", 1, errorResponse.getErrors().size());
assertEquals("Invalid error message", "Cannot parse content", errorResponse.getErrors().get(0).getMessage());
assertEquals("Invalid tenant", TENANT, errorResponse.getErrors().get(0).getTenantId());
assertEquals("Invalid status", HttpResponseStatus.BAD_REQUEST, argument.getValue().getStatus());
}
@Test
public void testEmptyJsonRequest() throws IOException {
String requestBody = "{}"; //causes JsonMappingException
FullHttpRequest request = createIngestRequest(requestBody);
ArgumentCaptor<FullHttpResponse> argument = ArgumentCaptor.forClass(FullHttpResponse.class);
handler.handle(context, request);
verify(channel).write(argument.capture());
String errorResponseBody = argument.getValue().content().toString(Charset.defaultCharset());
ErrorResponse errorResponse = getErrorResponse(errorResponseBody);
assertEquals("Number of errors invalid", 1, errorResponse.getErrors().size());
assertEquals("Invalid error message", "Cannot parse content", errorResponse.getErrors().get(0).getMessage());
assertEquals("Invalid tenant", TENANT, errorResponse.getErrors().get(0).getTenantId());
assertEquals("Invalid status", HttpResponseStatus.BAD_REQUEST, argument.getValue().getStatus());
}
@Test
public void testInvalidJsonRequest() throws IOException {
String requestBody = "{\"xxxx\": yyyy}"; //causes JsonMappingException
FullHttpRequest request = createIngestRequest(requestBody);
ArgumentCaptor<FullHttpResponse> argument = ArgumentCaptor.forClass(FullHttpResponse.class);
handler.handle(context, request);
verify(channel).write(argument.capture());
String errorResponseBody = argument.getValue().content().toString(Charset.defaultCharset());
ErrorResponse errorResponse = getErrorResponse(errorResponseBody);
assertEquals("Number of errors invalid", 1, errorResponse.getErrors().size());
assertEquals("Invalid error message", "Cannot parse content", errorResponse.getErrors().get(0).getMessage());
assertEquals("Invalid tenant", TENANT, errorResponse.getErrors().get(0).getTenantId());
assertEquals("Invalid status", HttpResponseStatus.BAD_REQUEST, argument.getValue().getStatus());
}
@Test
public void testEmptyJsonArrayRequest() throws IOException {
String requestBody = "[]"; //causes JsonMappingException
FullHttpRequest request = createIngestRequest(requestBody);
ArgumentCaptor<FullHttpResponse> argument = ArgumentCaptor.forClass(FullHttpResponse.class);
handler.handle(context, request);
verify(channel).write(argument.capture());
String errorResponseBody = argument.getValue().content().toString(Charset.defaultCharset());
ErrorResponse errorResponse = getErrorResponse(errorResponseBody);
assertEquals("Number of errors invalid", 1, errorResponse.getErrors().size());
assertEquals("Invalid error message", "No valid metrics",
errorResponse.getErrors().get(0).getMessage());
assertEquals("Invalid tenant", TENANT, errorResponse.getErrors().get(0).getTenantId());
assertEquals("Invalid status", HttpResponseStatus.BAD_REQUEST, argument.getValue().getStatus());
}
@Test
public void testEmptyMetricRequest() throws IOException {
String requestBody = "[{}]"; //causes JsonMappingException
FullHttpRequest request = createIngestRequest(requestBody);
ArgumentCaptor<FullHttpResponse> argument = ArgumentCaptor.forClass(FullHttpResponse.class);
handler.handle(context, request);
verify(channel).write(argument.capture());
String errorResponseBody = argument.getValue().content().toString(Charset.defaultCharset());
ErrorResponse errorResponse = getErrorResponse(errorResponseBody);
assertEquals("Number of errors invalid", 3, errorResponse.getErrors().size());
assertEquals("Invalid tenant", TENANT, errorResponse.getErrors().get(0).getTenantId());
assertEquals("Invalid status", HttpResponseStatus.BAD_REQUEST, argument.getValue().getStatus());
}
@Test
public void testSingleMetricInvalidMetricName() throws IOException {
String metricName = "";
String singleMetric = createRequestBody(metricName, new DefaultClockImpl().now().getMillis(),
24 * 60 * 60, 1); //empty metric name
String requestBody = "[" + singleMetric + "]";
FullHttpRequest request = createIngestRequest(requestBody);
ArgumentCaptor<FullHttpResponse> argument = ArgumentCaptor.forClass(FullHttpResponse.class);
handler.handle(context, request);
verify(channel).write(argument.capture());
String errorResponseBody = argument.getValue().content().toString(Charset.defaultCharset());
ErrorResponse errorResponse = getErrorResponse(errorResponseBody);
assertEquals("Number of errors invalid", 1, errorResponse.getErrors().size());
assertEquals("Invalid tenant", TENANT, errorResponse.getErrors().get(0).getTenantId());
assertEquals("Invalid metric name", metricName, errorResponse.getErrors().get(0).getMetricName());
assertEquals("Invalid status", HttpResponseStatus.BAD_REQUEST, argument.getValue().getStatus());
assertEquals("Invalid error source", "metricName", errorResponse.getErrors().get(0).getSource());
assertEquals("Invalid error message", "may not be empty", errorResponse.getErrors().get(0).getMessage());
}
@Test
public void testSingleMetricCollectionTimeInPast() throws IOException {
long collectionTimeInPast = new DefaultClockImpl().now().getMillis() - 1000
- Configuration.getInstance().getLongProperty( CoreConfig.BEFORE_CURRENT_COLLECTIONTIME_MS );
String metricName = "a.b.c";
String singleMetric = createRequestBody(metricName, collectionTimeInPast, 24 * 60 * 60, 1); //collection in past
String requestBody = "[" + singleMetric + "]";
FullHttpRequest request = createIngestRequest(requestBody);
ArgumentCaptor<FullHttpResponse> argument = ArgumentCaptor.forClass(FullHttpResponse.class);
handler.handle(context, request);
verify(channel).write(argument.capture());
String errorResponseBody = argument.getValue().content().toString(Charset.defaultCharset());
ErrorResponse errorResponse = getErrorResponse(errorResponseBody);
assertEquals("Number of errors invalid", 1, errorResponse.getErrors().size());
assertEquals("Invalid tenant", TENANT, errorResponse.getErrors().get(0).getTenantId());
assertEquals("Invalid metric name", metricName, errorResponse.getErrors().get(0).getMetricName());
assertEquals("Invalid status", HttpResponseStatus.BAD_REQUEST, argument.getValue().getStatus());
assertEquals("Invalid error source", "collectionTime", errorResponse.getErrors().get(0).getSource());
assertEquals("Invalid error message", "Out of bounds. Cannot be more than 259200000 milliseconds into the past." +
" Cannot be more than 600000 milliseconds into the future", errorResponse.getErrors().get(0).getMessage());
}
@Test
public void testSingleMetricCollectionTimeInFuture() throws IOException {
long collectionTimeInFuture = new DefaultClockImpl().now().getMillis() + 1000
+ Configuration.getInstance().getLongProperty( CoreConfig.AFTER_CURRENT_COLLECTIONTIME_MS );
String metricName = "a.b.c";
String singleMetric = createRequestBody(metricName, collectionTimeInFuture, 24 * 60 * 60, 1); //collection in future
String requestBody = "[" + singleMetric + "]";
FullHttpRequest request = createIngestRequest(requestBody);
ArgumentCaptor<FullHttpResponse> argument = ArgumentCaptor.forClass(FullHttpResponse.class);
handler.handle(context, request);
verify(channel).write(argument.capture());
String errorResponseBody = argument.getValue().content().toString(Charset.defaultCharset());
ErrorResponse errorResponse = getErrorResponse(errorResponseBody);
assertEquals("Number of errors invalid", 1, errorResponse.getErrors().size());
assertEquals("Invalid tenant", TENANT, errorResponse.getErrors().get(0).getTenantId());
assertEquals("Invalid metric name", metricName, errorResponse.getErrors().get(0).getMetricName());
assertEquals("Invalid status", HttpResponseStatus.BAD_REQUEST, argument.getValue().getStatus());
assertEquals("Invalid error source", "collectionTime", errorResponse.getErrors().get(0).getSource());
assertEquals("Invalid error message", "Out of bounds. Cannot be more than 259200000 milliseconds into the past." +
" Cannot be more than 600000 milliseconds into the future", errorResponse.getErrors().get(0).getMessage());
}
@Test
public void testSingleMetricInvalidTTL() throws IOException {
String metricName = "a.b.c";
String singleMetric = createRequestBody(metricName, new DefaultClockImpl().now().getMillis(), 0, 1); //ttl of 0
String requestBody = "[" + singleMetric + "]";
FullHttpRequest request = createIngestRequest(requestBody);
ArgumentCaptor<FullHttpResponse> argument = ArgumentCaptor.forClass(FullHttpResponse.class);
handler.handle(context, request);
verify(channel).write(argument.capture());
String errorResponseBody = argument.getValue().content().toString(Charset.defaultCharset());
ErrorResponse errorResponse = getErrorResponse(errorResponseBody);
assertEquals("Number of errors invalid", 1, errorResponse.getErrors().size());
assertEquals("Invalid tenant", TENANT, errorResponse.getErrors().get(0).getTenantId());
assertEquals("Invalid metric name", metricName, errorResponse.getErrors().get(0).getMetricName());
assertEquals("Invalid status", HttpResponseStatus.BAD_REQUEST, argument.getValue().getStatus());
assertEquals("Invalid error source", "ttlInSeconds", errorResponse.getErrors().get(0).getSource());
assertEquals("Invalid error message", "must be between 1 and 2147483647", errorResponse.getErrors().get(0).getMessage());
}
@Test
public void testSingleMetricInvalidMetricValue() throws IOException {
String metricName = "a.b.c";
String singleMetric = createRequestBody(metricName, new DefaultClockImpl().now().getMillis(),
24 * 60 * 60, null); //empty metric value
String requestBody = "[" + singleMetric + "]";
FullHttpRequest request = createIngestRequest(requestBody);
ArgumentCaptor<FullHttpResponse> argument = ArgumentCaptor.forClass(FullHttpResponse.class);
handler.handle(context, request);
verify(channel).write(argument.capture());
String errorResponseBody = argument.getValue().content().toString(Charset.defaultCharset());
ErrorResponse errorResponse = getErrorResponse(errorResponseBody);
System.out.println(errorResponse);
assertEquals("Number of errors invalid", 1, errorResponse.getErrors().size());
assertEquals("Invalid tenant", TENANT, errorResponse.getErrors().get(0).getTenantId());
assertEquals("Invalid metric name", "", errorResponse.getErrors().get(0).getMetricName());
assertEquals("Invalid status", HttpResponseStatus.BAD_REQUEST, argument.getValue().getStatus());
assertNull("Invalid error source", errorResponse.getErrors().get(0).getSource());
assertEquals("Invalid error message", "No valid metrics", errorResponse.getErrors().get(0).getMessage());
}
@Test
public void testMultiMetricsInvalidRequest() throws IOException {
String metricName1 = "a.b.c.1";
String metricName2 = "a.b.c.2";
FullHttpRequest request = createIngestRequest(generateInvalidMetrics(metricName1, metricName2));
ArgumentCaptor<FullHttpResponse> argument = ArgumentCaptor.forClass(FullHttpResponse.class);
handler.handle(context, request);
verify(channel).write(argument.capture());
String errorResponseBody = argument.getValue().content().toString(Charset.defaultCharset());
ErrorResponse errorResponse = getErrorResponse(errorResponseBody);
assertEquals("Number of errors invalid", 2, errorResponse.getErrors().size());
assertEquals("Invalid status", HttpResponseStatus.BAD_REQUEST, argument.getValue().getStatus());
assertEquals("Invalid tenant", TENANT, errorResponse.getErrors().get(0).getTenantId());
assertEquals("Invalid tenant", metricName1, errorResponse.getErrors().get(0).getMetricName());
assertEquals("Invalid error source", "ttlInSeconds", errorResponse.getErrors().get(0).getSource());
assertEquals("Invalid error message", "must be between 1 and 2147483647", errorResponse.getErrors().get(0).getMessage());
assertEquals("Invalid tenant", TENANT, errorResponse.getErrors().get(1).getTenantId());
assertEquals("Invalid tenant", metricName2, errorResponse.getErrors().get(1).getMetricName());
assertEquals("Invalid error source", "collectionTime", errorResponse.getErrors().get(1).getSource());
assertEquals("Invalid error message", "Out of bounds. Cannot be more than 259200000 milliseconds into the past." +
" Cannot be more than 600000 milliseconds into the future", errorResponse.getErrors().get(1).getMessage());
}
@Test
public void perTenantMetricsOn_emptyRequest_shouldNotRecordAnything() throws IOException {
String requestBody = "[{}]";
FullHttpRequest request = createIngestRequest(requestBody);
long ingestedMetricsBefore = ingestedMetrics.getCount();
long ingestedDelayedMetricsBefore = ingestedDelayedMetrics.getCount();
HttpMetricsIngestionHandler handler = spy(new HttpMetricsIngestionHandler(processor, new TimeValue(5, TimeUnit.SECONDS), true));
ArgumentCaptor<FullHttpResponse> argument = ArgumentCaptor.forClass(FullHttpResponse.class);
handler.handle(context, request);
verify(channel).write(argument.capture());
verify(handler, never()).recordPerTenantMetrics(eq(TENANT), anyInt(), anyInt());
assertEquals("ingested metrics count", 0, ingestedMetrics.getCount() - ingestedMetricsBefore);
assertEquals("ingested delayed metrics count", 0, ingestedDelayedMetrics.getCount() - ingestedDelayedMetricsBefore);
}
@Test
public void perTenantMetricsOn_invalidMetrics_shouldNotRecordAnything() throws IOException {
String m1 = "foo.bar";
String m2 = "gee.wish";
FullHttpRequest request = createIngestRequest(generateInvalidMetrics(m1, m2));
long ingestedMetricsBefore = ingestedMetrics.getCount();
long ingestedDelayedMetricsBefore = ingestedDelayedMetrics.getCount();
HttpMetricsIngestionHandler handler = spy(new HttpMetricsIngestionHandler(processor, new TimeValue(5, TimeUnit.SECONDS), true));
ArgumentCaptor<FullHttpResponse> argument = ArgumentCaptor.forClass(FullHttpResponse.class);
handler.handle(context, request);
verify(channel).write(argument.capture());
verify(handler, never()).recordPerTenantMetrics(eq(TENANT), anyInt(), anyInt());
assertEquals("ingested metrics count", 0, ingestedMetrics.getCount() - ingestedMetricsBefore);
assertEquals("ingested delayed metrics count", 0, ingestedDelayedMetrics.getCount() - ingestedDelayedMetricsBefore);
}
@Test
public void perTenantMetricsOn_shouldRecordDelayedMetrics() throws Exception {
String delayedMetric1 = "delayed.me.1";
String delayedMetric2 = "delayed.me.2";
FullHttpRequest request = createIngestRequest(generateDelayedMetricsRequestString(delayedMetric1, delayedMetric2));
long ingestedMetricsBefore = ingestedMetrics.getCount();
long ingestedDelayedMetricsBefore = ingestedDelayedMetrics.getCount();
ListenableFuture<List<Boolean>> futures = mock(ListenableFuture.class);
List<Boolean> answers = new ArrayList<>();
answers.add(Boolean.TRUE);
when(processor.apply(any())).thenReturn(futures);
when(futures.get(anyLong(), any())).thenReturn(answers);
HttpMetricsIngestionHandler handler = spy(new HttpMetricsIngestionHandler(processor, new TimeValue(5, TimeUnit.SECONDS), true));
ArgumentCaptor<FullHttpResponse> argument = ArgumentCaptor.forClass(FullHttpResponse.class);
handler.handle(context, request);
verify(channel).write(argument.capture());
verify(handler, times(1)).recordPerTenantMetrics(eq(TENANT), eq(0), eq(2));
assertEquals("ingested metrics count", 0, ingestedMetrics.getCount() - ingestedMetricsBefore);
assertEquals("ingested delayed metrics count", 2, ingestedDelayedMetrics.getCount() - ingestedDelayedMetricsBefore);
}
@Test
public void perTenantMetricsOn_shouldRecordNonDelayedMetrics() throws Exception {
String metric1 = "i.am.on.time";
String metric2 = "i.am.on.time.again";
FullHttpRequest request = createIngestRequest(generateNonDelayedMetricsRequestString(metric1, metric2));
long ingestedMetricsBefore = ingestedMetrics.getCount();
long ingestedDelayedMetricsBefore = ingestedDelayedMetrics.getCount();
ListenableFuture<List<Boolean>> futures = mock(ListenableFuture.class);
List<Boolean> answers = new ArrayList<>();
answers.add(Boolean.TRUE);
when(processor.apply(any())).thenReturn(futures);
when(futures.get(anyLong(), any())).thenReturn(answers);
HttpMetricsIngestionHandler handler = spy(new HttpMetricsIngestionHandler(processor, new TimeValue(5, TimeUnit.SECONDS), true));
ArgumentCaptor<FullHttpResponse> argument = ArgumentCaptor.forClass(FullHttpResponse.class);
handler.handle(context, request);
verify(channel).write(argument.capture());
verify(handler, times(1)).recordPerTenantMetrics(eq(TENANT), eq(2), eq(0));
assertEquals("ingested metrics count", 2, ingestedMetrics.getCount() - ingestedMetricsBefore);
assertEquals("ingested delayed metrics count", 0, ingestedDelayedMetrics.getCount() - ingestedDelayedMetricsBefore);
}
@Test
public void perTenantMetricsOff_shouldNotRecordMetrics() throws Exception {
String metric1 = "i.am.on.time";
String metric2 = "i.am.on.time.again";
FullHttpRequest request = createIngestRequest(generateNonDelayedMetricsRequestString(metric1, metric2));
long ingestedMetricsBefore = ingestedMetrics.getCount();
long ingestedDelayedMetricsBefore = ingestedDelayedMetrics.getCount();
ListenableFuture<List<Boolean>> futures = mock(ListenableFuture.class);
List<Boolean> answers = new ArrayList<>();
answers.add(Boolean.TRUE);
when(processor.apply(any())).thenReturn(futures);
when(futures.get(anyLong(), any())).thenReturn(answers);
// turn off per tenant metrics tracking
HttpMetricsIngestionHandler handler = spy(new HttpMetricsIngestionHandler(processor, new TimeValue(5, TimeUnit.SECONDS), false));
ArgumentCaptor<FullHttpResponse> argument = ArgumentCaptor.forClass(FullHttpResponse.class);
handler.handle(context, request);
verify(channel).write(argument.capture());
verify(handler, times(1)).recordPerTenantMetrics(eq(TENANT), eq(2), eq(0));
assertEquals("ingested metrics count", 0, ingestedMetrics.getCount() - ingestedMetricsBefore);
assertEquals("ingested delayed metrics count", 0, ingestedDelayedMetrics.getCount() - ingestedDelayedMetricsBefore);
}
private String generateInvalidMetrics(String invalidTtlMetricName, String invalidCollectionMetricName) throws IOException {
long collectionTimeInPast = new DefaultClockImpl().now().getMillis() - 1000
- Configuration.getInstance().getLongProperty( CoreConfig.BEFORE_CURRENT_COLLECTIONTIME_MS );
//invalid ttl value
String invalidTtl = createRequestBody(invalidTtlMetricName, new DefaultClockImpl().now().getMillis(), -1, 1);
// collection in the past
String invalidCollection = createRequestBody(invalidCollectionMetricName, collectionTimeInPast, 24 * 60 * 60, 1);
return "[" + invalidTtl + "," + invalidCollection + "]";
}
private String generateDelayedMetricsRequestString(String... metricNames) throws IOException {
long delayedTime = new DefaultClockImpl().now().getMillis() - 100 -
Configuration.getInstance().getLongProperty(CoreConfig.ROLLUP_DELAY_MILLIS);
List<String> jsonMetrics = new ArrayList<>();
for (String metricName : metricNames) {
jsonMetrics.add(createRequestBody(metricName, delayedTime, 24*60*60, 1));
}
return "[" + StringUtils.join(jsonMetrics, ",") + "]";
}
private String generateNonDelayedMetricsRequestString(String... metricNames) throws IOException {
long timestamp = new DefaultClockImpl().now().getMillis();
List<String> jsonMetrics = new ArrayList<>();
for (String metricName : metricNames) {
jsonMetrics.add(createRequestBody(metricName, timestamp, 24*60*60, 1));
}
return "[" + StringUtils.join(jsonMetrics, ",") + "]";
}
private String createRequestBody(String metricName, long collectionTime, int ttl, Object metricValue) throws IOException {
JSONMetric metric = new JSONMetric();
if (!StringUtils.isEmpty(metricName))
metric.setMetricName(metricName);
if (collectionTime > 0)
metric.setCollectionTime(collectionTime);
if (ttl > 0)
metric.setTtlInSeconds(ttl);
if (metricValue != null)
metric.setMetricValue(metricValue);
return new ObjectMapper().writeValueAsString(metric);
}
private FullHttpRequest createIngestRequest(String requestBody) {
return super.createPostRequest("/v2.0/" + TENANT + "/ingest/", requestBody);
}
public JSONMetricsContainer getContainer(String tenantId, String jsonBody) throws IOException {
HttpMetricsIngestionHandler handler = new HttpMetricsIngestionHandler(null, new TimeValue(5, TimeUnit.SECONDS));
return handler.createContainer(jsonBody, tenantId);
}
}