Completed
Pull Request — master (#12)
by Harry
08:53
created

Client::__destruct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 0
cts 0
cp 0
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 3
nc 2
nop 0
crap 6
1
<?php
2
/**
3
 * This file is part of graze/dog-statsd
4
 *
5
 * Copyright (c) 2016 Nature Delivered Ltd. <https://www.graze.com>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 *
10
 * @license https://github.com/graze/dog-statsd/blob/master/LICENSE.md
11
 * @link    https://github.com/graze/dog-statsd
12
 */
13
14
namespace Graze\DogStatsD;
15
16
use Graze\DogStatsD\Exception\ConfigurationException;
17
use Graze\DogStatsD\Exception\ConnectionException;
18
19
/**
20
 * StatsD Client Class - Modified to support DataDogs statsd server
21
 */
22
class Client
23
{
24
    const STATUS_OK       = 0;
25
    const STATUS_WARNING  = 1;
26
    const STATUS_CRITICAL = 2;
27
    const STATUS_UNKNOWN  = 3;
28
29
    const PRIORITY_LOW    = 'low';
30
    const PRIORITY_NORMAL = 'normal';
31
32
    const ALERT_ERROR   = 'error';
33
    const ALERT_WARNING = 'warning';
34
    const ALERT_INFO    = 'info';
35
    const ALERT_SUCCESS = 'success';
36
37
    /**
38
     * Instance instances array
39
     *
40
     * @var array
41
     */
42
    protected static $instances = [];
43
44
    /**
45
     * Instance ID
46
     *
47
     * @var string
48
     */
49
    protected $instanceId;
50
51
    /**
52
     * Server Host
53
     *
54
     * @var string
55
     */
56
    protected $host = '127.0.0.1';
57
58
    /**
59
     * Server Port
60
     *
61
     * @var integer
62
     */
63
    protected $port = 8125;
64
65
    /**
66
     * Last message sent to the server
67
     *
68
     * @var string
69
     */
70
    protected $message = '';
71
72
    /**
73
     * Class namespace
74
     *
75
     * @var string
76
     */
77
    protected $namespace = '';
78
79
    /**
80
     * Timeout for creating the socket connection
81
     *
82
     * @var null|int
83
     */
84
    protected $timeout;
85
86
    /**
87
     * Whether or not an exception should be thrown on failed connections
88
     *
89
     * @var bool
90
     */
91
    protected $throwExceptions = true;
92
93
    /**
94
     * Socket connection
95
     *
96
     * @var resource|null
97
     */
98
    protected $socket = null;
99
100
    /**
101
     * Metadata for the DataDog event message
102
     *
103
     * @var array - time - Assign a timestamp to the event.
104
     *            - hostname - Assign a hostname to the event
105
     *            - key - Assign an aggregation key to th event, to group it with some others
106
     *            - priority - Can be 'normal' or 'low'
107
     *            - source - Assign a source type to the event
108
     *            - alert - Can be 'error', 'warning', 'info' or 'success'
109
     */
110
    protected $eventMetaData = [
111
        'time'     => 'd',
112
        'hostname' => 'h',
113
        'key'      => 'k',
114
        'priority' => 'p',
115
        'source'   => 's',
116
        'alert'    => 't',
117
    ];
118
119
    /**
120
     * @var array - time - Assign a timestamp to the service check
121
     *            - hostname - Assign a hostname to the service check
122
     */
123
    protected $serviceCheckMetaData = [
124
        'time'     => 'd',
125
        'hostname' => 'h',
126
    ];
127
128
    /**
129
     * @var array - message - Assign a message to the service check
130
     */
131
    protected $serviceCheckMessage = [
132
        'message' => 'm',
133
    ];
134
135
    /**
136
     * Is the server type DataDog implementation
137
     *
138 1
     * @var bool
139
     */
140 1
    protected $dataDog = true;
141 1
142 1
    /**
143 1
     * Set of default tags to send to every request
144
     *
145
     * @var array
146
     */
147
    protected $tags = [];
148
149
    /**
150
     * Singleton Reference
151 45
     *
152
     * @param  string $name Instance name
153 45
     *
154
     * @return Client Client instance
155 45
     */
156 45
    public static function instance($name = 'default')
157 45
    {
158 45
        if (!isset(static::$instances[$name])) {
159
            static::$instances[$name] = new static($name);
160
        }
161
        return static::$instances[$name];
162
    }
163
164
    /**
165 2
     * Create a new instance
166
     *
167 2
     * @param string|null $instanceId
168
     */
169
    public function __construct($instanceId = null)
170
    {
171
        $this->instanceId = $instanceId ?: uniqid();
172
173
        if (empty($this->timeout)) {
174
            $this->timeout = (float) ini_get('default_socket_timeout');
0 ignored issues
show
Documentation Bug introduced by
It seems like (double) ini_get('default_socket_timeout') of type double is incompatible with the declared type null|integer of property $timeout.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
175
        }
176
    }
177
178
    /**
179
     * Get string value of instance
180
     *
181
     * @return string String representation of this instance
182
     */
183
    public function __toString()
184
    {
185 45
        return 'DogStatsD\Client::[' . $this->instanceId . ']';
186
    }
187
188 45
    /**
189 15
     * Initialize Connection Details
190 15
     *
191 45
     * @param array $options Configuration options
192
     *                       :host <string|ip> - host to talk to
193 45
     *                       :port <int> - Port to communicate with
194 45
     *                       :namespace <string> - Default namespace
195 45
     *                       :timeout <float> - Timeout in seconds
196 45
     *                       :throwExceptions <bool> - Throw an exception on connection error
197 45
     *                       :dataDog <bool> - Use DataDog's version of statsd (Default: true)
198 45
     *                       :tags <array> - List of tags to add to each message
199 45
     *
200
     * @return Client This instance
201 45
     * @throws ConfigurationException If port is invalid
202 2
     */
203
    public function configure(array $options = [])
204
    {
205 45
        $setOption = function ($name, $type = null) use ($options) {
206
            if (isset($options[$name])) {
207
                if (!is_null($type) && (gettype($options[$name]) != $type)) {
208
                    throw new ConfigurationException($this, sprintf(
209
                        "Option: %s is expected to be: '%s', was: '%s'",
210
                        $name,
211
                        $type,
212
                        gettype($options[$name])
213 1
                    ));
214
                }
215 1
                $this->{$name} = $options[$name];
216
            }
217
        };
218
219
        $setOption('host', 'string');
220
        $setOption('port', 'integer');
221
        $setOption('namespace', 'string');
222
        $setOption('timeout');
223 2
        $setOption('throwExceptions', 'boolean');
224
        $setOption('dataDog', 'boolean');
225 2
        $setOption('tags', 'array');
226
227
        if (!$this->port || !is_numeric($this->port) || $this->port > 65535) {
228
            throw new ConfigurationException($this, 'Option: Port is out of range');
229
        }
230
231
        return $this;
232
    }
233 1
234
    /**
235 1
     * Get Host
236
     *
237
     * @return string Host
238
     */
239
    public function getHost()
240
    {
241
        return $this->host;
242
    }
243 31
244
    /**
245 31
     * Get Port
246
     *
247
     * @return int Port
248
     */
249
    public function getPort()
250
    {
251
        return $this->port;
252
    }
253
254
    /**
255
     * Get Namespace
256
     *
257
     * @return string Namespace
258 14
     */
259
    public function getNamespace()
260 14
    {
261
        return $this->namespace;
262 14
    }
263 14
264 1
    /**
265 1
     * Get Last Message
266 1
     *
267 1
     * @return string Last message sent to server
268 1
     */
269 1
    public function getLastMessage()
270 13
    {
271 13
        return $this->message;
272 13
    }
273
274 14
    /**
275
     * Increment a metric
276
     *
277
     * @param string|string[] $metrics    Metric(s) to increment
278
     * @param int             $delta      Value to decrement the metric by
279
     * @param float           $sampleRate Sample rate of metric
280
     * @param string[]        $tags       List of tags for this metric
281
     *
282
     * @return Client This instance
283
     */
284
    public function increment($metrics, $delta = 1, $sampleRate = 1.0, array $tags = [])
285
    {
286
        $metrics = is_array($metrics) ? $metrics : [$metrics];
287 2
288
        $data = [];
289 2
        if ($sampleRate < 1.0) {
290
            foreach ($metrics as $metric) {
291 View Code Duplication
                if ((mt_rand() / mt_getrandmax()) <= $sampleRate) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
292
                    $data[$metric] = $delta . '|c|@' . $sampleRate;
293
                }
294
            }
295
        } else {
296
            foreach ($metrics as $metric) {
297
                $data[$metric] = $delta . '|c';
298
            }
299
        }
300
        return $this->send($data, $tags);
301 3
    }
302
303 3
    /**
304
     * Decrement a metric
305 3
     *
306 3
     * @param string|string[] $metrics    Metric(s) to decrement
307
     * @param int             $delta      Value to increment the metric by
308 3
     * @param int             $sampleRate Sample rate of metric
309
     * @param string[]        $tags       List of tags for this metric
310
     *
311
     * @return Client This instance
312
     */
313
    public function decrement($metrics, $delta = 1, $sampleRate = 1, array $tags = [])
314
    {
315
        return $this->increment($metrics, 0 - $delta, $sampleRate, $tags);
316
    }
317
318
    /**
319
     * Timing
320 1
     *
321
     * @param string   $metric Metric to track
322 1
     * @param float    $time   Time in milliseconds
323 1
     * @param string[] $tags   List of tags for this metric
324 1
     *
325 1
     * @return Client This instance
326 1
     */
327
    public function timing($metric, $time, array $tags = [])
328
    {
329
        return $this->send(
330
            [
331
                $metric => $time . '|ms',
332
            ],
333
            $tags
334
        );
335
    }
336
337
    /**
338
     * Time a function
339 2
     *
340
     * @param string   $metric Metric to time
341 2
     * @param callable $func   Function to record
342
     * @param string[] $tags   List of tags for this metric
343 2
     *
344 2
     * @return Client This instance
345
     */
346 2
    public function time($metric, callable $func, array $tags = [])
347
    {
348
        $timerStart = microtime(true);
349
        $func();
350
        $timerEnd = microtime(true);
351
        $time = round(($timerEnd - $timerStart) * 1000, 4);
352
        return $this->timing($metric, $time, $tags);
353
    }
354
355
    /**
356
     * Gauges
357
     *
358 2
     * @param string   $metric Metric to gauge
359
     * @param int      $value  Set the value of the gauge
360 2
     * @param string[] $tags   List of tags for this metric
361
     *
362 2
     * @return Client This instance
363 2
     */
364
    public function gauge($metric, $value, array $tags = [])
365 2
    {
366
        return $this->send(
367
            [
368
                $metric => $value . '|g',
369
            ],
370
            $tags
371
        );
372
    }
373
374
    /**
375
     * Histogram
376
     *
377
     * @param string   $metric     Metric to send
378
     * @param float    $value      Value to send
379
     * @param float    $sampleRate Sample rate of metric
380
     * @param string[] $tags       List of tags for this metric
381
     *
382
     * @return Client This instance
383
     */
384
    public function histogram($metric, $value, $sampleRate = 1.0, array $tags = [])
385
    {
386
        $data = [];
387 6
        if ($sampleRate < 1.0) {
388 View Code Duplication
            if ((mt_rand() / mt_getrandmax()) <= $sampleRate) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
389 6
                $data[$metric] = $value . '|h|@' . $sampleRate;
390 1
            }
391
        } else {
392
            $data[$metric] = $value . '|h';
393 5
        }
394 5
395 5
        return $this->send($data, $tags);
396 5
    }
