Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 50
0.00% covered (danger)
0.00%
0 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
SamplingStatsdClient
0.00% covered (danger)
0.00%
0 / 49
0.00% covered (danger)
0.00%
0 / 5
650
0.00% covered (danger)
0.00%
0 / 1
 setSamplingRates
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 appendSampleRate
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
42
 send
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
110
 sampleData
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
42
 throwException
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2/**
3 * Copyright 2015
4 *
5 * @license GPL-2.0-or-later
6 * @file
7 */
8
9namespace Wikimedia\Stats;
10
11use Exception;
12use InvalidArgumentException;
13use Liuggio\StatsdClient\Entity\StatsdData;
14use Liuggio\StatsdClient\Entity\StatsdDataInterface;
15use Liuggio\StatsdClient\StatsdClient;
16use LogicException;
17use Wikimedia\RequestTimeout\TimeoutException;
18
19/**
20 * A statsd client that applies the sampling rate to the data items before sending them.
21 *
22 * @deprecated since 1.43 No longer used after $wgSamplingStatsdClient was removed.
23 * @since 1.26
24 */
25class SamplingStatsdClient extends StatsdClient {
26    /** @var array */
27    protected $samplingRates = [];
28
29    /**
30     * Sampling rates as an associative array of patterns and rates.
31     * Patterns are Unix shell patterns (e.g. 'MediaWiki.api.*').
32     * Rates are sampling probabilities (e.g. 0.1 means 1 in 10 events are sampled).
33     * @param array $samplingRates
34     * @since 1.28
35     */
36    public function setSamplingRates( array $samplingRates ) {
37        $this->samplingRates = $samplingRates;
38    }
39
40    /**
41     * Sets sampling rate for all items in $data.
42     * The sample rate specified in a StatsdData entity overrides the sample rate specified here.
43     *
44     * @inheritDoc
45     */
46    public function appendSampleRate( $data, $sampleRate = 1 ) {
47        $samplingRates = $this->samplingRates;
48        if ( !$samplingRates && $sampleRate !== 1 ) {
49            $samplingRates = [ '*' => $sampleRate ];
50        }
51        if ( $samplingRates ) {
52            array_walk( $data, static function ( $item ) use ( $samplingRates ) {
53                /** @var StatsdData $item */
54                foreach ( $samplingRates as $pattern => $rate ) {
55                    if ( fnmatch( $pattern, $item->getKey(), FNM_NOESCAPE ) ) {
56                        $item->setSampleRate( $item->getSampleRate() * $rate );
57                        break;
58                    }
59                }
60            } );
61        }
62
63        return $data;
64    }
65
66    /**
67     * Send the metrics over UDP
68     * Sample the metrics according to their sample rate and send the remaining ones.
69     *
70     * @param StatsdDataInterface|StatsdDataInterface[] $data message(s) to sent
71     *        strings are not allowed here as sampleData requires a StatsdDataInterface
72     * @param int $sampleRate
73     *
74     * @return int the data sent in bytes
75     */
76    public function send( $data, $sampleRate = 1 ) {
77        if ( !is_array( $data ) ) {
78            $data = [ $data ];
79        }
80        if ( !$data ) {
81            return 0;
82        }
83        foreach ( $data as $item ) {
84            if ( !( $item instanceof StatsdDataInterface ) ) {
85                throw new InvalidArgumentException(
86                    'SamplingStatsdClient does not accept stringified messages' );
87            }
88        }
89
90        // add sampling
91        $data = $this->appendSampleRate( $data, $sampleRate );
92        $data = $this->sampleData( $data );
93
94        $data = array_map( 'strval', $data );
95
96        // reduce number of packets
97        if ( $this->getReducePacket() ) {
98            $data = $this->reduceCount( $data );
99        }
100
101        // failures in any of this should be silently ignored if ..
102        $written = 0;
103        try {
104            $fp = $this->getSender()->open();
105            if ( !$fp ) {
106                return 0;
107            }
108            foreach ( $data as $message ) {
109                $written += $this->getSender()->write( $fp, $message );
110            }
111            $this->getSender()->close( $fp );
112        } catch ( TimeoutException $e ) {
113            throw $e;
114        } catch ( Exception $e ) {
115            $this->throwException( $e );
116        }
117
118        return $written;
119    }
120
121    /**
122     * Throw away some of the data according to the sample rate.
123     * @param StatsdDataInterface[] $data
124     * @return StatsdDataInterface[]
125     */
126    protected function sampleData( $data ) {
127        $newData = [];
128        $mt_rand_max = mt_getrandmax();
129        foreach ( $data as $item ) {
130            $samplingRate = $item->getSampleRate();
131            if ( $samplingRate <= 0.0 || $samplingRate > 1.0 ) {
132                throw new LogicException( 'Sampling rate shall be within ]0, 1]' );
133            }
134            if (
135                $samplingRate === 1 ||
136                ( mt_rand() / $mt_rand_max <= $samplingRate )
137            ) {
138                $newData[] = $item;
139            }
140        }
141        return $newData;
142    }
143
144    /**
145     * Ideally this would override parent::throwException(), but that is private,
146     * not protected.
147     */
148    private function throwException( Exception $exception ) {
149        if ( !$this->getFailSilently() ) {
150            throw $exception;
151        }
152    }
153}
154
155/** @deprecated class alias since 1.43 */
156class_alias( SamplingStatsdClient::class, 'SamplingStatsdClient' );