Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
92.93% |
92 / 99 |
|
72.73% |
8 / 11 |
CRAP | |
0.00% |
0 / 1 |
InstrumentConfigsFetcher | |
92.93% |
92 / 99 |
|
72.73% |
8 / 11 |
28.28 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
getInstrumentConfigs | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getExperimentConfigs | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getConfigs | |
95.12% |
39 / 41 |
|
0.00% |
0 / 1 |
7 | |||
postProcessResult | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
getSampleConfig | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
5 | |||
areDependenciesMet | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
incrementApiRequestsTotal | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
logApiRequestDuration | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getServerErrorLabel | |
63.64% |
7 / 11 |
|
0.00% |
0 / 1 |
6.20 | |||
getClientErrorLabels | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\MetricsPlatform; |
4 | |
5 | use MediaWiki\Config\ServiceOptions; |
6 | use MediaWiki\Http\HttpRequestFactory; |
7 | use MediaWiki\Json\FormatJson; |
8 | use MediaWiki\MainConfigNames; |
9 | use MediaWiki\Status\Status; |
10 | use MediaWiki\Status\StatusFormatter; |
11 | use Psr\Log\LoggerInterface; |
12 | use Wikimedia\ObjectCache\WANObjectCache; |
13 | use Wikimedia\Stats\StatsFactory; |
14 | |
15 | class InstrumentConfigsFetcher { |
16 | private const VERSION = 1; |
17 | private const HTTP_TIMEOUT = 1; |
18 | private const INSTRUMENT = 1; |
19 | private const EXPERIMENT = 2; |
20 | public const MPIC_API_INSTRUMENTS_ENDPOINT = "/api/v1/instruments"; |
21 | public const MPIC_API_EXPERIMENTS_ENDPOINT = "/api/v1/experiments"; |
22 | |
23 | /** |
24 | * Name of the main config key(s) for instrument configuration. |
25 | * |
26 | * @var array |
27 | */ |
28 | public const CONSTRUCTOR_OPTIONS = [ |
29 | 'MetricsPlatformEnable', |
30 | 'MetricsPlatformInstrumentConfiguratorBaseUrl', |
31 | MainConfigNames::DBname, |
32 | ]; |
33 | private ServiceOptions $options; |
34 | private WANObjectCache $WANObjectCache; |
35 | private HttpRequestFactory $httpRequestFactory; |
36 | private LoggerInterface $logger; |
37 | private StatsFactory $statsFactory; |
38 | private StatusFormatter $statusFormatter; |
39 | |
40 | public function __construct( |
41 | ServiceOptions $options, |
42 | WANObjectCache $WANObjectCache, |
43 | HttpRequestFactory $httpRequestFactory, |
44 | LoggerInterface $logger, |
45 | StatsFactory $statsFactory, |
46 | StatusFormatter $statusFormatter |
47 | ) { |
48 | $this->options = $options; |
49 | $this->options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); |
50 | $this->WANObjectCache = $WANObjectCache; |
51 | $this->httpRequestFactory = $httpRequestFactory; |
52 | $this->logger = $logger; |
53 | $this->statsFactory = $statsFactory; |
54 | $this->statusFormatter = $statusFormatter; |
55 | } |
56 | |
57 | public function getInstrumentConfigs(): array { |
58 | return $this->getConfigs( 1 ); |
59 | } |
60 | |
61 | public function getExperimentConfigs(): array { |
62 | return $this->getConfigs( 2 ); |
63 | } |
64 | |
65 | /** |
66 | * Get the instruments and experiments configuration from the Metrics Platform Configurator API. |
67 | * |
68 | * @param int|null $flag Return only the specified kind of variables: self::INSTRUMENT or self::EXPERIMENT. |
69 | * For internal use only. |
70 | * @return array[] |
71 | */ |
72 | private function getConfigs( ?int $flag = null ): array { |
73 | if ( !$this->areDependenciesMet() ) { |
74 | $this->logger->warning( 'Dependencies not met for the Metrics Platform Instrument Configs Fetcher.' ); |
75 | return []; |
76 | } |
77 | $config = $this->options; |
78 | $cache = $this->WANObjectCache; |
79 | $fname = __METHOD__; |
80 | |
81 | // Check for which api endpoint should be queried and set corresponding cache key. |
82 | $type = $flag ?? self::INSTRUMENT; |
83 | $endpoint = ( $type > 1 ) ? self::MPIC_API_EXPERIMENTS_ENDPOINT : self::MPIC_API_INSTRUMENTS_ENDPOINT; |
84 | $cacheKey = ( $type === self::EXPERIMENT ) ? 'ExperimentConfigs' : 'InstrumentConfigs'; |
85 | |
86 | $result = $cache->getWithSetCallback( |
87 | $cache->makeKey( 'MetricsPlatform', $cacheKey, self::VERSION ), |
88 | $cache::TTL_MINUTE, |
89 | function () use ( $config, $endpoint, $fname ) { |
90 | $startTime = microtime( true ); |
91 | $baseUrl = $config->get( 'MetricsPlatformInstrumentConfiguratorBaseUrl' ); |
92 | $url = $baseUrl . $endpoint; |
93 | $request = $this->httpRequestFactory->create( $url, [ 'timeout' => self::HTTP_TIMEOUT ], $fname ); |
94 | $status = $request->execute(); |
95 | $labels = []; |
96 | if ( $status->isOK() ) { |
97 | $labels[] = 'success'; |
98 | $json = $request->getContent(); |
99 | } else { |
100 | $errors = $status->getMessages( 'error' ); |
101 | $this->logger->warning( $this->statusFormatter->getWikiText( Status::wrap( $status ), |
102 | [ 'error' => $errors, 'content' => $request->getContent() ] ) ); |
103 | |
104 | $labels = $this->getClientErrorLabels( $errors ); |
105 | $labels[] = $this->getServerErrorLabel( $status->getValue() ); |
106 | |
107 | $json = null; |
108 | } |
109 | // T368253 Use the Stats library for performance reporting. |
110 | foreach ( $labels as $label ) { |
111 | $this->incrementApiRequestsTotal( $label ); |
112 | } |
113 | $this->logApiRequestDuration( $startTime ); |
114 | |
115 | /* |
116 | HttpRequestFactory::create returns a MWHttpRequest object. |
117 | MWHttpRequest::execute returns a Status object which provides status |
118 | codes for more granular Stats reporting. For errors, we return null for |
119 | the json response which represents all the failure modes we care about: |
120 | the network being down (DNS resolution not working), connection timeout, |
121 | request timeout, etc. |
122 | */ |
123 | if ( $json === null ) { |
124 | $this->logger->warning( 'MPIC API is not working.' ); |
125 | return []; |
126 | } |
127 | return FormatJson::decode( $json, true ); |
128 | }, |
129 | [ |
130 | 'staleTTL' => $cache::TTL_DAY |
131 | ] |
132 | ); |
133 | |
134 | return $this->postProcessResult( $result ); |
135 | } |
136 | |
137 | /** |
138 | * Post-processes the result of successful request to MPIC by: |
139 | * |
140 | * 1. Filtering out disabled instruments/experiments (`status=0`) |
141 | * |
142 | * @param array $result An array of configs retrieved from MPIC |
143 | * TODO: Add a link to the latest response format specification |
144 | * @return array |
145 | */ |
146 | protected function postProcessResult( array $result ): array { |
147 | $dbName = $this->options->get( MainConfigNames::DBname ); |
148 | $processedResult = []; |
149 | |
150 | foreach ( $result as $config ) { |
151 | if ( !$config['status'] ) { |
152 | continue; |
153 | } |
154 | |
155 | $config['sample'] = $this->getSampleConfig( $config, $dbName ); |
156 | |
157 | $processedResult[] = $config; |
158 | } |
159 | |
160 | return $processedResult; |
161 | } |
162 | |
163 | /** |
164 | * @param array $config |
165 | * @param string $dbName |
166 | * @return array |
167 | */ |
168 | private function getSampleConfig( array $config, string $dbName ) { |
169 | $sampleConfig = [ |
170 | 'rate' => 0.0, |
171 | 'unit' => 'session', |
172 | ]; |
173 | |
174 | if ( array_key_exists( 'sample_rate', $config ) ) { |
175 | $sampleRates = $config['sample_rate']; |
176 | $sampleConfig['rate'] = $sampleRates['default']; |
177 | unset( $sampleRates['default'] ); |
178 | |
179 | foreach ( $sampleRates as $rate => $wikis ) { |
180 | if ( in_array( $dbName, $wikis ) ) { |
181 | $sampleConfig['rate'] = $rate; |
182 | |
183 | break; |
184 | } |
185 | } |
186 | } |
187 | |
188 | if ( array_key_exists( 'sample_unit', $config ) ) { |
189 | $sampleConfig['unit'] = $config['sample_unit']; |
190 | } |
191 | |
192 | return $sampleConfig; |
193 | } |
194 | |
195 | public function areDependenciesMet(): bool { |
196 | return $this->options->get( 'MetricsPlatformEnable' ); |
197 | } |
198 | |
199 | /** |
200 | * Increment success/failure of MPIC api requests. |
201 | * |
202 | * @param string $label |
203 | */ |
204 | private function incrementApiRequestsTotal( string $label ): void { |
205 | $this->statsFactory->getCounter( 'mpic_api_requests_total' ) |
206 | ->setLabel( 'status', $label ) |
207 | ->increment(); |
208 | } |
209 | |
210 | /** |
211 | * Record length of MPIC api requests. |
212 | * |
213 | * @param float $startTime |
214 | */ |
215 | private function logApiRequestDuration( float $startTime ): void { |
216 | $this->statsFactory->getTiming( 'mpic_api_request_duration_seconds' ) |
217 | ->observe( ( microtime( true ) - $startTime ) * 1000 ); |
218 | } |
219 | |
220 | /** |
221 | * Get error label based on http status code. |
222 | * |
223 | * @param int $statusCode |
224 | * @return string |
225 | */ |
226 | private function getServerErrorLabel( int $statusCode ): string { |
227 | switch ( $statusCode ) { |
228 | case 400: |
229 | $label = 'bad-request'; |
230 | break; |
231 | case 408: |
232 | $label = 'server-timeout'; |
233 | break; |
234 | case 500: |
235 | $label = 'internal-server-error'; |
236 | break; |
237 | default: |
238 | $label = 'failure'; |
239 | } |
240 | return $label; |
241 | } |
242 | |
243 | /** |
244 | * Get client error labels based on message keys and params. |
245 | * |
246 | * Error labels are crafted by the message key that is passed into the |
247 | * fatal method of the status property of the request object when connection |
248 | * exceptions are thrown by GuzzleHttpRequest::execute(). Examples include: |
249 | * - 'http-timed-out' |
250 | * - 'http-curl-error' |
251 | * - 'http-request-error' |
252 | * - 'http-internal-error' |
253 | * |
254 | * @param array $errors |
255 | * @return array |
256 | */ |
257 | private function getClientErrorLabels( array $errors ): array { |
258 | $labels = []; |
259 | // Loop through error messages to aggregate counters of different types. |
260 | foreach ( $errors as $error ) { |
261 | $key = $error->getKey(); |
262 | |
263 | $paramValues = array_map( static function ( $param ) { |
264 | return $param->getValue(); |
265 | }, $error->getParams() ); |
266 | $paramString = implode( ', ', $paramValues ); |
267 | |
268 | $labels[] = $key . ': ' . $paramString; |
269 | } |
270 | return $labels; |
271 | } |
272 | } |