MediaWiki  master
MetricsFactory.php
Go to the documentation of this file.
1 <?php
29 declare( strict_types=1 );
30 
31 namespace Wikimedia\Metrics;
32 
33 use Psr\Log\LoggerInterface;
34 use TypeError;
35 use UDPTransport;
43 
45 
47  private const SUPPORTED_OUTPUT_FORMATS = [ 'statsd', 'dogstatsd', 'null' ];
48 
50  private const NAME_DELIMITER = '.';
51 
53  private const DEFAULT_METRIC_CONFIG = [
54  // 'name' => required,
55  // 'component' => required,
56  'labels' => [],
57  'sampleRate' => 1.0,
58  'service' => '',
59  'format' => 'statsd',
60  ];
61 
63  private $cache = [];
64 
66  private $target;
67 
69  private $format;
70 
72  private $prefix;
73 
75  private $logger;
76 
88  public function __construct( array $config, LoggerInterface $logger ) {
89  $this->logger = $logger;
90  $this->target = $config['target'] ?? null;
91  $this->format = $config['format'] ?? 'null';
92  $this->prefix = $config['prefix'] ?? '';
93  if ( $this->prefix === '' ) {
94  throw new UndefinedPrefixException( '\'prefix\' option is required and cannot be empty.' );
95  }
96  $this->prefix = self::normalizeString( $config['prefix'] );
97  if ( !in_array( $this->format, self::SUPPORTED_OUTPUT_FORMATS ) ) {
99  'Format "' . $this->format . '" not supported. Expected one of '
100  . json_encode( self::SUPPORTED_OUTPUT_FORMATS )
101  );
102  }
103  }
104 
117  public function getCounter( array $config = [] ) {
118  $config = $this->getValidConfig( $config );
119  $name = self::getFormattedName( $config['name'], $config['component'] );
120  try {
121  $metric = $this->getCachedMetric( $name, CounterMetric::class );
122  } catch ( TypeError $ex ) {
123  return new NullMetric();
124  }
125 
126  if ( $metric ) {
127  $metric->validateLabels( $config['labels'] );
128  return $metric;
129  }
130  $this->cache[$name] = new CounterMetric( $config, new MetricUtils() );
131  return $this->cache[$name];
132  }
133 
145  public function getGauge( array $config = [] ) {
146  $config = $this->getValidConfig( $config );
147  $name = self::getFormattedName( $config['name'], $config['component'] );
148  try {
149  $metric = $this->getCachedMetric( $name, GaugeMetric::class );
150  } catch ( TypeError $ex ) {
151  return new NullMetric();
152  }
153 
154  if ( $metric ) {
155  $metric->validateLabels( $config['labels'] );
156  return $metric;
157  }
158  $this->cache[$name] = new GaugeMetric( $config, new MetricUtils() );
159  return $this->cache[$name];
160  }
161 
174  public function getTiming( array $config = [] ) {
175  $config = $this->getValidConfig( $config );
176  $name = self::getFormattedName( $config['name'], $config['component'] );
177  try {
178  $metric = $this->getCachedMetric( $name, TimingMetric::class );
179  } catch ( TypeError $ex ) {
180  return new NullMetric();
181  }
182  if ( $metric ) {
183  $metric->validateLabels( $config['labels'] );
184  return $metric;
185  }
186  $this->cache[$name] = new TimingMetric( $config, new MetricUtils() );
187  return $this->cache[$name];
188  }
189 
193  public function flush(): void {
194  if ( $this->format !== 'null' && $this->target ) {
195  $this->send( UDPTransport::newFromString( $this->target ) );
196  }
197  $this->cache = [];
198  }
199 
206  private function getRenderedSamples( array $cache ): array {
207  $renderedSamples = [];
208  foreach ( $cache as $metric ) {
209  foreach ( $metric->render() as $rendered ) {
210  $renderedSamples[] = $rendered;
211  }
212  }
213  return $renderedSamples;
214  }
215 
228  private function getCachedMetric( string $name, string $requested_type ) {
229  if ( !array_key_exists( $name, $this->cache ) ) {
230  return null;
231  }
232 
233  $metric = $this->cache[$name];
234  if ( get_class( $metric ) !== $requested_type ) {
235  $msg = 'Metric name collision detected: \'' . $name . '\' defined as type \'' . get_class( $metric )
236  . '\' but a \'' . $requested_type . '\' was requested.';
237  $this->logger->error( $msg );
238  throw new TypeError( $msg );
239  }
240 
241  return $metric;
242  }
243 
250  protected function send( UDPTransport $transport ): void {
251  $payload = '';
252  $renderedSamples = $this->getRenderedSamples( $this->cache );
253  foreach ( $renderedSamples as $sample ) {
254  if ( strlen( $payload ) + strlen( $sample ) + 1 < UDPTransport::MAX_PAYLOAD_SIZE ) {
255  $payload .= $sample . "\n";
256  } else {
257  // Send this payload and make a new one
258  $transport->emit( $payload );
259  $payload = '';
260  }
261  }
262  // Send what is left in the payload
263  if ( strlen( $payload ) > 0 ) {
264  $transport->emit( $payload );
265  }
266  }
267 
278  private function getFormattedName( string $name, string $component ): string {
279  return implode(
280  self::NAME_DELIMITER,
281  [ $this->prefix, $component, self::normalizeString( $name ) ]
282  );
283  }
284 
300  private function getValidConfig( array $config = [] ): array {
301  if ( !isset( $config['name'] ) ) {
302  throw new InvalidConfigurationException(
303  '\'name\' configuration option is required and cannot be empty.'
304  );
305  }
306  if ( !isset( $config['component'] ) ) {
308  '\'component\' configuration option is required and cannot be empty.'
309  );
310  }
311 
312  $config['prefix'] = $this->prefix;
313  $config['format'] = $this->format;
314  $config['name'] = self::normalizeString( $config['name'] );
315  $config['component'] = self::normalizeString( $config['component'] );
316  $config['labels'] = self::normalizeArray( $config['labels'] ?? [] );
317 
318  return $config + self::DEFAULT_METRIC_CONFIG;
319  }
320 
331  public static function normalizeString( string $entity ): string {
332  $entity = preg_replace( '/[^a-z0-9]/i', '_', $entity );
333  $entity = preg_replace( '/_+/', '_', $entity );
334  return trim( $entity, '_' );
335  }
336 
343  public static function normalizeArray( array $entities ): array {
344  $normalizedEntities = [];
345  foreach ( $entities as $entity ) {
346  $normalizedEntities[] = self::normalizeString( $entity );
347  }
348  return $normalizedEntities;
349  }
350 }
if(!defined('MW_SETUP_CALLBACK'))
The persistent session ID (if any) loaded at startup.
Definition: WebStart.php:82
A generic class to send a message over UDP.
static newFromString( $info)
getCounter(array $config=[])
Makes a new CounterMetric or fetches one from cache.
send(UDPTransport $transport)
Render the buffer of samples, group them into payloads, and send them through the provided UDPTranspo...
getTiming(array $config=[])
Makes a new TimingMetric or fetches one from cache.
static normalizeArray(array $entities)
Normalize an array of strings.
__construct(array $config, LoggerInterface $logger)
MetricsFactory builds, configures, and caches Metrics.
flush()
Send all buffered metrics to the target and destroy the cache.
getGauge(array $config=[])
Makes a new GaugeMetric or fetches one from cache.
static normalizeString(string $entity)
Normalize strings to a metrics-compatible format.
$cache
Definition: mcc.php:33