397
398 5
    /**
399 2
     * Sets - count the number of unique elements for a group
400 2
     *
401 2
     * @param string   $metric
402 5
     * @param int      $value
403
     * @param string[] $tags List of tags for this metric
404 5
     *
405
     * @return Client This instance
406 5
     */
407 5
    public function set($metric, $value, array $tags = [])
408 5
    {
409
        return $this->send(
410
            [
411
                $metric => $value . '|s',
412
            ],
413
            $tags
414
        );
415
    }
416
417
    /**
418
     * Send a event notification
419
     *
420
     * @link http://docs.datadoghq.com/guides/dogstatsd/#events
421
     *
422
     * @param string   $title     Event Title
423
     * @param string   $text      Event Text
424
     * @param array    $metadata  Set of metadata for this event:
425
     *                            - time - Assign a timestamp to the event.
426 6
     *                            - hostname - Assign a hostname to the event
427
     *                            - key - Assign an aggregation key to th event, to group it with some others
428 6
     *                            - priority - Can be 'normal' or 'low'
429 1
     *                            - source - Assign a source type to the event
430
     *                            - alert - Can be 'error', 'warning', 'info' or 'success'
431
     * @param string[] $tags      List of tags for this event
432 5
     *
433 5
     * @return Client This instance
434
     * @throws ConnectionException If there is a connection problem with the host
435 5
     */
