Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
51.35% covered (warning)
51.35%
19 / 37
41.67% covered (danger)
41.67%
5 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
StatsFactory
51.35% covered (warning)
51.35%
19 / 37
41.67% covered (danger)
41.67%
5 / 12
50.27
0.00% covered (danger)
0.00%
0 / 1
 __construct
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 withComponent
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 withStatsdDataFactory
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getCounter
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getGauge
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTiming
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getHistogram
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 flush
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 getCacheCount
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getMetric
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
3
 newNull
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 newUnitTestingHelper
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 */
6
7declare( strict_types=1 );
8
9namespace Wikimedia\Stats;
10
11use InvalidArgumentException;
12use Psr\Log\LoggerInterface;
13use Psr\Log\NullLogger;
14use TypeError;
15use Wikimedia\Stats\Emitters\EmitterInterface;
16use Wikimedia\Stats\Emitters\NullEmitter;
17use Wikimedia\Stats\Exceptions\InvalidConfigurationException;
18use Wikimedia\Stats\Metrics\BaseMetric;
19use Wikimedia\Stats\Metrics\CounterMetric;
20use Wikimedia\Stats\Metrics\GaugeMetric;
21use Wikimedia\Stats\Metrics\HistogramMetric;
22use Wikimedia\Stats\Metrics\NullMetric;
23use Wikimedia\Stats\Metrics\TimingMetric;
24
25/**
26 * This is the primary interface for validating metrics definitions,
27 * caching defined metrics, and returning metric instances from cache
28 * if previously defined.
29 *
30 * @author Cole White
31 * @since 1.41
32 */
33class StatsFactory {
34
35    private string $component = '';
36    private StatsCache $cache;
37    private EmitterInterface $emitter;
38    private LoggerInterface $logger;
39
40    private ?IBufferingStatsdDataFactory $statsdDataFactory = null;
41
42    /**
43     * StatsFactory builds, configures, and caches Metrics.
44     */
45    public function __construct(
46        StatsCache $cache,
47        EmitterInterface $emitter,
48        LoggerInterface $logger,
49        ?string $component = null
50    ) {
51        if ( $component !== null ) {
52            $this->component = StatsUtils::normalizeString( $component );
53        }
54        $this->cache = $cache;
55        $this->emitter = $emitter;
56        $this->logger = $logger;
57    }
58
59    /**
60     * Returns a new StatsFactory instance prefixed by component.
61     *
62     * @param string $component
63     * @return StatsFactory
64     */
65    public function withComponent( string $component ): StatsFactory {
66        $statsFactory = new StatsFactory( $this->cache, $this->emitter, $this->logger, $component );
67        return $statsFactory->withStatsdDataFactory( $this->statsdDataFactory );
68    }
69
70    /**
71     * This function existed to support the Graphite->Prometheus transition and is no longer needed.
72     * @deprecated since 1.45, see: https://www.mediawiki.org/wiki/Manual:Stats
73     */
74    public function withStatsdDataFactory( ?IBufferingStatsdDataFactory $statsdDataFactory ): StatsFactory {
75        // TODO: remove this and all other copyToStatsdAt() usage and implementation
76        //$this->statsdDataFactory = $statsdDataFactory;
77        return $this;
78    }
79
80    /**
81     * Makes a new CounterMetric or fetches one from cache.
82     *
83     * If a collision occurs, returns a NullMetric to suppress exceptions.
84     *
85     * @param string $name
86     * @return CounterMetric|NullMetric
87     */
88    public function getCounter( string $name ) {
89        return $this->getMetric( $name, CounterMetric::class );
90    }
91
92    /**
93     * Makes a new GaugeMetric or fetches one from cache.
94     *
95     * If a collision occurs, returns a NullMetric to suppress exceptions.
96     *
97     * @param string $name
98     * @return GaugeMetric|NullMetric
99     */
100    public function getGauge( string $name ) {
101        return $this->getMetric( $name, GaugeMetric::class );
102    }
103
104    /**
105     * Makes a new TimingMetric or fetches one from cache.
106     *
107     * The timing data should be in the range [5ms, 60s]; use
108     * ::getHistogram() if you need a different range.
109     *
110     * This range limitation is a consequence of the recommended
111     * setup with prometheus/statsd_exporter (as dogstatsd target)
112     * and Prometheus (as time series database), with
113     * `statsd_exporter::histogram_buckets` set to a 5ms-60s range.
114     *
115     * If a collision occurs, returns a NullMetric to suppress exceptions.
116     *
117     * @param string $name
118     * @return TimingMetric|NullMetric
119     */
120    public function getTiming( string $name ) {
121        return $this->getMetric( $name, TimingMetric::class );
122    }
123
124    /**
125     * Makes a new HistogramMetric from a list of buckets.
126     *
127     * Beware: this is for storing non-time data in histograms, like byte
128     * sizes, or time data outside of the range [5ms, 60s].
129     *
130     * Avoid changing the bucket list once a metric has been
131     * deployed.  When bucket list changes are unavoidable, change the metric
132     * name and handle the transition in PromQL.
133     *
134     * @param string $name
135     * @param array<int|float> $buckets
136     * @return HistogramMetric
137     */
138    public function getHistogram( string $name, array $buckets ) {
139        $name = StatsUtils::normalizeString( $name );
140        StatsUtils::validateMetricName( $name );
141        return new HistogramMetric( $this, $name, $buckets );
142    }
143
144    /**
145     * Send all buffered metrics to the target and destroy the cache.
146     */
147    public function flush(): void {
148        $cacheSize = $this->getCacheCount();
149
150        // Optimization: To encourage long-running scripts to frequently yield
151        // and flush (T181385), it is important that we don't do any work here
152        // unless new stats were added to the cache since the last flush.
153        if ( $cacheSize > 0 ) {
154            $this->getCounter( 'stats_buffered_total' )
155                ->incrementBy( $cacheSize );
156
157            $this->emitter->send();
158            $this->cache->clear();
159        }
160    }
161
162    /**
163     * Get a total of the number of samples in cache.
164     */
165    private function getCacheCount(): int {
166        $accumulator = 0;
167        foreach ( $this->cache->getAllMetrics() as $metric ) {
168            $accumulator += $metric->getSampleCount();
169        }
170        return $accumulator;
171    }
172
173    /**
174     * Fetches a metric from cache or makes a new metric.
175     *
176     * If a metric name collision occurs, returns a NullMetric to suppress runtime exceptions.
177     *
178     * @param string $name
179     * @param class-string $className
180     * @return CounterMetric|TimingMetric|GaugeMetric|NullMetric
181     */
182    private function getMetric( string $name, string $className ) {
183        $name = StatsUtils::normalizeString( $name );
184        StatsUtils::validateMetricName( $name );
185        try {
186            $metric = $this->cache->get( $this->component, $name, $className );
187        } catch ( TypeError | InvalidArgumentException | InvalidConfigurationException $ex ) {
188            // Log the condition and give the caller something that will absorb calls.
189            trigger_error( "Stats: {$name}{$ex->getMessage()}", E_USER_WARNING );
190            return new NullMetric;
191        }
192        if ( $metric === null ) {
193            $baseMetric = new BaseMetric( $this->component, $name );
194            $metric = new $className( $baseMetric->withStatsdDataFactory( $this->statsdDataFactory ), $this->logger );
195            $this->cache->set( $this->component, $name, $metric );
196        }
197        return $metric->fresh();
198    }
199
200    /**
201     * Create a no-op StatsFactory.
202     *
203     * Use this as the default in a service that takes an optional StatsFactory,
204     * or as null implementation in PHPUnit tests, where we don't need to send
205     * output to an actual network service.
206     *
207     * @since 1.42
208     * @return self
209     */
210    public static function newNull(): self {
211        return new self( new StatsCache(), new NullEmitter(), new NullLogger() );
212    }
213
214    /**
215     * Create a stats helper for use in PHPUnit tests.
216     *
217     * Example:
218     *
219     * ```php
220     * $statsHelper = StatsFactory::newUnitTestingHelper();
221     *
222     * $x = new MySubject( $statsHelper->getStatsFactory() );
223     * $x->execute();
224     *
225     * // Assert full (emitting more is unexpected)
226     * $this->assertSame(
227     *     [
228     *         'example_executions_total:1|c|#foo:bar'
229     *     ],
230     *     $statsHelper->consumeAllFormatted()
231     * );
232     *
233     * // Assert partially (at least this should be emitted)
234     * $this->assertSame( 1, $statsHelper->count( 'example_executions_total{foo="bar"}' ) );
235     * ```
236     *
237     * @since 1.44
238     * @return UnitTestingHelper
239     */
240    public static function newUnitTestingHelper() {
241        return new UnitTestingHelper();
242    }
243}