Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
57.58% covered (warning)
57.58%
19 / 33
50.00% covered (danger)
50.00%
5 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
StatsFactory
57.58% covered (warning)
57.58%
19 / 33
50.00% covered (danger)
50.00%
5 / 10
28.97
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
 flush
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 trackUsage
0.00% covered (danger)
0.00%
0 / 6
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
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\NullMetric;
35use Wikimedia\Stats\Metrics\TimingMetric;
36
37/**
38 * This is the primary interface for validating metrics definitions,
39 * caching defined metrics, and returning metric instances from cache
40 * if previously defined.
41 *
42 * @author Cole White
43 * @since 1.41
44 */
45class StatsFactory {
46
47    private string $component = '';
48    private StatsCache $cache;
49    private EmitterInterface $emitter;
50    private LoggerInterface $logger;
51
52    private ?IBufferingStatsdDataFactory $statsdDataFactory = null;
53
54    /**
55     * StatsFactory builds, configures, and caches Metrics.
56     */
57    public function __construct(
58        StatsCache $cache,
59        EmitterInterface $emitter,
60        LoggerInterface $logger,
61        ?string $component = null
62    ) {
63        if ( $component !== null ) {
64            $this->component = StatsUtils::normalizeString( $component );
65        }
66        $this->cache = $cache;
67        $this->emitter = $emitter;
68        $this->logger = $logger;
69    }
70
71    /**
72     * Returns a new StatsFactory instance prefixed by component.
73     *
74     * @param string $component
75     * @return StatsFactory
76     */
77    public function withComponent( string $component ): StatsFactory {
78        $statsFactory = new StatsFactory( $this->cache, $this->emitter, $this->logger, $component );
79        return $statsFactory->withStatsdDataFactory( $this->statsdDataFactory );
80    }
81
82    public function withStatsdDataFactory( ?IBufferingStatsdDataFactory $statsdDataFactory ): StatsFactory {
83        $this->statsdDataFactory = $statsdDataFactory;
84        return $this;
85    }
86
87    /**
88     * Makes a new CounterMetric or fetches one from cache.
89     *
90     * If a collision occurs, returns a NullMetric to suppress exceptions.
91     *
92     * @param string $name
93     * @return CounterMetric|NullMetric
94     */
95    public function getCounter( string $name ) {
96        return $this->getMetric( $name, CounterMetric::class );
97    }
98
99    /**
100     * Makes a new GaugeMetric or fetches one from cache.
101     *
102     * If a collision occurs, returns a NullMetric to suppress exceptions.
103     *
104     * @param string $name
105     * @return GaugeMetric|NullMetric
106     */
107    public function getGauge( string $name ) {
108        return $this->getMetric( $name, GaugeMetric::class );
109    }
110
111    /**
112     * Makes a new TimingMetric or fetches one from cache.
113     *
114     * If a collision occurs, returns a NullMetric to suppress exceptions.
115     *
116     * @param string $name
117     * @return TimingMetric|NullMetric
118     */
119    public function getTiming( string $name ) {
120        return $this->getMetric( $name, TimingMetric::class );
121    }
122
123    /**
124     * Send all buffered metrics to the target and destroy the cache.
125     */
126    public function flush(): void {
127        $this->trackUsage();
128        $this->emitter->send();
129        $this->cache->clear();
130    }
131
132    /**
133     * Create a metric totaling all samples in the cache.
134     */
135    private function trackUsage(): void {
136        $accumulator = 0;
137        foreach ( $this->cache->getAllMetrics() as $metric ) {
138            $accumulator += $metric->getSampleCount();
139        }
140        $this->getCounter( 'stats_buffered_total' )
141            ->copyToStatsdAt( 'stats.statslib.buffered' )
142            ->incrementBy( $accumulator );
143    }
144
145    /**
146     * Fetches a metric from cache or makes a new metric.
147     *
148     * If a metric name collision occurs, returns a NullMetric to suppress runtime exceptions.
149     *
150     * @param string $name
151     * @param string $className
152     * @return CounterMetric|TimingMetric|GaugeMetric|NullMetric
153     */
154    private function getMetric( string $name, string $className ) {
155        $name = StatsUtils::normalizeString( $name );
156        StatsUtils::validateMetricName( $name );
157        try {
158            $metric = $this->cache->get( $this->component, $name, $className );
159        } catch ( TypeError | InvalidArgumentException | InvalidConfigurationException $ex ) {
160            // Log the condition and give the caller something that will absorb calls.
161            trigger_error( $ex->getMessage(), E_USER_WARNING );
162            return new NullMetric;
163        }
164        if ( $metric === null ) {
165            $baseMetric = new BaseMetric( $this->component, $name );
166            $metric = new $className( $baseMetric->withStatsdDataFactory( $this->statsdDataFactory ), $this->logger );
167            $this->cache->set( $this->component, $name, $metric );
168        }
169        return $metric->fresh();
170    }
171
172    /**
173     * Returns an instance of StatsFactory as a NULL value object
174     * as a default for consumer code to fall back to. This can also
175     * be used in tests environment where we don't need the full
176     * UDP emitter object.
177     *
178     * @since 1.42
179     *
180     * @return self
181     */
182    public static function newNull(): self {
183        return new self( new StatsCache(), new NullEmitter(), new NullLogger() );
184    }
185}