436 5
    public function event($title, $text, array $metadata = [], array $tags = [])
437 3
    {
438 3
        if (!$this->dataDog) {
439 3
            return $this;
440 5
        }
441 5
442
        $text = str_replace(["\r", "\n"], ['', "\\n"], $text);
443 5
        $metric = sprintf('_e{%d,%d}', strlen($title), strlen($text));
444 5
        $prefix = $this->namespace ? $this->namespace . '.' : '';
445 5
        $value = sprintf('%s|%s', $prefix . $title, $text);
446
447 5
        foreach ($metadata as $key => $data) {
448 5
            if (isset($this->eventMetaData[$key])) {
449 5
                $value .= sprintf('|%s:%s', $this->eventMetaData[$key], $data);
450
            }
451
        }
452
453
        $value .= $this->formatTags($tags);
454
455
        return $this->sendMessages([
456
            sprintf('%s:%s', $metric, $value),
457 31
        ]);
458
    }
459 31
460 20
    /**
461
     * Service Checks
462
     *
463 11
     * @link http://docs.datadoghq.com/guides/dogstatsd/#service-checks
464 11
     *
465 11
     * @param string   $name     Name of the service
466 10
     * @param int      $status   digit corresponding to the status you’re reporting (OK = 0, WARNING = 1, CRITICAL = 2,
467 10
     *                           UNKNOWN = 3)
468 2
     * @param array    $metadata - time - Assign a timestamp to the service check
469
     *                           - hostname - Assign a hostname to the service check
470 11
     * @param string[] $tags     List of tags for this event
471
     *
472 11
     * @return Client This instance
473
     * @throws ConnectionException If there is a connection problem with the host
474
     */
