Passed
Pull Request — master (#36)
by
unknown
01:30
created

Client::distribution()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 5
c 1
b 0
f 0
nc 2
nop 4
dl 0
loc 9
rs 10
ccs 6
cts 6
cp 1
crap 2
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
use Graze\DogStatsD\Stream\StreamWriter;
19
use Graze\DogStatsD\Stream\WriterInterface;
20
21
/**
22
 * StatsD Client Class - Modified to support DataDogs statsd server
23
 */
24
class Client
25
{
26
    const STATUS_OK       = 0;
27
    const STATUS_WARNING  = 1;
28
    const STATUS_CRITICAL = 2;
29
    const STATUS_UNKNOWN  = 3;
30
31
    const PRIORITY_LOW    = 'low';
32
    const PRIORITY_NORMAL = 'normal';
33
34
    const ALERT_ERROR   = 'error';
35
    const ALERT_WARNING = 'warning';
36
    const ALERT_INFO    = 'info';
37
    const ALERT_SUCCESS = 'success';
38
39
    /**
40
     * Instance instances array
41
     *
42
     * @var array
43
     */
44
    protected static $instances = [];
45
46
    /**
47
     * Instance ID
48
     *
49
     * @var string
50
     */
51
    protected $instanceId;
52
53
    /**
54
     * Server Host
55
     *
56
     * @var string
57
     */
58
    protected $host = '127.0.0.1';
59
60
    /**
61
     * Server Port
62
     *
63
     * @var integer
64
     */
65
    protected $port = 8125;
66
67
    /**
68
     * Last message sent to the server
69
     *
70
     * @var string
71
     */
72
    protected $message = '';
73
74
    /**
75
     * Was the last message sucessfully sent
76
     *
77
     * @var bool
78
     */
79
    protected $written;
80
81
    /**
82
     * Class namespace
83
     *
84
     * @var string
85
     */
86
    protected $namespace = '';
87
88
    /**
89
     * Timeout for creating the socket connection
90
     *
91
     * @var null|float
92
     */
93
    protected $timeout;
94
95
    /**
96
     * What we should do on connection failure
97
     *
98
     * Options:
99
     *  - error
100
     *  - exception
101
     *  - ignore
102
     *
103
     * @var string
104
     */
105
    protected $onError = 'exception';
106
107
    /**
108
     * Socket connection
109
     *
110
     * @var WriterInterface
111
     */
112
    protected $stream;
113
114
    /**
115
     * Metadata for the DataDog event message
116
     *
117
     * @var array - time - Assign a timestamp to the event.
118
     *            - hostname - Assign a hostname to the event
119
     *            - key - Assign an aggregation key to the event, to group it with some others
120
     *            - priority - Can be 'normal' or 'low'
121
     *            - source - Assign a source type to the event
122
     *            - alert - Can be 'error', 'warning', 'info' or 'success'
123
     */
124
    protected $eventMetaData = [
125
        'time'     => 'd',
126
        'hostname' => 'h',
127
        'key'      => 'k',
128
        'priority' => 'p',
129
        'source'   => 's',
130
        'alert'    => 't',
131
    ];
132
133
    /**
134
     * @var array - time - Assign a timestamp to the service check
135
     *            - hostname - Assign a hostname to the service check
136
     */
137
    protected $serviceCheckMetaData = [
138
        'time'     => 'd',
139
        'hostname' => 'h',
140
    ];
141
142
    /**
143
     * @var array - message - Assign a message to the service check
144
     */
145
    protected $serviceCheckMessage = [
146
        'message' => 'm',
147
    ];
148
149
    /**
150
     * Is the server type DataDog implementation
151
     *
152
     * @var bool
153
     */
154
    protected $dataDog = true;
155
156
    /**
157
     * Set of default tags to send to every request
158
     *
159
     * @var array
160
     */
161
    protected $tags = [];
162
163
    /**
164
     * List of tags processors to apply to every metric being sent out
165
     *
166
     * @var callable[]
167
     */
168
    protected $tagProcessors = [];
169
170
    /**
171
     * Singleton Reference
172
     *
173
     * @param  string $name Instance name
174
     *
175
     * @return Client Client instance
176
     */
177 3
    public static function instance($name = 'default')
178
    {
179 3
        if (!isset(static::$instances[$name])) {
180 3
            static::$instances[$name] = new static($name);
181
        }
182 3
        return static::$instances[$name];
183
    }
184
185
    /**
186
     * @param string $name Instance name
187
     *
188
     * @return bool true if an instance has been found and removed
189
     */
190 3
    public static function deleteInstance($name = 'default')
191
    {
192 3
        if (isset(static::$instances[$name])) {
193 2
            unset(static::$instances[$name]);
194 2
            return true;
195
        }
196 2
        return false;
197
    }
198
199
    /**
200
     * Create a new instance
201
     *
202
     * @param string|null $instanceId
203
     */
204 82
    public function __construct($instanceId = null)
205
    {
206 82
        $this->instanceId = $instanceId ?: uniqid();
207
208 82
        if (empty($this->timeout)) {
209 82
            $this->timeout = (float) ini_get('default_socket_timeout');
210
        }
211 82
    }
212
213
    /**
214
     * Get string value of instance
215
     *
216
     * @return string String representation of this instance
217
     */
218 2
    public function __toString()
219
    {
220 2
        return 'DogStatsD\Client::[' . $this->instanceId . ']';
221
    }
222
223
    /**
224
     * Initialize Connection Details
225
     *
226
     * @param array $options Configuration options
227
     *                       :host <string|ip> - host to talk to
228
     *                       :port <int> - Port to communicate with
229
     *                       :namespace <string> - Default namespace
230
     *                       :timeout <float> - Timeout in seconds
231
     *                       :onError <enum[error,exception,ignore]> - What we should do on error
232
     *                       :dataDog <bool> - Use DataDog's version of statsd (Default: true)
233
     *                       :tags <array> - List of tags to add to each message
234
     *                       :tagProcessors <array> - List of tags processors to use
235
     *
236
     * @return Client This instance
237
     * @throws ConfigurationException If port is invalid
238
     */
239 82
    public function configure(array $options = [])
240
    {
241
        $setOption = function ($name, $type = null, $default = false) use ($options) {
242 82
            if (isset($options[$name])) {
243 24
                if (!is_null($type) && (gettype($options[$name]) != $type)) {
244 4
                    throw new ConfigurationException($this->instanceId, sprintf(
245 4
                        "Option: %s is expected to be: '%s', was: '%s'",
246 4
                        $name,
247 4
                        $type,
248 4
                        gettype($options[$name])
249
                    ));
250
                }
251 20
                $this->{$name} = $options[$name];
252 82
            } elseif ($default) {
253 4
                $this->{$name} = $default;
254
            }
255 82
        };
256
257 82
        $setOption('host', 'string', getenv('DD_AGENT_HOST'));
258 82
        $setOption('port', null, getenv('DD_DOGSTATSD_PORT'));
259 82
        $setOption('namespace', 'string');
260 82
        $setOption('timeout');
261 82
        $setOption('onError', 'string');
262 82
        $setOption('dataDog', 'boolean');
263 82
        $setOption('tags', 'array');
264
265 82
        if (getenv('DD_ENTITY_ID')) {
266 2
            $this->tags['dd.internal.entity_id'] = getenv('DD_ENTITY_ID');
267
        }
268
269 82
        if (isset($options['tagProcessors']) && is_array($options['tagProcessors'])) {
270 2
            foreach ($options['tagProcessors'] as $tagProcessor) {
271 2
                if (!is_callable($tagProcessor)) {
272 1
                    throw new ConfigurationException($this->instanceId, 'supplied tag processor is not a callable');
273
                }
274 1
                $this->addTagProcessor($tagProcessor);
275
            }
276
        }
277
278 82
        $this->port = (int) $this->port;
279 82
        if (!$this->port || !is_numeric($this->port) || $this->port > 65535) {
280 4
            throw new ConfigurationException($this->instanceId, 'Option: Port is invalid or is out of range');
281
        }
282
283 82
        if (!in_array(
284 82
            $this->onError,
285 82
            [StreamWriter::ON_ERROR_ERROR, StreamWriter::ON_ERROR_EXCEPTION, StreamWriter::ON_ERROR_IGNORE]
286
        )) {
287 1
            throw new ConfigurationException(
288 1
                $this->instanceId,
289 1
                sprintf("Option: onError '%s' is not one of: [error,exception,ignore]", $this->onError)
290
            );
291
        }
292
293 82
        return $this;
294
    }
295
296
    /**
297
     * @param callable $tagsProcessor
298
     *
299
     * @return Client
300
     */
301 3
    public function addTagProcessor(callable $tagsProcessor)
302
    {
303 3
        $this->tagProcessors[] = $tagsProcessor;
304 3
        return $this;
305
    }
306
307
    /**
308
     * Get Host
309
     *
310
     * @return string Host
311
     */
312 2
    public function getHost()
313
    {
314 2
        return $this->host;
315
    }
316
317
    /**
318
     * Get Port
319
     *
320
     * @return int Port
321
     */
322 4
    public function getPort()
323
    {
324 4
        return $this->port;
325
    }
326
327
    /**
328
     * Get Namespace
329
     *
330
     * @return string Namespace
331
     */
332 1
    public function getNamespace()
333
    {
334 1
        return $this->namespace;
335
    }
336
337
    /**
338
     * Get Last Message
339
     *
340
     * @return string Last message sent to server
341
     */
342 45
    public function getLastMessage()
343
    {
344 45
        return $this->message;
345
    }
346
347
    /**
348
     * Was the last write successful
349
     *
350
     * @return bool
351
     */
352 2
    public function wasSuccessful()
353
    {
354 2
        return $this->written;
355
    }
356
357
    /**
358
     * Increment a metric
359
     *
360
     * @param string|string[] $metrics    Metric(s) to increment
361
     * @param int             $delta      Value to decrement the metric by
362
     * @param float           $sampleRate Sample rate of metric
363
     * @param string[]        $tags       List of tags for this metric
364
     *
365
     * @return Client This instance
366
     */
367 20
    public function increment($metrics, $delta = 1, $sampleRate = 1.0, array $tags = [])
368
    {
369 20
        $metrics = is_array($metrics) ? $metrics : [$metrics];
370
371 20
        if ($this->isSampled($sampleRate, $postfix)) {
372 19
            $data = [];
373 19
            foreach ($metrics as $metric) {
374 19
                $data[$metric] = $delta . '|c' . $postfix;
375
            }
376 19
            return $this->send($data, $tags);
377
        }
378 1
        return $this;
379
    }
380
381
    /**
382
     * Decrement a metric
383
     *
384
     * @param string|string[] $metrics    Metric(s) to decrement
385
     * @param int             $delta      Value to increment the metric by
386
     * @param int             $sampleRate Sample rate of metric
387
     * @param string[]        $tags       List of tags for this metric
388
     *
389
     * @return Client This instance
390
     */
391 2
    public function decrement($metrics, $delta = 1, $sampleRate = 1, array $tags = [])
392
    {
393 2
        return $this->increment($metrics, 0 - $delta, $sampleRate, $tags);
394
    }
395
396
    /**
397
     * Timing
398
     *
399
     * @param string   $metric Metric to track
400
     * @param float    $time   Time in milliseconds
401
     * @param string[] $tags   List of tags for this metric
402
     *
403
     * @return Client This instance
404
     */
405 3
    public function timing($metric, $time, array $tags = [])
406
    {
407 3
        return $this->send(
408
            [
409 3
                $metric => $time . '|ms',
410
            ],
411 3
            $tags
412
        );
413
    }
414
415
    /**
416
     * Time a function
417
     *
418
     * @param string   $metric Metric to time
419
     * @param callable $func   Function to record
420
     * @param string[] $tags   List of tags for this metric
421
     *
422
     * @return Client This instance
423
     */
424 1
    public function time($metric, callable $func, array $tags = [])
425
    {
426 1
        $timerStart = microtime(true);
427 1
        $func();
428 1
        $timerEnd = microtime(true);
429 1
        $time = round(($timerEnd - $timerStart) * 1000, 4);
430 1
        return $this->timing($metric, $time, $tags);
431
    }
432
433
    /**
434
     * Gauges
435
     *
436
     * @param string   $metric Metric to gauge
437
     * @param int      $value  Set the value of the gauge
438
     * @param string[] $tags   List of tags for this metric
439
     *
440
     * @return Client This instance
441
     */
442 4
    public function gauge($metric, $value, array $tags = [])
443
    {
444 4
        return $this->send(
445
            [
446 4
                $metric => $value . '|g',
447
            ],
448 4
            $tags
449
        );
450
    }
451
452
    /**
453
     * Histogram
454
     *
455
     * @param string   $metric     Metric to send
456
     * @param float    $value      Value to send
457
     * @param float    $sampleRate Sample rate of metric
458
     * @param string[] $tags       List of tags for this metric
459
     *
460
     * @return Client This instance
461
     */
462 4
    public function histogram($metric, $value, $sampleRate = 1.0, array $tags = [])
463
    {
464 4
        if ($this->isSampled($sampleRate, $postfix)) {
465 3
            return $this->send(
466 3
                [$metric => $value . '|h' . $postfix],
467 3
                $tags
468
            );
469
        }
470 1
        return $this;
471
    }
472
473
    /**
474
     * Distribution
475
     *
476
     * @param string   $metric     Metric to send
477
     * @param float    $value      Value to send
478
     * @param float    $sampleRate Sample rate of metric
479
     * @param string[] $tags       List of tags for this metric
480
     *
481
     * @return Client This instance
482
     */
483 4
    public function distribution($metric, $value, $sampleRate = 1.0, array $tags = [])
484
    {
485 4
        if ($this->isSampled($sampleRate, $postfix)) {
486 3
            return $this->send(
487 3
                [$metric => $value . '|d' . $postfix],
488 3
                $tags
489
            );
490
        }
491 1
        return $this;
492
    }
493
494
    /**
495
     * Sets - count the number of unique elements for a group
496
     *
497
     * @param string   $metric
498
     * @param int      $value
499
     * @param string[] $tags List of tags for this metric
500
     *
501
     * @return Client This instance
502
     */
503 2
    public function set($metric, $value, array $tags = [])
504
    {
505 2
        return $this->send(
506
            [
507 2
                $metric => $value . '|s',
508
            ],
509 2
            $tags
510
        );
511
    }
512
513
    /**
514
     * Send a event notification
515
     *
516
     * @link http://docs.datadoghq.com/guides/dogstatsd/#events
517
     *
518
     * @param string   $title     Event Title
519
     * @param string   $text      Event Text
520
     * @param array    $metadata  Set of metadata for this event:
521
     *                            - time - Assign a timestamp to the event.
522
     *                            - hostname - Assign a hostname to the event
523
     *                            - key - Assign an aggregation key to th event, to group it with some others
524
     *                            - priority - Can be 'normal' or 'low'
525
     *                            - source - Assign a source type to the event
526
     *                            - alert - Can be 'error', 'warning', 'info' or 'success'
527
     * @param string[] $tags      List of tags for this event
528
     *
529
     * @return Client This instance
530
     * @throws ConnectionException If there is a connection problem with the host
531
     */
532 7
    public function event($title, $text, array $metadata = [], array $tags = [])
533
    {
534 7
        if (!$this->dataDog) {
535 1
            return $this;
536
        }
537
538 6
        $text = str_replace(["\r", "\n"], ['', "\\n"], $text);
539 6
        $metric = sprintf('_e{%d,%d}', strlen($title), strlen($text));
540 6
        $prefix = $this->namespace ? $this->namespace . '.' : '';
541 6
        $value = sprintf('%s|%s', $prefix . $title, $text);
542
543 6
        foreach ($metadata as $key => $data) {
544 2
            if (isset($this->eventMetaData[$key])) {
545 2
                $value .= sprintf('|%s:%s', $this->eventMetaData[$key], $data);
546
            }
547
        }
548
549 6
        $value .= $this->formatTags($tags);
550
551 6
        return $this->sendMessages([
552 6
            sprintf('%s:%s', $metric, $value),
553
        ]);
554
    }
555
556
    /**
557
     * Service Checks
558
     *
559
     * @link http://docs.datadoghq.com/guides/dogstatsd/#service-checks
560
     *
561
     * @param string   $name     Name of the service
562
     * @param int      $status   digit corresponding to the status you’re reporting (OK = 0, WARNING = 1, CRITICAL = 2,
563
     *                           UNKNOWN = 3)
564
     * @param array    $metadata - time - Assign a timestamp to the service check
565
     *                           - hostname - Assign a hostname to the service check
566
     * @param string[] $tags     List of tags for this event
567
     *
568
     * @return Client This instance
569
     * @throws ConnectionException If there is a connection problem with the host
570
     */
571 6
    public function serviceCheck($name, $status, array $metadata = [], array $tags = [])
572
    {
573 6
        if (!$this->dataDog) {
574 1
            return $this;
575
        }
576
577 5
        $prefix = $this->namespace ? $this->namespace . '.' : '';
578 5
        $value = sprintf('_sc|%s|%d', $prefix . $name, $status);
579
580 5
        $applyMetadata = function ($metadata, $definition) use (&$value) {
581 5
            foreach ($metadata as $key => $data) {
582 3
                if (isset($definition[$key])) {
583 3
                    $value .= sprintf('|%s:%s', $definition[$key], $data);
584
                }
585
            }
586 5
        };
587
588 5
        $applyMetadata($metadata, $this->serviceCheckMetaData);
589 5
        $value .= $this->formatTags($tags);
590 5
        $applyMetadata($metadata, $this->serviceCheckMessage);
591
592 5
        return $this->sendMessages([
593 5
            $value,
594
        ]);
595
    }
596
597
    /**
598
     * @param float  $rate
599
     * @param string $postfix
600
     *
601
     * @return bool
602
     */
603 28
    private function isSampled($rate = 1.0, &$postfix = '')
604
    {
605 28
        if ($rate == 1.0) {
606 22
            return true;
607
        }
608 6
        if ((mt_rand() / mt_getrandmax()) <= $rate) {
609 3
            $postfix = '|@' . $rate;
610 3
            return true;
611
        }
612 3
        return false;
613
    }
614
615
    /**
616
     * @param string[] $tags A list of tags to apply to each message
617
     *
618
     * @return string
619
     */
620 45
    private function formatTags(array $tags = [])
621
    {
622 45
        if (!$this->dataDog || count($tags) === 0) {
623 30
            return '';
624
        }
625
626 17
        $result = [];
627 17
        foreach ($tags as $key => $value) {
628 17
            if (is_numeric($key)) {
629 13
                $result[] = $value;
630
            } else {
631 17
                $result[] = sprintf('%s:%s', $key, $value);
632
            }
633
        }
634
635 17
        return sprintf('|#%s', implode(',', $result));
636
    }
637
638
    /**
639
     * Send Data to StatsD Server
640
     *
641
     * @param string[] $data A list of messages to send to the server
642
     * @param string[] $tags A list of tags to apply to each message
643
     *
644
     * @return Client This instance
645
     * @throws ConnectionException If there is a connection problem with the host
646
     */
647 34
    protected function send(array $data, array $tags = [])
648
    {
649 34
        $messages = [];
650 34
        $prefix = $this->namespace ? $this->namespace . '.' : '';
651 34
        $formattedTags = $this->formatTags($this->processTags(array_merge($this->tags, $tags)));
652 34
        foreach ($data as $key => $value) {
653 34
            $messages[] = $prefix . $key . ':' . $value . $formattedTags;
654
        }
655 34
        return $this->sendMessages($messages);
656
    }
657
658
    /**
659
     * @param string[] $messages
660
     *
661
     * @return Client This instance
662
     * @throws ConnectionException If there is a connection problem with the host
663
     */
664 45
    protected function sendMessages(array $messages)
665
    {
666 45
        if (is_null($this->stream)) {
667 45
            $this->stream = new StreamWriter(
668 45
                $this->instanceId,
669 45
                $this->host,
670 45
                $this->port,
671 45
                $this->onError,
672 45
                $this->timeout
673
            );
674
        }
675 45
        $this->message = implode("\n", $messages);
676 45
        $this->written = $this->stream->write($this->message);
677
678 44
        return $this;
679
    }
680
681
    /**
682
     * Process a set of tags with some user defined processes to add custom runtime data
683
     *
684
     * @param array $tags
685
     *
686
     * @return array|mixed
687
     */
688 34
    private function processTags(array $tags)
689
    {
690 34
        foreach ($this->tagProcessors as $tagProcessor) {
691 2
            $tags = call_user_func($tagProcessor, $tags);
692
        }
693 34
        return $tags;
694
    }
695
}
696