/*- * -\-\- * Helios Services * -- * Copyright (C) 2016 Spotify AB * -- * 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 com.spotify.helios.servicescommon.statistics; import static org.mockito.Matchers.argThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; import com.codahale.metrics.Gauge; import com.codahale.metrics.Histogram; import com.codahale.metrics.MetricRegistry; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.spotify.helios.common.Version; import eu.toolchain.ffwd.FastForward; import eu.toolchain.ffwd.Metric; import java.util.Collections; import java.util.Map; import java.util.Set; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Supplier; import java.util.stream.IntStream; import org.hamcrest.CoreMatchers; import org.hamcrest.CustomTypeSafeMatcher; import org.hamcrest.Matcher; import org.junit.Before; import org.junit.Test; public class FastForwardReporterTest { private final FastForward ffwd = mock(FastForward.class); private final MetricRegistry metricRegistry = new MetricRegistry(); private ScheduledExecutorService executor; private FastForwardReporter reporter; @Before public void setUp() throws Exception { final ThreadFactory threadFactory = new ThreadFactoryBuilder().setDaemon(true).build(); this.executor = Executors.newSingleThreadScheduledExecutor(threadFactory); this.reporter = new FastForwardReporter(ffwd, metricRegistry, executor, "helios.test", // these interval values do not matter for this test: 30, TimeUnit.SECONDS, Collections::emptyMap); } /** * A matcher that matches when the actual object contains all of the given attributes * (note that the reverse isn't necessarily true; this is a partial matcher). */ private static Matcher<Metric> containsAttributes(Map<String, String> attributes) { final String description = String.format("a metric containing attributes=%s", attributes); return new CustomTypeSafeMatcher<Metric>(description) { @Override protected boolean matchesSafely(final Metric item) { return item.getAttributes().entrySet().containsAll(attributes.entrySet()); } }; } private static Matcher<Metric> containsAttributes(String... strings) { final ImmutableMap.Builder<String, String> builder = ImmutableMap.builder(); for (int i = 0; i < strings.length; i += 2) { builder.put(strings[i], strings[i + 1]); } return containsAttributes(builder.build()); } private static Matcher<Metric> hasValue(double value) { return new CustomTypeSafeMatcher<Metric>("a metric with value=" + value) { @Override protected boolean matchesSafely(final Metric item) { return item.getValue() == value; } }; } private static Matcher<Metric> hasKey(String key) { return new CustomTypeSafeMatcher<Metric>("a metric with key=" + key) { @Override protected boolean matchesSafely(final Metric item) { return item.getKey().equals(key); } }; } // The compiler has a very hard time inferring the type of an expression like // argThat(allOf(foo(), bar())) as being Matcher<Metric>, so we define the method here // to help out poor confused javac. @SafeVarargs private static Matcher<Metric> allOf(Matcher<Metric>... matchers) { return CoreMatchers.allOf(matchers); } @Test public void testGauges() throws Exception { metricRegistry.register("some.gauge1", (Gauge<Integer>) () -> 1); metricRegistry.register("some.gauge2", (Gauge<Integer>) () -> 2); reporter.reportOnce(); verify(ffwd).send(argThat(allOf( hasKey("helios.test"), containsAttributes("what", "some.gauge1", "metric_type", "gauge"), hasValue(1) ))); verify(ffwd).send(argThat(allOf( hasKey("helios.test"), containsAttributes("what", "some.gauge2", "metric_type", "gauge"), hasValue(2) ))); } @Test public void testCounter() throws Exception { metricRegistry.counter("counting.is.fun") .inc(7982); reporter.reportOnce(); verify(ffwd).send(argThat(allOf( hasKey("helios.test"), containsAttributes("what", "counting.is.fun", "metric_type", "counter"), hasValue(7982) ))); } @Test public void testMeter() throws Exception { metricRegistry.meter("the-meter"); reporter.reportOnce(); verifyMeterStats("the-meter", "meter"); } private void verifyMeterStats(String what, String type) throws Exception { for (final String stat : new String[]{"1m", "5m"}) { verify(ffwd).send(argThat(allOf( hasKey("helios.test"), containsAttributes("what", what, "metric_type", type, "stat", stat), hasValue(0) ))); } } @Test public void testHistogram() throws Exception { final Histogram h = metricRegistry.histogram("histo.gram"); IntStream.range(1, 10).forEach(h::update); reporter.reportOnce(); verifyHistogramStats("histo.gram", "histogram"); } private void verifyHistogramStats(String what, String type) throws Exception { final Set<String> expectedStats = ImmutableSet.of("median", "p75", "p99", "mean", "min", "max", "stddev"); for (final String stat : expectedStats) { verify(ffwd).send(argThat(allOf( hasKey("helios.test"), containsAttributes("what", what, "metric_type", type, "stat", stat)))); } } @Test public void testTimer() throws Exception { metricRegistry.timer("blah-timer"); reporter.reportOnce(); verifyHistogramStats("blah-timer", "timer"); verifyMeterStats("blah-timer", "timer"); } @Test public void testAttributesIncludeHeliosVersion() throws Exception { metricRegistry.register("something", (Gauge<Integer>) () -> 1); reporter.reportOnce(); verify(ffwd).send(argThat(containsAttributes("helios_version", Version.POM_VERSION))); } @Test public void testAttributesIncludeAdditionalAttributes() throws Exception { // a counter to keep track of how often the Supplier is called final AtomicInteger counter = new AtomicInteger(0); final Supplier<Map<String, String>> additionalAttributes = () -> { final int count = counter.incrementAndGet(); return ImmutableMap.of("foo", "bar", "counter", String.valueOf(count)); }; this.reporter = new FastForwardReporter(ffwd, metricRegistry, executor, "helios.test", 30, TimeUnit.SECONDS, additionalAttributes); metricRegistry.register("gauge1", (Gauge<Integer>) () -> 1); metricRegistry.register("gauge2", (Gauge<Integer>) () -> 2); reporter.reportOnce(); verify(ffwd).send(argThat(containsAttributes("what", "gauge1", "foo", "bar", "counter", "1"))); verify(ffwd).send(argThat(containsAttributes("what", "gauge2", "foo", "bar", "counter", "1"))); reset(ffwd); reporter.reportOnce(); verify(ffwd).send(argThat(containsAttributes("what", "gauge1", "foo", "bar", "counter", "2"))); verify(ffwd).send(argThat(containsAttributes("what", "gauge2", "foo", "bar", "counter", "2"))); } }