500 lines
19 KiB
Python
500 lines
19 KiB
Python
import sys
|
|
import time
|
|
|
|
import pytest
|
|
|
|
from kafka.errors import QuotaViolationError
|
|
from kafka.metrics import DictReporter, MetricConfig, MetricName, Metrics, Quota
|
|
from kafka.metrics.measurable import AbstractMeasurable
|
|
from kafka.metrics.stats import (Avg, Count, Max, Min, Percentile, Percentiles,
|
|
Rate, Total)
|
|
from kafka.metrics.stats.percentiles import BucketSizing
|
|
from kafka.metrics.stats.rate import TimeUnit
|
|
|
|
EPS = 0.000001
|
|
|
|
|
|
@pytest.fixture
|
|
def time_keeper():
|
|
return TimeKeeper()
|
|
|
|
|
|
@pytest.fixture
|
|
def config():
|
|
return MetricConfig()
|
|
|
|
|
|
@pytest.fixture
|
|
def reporter():
|
|
return DictReporter()
|
|
|
|
|
|
@pytest.fixture
|
|
def metrics(request, config, reporter):
|
|
metrics = Metrics(config, [reporter], enable_expiration=True)
|
|
request.addfinalizer(lambda: metrics.close())
|
|
return metrics
|
|
|
|
|
|
def test_MetricName():
|
|
# The Java test only cover the differences between the deprecated
|
|
# constructors, so I'm skipping them but doing some other basic testing.
|
|
|
|
# In short, metrics should be equal IFF their name, group, and tags are
|
|
# the same. Descriptions do not matter.
|
|
name1 = MetricName('name', 'group', 'A metric.', {'a': 1, 'b': 2})
|
|
name2 = MetricName('name', 'group', 'A description.', {'a': 1, 'b': 2})
|
|
assert name1 == name2
|
|
|
|
name1 = MetricName('name', 'group', tags={'a': 1, 'b': 2})
|
|
name2 = MetricName('name', 'group', tags={'a': 1, 'b': 2})
|
|
assert name1 == name2
|
|
|
|
name1 = MetricName('foo', 'group')
|
|
name2 = MetricName('name', 'group')
|
|
assert name1 != name2
|
|
|
|
name1 = MetricName('name', 'foo')
|
|
name2 = MetricName('name', 'group')
|
|
assert name1 != name2
|
|
|
|
# name and group must be non-empty. Everything else is optional.
|
|
with pytest.raises(Exception):
|
|
MetricName('', 'group')
|
|
with pytest.raises(Exception):
|
|
MetricName('name', None)
|
|
# tags must be a dict if supplied
|
|
with pytest.raises(Exception):
|
|
MetricName('name', 'group', tags=set())
|
|
|
|
# Because of the implementation of __eq__ and __hash__, the values of
|
|
# a MetricName cannot be mutable.
|
|
tags = {'a': 1}
|
|
name = MetricName('name', 'group', 'description', tags=tags)
|
|
with pytest.raises(AttributeError):
|
|
name.name = 'new name'
|
|
with pytest.raises(AttributeError):
|
|
name.group = 'new name'
|
|
with pytest.raises(AttributeError):
|
|
name.tags = {}
|
|
# tags is a copy, so the instance isn't altered
|
|
name.tags['b'] = 2
|
|
assert name.tags == tags
|
|
|
|
|
|
def test_simple_stats(mocker, time_keeper, config, metrics):
|
|
mocker.patch('time.time', side_effect=time_keeper.time)
|
|
|
|
measurable = ConstantMeasurable()
|
|
|
|
metrics.add_metric(metrics.metric_name('direct.measurable', 'grp1',
|
|
'The fraction of time an appender waits for space allocation.'),
|
|
measurable)
|
|
sensor = metrics.sensor('test.sensor')
|
|
sensor.add(metrics.metric_name('test.avg', 'grp1'), Avg())
|
|
sensor.add(metrics.metric_name('test.max', 'grp1'), Max())
|
|
sensor.add(metrics.metric_name('test.min', 'grp1'), Min())
|
|
sensor.add(metrics.metric_name('test.rate', 'grp1'), Rate(TimeUnit.SECONDS))
|
|
sensor.add(metrics.metric_name('test.occurences', 'grp1'),Rate(TimeUnit.SECONDS, Count()))
|
|
sensor.add(metrics.metric_name('test.count', 'grp1'), Count())
|
|
percentiles = [Percentile(metrics.metric_name('test.median', 'grp1'), 50.0),
|
|
Percentile(metrics.metric_name('test.perc99_9', 'grp1'), 99.9)]
|
|
sensor.add_compound(Percentiles(100, BucketSizing.CONSTANT, 100, -100,
|
|
percentiles=percentiles))
|
|
|
|
sensor2 = metrics.sensor('test.sensor2')
|
|
sensor2.add(metrics.metric_name('s2.total', 'grp1'), Total())
|
|
sensor2.record(5.0)
|
|
|
|
sum_val = 0
|
|
count = 10
|
|
for i in range(count):
|
|
sensor.record(i)
|
|
sum_val += i
|
|
|
|
# prior to any time passing
|
|
elapsed_secs = (config.time_window_ms * (config.samples - 1)) / 1000.0
|
|
assert abs(count / elapsed_secs -
|
|
metrics.metrics.get(metrics.metric_name('test.occurences', 'grp1')).value()) \
|
|
< EPS, 'Occurrences(0...%d) = %f' % (count, count / elapsed_secs)
|
|
|
|
# pretend 2 seconds passed...
|
|
sleep_time_seconds = 2.0
|
|
time_keeper.sleep(sleep_time_seconds)
|
|
elapsed_secs += sleep_time_seconds
|
|
|
|
assert abs(5.0 - metrics.metrics.get(metrics.metric_name('s2.total', 'grp1')).value()) \
|
|
< EPS, 's2 reflects the constant value'
|
|
assert abs(4.5 - metrics.metrics.get(metrics.metric_name('test.avg', 'grp1')).value()) \
|
|
< EPS, 'Avg(0...9) = 4.5'
|
|
assert abs((count - 1) - metrics.metrics.get(metrics.metric_name('test.max', 'grp1')).value()) \
|
|
< EPS, 'Max(0...9) = 9'
|
|
assert abs(0.0 - metrics.metrics.get(metrics.metric_name('test.min', 'grp1')).value()) \
|
|
< EPS, 'Min(0...9) = 0'
|
|
assert abs((sum_val / elapsed_secs) - metrics.metrics.get(metrics.metric_name('test.rate', 'grp1')).value()) \
|
|
< EPS, 'Rate(0...9) = 1.40625'
|
|
assert abs((count / elapsed_secs) - metrics.metrics.get(metrics.metric_name('test.occurences', 'grp1')).value()) \
|
|
< EPS, 'Occurrences(0...%d) = %f' % (count, count / elapsed_secs)
|
|
assert abs(count - metrics.metrics.get(metrics.metric_name('test.count', 'grp1')).value()) \
|
|
< EPS, 'Count(0...9) = 10'
|
|
|
|
|
|
def test_hierarchical_sensors(metrics):
|
|
parent1 = metrics.sensor('test.parent1')
|
|
parent1.add(metrics.metric_name('test.parent1.count', 'grp1'), Count())
|
|
parent2 = metrics.sensor('test.parent2')
|
|
parent2.add(metrics.metric_name('test.parent2.count', 'grp1'), Count())
|
|
child1 = metrics.sensor('test.child1', parents=[parent1, parent2])
|
|
child1.add(metrics.metric_name('test.child1.count', 'grp1'), Count())
|
|
child2 = metrics.sensor('test.child2', parents=[parent1])
|
|
child2.add(metrics.metric_name('test.child2.count', 'grp1'), Count())
|
|
grandchild = metrics.sensor('test.grandchild', parents=[child1])
|
|
grandchild.add(metrics.metric_name('test.grandchild.count', 'grp1'), Count())
|
|
|
|
# increment each sensor one time
|
|
parent1.record()
|
|
parent2.record()
|
|
child1.record()
|
|
child2.record()
|
|
grandchild.record()
|
|
|
|
p1 = parent1.metrics[0].value()
|
|
p2 = parent2.metrics[0].value()
|
|
c1 = child1.metrics[0].value()
|
|
c2 = child2.metrics[0].value()
|
|
gc = grandchild.metrics[0].value()
|
|
|
|
# each metric should have a count equal to one + its children's count
|
|
assert 1.0 == gc
|
|
assert 1.0 + gc == c1
|
|
assert 1.0 == c2
|
|
assert 1.0 + c1 == p2
|
|
assert 1.0 + c1 + c2 == p1
|
|
assert [child1, child2] == metrics._children_sensors.get(parent1)
|
|
assert [child1] == metrics._children_sensors.get(parent2)
|
|
assert metrics._children_sensors.get(grandchild) is None
|
|
|
|
|
|
def test_bad_sensor_hierarchy(metrics):
|
|
parent = metrics.sensor('parent')
|
|
child1 = metrics.sensor('child1', parents=[parent])
|
|
child2 = metrics.sensor('child2', parents=[parent])
|
|
|
|
with pytest.raises(ValueError):
|
|
metrics.sensor('gc', parents=[child1, child2])
|
|
|
|
|
|
def test_remove_sensor(metrics):
|
|
size = len(metrics.metrics)
|
|
parent1 = metrics.sensor('test.parent1')
|
|
parent1.add(metrics.metric_name('test.parent1.count', 'grp1'), Count())
|
|
parent2 = metrics.sensor('test.parent2')
|
|
parent2.add(metrics.metric_name('test.parent2.count', 'grp1'), Count())
|
|
child1 = metrics.sensor('test.child1', parents=[parent1, parent2])
|
|
child1.add(metrics.metric_name('test.child1.count', 'grp1'), Count())
|
|
child2 = metrics.sensor('test.child2', parents=[parent2])
|
|
child2.add(metrics.metric_name('test.child2.count', 'grp1'), Count())
|
|
grandchild1 = metrics.sensor('test.gchild2', parents=[child2])
|
|
grandchild1.add(metrics.metric_name('test.gchild2.count', 'grp1'), Count())
|
|
|
|
sensor = metrics.get_sensor('test.parent1')
|
|
assert sensor is not None
|
|
metrics.remove_sensor('test.parent1')
|
|
assert metrics.get_sensor('test.parent1') is None
|
|
assert metrics.metrics.get(metrics.metric_name('test.parent1.count', 'grp1')) is None
|
|
assert metrics.get_sensor('test.child1') is None
|
|
assert metrics._children_sensors.get(sensor) is None
|
|
assert metrics.metrics.get(metrics.metric_name('test.child1.count', 'grp1')) is None
|
|
|
|
sensor = metrics.get_sensor('test.gchild2')
|
|
assert sensor is not None
|
|
metrics.remove_sensor('test.gchild2')
|
|
assert metrics.get_sensor('test.gchild2') is None
|
|
assert metrics._children_sensors.get(sensor) is None
|
|
assert metrics.metrics.get(metrics.metric_name('test.gchild2.count', 'grp1')) is None
|
|
|
|
sensor = metrics.get_sensor('test.child2')
|
|
assert sensor is not None
|
|
metrics.remove_sensor('test.child2')
|
|
assert metrics.get_sensor('test.child2') is None
|
|
assert metrics._children_sensors.get(sensor) is None
|
|
assert metrics.metrics.get(metrics.metric_name('test.child2.count', 'grp1')) is None
|
|
|
|
sensor = metrics.get_sensor('test.parent2')
|
|
assert sensor is not None
|
|
metrics.remove_sensor('test.parent2')
|
|
assert metrics.get_sensor('test.parent2') is None
|
|
assert metrics._children_sensors.get(sensor) is None
|
|
assert metrics.metrics.get(metrics.metric_name('test.parent2.count', 'grp1')) is None
|
|
|
|
assert size == len(metrics.metrics)
|
|
|
|
|
|
def test_remove_inactive_metrics(mocker, time_keeper, metrics):
|
|
mocker.patch('time.time', side_effect=time_keeper.time)
|
|
|
|
s1 = metrics.sensor('test.s1', None, 1)
|
|
s1.add(metrics.metric_name('test.s1.count', 'grp1'), Count())
|
|
|
|
s2 = metrics.sensor('test.s2', None, 3)
|
|
s2.add(metrics.metric_name('test.s2.count', 'grp1'), Count())
|
|
|
|
purger = Metrics.ExpireSensorTask
|
|
purger.run(metrics)
|
|
assert metrics.get_sensor('test.s1') is not None, \
|
|
'Sensor test.s1 must be present'
|
|
assert metrics.metrics.get(metrics.metric_name('test.s1.count', 'grp1')) is not None, \
|
|
'MetricName test.s1.count must be present'
|
|
assert metrics.get_sensor('test.s2') is not None, \
|
|
'Sensor test.s2 must be present'
|
|
assert metrics.metrics.get(metrics.metric_name('test.s2.count', 'grp1')) is not None, \
|
|
'MetricName test.s2.count must be present'
|
|
|
|
time_keeper.sleep(1.001)
|
|
purger.run(metrics)
|
|
assert metrics.get_sensor('test.s1') is None, \
|
|
'Sensor test.s1 should have been purged'
|
|
assert metrics.metrics.get(metrics.metric_name('test.s1.count', 'grp1')) is None, \
|
|
'MetricName test.s1.count should have been purged'
|
|
assert metrics.get_sensor('test.s2') is not None, \
|
|
'Sensor test.s2 must be present'
|
|
assert metrics.metrics.get(metrics.metric_name('test.s2.count', 'grp1')) is not None, \
|
|
'MetricName test.s2.count must be present'
|
|
|
|
# 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_keeper.sleep(2)
|
|
purger.run(metrics)
|
|
assert metrics.get_sensor('test.s2') is not None, \
|
|
'Sensor test.s2 must be present'
|
|
assert metrics.metrics.get(metrics.metric_name('test.s2.count', 'grp1')) is not None, \
|
|
'MetricName test.s2.count must be present'
|
|
|
|
# After another 1 second sleep, the metric should be purged
|
|
time_keeper.sleep(1)
|
|
purger.run(metrics)
|
|
assert metrics.get_sensor('test.s1') is None, \
|
|
'Sensor test.s2 should have been purged'
|
|
assert metrics.metrics.get(metrics.metric_name('test.s1.count', 'grp1')) is None, \
|
|
'MetricName test.s2.count should have been purged'
|
|
|
|
# After purging, it should be possible to recreate a metric
|
|
s1 = metrics.sensor('test.s1', None, 1)
|
|
s1.add(metrics.metric_name('test.s1.count', 'grp1'), Count())
|
|
assert metrics.get_sensor('test.s1') is not None, \
|
|
'Sensor test.s1 must be present'
|
|
assert metrics.metrics.get(metrics.metric_name('test.s1.count', 'grp1')) is not None, \
|
|
'MetricName test.s1.count must be present'
|
|
|
|
|
|
def test_remove_metric(metrics):
|
|
size = len(metrics.metrics)
|
|
metrics.add_metric(metrics.metric_name('test1', 'grp1'), Count())
|
|
metrics.add_metric(metrics.metric_name('test2', 'grp1'), Count())
|
|
|
|
assert metrics.remove_metric(metrics.metric_name('test1', 'grp1')) is not None
|
|
assert metrics.metrics.get(metrics.metric_name('test1', 'grp1')) is None
|
|
assert metrics.metrics.get(metrics.metric_name('test2', 'grp1')) is not None
|
|
|
|
assert metrics.remove_metric(metrics.metric_name('test2', 'grp1')) is not None
|
|
assert metrics.metrics.get(metrics.metric_name('test2', 'grp1')) is None
|
|
|
|
assert size == len(metrics.metrics)
|
|
|
|
|
|
def test_event_windowing(mocker, time_keeper):
|
|
mocker.patch('time.time', side_effect=time_keeper.time)
|
|
|
|
count = Count()
|
|
config = MetricConfig(event_window=1, samples=2)
|
|
count.record(config, 1.0, time_keeper.ms())
|
|
count.record(config, 1.0, time_keeper.ms())
|
|
assert 2.0 == count.measure(config, time_keeper.ms())
|
|
count.record(config, 1.0, time_keeper.ms()) # first event times out
|
|
assert 2.0 == count.measure(config, time_keeper.ms())
|
|
|
|
|
|
def test_time_windowing(mocker, time_keeper):
|
|
mocker.patch('time.time', side_effect=time_keeper.time)
|
|
|
|
count = Count()
|
|
config = MetricConfig(time_window_ms=1, samples=2)
|
|
count.record(config, 1.0, time_keeper.ms())
|
|
time_keeper.sleep(.001)
|
|
count.record(config, 1.0, time_keeper.ms())
|
|
assert 2.0 == count.measure(config, time_keeper.ms())
|
|
time_keeper.sleep(.001)
|
|
count.record(config, 1.0, time_keeper.ms()) # oldest event times out
|
|
assert 2.0 == count.measure(config, time_keeper.ms())
|
|
|
|
|
|
def test_old_data_has_no_effect(mocker, time_keeper):
|
|
mocker.patch('time.time', side_effect=time_keeper.time)
|
|
|
|
max_stat = Max()
|
|
min_stat = Min()
|
|
avg_stat = Avg()
|
|
count_stat = Count()
|
|
window_ms = 100
|
|
samples = 2
|
|
config = MetricConfig(time_window_ms=window_ms, samples=samples)
|
|
max_stat.record(config, 50, time_keeper.ms())
|
|
min_stat.record(config, 50, time_keeper.ms())
|
|
avg_stat.record(config, 50, time_keeper.ms())
|
|
count_stat.record(config, 50, time_keeper.ms())
|
|
|
|
time_keeper.sleep(samples * window_ms / 1000.0)
|
|
assert float('-inf') == max_stat.measure(config, time_keeper.ms())
|
|
assert float(sys.maxsize) == min_stat.measure(config, time_keeper.ms())
|
|
assert 0.0 == avg_stat.measure(config, time_keeper.ms())
|
|
assert 0 == count_stat.measure(config, time_keeper.ms())
|
|
|
|
|
|
def test_duplicate_MetricName(metrics):
|
|
metrics.sensor('test').add(metrics.metric_name('test', 'grp1'), Avg())
|
|
with pytest.raises(ValueError):
|
|
metrics.sensor('test2').add(metrics.metric_name('test', 'grp1'), Total())
|
|
|
|
|
|
def test_Quotas(metrics):
|
|
sensor = metrics.sensor('test')
|
|
sensor.add(metrics.metric_name('test1.total', 'grp1'), Total(),
|
|
MetricConfig(quota=Quota.upper_bound(5.0)))
|
|
sensor.add(metrics.metric_name('test2.total', 'grp1'), Total(),
|
|
MetricConfig(quota=Quota.lower_bound(0.0)))
|
|
sensor.record(5.0)
|
|
with pytest.raises(QuotaViolationError):
|
|
sensor.record(1.0)
|
|
|
|
assert abs(6.0 - metrics.metrics.get(metrics.metric_name('test1.total', 'grp1')).value()) \
|
|
< EPS
|
|
|
|
sensor.record(-6.0)
|
|
with pytest.raises(QuotaViolationError):
|
|
sensor.record(-1.0)
|
|
|
|
|
|
def test_Quotas_equality():
|
|
quota1 = Quota.upper_bound(10.5)
|
|
quota2 = Quota.lower_bound(10.5)
|
|
assert quota1 != quota2, 'Quota with different upper values should not be equal'
|
|
|
|
quota3 = Quota.lower_bound(10.5)
|
|
assert quota2 == quota3, 'Quota with same upper and bound values should be equal'
|
|
|
|
|
|
def test_Percentiles(metrics):
|
|
buckets = 100
|
|
_percentiles = [
|
|
Percentile(metrics.metric_name('test.p25', 'grp1'), 25),
|
|
Percentile(metrics.metric_name('test.p50', 'grp1'), 50),
|
|
Percentile(metrics.metric_name('test.p75', 'grp1'), 75),
|
|
]
|
|
percs = Percentiles(4 * buckets, BucketSizing.CONSTANT, 100.0, 0.0,
|
|
percentiles=_percentiles)
|
|
config = MetricConfig(event_window=50, samples=2)
|
|
sensor = metrics.sensor('test', config)
|
|
sensor.add_compound(percs)
|
|
p25 = metrics.metrics.get(metrics.metric_name('test.p25', 'grp1'))
|
|
p50 = metrics.metrics.get(metrics.metric_name('test.p50', 'grp1'))
|
|
p75 = metrics.metrics.get(metrics.metric_name('test.p75', 'grp1'))
|
|
|
|
# record two windows worth of sequential values
|
|
for i in range(buckets):
|
|
sensor.record(i)
|
|
|
|
assert abs(p25.value() - 25) < 1.0
|
|
assert abs(p50.value() - 50) < 1.0
|
|
assert abs(p75.value() - 75) < 1.0
|
|
|
|
for i in range(buckets):
|
|
sensor.record(0.0)
|
|
|
|
assert p25.value() < 1.0
|
|
assert p50.value() < 1.0
|
|
assert p75.value() < 1.0
|
|
|
|
def test_rate_windowing(mocker, time_keeper, metrics):
|
|
mocker.patch('time.time', side_effect=time_keeper.time)
|
|
|
|
# Use the default time window. Set 3 samples
|
|
config = MetricConfig(samples=3)
|
|
sensor = metrics.sensor('test.sensor', config)
|
|
sensor.add(metrics.metric_name('test.rate', 'grp1'), Rate(TimeUnit.SECONDS))
|
|
|
|
sum_val = 0
|
|
count = config.samples - 1
|
|
# Advance 1 window after every record
|
|
for i in range(count):
|
|
sensor.record(100)
|
|
sum_val += 100
|
|
time_keeper.sleep(config.time_window_ms / 1000.0)
|
|
|
|
# Sleep for half the window.
|
|
time_keeper.sleep(config.time_window_ms / 2.0 / 1000.0)
|
|
|
|
# prior to any time passing
|
|
elapsed_secs = (config.time_window_ms * (config.samples - 1) + config.time_window_ms / 2.0) / 1000.0
|
|
|
|
kafka_metric = metrics.metrics.get(metrics.metric_name('test.rate', 'grp1'))
|
|
assert abs((sum_val / elapsed_secs) - kafka_metric.value()) < EPS, \
|
|
'Rate(0...2) = 2.666'
|
|
assert abs(elapsed_secs - (kafka_metric.measurable.window_size(config, time.time() * 1000) / 1000.0)) \
|
|
< EPS, 'Elapsed Time = 75 seconds'
|
|
|
|
|
|
def test_reporter(metrics):
|
|
reporter = DictReporter()
|
|
foo_reporter = DictReporter(prefix='foo')
|
|
metrics.add_reporter(reporter)
|
|
metrics.add_reporter(foo_reporter)
|
|
sensor = metrics.sensor('kafka.requests')
|
|
sensor.add(metrics.metric_name('pack.bean1.avg', 'grp1'), Avg())
|
|
sensor.add(metrics.metric_name('pack.bean2.total', 'grp2'), Total())
|
|
sensor2 = metrics.sensor('kafka.blah')
|
|
sensor2.add(metrics.metric_name('pack.bean1.some', 'grp1'), Total())
|
|
sensor2.add(metrics.metric_name('pack.bean2.some', 'grp1',
|
|
tags={'a': 42, 'b': 'bar'}), Total())
|
|
|
|
# kafka-metrics-count > count is the total number of metrics and automatic
|
|
expected = {
|
|
'kafka-metrics-count': {'count': 5.0},
|
|
'grp2': {'pack.bean2.total': 0.0},
|
|
'grp1': {'pack.bean1.avg': 0.0, 'pack.bean1.some': 0.0},
|
|
'grp1.a=42,b=bar': {'pack.bean2.some': 0.0},
|
|
}
|
|
assert expected == reporter.snapshot()
|
|
|
|
for key in list(expected.keys()):
|
|
metrics = expected.pop(key)
|
|
expected['foo.%s' % key] = metrics
|
|
assert expected == foo_reporter.snapshot()
|
|
|
|
|
|
class ConstantMeasurable(AbstractMeasurable):
|
|
_value = 0.0
|
|
|
|
def measure(self, config, now):
|
|
return self._value
|
|
|
|
|
|
class TimeKeeper(object):
|
|
"""
|
|
A clock that you can manually advance by calling sleep
|
|
"""
|
|
def __init__(self, auto_tick_ms=0):
|
|
self._millis = time.time() * 1000
|
|
self._auto_tick_ms = auto_tick_ms
|
|
|
|
def time(self):
|
|
return self.ms() / 1000.0
|
|
|
|
def ms(self):
|
|
self.sleep(self._auto_tick_ms)
|
|
return self._millis
|
|
|
|
def sleep(self, seconds):
|
|
self._millis += (seconds * 1000)
|