Client::serviceCheck()   A
last analyzed

Complexity

Conditions 5
Paths 3

Size

Total Lines 23
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 5

Importance

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