475
    public function serviceCheck($name, $status, array $metadata = [], array $tags = [])
476
    {
477
        if (!$this->dataDog) {
478
            return $this;
479
        }
480
481
        $prefix = $this->namespace ? $this->namespace . '.' : '';
482
        $value = sprintf('_sc|%s|%d', $prefix . $name, $status);
483
484 21
        $applyMetadata = function ($metadata, $definition) use (&$value) {
485
            foreach ($metadata as $key => $data) {
486 21
                if (isset($definition[$key])) {
487 21
                    $value .= sprintf('|%s:%s', $definition[$key], $data);
488 21
                }
489 21
            }
490 21
        };
491 21
492 21
        $applyMetadata($metadata, $this->serviceCheckMetaData);
493
        $value .= $this->formatTags($tags);
494
        $applyMetadata($metadata, $this->serviceCheckMessage);
495
496
        return $this->sendMessages([
497
            $value,
498
        ]);
499
    }
500
501 31
    /**
502
     * @param string[] $tags A list of tags to apply to each message
503 31
     *
504 31
     * @return string
505 2
     */
506 1
    private function formatTags(array $tags = [])
507
    {
508 1
        if (!$this->dataDog || count($tags) === 0) {
509 1
            return '';
510
        }
511 1
512 1
        $result = [];
513
        foreach ($tags as $key => $value) {
514
            if (is_numeric($key)) {
515 29
                $result[] = $value;
516 29
            } else {
517 29
                $result[] = sprintf('%s:%s', $key, $value);
518 29
            }
519
        }
520
521
        return sprintf('|#%s', implode(',', $result));
522
    }
523
524
    /**
525
     * Send Data to StatsD Server
526
     *
527
     * @param string[] $data A list of messages to send to the server
528
     * @param string[] $tags A list of tags to apply to each message
529
     *
530
     * @return Client This instance
531
     * @throws ConnectionException If there is a connection problem with the host
532
     */
533
    protected function send(array $data, array $tags = [])
534
    {
535
        $messages = [];
536
        $prefix = $this->namespace ? $this->namespace . '.' : '';
537
        $formattedTags = $this->formatTags(array_merge($this->tags, $tags));
538
        foreach ($data as $key => $value) {
539
            $messages[] = $prefix . $key . ':' . $value . $formattedTags;
540
        }
541
        return $this->sendMessages($messages);
542
    }
543
544
    /**
545
     * @param string[] $messages
546
     *
547
     * @return Client This instance
548
     * @throws ConnectionException If there is a connection problem with the host
549
     */
550
    protected function sendMessages(array $messages)
551
    {
552
        if (is_null($this->socket)) {
553
            $this->socket = $this->connect();
554
        }
555
        if ($this->socket) {
556
            $this->message = implode("\n", $messages);
557
            if (@fwrite($this->socket, $this->message) === false) {
558
                // attempt to re-send on socket resource failure
559
                $this->socket = $this->connect();
560
                @fwrite($this->socket, $this->message);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
561
            }
562
        }
563
564
        return $this;
565
    }
566
567
    /**
568
     * Creates a persistent connection to the udp host:port
569
     *
570
     * @return resource
571
     */
572
    protected function connect()
573
    {
574
        $socket = @fsockopen('udp://' . $this->host, $this->port, $errno, $errstr, $this->timeout);
575
        if ($socket === false) {
576
            $socket = null;
577
            if ($this->throwExceptions) {
578
                throw new ConnectionException($this, '(' . $errno . ') ' . $errstr);
579
            } else {
580
                trigger_error(
581
                    sprintf('StatsD server connection failed (udp://%s:%d)', $this->host, $this->port),
582
                    E_USER_WARNING
583
                );
584
            }
585
        } else {
586
            list ($sec, $ms) = sscanf($this->timeout, '%d.%d');
587
            stream_set_timeout($socket, $sec, $ms);
588
        }
589
        return $socket;
590
    }
591
592
    public function __destruct()
593
    {
594
        if ($this->socket) {
595
            fclose($this->socket);
596
        }
597
    }
598
}
599