429 lines
16 KiB
PHP
429 lines
16 KiB
PHP
<?php
|
|
|
|
namespace Wikimedia\Tests\Stats;
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
use Psr\Log\NullLogger;
|
|
use Wikimedia\Stats\IBufferingStatsdDataFactory;
|
|
use Wikimedia\Stats\Metrics\BaseMetric;
|
|
use Wikimedia\Stats\Metrics\CounterMetric;
|
|
use Wikimedia\Stats\Metrics\NullMetric;
|
|
use Wikimedia\Stats\OutputFormats;
|
|
use Wikimedia\Stats\StatsCache;
|
|
use Wikimedia\Stats\StatsFactory;
|
|
use Wikimedia\TestingAccessWrapper;
|
|
|
|
/**
|
|
* @covers \Wikimedia\Stats\Metrics\NullMetric
|
|
* @covers \Wikimedia\Stats\Metrics\CounterMetric
|
|
* @covers \Wikimedia\Stats\Metrics\GaugeMetric
|
|
* @covers \Wikimedia\Stats\Metrics\TimingMetric
|
|
* @covers \Wikimedia\Stats\StatsUtils
|
|
*/
|
|
class MetricTest extends TestCase {
|
|
|
|
public const FORMATS = [ 'statsd', 'dogstatsd' ];
|
|
|
|
public const TYPES = [ 'counter', 'gauge', 'timing' ];
|
|
|
|
public const TESTS = [
|
|
'basic' => [
|
|
'config' => [
|
|
'name' => 'test.unit',
|
|
'component' => 'testComponent',
|
|
],
|
|
'value' => 2,
|
|
'labels' => [],
|
|
],
|
|
'invalidLabel' => [
|
|
'config' => [
|
|
'name' => 'test.unit',
|
|
'component' => 'testComponent',
|
|
],
|
|
'value' => 2,
|
|
'labels' => [ ': x' => 'labelOne' ],
|
|
],
|
|
'oneLabel' => [
|
|
'config' => [
|
|
'name' => 'test.unit',
|
|
'component' => 'testComponent',
|
|
],
|
|
'value' => 2,
|
|
'labels' => [ 'x' => 'labelOne' ],
|
|
],
|
|
'multiLabel' => [
|
|
'config' => [
|
|
'name' => 'test.unit',
|
|
'component' => 'testComponent',
|
|
],
|
|
'value' => 2,
|
|
'labels' => [ 'x' => 'labelOne', 'y' => 'labelTwo' ],
|
|
],
|
|
'noComponent' => [
|
|
'config' => [
|
|
'name' => 'test.unit',
|
|
'component' => null,
|
|
],
|
|
'value' => 2,
|
|
'labels' => [],
|
|
]
|
|
];
|
|
|
|
public const RESULTS = [
|
|
'statsd.counter.basic' => [ 'mediawiki.testComponent.test_unit:2|c' ],
|
|
'statsd.counter.invalidLabel' => [ 'mediawiki.testComponent.test_unit.labelOne:2|c' ],
|
|
'statsd.counter.oneLabel' => [ 'mediawiki.testComponent.test_unit.labelOne:2|c' ],
|
|
'statsd.counter.multiLabel' => [ 'mediawiki.testComponent.test_unit.labelOne.labelTwo:2|c' ],
|
|
'statsd.counter.noComponent' => [ 'mediawiki.test_unit:2|c' ],
|
|
'statsd.gauge.basic' => [ 'mediawiki.testComponent.test_unit:2|g' ],
|
|
'statsd.gauge.invalidLabel' => [ 'mediawiki.testComponent.test_unit.labelOne:2|g' ],
|
|
'statsd.gauge.oneLabel' => [ 'mediawiki.testComponent.test_unit.labelOne:2|g' ],
|
|
'statsd.gauge.multiLabel' => [ 'mediawiki.testComponent.test_unit.labelOne.labelTwo:2|g' ],
|
|
'statsd.gauge.noComponent' => [ 'mediawiki.test_unit:2|g' ],
|
|
'statsd.timing.basic' => [ 'mediawiki.testComponent.test_unit:2|ms' ],
|
|
'statsd.timing.invalidLabel' => [ 'mediawiki.testComponent.test_unit.labelOne:2|ms' ],
|
|
'statsd.timing.oneLabel' => [ 'mediawiki.testComponent.test_unit.labelOne:2|ms' ],
|
|
'statsd.timing.multiLabel' => [ 'mediawiki.testComponent.test_unit.labelOne.labelTwo:2|ms' ],
|
|
'statsd.timing.noComponent' => [ 'mediawiki.test_unit:2|ms' ],
|
|
|
|
'dogstatsd.counter.basic' => [ 'mediawiki.testComponent.test_unit:2|c' ],
|
|
'dogstatsd.counter.invalidLabel' => [ 'mediawiki.testComponent.test_unit:2|c|#x:labelOne' ],
|
|
'dogstatsd.counter.oneLabel' => [ 'mediawiki.testComponent.test_unit:2|c|#x:labelOne' ],
|
|
'dogstatsd.counter.multiLabel' => [
|
|
'mediawiki.testComponent.test_unit:2|c|#x:labelOne,y:labelTwo' ],
|
|
'dogstatsd.counter.noComponent' => [ 'mediawiki.test_unit:2|c' ],
|
|
'dogstatsd.gauge.basic' => [ 'mediawiki.testComponent.test_unit:2|g' ],
|
|
'dogstatsd.gauge.invalidLabel' => [ 'mediawiki.testComponent.test_unit:2|g|#x:labelOne' ],
|
|
'dogstatsd.gauge.oneLabel' => [ 'mediawiki.testComponent.test_unit:2|g|#x:labelOne' ],
|
|
'dogstatsd.gauge.multiLabel' => [
|
|
'mediawiki.testComponent.test_unit:2|g|#x:labelOne,y:labelTwo' ],
|
|
'dogstatsd.gauge.noComponent' => [ 'mediawiki.test_unit:2|g' ],
|
|
'dogstatsd.timing.basic' => [ 'mediawiki.testComponent.test_unit:2|ms' ],
|
|
'dogstatsd.timing.invalidLabel' => [ 'mediawiki.testComponent.test_unit:2|ms|#x:labelOne' ],
|
|
'dogstatsd.timing.oneLabel' => [ 'mediawiki.testComponent.test_unit:2|ms|#x:labelOne' ],
|
|
'dogstatsd.timing.multiLabel' => [
|
|
'mediawiki.testComponent.test_unit:2|ms|#x:labelOne,y:labelTwo' ],
|
|
'dogstatsd.timing.noComponent' => [ 'mediawiki.test_unit:2|ms' ],
|
|
];
|
|
|
|
/** @var StatsCache */
|
|
private $cache;
|
|
|
|
public function handleTest( $test, $type, $format ) {
|
|
$config = self::TESTS[$test];
|
|
$name = implode( '.', [ $format, $type, $test ] );
|
|
$this->setName( $name );
|
|
$this->cache->clear();
|
|
$formatter = OutputFormats::getNewFormatter( OutputFormats::getFormatFromString( $format ) );
|
|
$emitter = OutputFormats::getNewEmitter( 'mediawiki', $this->cache, $formatter );
|
|
$statsFactory = new StatsFactory( $this->cache, $emitter, new NullLogger );
|
|
if ( $config['config']['component'] !== null ) {
|
|
$statsFactory = $statsFactory->withComponent( $config['config']['component'] );
|
|
}
|
|
switch ( $type ) {
|
|
case 'counter':
|
|
$metric = $statsFactory->getCounter( $config['config']['name'] );
|
|
$metric->setLabels( $config['labels'] );
|
|
$metric->incrementBy( $config['value'] );
|
|
break;
|
|
case 'gauge':
|
|
$metric = $statsFactory->getGauge( $config['config']['name'] );
|
|
$metric->setLabels( $config['labels'] );
|
|
$metric->set( $config['value'] );
|
|
break;
|
|
case 'timing':
|
|
$metric = $statsFactory->getTiming( $config['config']['name'] );
|
|
$metric->setLabels( $config['labels'] );
|
|
$metric->observe( $config['value'] );
|
|
break;
|
|
case 'default':
|
|
break;
|
|
}
|
|
$this->assertEquals( self::RESULTS[$name], TestingAccessWrapper::newFromObject( $emitter )->render() );
|
|
}
|
|
|
|
public function handleType( $type, $format ) {
|
|
foreach ( self::TESTS as $test => $_ ) {
|
|
$this->handleTest( $test, $type, $format );
|
|
}
|
|
}
|
|
|
|
public function handleFormat( $format ) {
|
|
$this->cache = new StatsCache();
|
|
foreach ( self::TYPES as $type ) {
|
|
$this->handleType( $type, $format );
|
|
}
|
|
}
|
|
|
|
public function testMetrics() {
|
|
foreach ( self::FORMATS as $format ) {
|
|
$this->handleFormat( $format );
|
|
}
|
|
}
|
|
|
|
public function testSampledMetrics() {
|
|
$rounds = 10;
|
|
$m = StatsFactory::newNull();
|
|
$ten_percent_metrics = $m->getCounter( 'test.sampled.ten' )->setSampleRate( 0.1 );
|
|
$m = StatsFactory::newNull();
|
|
$all_metrics = $m->getCounter( 'test.sampled.hundred' )->setSampleRate( 1.0 );
|
|
$m = StatsFactory::newNull();
|
|
$zero_metrics = $m->getCounter( 'test.sampled.zero' )->setSampleRate( 0.0 );
|
|
for ( $i = 0; $i < $rounds; $i++ ) {
|
|
$ten_percent_metrics->increment();
|
|
$all_metrics->increment();
|
|
$zero_metrics->increment();
|
|
}
|
|
$this->assertLessThan( $rounds, count( $ten_percent_metrics->getSamples() ) ); // random
|
|
$this->assertCount( $rounds, $all_metrics->getSamples() );
|
|
$this->assertCount( 0, $zero_metrics->getSamples() );
|
|
|
|
# getSampleCount() should count all samples regardless of sample rate
|
|
$this->assertEquals( 10, $ten_percent_metrics->getSampleCount() );
|
|
$this->assertEquals( 10, $all_metrics->getSampleCount() );
|
|
$this->assertEquals( 10, $zero_metrics->getSampleCount() );
|
|
}
|
|
|
|
public function testTimerNotStarted() {
|
|
$m = StatsFactory::newNull();
|
|
$this->expectPHPWarning(
|
|
'Stats: stop() called before start() for metric \'test\'',
|
|
static function () use ( $m ) {
|
|
$m->getTiming( 'test' )->stop();
|
|
}
|
|
);
|
|
}
|
|
|
|
public function testChangingLabelsToUsedMetric() {
|
|
$m = StatsFactory::newNull();
|
|
$m->getCounter( 'testMetricCounter' )->setLabel( 'labelOne', 'a' )->increment();
|
|
$counter = @$m->getCounter( 'testMetricCounter' )->setLabel( 'labelTwo', 'b' );
|
|
$this->assertInstanceOf( NullMetric::class, $counter );
|
|
$m->getGauge( 'testMetricGauge' )->setLabel( 'labelOne', 'a' )->set( 1 );
|
|
$gauge = @$m->getGauge( 'testMetricGauge' )->setLabel( 'labelTwo', 'b' );
|
|
$this->assertInstanceOf( NullMetric::class, $gauge );
|
|
$m->getTiming( 'testMetricTiming' )->setLabel( 'labelOne', 'a' )->observe( 1 );
|
|
$timer = @$m->getTiming( 'testMetricTiming' )->setLabel( 'labelTwo', 'b' );
|
|
$this->assertInstanceOf( NullMetric::class, $timer );
|
|
}
|
|
|
|
public function testCounterHandleNotAllLabelsHaveValues() {
|
|
$m = StatsFactory::newNull();
|
|
$m->getCounter( 'testMetricCounter' )->setLabel( 'labelOne', 'a' )->increment();
|
|
$this->expectPHPWarning(
|
|
'Stats: Cannot associate label keys with label values: Not all initialized labels have an assigned value.',
|
|
static function () use ( $m ) {
|
|
$m->getCounter( 'testMetricCounter' )->increment();
|
|
}
|
|
);
|
|
}
|
|
|
|
public function testGaugeHandleNotAllLabelsHaveValues() {
|
|
$m = StatsFactory::newNull();
|
|
$m->getGauge( 'testMetricGauge' )->setLabel( 'labelOne', 'a' )->set( 1 );
|
|
$this->expectPHPWarning(
|
|
'Stats: Cannot associate label keys with label values: Not all initialized labels have an assigned value.',
|
|
static function () use ( $m ) {
|
|
$m->getGauge( 'testMetricGauge' )->set( 1 );
|
|
}
|
|
);
|
|
}
|
|
|
|
public function testTimingHandleNotAllLabelsHaveValues() {
|
|
$m = StatsFactory::newNull();
|
|
$m->getTiming( 'testMetricTiming' )->setLabel( 'labelOne', 'a' )->observe( 1 );
|
|
$this->expectPHPWarning(
|
|
'Stats: Cannot associate label keys with label values: Not all initialized labels have an assigned value.',
|
|
static function () use ( $m ) {
|
|
$m->getTiming( 'testMetricTiming' )->observe( 1 );
|
|
}
|
|
);
|
|
}
|
|
|
|
public function testSampleRateOOB() {
|
|
$m = StatsFactory::newNull();
|
|
$metric = @$m->getCounter( 'testMetricCounter' )->setSampleRate( 1.1 );
|
|
$this->assertInstanceOf( NullMetric::class, $metric );
|
|
$metric = @$m->getGauge( 'testMetricGauge' )->setSampleRate( -1 );
|
|
$this->assertInstanceOf( NullMetric::class, $metric );
|
|
$metric = @$m->getTiming( 'testMetricTimer' )->setSampleRate( 20 );
|
|
$this->assertInstanceOf( NullMetric::class, $metric );
|
|
}
|
|
|
|
public function testSampleRateDisallowed() {
|
|
$m = StatsFactory::newNull();
|
|
$m->getCounter( 'testMetric' )->increment();
|
|
$metric = @$m->getCounter( 'testMetric' )->setSampleRate( 0.5 );
|
|
$this->assertInstanceOf( NullMetric::class, $metric );
|
|
$m->getGauge( 'testMetricGauge' )->set( 1 );
|
|
$metric = @$m->getGauge( 'testMetricGauge' )->setSampleRate( 0.5 );
|
|
$this->assertInstanceOf( NullMetric::class, $metric );
|
|
$m->getTiming( 'testMetricTiming' )->observe( 1 );
|
|
$metric = @$m->getTiming( 'testMetricTiming' )->setSampleRate( 0.5 );
|
|
$this->assertInstanceOf( NullMetric::class, $metric );
|
|
}
|
|
|
|
public function testCopyToStatsdAtEmptyArrayResetsValue() {
|
|
$baseMetric = new BaseMetric( '', 'testMetric' );
|
|
$metric = new CounterMetric(
|
|
$baseMetric->withStatsdDataFactory( $this->createMock( IBufferingStatsdDataFactory::class ) ),
|
|
new NullLogger
|
|
);
|
|
$metric->copyToStatsdAt( 'test' );
|
|
$this->assertEquals( [ 'test' ], $baseMetric->getStatsdNamespaces() );
|
|
$metric->copyToStatsdAt( [] );
|
|
$this->assertEquals( [], $baseMetric->getStatsdNamespaces() );
|
|
}
|
|
|
|
public function testStatsdDataFactoryCopyToStatsdAtWithGauge() {
|
|
$statsFactory = StatsFactory::newNull();
|
|
|
|
$gaugeArgs = [ 'test.gauge.1', 'test.gauge.2' ];
|
|
$statsdMock = $this->createMock( IBufferingStatsdDataFactory::class );
|
|
$statsdMock->expects( $this->exactly( 2 ) )
|
|
->method( 'gauge' )
|
|
->with( $this->callback( static function ( $key ) use ( &$gaugeArgs ) {
|
|
$nextKey = array_shift( $gaugeArgs );
|
|
return $nextKey === $key;
|
|
} ) );
|
|
|
|
$statsFactory = $statsFactory->withStatsdDataFactory( $statsdMock );
|
|
$statsFactory->getGauge( 'testMetricGauge' )
|
|
->copyToStatsdAt( [ 'test.gauge.1', 'test.gauge.2' ] )
|
|
->set( 1 );
|
|
}
|
|
|
|
public function testHandleInvalidStatsdNamespace() {
|
|
$m = StatsFactory::newNull();
|
|
$m = $m->withStatsdDataFactory( $this->createMock( IBufferingStatsdDataFactory::class ) );
|
|
$this->expectPHPWarning(
|
|
'Stats: StatsD namespace must be a string.',
|
|
function () use ( $m ) {
|
|
$metric = $m->getCounter( 'testMetricCounter' )->copyToStatsdAt( null );
|
|
$this->assertInstanceOf( NullMetric::class, $metric );
|
|
}
|
|
);
|
|
}
|
|
|
|
public function testHandleEmptyStatsdNamespace() {
|
|
$m = StatsFactory::newNull();
|
|
$m = $m->withStatsdDataFactory( $this->createMock( IBufferingStatsdDataFactory::class ) );
|
|
$this->expectPHPWarning(
|
|
'Stats: StatsD namespace cannot be empty.',
|
|
function () use ( $m ) {
|
|
$metric = $m->getCounter( 'testMetricCounter' )->copyToStatsdAt( '' );
|
|
$this->assertInstanceOf( NullMetric::class, $metric );
|
|
}
|
|
);
|
|
}
|
|
|
|
public function testHandleNonStringStatsdNamespaceInArray() {
|
|
$m = StatsFactory::newNull();
|
|
$m = $m->withStatsdDataFactory( $this->createMock( IBufferingStatsdDataFactory::class ) );
|
|
$this->expectPHPWarning(
|
|
'Stats: StatsD namespace must be a string.',
|
|
function () use ( $m ) {
|
|
$metric = $m->getCounter( 'testMetricCounter' )->copyToStatsdAt( [ null ] );
|
|
$this->assertInstanceOf( NullMetric::class, $metric );
|
|
}
|
|
);
|
|
}
|
|
|
|
public function testCanChangeLabelsWhileTimerIsStarted() {
|
|
$cache = new StatsCache;
|
|
$formatter = OutputFormats::getNewFormatter( OutputFormats::getFormatFromString( 'dogstatsd' ) );
|
|
$emitter = OutputFormats::getNewEmitter( 'mediawiki', $cache, $formatter );
|
|
$statsFactory = new StatsFactory( $cache, $emitter, new NullLogger );
|
|
$timer = $statsFactory->getTiming( 'test', )->setLabel( 'foo', 'bar' );
|
|
|
|
// start() and stop() called so close together here should be fractions of a millisecond
|
|
$timer->start();
|
|
$timer->setLabel( 'foo', 'baz' )->stop();
|
|
$this->assertMatchesRegularExpression(
|
|
'/^mediawiki\.test:(0\.[0-9]+)\|ms\|#foo:baz$/',
|
|
TestingAccessWrapper::newFromObject( $emitter )->render()[0]
|
|
);
|
|
}
|
|
|
|
public function testStatsdDataFactoryPersistsWithComponent() {
|
|
$statsFactory = StatsFactory::newNull();
|
|
|
|
$timingArgs = [ 'test.timing.1', 'test.timing.2' ];
|
|
$statsdMock = $this->createMock( IBufferingStatsdDataFactory::class );
|
|
$statsdMock->expects( $this->exactly( 2 ) )
|
|
->method( 'timing' )
|
|
->with( $this->callback( static function ( $key ) use ( &$timingArgs ) {
|
|
$nextKey = array_shift( $timingArgs );
|
|
return $nextKey === $key;
|
|
}, 1.0 ) );
|
|
|
|
$statsFactory = $statsFactory->withStatsdDataFactory( $statsdMock );
|
|
|
|
// withComponent() returns a whole new StatsFactory instance
|
|
$componentStatsFactory = $statsFactory->withComponent( 'TestComponent' );
|
|
|
|
$componentStatsFactory->getTiming( 'testMetricTiming' )
|
|
->copyToStatsdAt( [ 'test.timing.1', 'test.timing.2' ] )
|
|
->observe( 1 );
|
|
}
|
|
|
|
public function testWithComponentStatsdFactoryChanges() {
|
|
$statsFactory = StatsFactory::newNull();
|
|
|
|
// statsdDataFactory should be null if withComponent called prior to calling withStatsdDataFactory()
|
|
$noStatsd = TestingAccessWrapper::newFromObject( $statsFactory->withComponent( 'foo' ) );
|
|
$this->assertNull( $noStatsd->statsdDataFactory );
|
|
|
|
// call withStatsdDataFactory
|
|
$statsFactory = $statsFactory->withStatsdDataFactory( $this->createMock( IBufferingStatsdDataFactory::class ) );
|
|
|
|
// statsdDataFactory should be an instance of IBufferingStatsdDataFactory
|
|
$yesStatsd = TestingAccessWrapper::newFromObject( $statsFactory->withComponent( 'foo' ) );
|
|
$this->assertInstanceOf( IBufferingStatsdDataFactory::class, $yesStatsd->statsdDataFactory );
|
|
|
|
// disable statsdDataFactory
|
|
$statsFactory = $statsFactory->withStatsdDataFactory( null );
|
|
|
|
// statsdDataFactory should be null again
|
|
$noStatsdAgain = TestingAccessWrapper::newFromObject( $statsFactory->withComponent( 'foo' ) );
|
|
$this->assertNull( $noStatsdAgain->statsdDataFactory );
|
|
}
|
|
|
|
/**
|
|
* PHPUnit 10 compatible replacement for expectWarning().
|
|
*
|
|
* @param string $msg
|
|
* @param callable $callback
|
|
* @return void
|
|
*/
|
|
private function expectPHPWarning( string $msg, callable $callback ): void {
|
|
try {
|
|
$errorEmitted = false;
|
|
set_error_handler( function ( $_, $actualMsg ) use ( $msg, &$errorEmitted ) {
|
|
$this->assertStringContainsString( $msg, $actualMsg );
|
|
$errorEmitted = true;
|
|
}, E_USER_WARNING );
|
|
$callback();
|
|
$this->assertTrue( $errorEmitted, "No PHP warning was emitted." );
|
|
} finally {
|
|
restore_error_handler();
|
|
}
|
|
}
|
|
|
|
public function testMsConversion() {
|
|
$statsFactory = StatsFactory::newNull();
|
|
$statsFactory->getTiming( 'test_seconds' )->observeSeconds( 1 );
|
|
$statsFactory->getTiming( 'test_seconds' )->observeNanoseconds( 1000000 );
|
|
$samples = $statsFactory->getTiming( 'test_seconds' )->getSamples();
|
|
$this->assertEquals(
|
|
[ 1000, 1 ],
|
|
array_map(
|
|
static function ( $sample ) {
|
|
return $sample->getValue();
|
|
},
|
|
$samples
|
|
)
|
|
);
|
|
}
|
|
}
|