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