/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.kafka.common.metrics;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.apache.kafka.common.Metric;
import org.apache.kafka.common.MetricName;
import org.apache.kafka.common.metrics.stats.Avg;
import org.apache.kafka.common.metrics.stats.Count;
import org.apache.kafka.common.metrics.stats.Max;
import org.apache.kafka.common.metrics.stats.Min;
import org.apache.kafka.common.metrics.stats.Percentile;
import org.apache.kafka.common.metrics.stats.Percentiles;
import org.apache.kafka.common.metrics.stats.Percentiles.BucketSizing;
import org.apache.kafka.common.metrics.stats.Rate;
import org.apache.kafka.common.metrics.stats.Total;
import org.apache.kafka.common.metrics.stats.SimpleRate;
import org.apache.kafka.common.utils.MockTime;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
public class MetricsTest {
private static final double EPS = 0.000001;
private MockTime time = new MockTime();
private MetricConfig config = new MetricConfig();
private Metrics metrics;
@Before
public void setup() {
this.metrics = new Metrics(config, Arrays.asList((MetricsReporter) new JmxReporter()), time, true);
}
@After
public void tearDown() {
this.metrics.close();
}
@Test
public void testMetricName() {
MetricName n1 = metrics.metricName("name", "group", "description", "key1", "value1", "key2", "value2");
Map<String, String> tags = new HashMap<String, String>();
tags.put("key1", "value1");
tags.put("key2", "value2");
MetricName n2 = metrics.metricName("name", "group", "description", tags);
assertEquals("metric names created in two different ways should be equal", n1, n2);
try {
metrics.metricName("name", "group", "description", "key1");
fail("Creating MetricName with an odd number of keyValue should fail");
} catch (IllegalArgumentException e) {
// this is expected
}
}
@Test
public void testSimpleStats() throws Exception {
ConstantMeasurable measurable = new ConstantMeasurable();
metrics.addMetric(metrics.metricName("direct.measurable", "grp1", "The fraction of time an appender waits for space allocation."), measurable);
Sensor s = metrics.sensor("test.sensor");
s.add(metrics.metricName("test.avg", "grp1"), new Avg());
s.add(metrics.metricName("test.max", "grp1"), new Max());
s.add(metrics.metricName("test.min", "grp1"), new Min());
s.add(metrics.metricName("test.rate", "grp1"), new Rate(TimeUnit.SECONDS));
s.add(metrics.metricName("test.occurences", "grp1"), new Rate(TimeUnit.SECONDS, new Count()));
s.add(metrics.metricName("test.count", "grp1"), new Count());
s.add(new Percentiles(100, -100, 100, BucketSizing.CONSTANT,
new Percentile(metrics.metricName("test.median", "grp1"), 50.0),
new Percentile(metrics.metricName("test.perc99_9", "grp1"), 99.9)));
Sensor s2 = metrics.sensor("test.sensor2");
s2.add(metrics.metricName("s2.total", "grp1"), new Total());
s2.record(5.0);
int sum = 0;
int count = 10;
for (int i = 0; i < count; i++) {
s.record(i);
sum += i;
}
// prior to any time passing
double elapsedSecs = (config.timeWindowMs() * (config.samples() - 1)) / 1000.0;
assertEquals(String.format("Occurrences(0...%d) = %f", count, count / elapsedSecs), count / elapsedSecs,
metrics.metrics().get(metrics.metricName("test.occurences", "grp1")).value(), EPS);
// pretend 2 seconds passed...
long sleepTimeMs = 2;
time.sleep(sleepTimeMs * 1000);
elapsedSecs += sleepTimeMs;
assertEquals("s2 reflects the constant value", 5.0, metrics.metrics().get(metrics.metricName("s2.total", "grp1")).value(), EPS);
assertEquals("Avg(0...9) = 4.5", 4.5, metrics.metrics().get(metrics.metricName("test.avg", "grp1")).value(), EPS);
assertEquals("Max(0...9) = 9", count - 1, metrics.metrics().get(metrics.metricName("test.max", "grp1")).value(), EPS);
assertEquals("Min(0...9) = 0", 0.0, metrics.metrics().get(metrics.metricName("test.min", "grp1")).value(), EPS);
assertEquals("Rate(0...9) = 1.40625",
sum / elapsedSecs, metrics.metrics().get(metrics.metricName("test.rate", "grp1")).value(), EPS);
assertEquals(String.format("Occurrences(0...%d) = %f", count, count / elapsedSecs),
count / elapsedSecs,
metrics.metrics().get(metrics.metricName("test.occurences", "grp1")).value(), EPS);
assertEquals("Count(0...9) = 10",
(double) count, metrics.metrics().get(metrics.metricName("test.count", "grp1")).value(), EPS);
}
@Test
public void testHierarchicalSensors() {
Sensor parent1 = metrics.sensor("test.parent1");
parent1.add(metrics.metricName("test.parent1.count", "grp1"), new Count());
Sensor parent2 = metrics.sensor("test.parent2");
parent2.add(metrics.metricName("test.parent2.count", "grp1"), new Count());
Sensor child1 = metrics.sensor("test.child1", parent1, parent2);
child1.add(metrics.metricName("test.child1.count", "grp1"), new Count());
Sensor child2 = metrics.sensor("test.child2", parent1);
child2.add(metrics.metricName("test.child2.count", "grp1"), new Count());
Sensor grandchild = metrics.sensor("test.grandchild", child1);
grandchild.add(metrics.metricName("test.grandchild.count", "grp1"), new Count());
/* increment each sensor one time */
parent1.record();
parent2.record();
child1.record();
child2.record();
grandchild.record();
double p1 = parent1.metrics().get(0).value();
double p2 = parent2.metrics().get(0).value();
double c1 = child1.metrics().get(0).value();
double c2 = child2.metrics().get(0).value();
double gc = grandchild.metrics().get(0).value();
/* each metric should have a count equal to one + its children's count */
assertEquals(1.0, gc, EPS);
assertEquals(1.0 + gc, c1, EPS);
assertEquals(1.0, c2, EPS);
assertEquals(1.0 + c1, p2, EPS);
assertEquals(1.0 + c1 + c2, p1, EPS);
assertEquals(Arrays.asList(child1, child2), metrics.childrenSensors().get(parent1));
assertEquals(Arrays.asList(child1), metrics.childrenSensors().get(parent2));
assertNull(metrics.childrenSensors().get(grandchild));
}
@Test(expected = IllegalArgumentException.class)
public void testBadSensorHierarchy() {
Sensor p = metrics.sensor("parent");
Sensor c1 = metrics.sensor("child1", p);
Sensor c2 = metrics.sensor("child2", p);
metrics.sensor("gc", c1, c2); // should fail
}
@Test
public void testRemoveSensor() {
int size = metrics.metrics().size();
Sensor parent1 = metrics.sensor("test.parent1");
parent1.add(metrics.metricName("test.parent1.count", "grp1"), new Count());
Sensor parent2 = metrics.sensor("test.parent2");
parent2.add(metrics.metricName("test.parent2.count", "grp1"), new Count());
Sensor child1 = metrics.sensor("test.child1", parent1, parent2);
child1.add(metrics.metricName("test.child1.count", "grp1"), new Count());
Sensor child2 = metrics.sensor("test.child2", parent2);
child2.add(metrics.metricName("test.child2.count", "grp1"), new Count());
Sensor grandChild1 = metrics.sensor("test.gchild2", child2);
grandChild1.add(metrics.metricName("test.gchild2.count", "grp1"), new Count());
Sensor sensor = metrics.getSensor("test.parent1");
assertNotNull(sensor);
metrics.removeSensor("test.parent1");
assertNull(metrics.getSensor("test.parent1"));
assertNull(metrics.metrics().get(metrics.metricName("test.parent1.count", "grp1")));
assertNull(metrics.getSensor("test.child1"));
assertNull(metrics.childrenSensors().get(sensor));
assertNull(metrics.metrics().get(metrics.metricName("test.child1.count", "grp1")));
sensor = metrics.getSensor("test.gchild2");
assertNotNull(sensor);
metrics.removeSensor("test.gchild2");
assertNull(metrics.getSensor("test.gchild2"));
assertNull(metrics.childrenSensors().get(sensor));
assertNull(metrics.metrics().get(metrics.metricName("test.gchild2.count", "grp1")));
sensor = metrics.getSensor("test.child2");
assertNotNull(sensor);
metrics.removeSensor("test.child2");
assertNull(metrics.getSensor("test.child2"));
assertNull(metrics.childrenSensors().get(sensor));
assertNull(metrics.metrics().get(metrics.metricName("test.child2.count", "grp1")));
sensor = metrics.getSensor("test.parent2");
assertNotNull(sensor);
metrics.removeSensor("test.parent2");
assertNull(metrics.getSensor("test.parent2"));
assertNull(metrics.childrenSensors().get(sensor));
assertNull(metrics.metrics().get(metrics.metricName("test.parent2.count", "grp1")));
assertEquals(size, metrics.metrics().size());
}
@Test
public void testRemoveInactiveMetrics() {
Sensor s1 = metrics.sensor("test.s1", null, 1);
s1.add(metrics.metricName("test.s1.count", "grp1"), new Count());
Sensor s2 = metrics.sensor("test.s2", null, 3);
s2.add(metrics.metricName("test.s2.count", "grp1"), new Count());
Metrics.ExpireSensorTask purger = metrics.new ExpireSensorTask();
purger.run();
assertNotNull("Sensor test.s1 must be present", metrics.getSensor("test.s1"));
assertNotNull("MetricName test.s1.count must be present",
metrics.metrics().get(metrics.metricName("test.s1.count", "grp1")));
assertNotNull("Sensor test.s2 must be present", metrics.getSensor("test.s2"));
assertNotNull("MetricName test.s2.count must be present",
metrics.metrics().get(metrics.metricName("test.s2.count", "grp1")));
time.sleep(1001);
purger.run();
assertNull("Sensor test.s1 should have been purged", metrics.getSensor("test.s1"));
assertNull("MetricName test.s1.count should have been purged",
metrics.metrics().get(metrics.metricName("test.s1.count", "grp1")));
assertNotNull("Sensor test.s2 must be present", metrics.getSensor("test.s2"));
assertNotNull("MetricName test.s2.count must be present",
metrics.metrics().get(metrics.metricName("test.s2.count", "grp1")));
// record a value in sensor s2. This should reset the clock for that sensor.
// It should not get purged at the 3 second mark after creation
s2.record();
time.sleep(2000);
purger.run();
assertNotNull("Sensor test.s2 must be present", metrics.getSensor("test.s2"));
assertNotNull("MetricName test.s2.count must be present",
metrics.metrics().get(metrics.metricName("test.s2.count", "grp1")));
// After another 1 second sleep, the metric should be purged
time.sleep(1000);
purger.run();
assertNull("Sensor test.s2 should have been purged", metrics.getSensor("test.s1"));
assertNull("MetricName test.s2.count should have been purged",
metrics.metrics().get(metrics.metricName("test.s1.count", "grp1")));
// After purging, it should be possible to recreate a metric
s1 = metrics.sensor("test.s1", null, 1);
s1.add(metrics.metricName("test.s1.count", "grp1"), new Count());
assertNotNull("Sensor test.s1 must be present", metrics.getSensor("test.s1"));
assertNotNull("MetricName test.s1.count must be present",
metrics.metrics().get(metrics.metricName("test.s1.count", "grp1")));
}
@Test
public void testRemoveMetric() {
int size = metrics.metrics().size();
metrics.addMetric(metrics.metricName("test1", "grp1"), new Count());
metrics.addMetric(metrics.metricName("test2", "grp1"), new Count());
assertNotNull(metrics.removeMetric(metrics.metricName("test1", "grp1")));
assertNull(metrics.metrics().get(metrics.metricName("test1", "grp1")));
assertNotNull(metrics.metrics().get(metrics.metricName("test2", "grp1")));
assertNotNull(metrics.removeMetric(metrics.metricName("test2", "grp1")));
assertNull(metrics.metrics().get(metrics.metricName("test2", "grp1")));
assertEquals(size, metrics.metrics().size());
}
@Test
public void testEventWindowing() {
Count count = new Count();
MetricConfig config = new MetricConfig().eventWindow(1).samples(2);
count.record(config, 1.0, time.milliseconds());
count.record(config, 1.0, time.milliseconds());
assertEquals(2.0, count.measure(config, time.milliseconds()), EPS);
count.record(config, 1.0, time.milliseconds()); // first event times out
assertEquals(2.0, count.measure(config, time.milliseconds()), EPS);
}
@Test
public void testTimeWindowing() {
Count count = new Count();
MetricConfig config = new MetricConfig().timeWindow(1, TimeUnit.MILLISECONDS).samples(2);
count.record(config, 1.0, time.milliseconds());
time.sleep(1);
count.record(config, 1.0, time.milliseconds());
assertEquals(2.0, count.measure(config, time.milliseconds()), EPS);
time.sleep(1);
count.record(config, 1.0, time.milliseconds()); // oldest event times out
assertEquals(2.0, count.measure(config, time.milliseconds()), EPS);
}
@Test
public void testOldDataHasNoEffect() {
Max max = new Max();
long windowMs = 100;
int samples = 2;
MetricConfig config = new MetricConfig().timeWindow(windowMs, TimeUnit.MILLISECONDS).samples(samples);
max.record(config, 50, time.milliseconds());
time.sleep(samples * windowMs);
assertEquals(Double.NEGATIVE_INFINITY, max.measure(config, time.milliseconds()), EPS);
}
@Test
public void testSampledStatInitialValue() {
// initialValue from each SampledStat is set as the initialValue on its Sample.
// The only way to test the initialValue is to infer it by having a SampledStat
// with expired Stats, because their values are reset to the initial values.
// Most implementations of combine on SampledStat end up returning the default
// value, so we can use this. This doesn't work for Percentiles though.
// This test looks a lot like testOldDataHasNoEffect because it's the same
// flow that leads to this state.
Max max = new Max();
Min min = new Min();
Avg avg = new Avg();
Count count = new Count();
Rate.SampledTotal sampledTotal = new Rate.SampledTotal();
long windowMs = 100;
int samples = 2;
MetricConfig config = new MetricConfig().timeWindow(windowMs, TimeUnit.MILLISECONDS).samples(samples);
max.record(config, 50, time.milliseconds());
min.record(config, 50, time.milliseconds());
avg.record(config, 50, time.milliseconds());
count.record(config, 50, time.milliseconds());
sampledTotal.record(config, 50, time.milliseconds());
time.sleep(samples * windowMs);
assertEquals(Double.NEGATIVE_INFINITY, max.measure(config, time.milliseconds()), EPS);
assertEquals(Double.MAX_VALUE, min.measure(config, time.milliseconds()), EPS);
assertEquals(0.0, avg.measure(config, time.milliseconds()), EPS);
assertEquals(0, count.measure(config, time.milliseconds()), EPS);
assertEquals(0.0, sampledTotal.measure(config, time.milliseconds()), EPS);
}
@Test(expected = IllegalArgumentException.class)
public void testDuplicateMetricName() {
metrics.sensor("test").add(metrics.metricName("test", "grp1"), new Avg());
metrics.sensor("test2").add(metrics.metricName("test", "grp1"), new Total());
}
@Test
public void testQuotas() {
Sensor sensor = metrics.sensor("test");
sensor.add(metrics.metricName("test1.total", "grp1"), new Total(), new MetricConfig().quota(Quota.upperBound(5.0)));
sensor.add(metrics.metricName("test2.total", "grp1"), new Total(), new MetricConfig().quota(Quota.lowerBound(0.0)));
sensor.record(5.0);
try {
sensor.record(1.0);
fail("Should have gotten a quota violation.");
} catch (QuotaViolationException e) {
// this is good
}
assertEquals(6.0, metrics.metrics().get(metrics.metricName("test1.total", "grp1")).value(), EPS);
sensor.record(-6.0);
try {
sensor.record(-1.0);
fail("Should have gotten a quota violation.");
} catch (QuotaViolationException e) {
// this is good
}
}
@Test
public void testQuotasEquality() {
final Quota quota1 = Quota.upperBound(10.5);
final Quota quota2 = Quota.lowerBound(10.5);
assertFalse("Quota with different upper values shouldn't be equal", quota1.equals(quota2));
final Quota quota3 = Quota.lowerBound(10.5);
assertTrue("Quota with same upper and bound values should be equal", quota2.equals(quota3));
}
@Test
public void testPercentiles() {
int buckets = 100;
Percentiles percs = new Percentiles(4 * buckets,
0.0,
100.0,
BucketSizing.CONSTANT,
new Percentile(metrics.metricName("test.p25", "grp1"), 25),
new Percentile(metrics.metricName("test.p50", "grp1"), 50),
new Percentile(metrics.metricName("test.p75", "grp1"), 75));
MetricConfig config = new MetricConfig().eventWindow(50).samples(2);
Sensor sensor = metrics.sensor("test", config);
sensor.add(percs);
Metric p25 = this.metrics.metrics().get(metrics.metricName("test.p25", "grp1"));
Metric p50 = this.metrics.metrics().get(metrics.metricName("test.p50", "grp1"));
Metric p75 = this.metrics.metrics().get(metrics.metricName("test.p75", "grp1"));
// record two windows worth of sequential values
for (int i = 0; i < buckets; i++)
sensor.record(i);
assertEquals(25, p25.value(), 1.0);
assertEquals(50, p50.value(), 1.0);
assertEquals(75, p75.value(), 1.0);
for (int i = 0; i < buckets; i++)
sensor.record(0.0);
assertEquals(0.0, p25.value(), 1.0);
assertEquals(0.0, p50.value(), 1.0);
assertEquals(0.0, p75.value(), 1.0);
// record two more windows worth of sequential values
for (int i = 0; i < buckets; i++)
sensor.record(i);
assertEquals(25, p25.value(), 1.0);
assertEquals(50, p50.value(), 1.0);
assertEquals(75, p75.value(), 1.0);
}
@Test
public void testRateWindowing() throws Exception {
// Use the default time window. Set 3 samples
MetricConfig cfg = new MetricConfig().samples(3);
Sensor s = metrics.sensor("test.sensor", cfg);
s.add(metrics.metricName("test.rate", "grp1"), new Rate(TimeUnit.SECONDS));
int sum = 0;
int count = cfg.samples() - 1;
// Advance 1 window after every record
for (int i = 0; i < count; i++) {
s.record(100);
sum += 100;
time.sleep(cfg.timeWindowMs());
}
// Sleep for half the window.
time.sleep(cfg.timeWindowMs() / 2);
// prior to any time passing
double elapsedSecs = (cfg.timeWindowMs() * (cfg.samples() - 1) + cfg.timeWindowMs() / 2) / 1000.0;
KafkaMetric km = metrics.metrics().get(metrics.metricName("test.rate", "grp1"));
assertEquals("Rate(0...2) = 2.666", sum / elapsedSecs, km.value(), EPS);
assertEquals("Elapsed Time = 75 seconds", elapsedSecs,
((Rate) km.measurable()).windowSize(cfg, time.milliseconds()) / 1000, EPS);
}
public static class ConstantMeasurable implements Measurable {
public double value = 0.0;
@Override
public double measure(MetricConfig config, long now) {
return value;
}
}
@Test
public void testSimpleRate() {
SimpleRate rate = new SimpleRate();
//Given
MetricConfig config = new MetricConfig().timeWindow(1, TimeUnit.SECONDS).samples(10);
//In the first window the rate is a fraction of the whole (1s) window
//So when we record 1000 at t0, the rate should be 1000 until the window completes, or more data is recorded.
record(rate, config, 1000);
assertEquals(1000, measure(rate, config), 0);
time.sleep(100);
assertEquals(1000, measure(rate, config), 0); // 1000B / 0.1s
time.sleep(100);
assertEquals(1000, measure(rate, config), 0); // 1000B / 0.2s
time.sleep(200);
assertEquals(1000, measure(rate, config), 0); // 1000B / 0.4s
//In the second (and subsequent) window(s), the rate will be in proportion to the elapsed time
//So the rate will degrade over time, as the time between measurement and the initial recording grows.
time.sleep(600);
assertEquals(1000, measure(rate, config), 0); // 1000B / 1.0s
time.sleep(200);
assertEquals(1000 / 1.2, measure(rate, config), 0); // 1000B / 1.2s
time.sleep(200);
assertEquals(1000 / 1.4, measure(rate, config), 0); // 1000B / 1.4s
//Adding another value, inside the same window should double the rate
record(rate, config, 1000);
assertEquals(2000 / 1.4, measure(rate, config), 0); // 2000B / 1.4s
//Going over the next window, should not change behaviour
time.sleep(1100);
assertEquals(2000 / 2.5, measure(rate, config), 0); // 2000B / 2.5s
record(rate, config, 1000);
assertEquals(3000 / 2.5, measure(rate, config), 0); // 3000B / 2.5s
//Sleeping for another 6.5 windows also should be the same
time.sleep(6500);
assertEquals(3000 / 9, measure(rate, config), 1); // 3000B / 9s
record(rate, config, 1000);
assertEquals(4000 / 9, measure(rate, config), 1); // 4000B / 9s
//Going over the 10 window boundary should cause the first window's values (1000) will be purged.
//So the rate is calculated based on the oldest reading, which is inside the second window, at 1.4s
time.sleep(1500);
assertEquals((4000 - 1000) / (10.5 - 1.4), measure(rate, config), 1);
record(rate, config, 1000);
assertEquals((5000 - 1000) / (10.5 - 1.4), measure(rate, config), 1);
}
private void record(Rate rate, MetricConfig config, int value) {
rate.record(config, value, time.milliseconds());
}
private Double measure(Measurable rate, MetricConfig config) {
return rate.measure(config, time.milliseconds());
}
}