package org.stagemonitor.core.metrics.metrics2; import com.codahale.metrics.Clock; import com.codahale.metrics.Counter; import com.codahale.metrics.Gauge; import com.codahale.metrics.Histogram; import com.codahale.metrics.Meter; import com.codahale.metrics.Timer; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.slf4j.Logger; import org.stagemonitor.core.CorePlugin; import org.stagemonitor.core.elasticsearch.ElasticsearchClient; import org.stagemonitor.core.metrics.MetricNameFilter; import org.stagemonitor.core.util.HttpClient; import org.stagemonitor.core.util.JsonUtils; import org.stagemonitor.util.StringUtils; import java.io.ByteArrayOutputStream; import java.net.HttpURLConnection; import java.util.Map; import java.util.TreeMap; import java.util.concurrent.TimeUnit; import static java.util.Collections.singleton; import static java.util.Collections.singletonMap; import static org.junit.Assert.assertEquals; import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.stagemonitor.core.metrics.MetricsReporterTestHelper.counter; import static org.stagemonitor.core.metrics.MetricsReporterTestHelper.gauge; import static org.stagemonitor.core.metrics.MetricsReporterTestHelper.histogram; import static org.stagemonitor.core.metrics.MetricsReporterTestHelper.map; import static org.stagemonitor.core.metrics.MetricsReporterTestHelper.meter; import static org.stagemonitor.core.metrics.MetricsReporterTestHelper.metricNameMap; import static org.stagemonitor.core.metrics.MetricsReporterTestHelper.objectMap; import static org.stagemonitor.core.metrics.MetricsReporterTestHelper.timer; import static org.stagemonitor.core.metrics.metrics2.MetricName.name; public class ElasticsearchReporterTest { private static final TimeUnit DURATION_UNIT = TimeUnit.MICROSECONDS; private static final double DURATION_FACTOR = 1.0 / DURATION_UNIT.toNanos(1); private ElasticsearchReporter elasticsearchReporter; private long timestamp; private ByteArrayOutputStream out; private Logger metricsLogger; private CorePlugin corePlugin; private Metric2Registry registry; private Clock clock; @Before public void setUp() throws Exception { this.clock = mock(Clock.class); timestamp = System.currentTimeMillis(); when(clock.getTime()).thenReturn(timestamp); final HttpClient httpClient = mock(HttpClient.class); when(httpClient.send(any(), any(), any(), any(), any())).thenAnswer(new Answer<Integer>() { @Override public Integer answer(InvocationOnMock invocation) throws Throwable { HttpClient.OutputStreamHandler handler = (HttpClient.OutputStreamHandler) invocation.getArguments()[3]; final HttpURLConnection connection = mock(HttpURLConnection.class); when(connection.getOutputStream()).thenReturn(out); handler.withHttpURLConnection(connection.getOutputStream()); return 200; } }); metricsLogger = mock(Logger.class); corePlugin = mock(CorePlugin.class); registry = new Metric2Registry(); final ElasticsearchClient elasticsearchClient = mock(ElasticsearchClient.class); when(elasticsearchClient.isElasticsearchAvailable()).thenReturn(true); when(corePlugin.getElasticsearchClient()).thenReturn(elasticsearchClient); elasticsearchReporter = ElasticsearchReporter.forRegistry(registry, corePlugin) .convertDurationsTo(DURATION_UNIT) .globalTags(singletonMap("app", "test")) .httpClient(httpClient) .clock(clock) .elasticsearchMetricsLogger(metricsLogger) .filter(MetricNameFilter.excludePatterns(singleton(name("reporting_time").build()))) .build(); out = new ByteArrayOutputStream(); } @After public void tearDown() throws Exception { elasticsearchReporter.close(); } @Test(expected = IllegalStateException.class) public void testScheduleTwice() throws Exception { elasticsearchReporter.start(100, TimeUnit.MILLISECONDS); elasticsearchReporter.start(100, TimeUnit.MILLISECONDS); } @Test public void testSchedule() throws Exception { when(corePlugin.isOnlyLogElasticsearchMetricReports()).thenReturn(true); // this clock starts at 0 and then progresses normally when(clock.getTime()).then(new Answer<Long>() { long firstTimestamp; @Override public Long answer(InvocationOnMock invocation) throws Throwable { final long time = System.currentTimeMillis(); if (firstTimestamp == 0) { firstTimestamp = time; } return (time - firstTimestamp); } }); registry.register(name("test").build(), new Gauge<Integer>() { @Override public Integer getValue() { return 1; } }); elasticsearchReporter.start(100, TimeUnit.MILLISECONDS); Thread.sleep(300); verify(metricsLogger).info(eq(String.format("{\"index\":{\"_index\":\"stagemonitor-metrics-%s\",\"_type\":\"metrics\"}}\n" + "{\"@timestamp\":100,\"name\":\"test\",\"app\":\"test\",\"value\":1.0}\n", StringUtils.getLogstashStyleDate()))); verify(metricsLogger).info(eq(String.format("{\"index\":{\"_index\":\"stagemonitor-metrics-%s\",\"_type\":\"metrics\"}}\n" + "{\"@timestamp\":200,\"name\":\"test\",\"app\":\"test\",\"value\":1.0}\n", StringUtils.getLogstashStyleDate()))); } @Test public void testReportGauges() throws Exception { elasticsearchReporter.reportMetrics( metricNameMap( name("cpu_usage").type("user").tag("core", "1").build(), gauge(3) ), metricNameMap(Counter.class), metricNameMap(Histogram.class), metricNameMap(Meter.class), metricNameMap(Timer.class)); final String jsons = new String(out.toByteArray()); assertEquals(jsons, 2, jsons.split("\n").length); assertEquals( objectMap("index", map("_type", "metrics") .add("_index", "stagemonitor-metrics-" + StringUtils.getLogstashStyleDate())), asMap(jsons.split("\n")[0])); assertEquals( objectMap("@timestamp", timestamp) .add("name", "cpu_usage") .add("app", "test") .add("type", "user") .add("core", "1") .add("value", 3.0), asMap(jsons.split("\n")[1])); } @Test public void testReportNullGauge() throws Exception { elasticsearchReporter.reportMetrics( metricNameMap(name("gauge").build(), gauge(null)), metricNameMap(Counter.class), metricNameMap(Histogram.class), metricNameMap(Meter.class), metricNameMap(Timer.class)); assertEquals( objectMap("@timestamp", timestamp) .add("name", "gauge") .add("app", "test"), asMap(out)); } @Test public void testReportToLog() throws Exception { when(corePlugin.isOnlyLogElasticsearchMetricReports()).thenReturn(true); elasticsearchReporter.reportMetrics( metricNameMap(name("gauge").build(), gauge(1)), metricNameMap(Counter.class), metricNameMap(Histogram.class), metricNameMap(Meter.class), metricNameMap(Timer.class)); verify(metricsLogger).info(eq(String.format("{\"index\":{\"_index\":\"stagemonitor-metrics-%s\",\"_type\":\"metrics\"}}\n" + "{\"@timestamp\":%d,\"name\":\"gauge\",\"app\":\"test\",\"value\":1.0}\n", StringUtils.getLogstashStyleDate(), timestamp))); } @Test public void testReportBooleanGauge() throws Exception { elasticsearchReporter.reportMetrics( metricNameMap(name("gauge").build(), gauge(true)), metricNameMap(Counter.class), metricNameMap(Histogram.class), metricNameMap(Meter.class), metricNameMap(Timer.class)); assertEquals( objectMap("@timestamp", timestamp) .add("name", "gauge") .add("app", "test") .add("value_boolean", true), asMap(out)); } @Test public void testReportStringGauge() throws Exception { elasticsearchReporter.reportMetrics( metricNameMap(name("gauge").build(), gauge("foo")), metricNameMap(Counter.class), metricNameMap(Histogram.class), metricNameMap(Meter.class), metricNameMap(Timer.class)); assertEquals( objectMap("@timestamp", timestamp) .add("name", "gauge") .add("app", "test") .add("value_string", "foo"), asMap(out)); } @Test public void testReportCounters() throws Exception { elasticsearchReporter.reportMetrics( metricNameMap(Gauge.class), metricNameMap(name("web_sessions").build(), counter(123)), metricNameMap(Histogram.class), metricNameMap(Meter.class), metricNameMap(Timer.class)); assertEquals( map("@timestamp", timestamp, Object.class) .add("name", "web_sessions") .add("app", "test") .add("count", 123), asMap(out)); } @Test public void testReportHistograms() throws Exception { elasticsearchReporter.reportMetrics( metricNameMap(Gauge.class), metricNameMap(Counter.class), metricNameMap(name("histogram").build(), histogram(400)), metricNameMap(Meter.class), metricNameMap(Timer.class)); assertEquals(objectMap("@timestamp", timestamp) .add("name", "histogram") .add("app", "test") .add("count", 1) .add("max", 200.0) .add("mean", 400.0) .add("p50", 600.0) .add("min", 400.0) .add("p25", 0.0) .add("p75", 700.0) .add("p95", 800.0) .add("p98", 900.0) .add("p99", 1000.0) .add("p999", 1100.0) .add("std", 500.0), asMap(out)); } @Test public void testReportMeters() throws Exception { elasticsearchReporter.reportMetrics( metricNameMap(Gauge.class), metricNameMap(Counter.class), metricNameMap(Histogram.class), metricNameMap(name("meter").build(), meter(10)), metricNameMap(Timer.class)); assertEquals(map("@timestamp", timestamp, Object.class) .add("name", "meter") .add("app", "test") .add("count", 10) .add("m15_rate", 5.0) .add("m1_rate", 3.0) .add("m5_rate", 4.0) .add("mean_rate", 2.0), asMap(out)); } @Test public void testReportTimers() throws Exception { elasticsearchReporter.reportMetrics( metricNameMap(Gauge.class), metricNameMap(Counter.class), metricNameMap(Histogram.class), metricNameMap(Meter.class), metricNameMap(name("response_time").build(), timer(400))); assertEquals(map("@timestamp", timestamp, Object.class) .add("name", "response_time") .add("app", "test") .add("count", 1) .add("m15_rate", 5.0) .add("m1_rate", 3.0) .add("m5_rate", 4.0) .add("mean_rate", 2.0) .add("max", 200.0 * DURATION_FACTOR) .add("mean", 400.0 * DURATION_FACTOR) .add("p50", 600.0 * DURATION_FACTOR) .add("min", 400.0 * DURATION_FACTOR) .add("p25", 0.0 * DURATION_FACTOR) .add("p75", 700.0 * DURATION_FACTOR) .add("p95", 800.0 * DURATION_FACTOR) .add("p98", 900.0 * DURATION_FACTOR) .add("p99", 1000.0 * DURATION_FACTOR) .add("p999", 1100.0 * DURATION_FACTOR) .add("std", 500.0 * DURATION_FACTOR), asMap(out)); } private Map<String, Object> asMap(ByteArrayOutputStream os) throws java.io.IOException { return asMap(new String(os.toByteArray()).split("\n")[1]); } @SuppressWarnings("unchecked") private Map<String, Object> asMap(String json) throws java.io.IOException { final TreeMap<String, Object> result = new TreeMap<String, Object>(); result.putAll(JsonUtils.getMapper().readValue(json, Map.class)); return result; } }