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;
39 
41 
43  private const SUPPORTED_OUTPUT_FORMATS = [ 'statsd', 'dogstatsd', 'null' ];
44 
46  private const NAME_DELIMITER = '.';
47 
49  private const DEFAULT_METRIC_CONFIG = [
50  // 'name' => required,
51  // 'extension' => required,
52  'labels' => [],
53  'sampleRate' => 1.0,
54  'service' => '',
55  'format' => 'statsd',
56  ];
57 
59  private $cache = [];
60 
62  private $target;
63 
65  private $format;
66 
68  private $prefix;
69 
71  private $logger;
72 
84  public function __construct( array $config, LoggerInterface $logger ) {
85  $this->logger = $logger;
86  $this->target = $config['target'] ?? null;
87  $this->format = $config['format'] ?? 'null';
88  $this->prefix = $config['prefix'] ?? '';
89  if ( $this->prefix === '' ) {
90  throw new UndefinedPrefixException( '\'prefix\' option is required and cannot be empty.' );
91  }
92  $this->prefix = self::normalizeString( $config['prefix'] );
93  if ( !in_array( $this->format, self::SUPPORTED_OUTPUT_FORMATS ) ) {
95  'Format "' . $this->format . '" not supported. Expected one of '
96  . json_encode( self::SUPPORTED_OUTPUT_FORMATS )
97  );
98  }
99  }
100 
113  public function getCounter( array $config = [] ) {
114  $config = $this->getValidConfig( $config );
115  $name = self::getFormattedName( $config['name'], $config['extension'] );
116  try {
117  $metric = $this->getCachedMetric( $name, CounterMetric::class );
118  } catch ( TypeError $ex ) {
119  return new NullMetric();
120  }
121 
122  if ( $metric ) {
123  $metric->validateLabels( $config['labels'] );
124  return $metric;
125  }
126  $this->cache[$name] = new CounterMetric( $config, new MetricUtils() );
127  return $this->cache[$name];
128  }
129 
141  public function getGauge( array $config = [] ) {
142  $config = $this->getValidConfig( $config );
143  $name = self::getFormattedName( $config['name'], $config['extension'] );
144  try {
145  $metric = $this->getCachedMetric( $name, GaugeMetric::class );
146  } catch ( TypeError $ex ) {
147  return new NullMetric();
148  }
149 
150  if ( $metric ) {
151  $metric->validateLabels( $config['labels'] );
152  return $metric;
153  }
154  $this->cache[$name] = new GaugeMetric( $config, new MetricUtils() );
155  return $this->cache[$name];
156  }
157 
170  public function getTiming( array $config = [] ) {
171  $config = $this->getValidConfig( $config );
172  $name = self::getFormattedName( $config['name'], $config['extension'] );
173  try {
174  $metric = $this->getCachedMetric( $name, TimingMetric::class );
175  } catch ( TypeError $ex ) {
176  return new NullMetric();
177  }
178  if ( $metric ) {
179  $metric->validateLabels( $config['labels'] );
180  return $metric;
181  }
182  $this->cache[$name] = new TimingMetric( $config, new MetricUtils() );
183  return $this->cache[$name];
184  }
185 
189  public function flush(): void {
190  if ( $this->format !== 'null' && $this->target ) {
191  $this->send( UDPTransport::newFromString( $this->target ) );
192  }
193  $this->cache = [];
194  }
195 
202  private function getRenderedSamples( array $cache ): array {
203  $renderedSamples = [];
204  foreach ( $cache as $metric ) {
205  foreach ( $metric->render() as $rendered ) {
206  $renderedSamples[] = $rendered;
207  }
208  }
209  return $renderedSamples;
210  }
211 
224  private function getCachedMetric( string $name, string $requested_type ) {
225  if ( !array_key_exists( $name, $this->cache ) ) {
226  return null;
227  }
228 
229  $metric = $this->cache[$name];
230  if ( get_class( $metric ) !== $requested_type ) {
231  $msg = 'Metric name collision detected: \'' . $name . '\' defined as type \'' . get_class( $metric )
232  . '\' but a \'' . $requested_type . '\' was requested.';
233  $this->logger->error( $msg );
234  throw new TypeError( $msg );
235  }
236 
237  return $metric;
238  }
239 
246  protected function send( UDPTransport $transport ): void {
247  $payload = '';
248  $renderedSamples = $this->getRenderedSamples( $this->cache );
249  foreach ( $renderedSamples as $sample ) {
250  if ( strlen( $payload ) + strlen( $sample ) + 1 < UDPTransport::MAX_PAYLOAD_SIZE ) {
251  $payload .= $sample . "\n";
252  } else {
253  // Send this payload and make a new one
254  $transport->emit( $payload );
255  $payload = '';
256  }
257  }
258  // Send what is left in the payload
259  if ( strlen( $payload ) > 0 ) {
260  $transport->emit( $payload );
261  }
262  }
263 
274  private function getFormattedName( string $name, string $extension ): string {
275  return implode(
276  self::NAME_DELIMITER,
277  [ $this->prefix, $extension, self::normalizeString( $name ) ]
278  );
279  }
280 
296  private function getValidConfig( array $config = [] ): array {
297  if ( !isset( $config['name'] ) ) {
298  throw new InvalidConfigurationException(
299  '\'name\' configuration option is required and cannot be empty.'
300  );
301  }
302  if ( !isset( $config['extension'] ) ) {
304  '\'extension\' configuration option is required and cannot be empty.'
305  );
306  }
307 
308  $config['prefix'] = $this->prefix;
309  $config['format'] = $this->format;
310  $config['name'] = self::normalizeString( $config['name'] );
311  $config['extension'] = self::normalizeString( $config['extension'] );
312  $config['labels'] = self::normalizeArray( $config['labels'] ?? [] );
313 
314  return $config + self::DEFAULT_METRIC_CONFIG;
315  }
316 
327  public static function normalizeString( string $entity ): string {
328  $entity = preg_replace( '/[^a-z0-9]/i', '_', $entity );
329  $entity = preg_replace( '/_+/', '_', $entity );
330  return trim( $entity, '_' );
331  }
332 
339  public static function normalizeArray( array $entities ): array {
340  $normalizedEntities = [];
341  foreach ( $entities as $entity ) {
342  $normalizedEntities[] = self::normalizeString( $entity );
343  }
344  return $normalizedEntities;
345  }
346 }
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...
getFormattedName(string $name, string $extension)
Get the metric formatted name.
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.
getValidConfig(array $config=[])
Renders a valid configuration.
getRenderedSamples(array $cache)
Get all rendered samples from cache.
getGauge(array $config=[])
Makes a new GaugeMetric or fetches one from cache.
static normalizeString(string $entity)
Normalize strings to a metrics-compatible format.
getCachedMetric(string $name, string $requested_type)
Searches the cache for an instance of the requested metric.
array< CounterMetric|GaugeMetric|TimingMetric > $cache
$cache
Definition: mcc.php:33