Passed
Push — master ( c53463...c477f2 )
by Harry
02:10
created

Client::__construct()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
nc 2
nop 1
dl 0
loc 6
rs 10
c 0
b 0
f 0
ccs 4
cts 4
cp 1
crap 3
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
     * Singleton Reference
165
     *
166
     * @param  string $name Instance name
167
     *
168
     * @return Client Client instance
169
     */
170 3
    public static function instance($name = 'default')
171
    {
172 3
        if (!isset(static::$instances[$name])) {
173 3
            static::$instances[$name] = new static($name);
174
        }
175 3
        return static::$instances[$name];
176
    }
177
178
    /**
179
     * @param string $name Instance name
180
     *
181
     * @return bool true if an instance has been found and removed
182
     */
183 3
    public static function deleteInstance($name = 'default')
184
    {
185 3
        if (isset(static::$instances[$name])) {
186 2
            unset(static::$instances[$name]);
187 2
            return true;
188
        }
189 2
        return false;
190
    }
191
192
    /**
193
     * Create a new instance
194
     *
195
     * @param string|null $instanceId
196
     */
197 68
    public function __construct($instanceId = null)
198
    {
199 68
        $this->instanceId = $instanceId ?: uniqid();
200
201 68
        if (empty($this->timeout)) {
202 68
            $this->timeout = (float) ini_get('default_socket_timeout');
203
        }
204 68
    }
205
206
    /**
207
     * Get string value of instance
208
     *
209
     * @return string String representation of this instance
210
     */
211 2
    public function __toString()
212
    {
213 2
        return 'DogStatsD\Client::[' . $this->instanceId . ']';
214
    }
215
216
    /**
217
     * Initialize Connection Details
218
     *
219
     * @param array $options Configuration options
220
     *                       :host <string|ip> - host to talk to
221
     *                       :port <int> - Port to communicate with
222
     *                       :namespace <string> - Default namespace
223
     *                       :timeout <float> - Timeout in seconds
224
     *                       :onError <enum[error,exception,ignore]> - What we should do on error
225
     *                       :dataDog <bool> - Use DataDog's version of statsd (Default: true)
226
     *                       :tags <array> - List of tags to add to each message
227
     *
228
     * @return Client This instance
229
     * @throws ConfigurationException If port is invalid
230
     */
231 68
    public function configure(array $options = [])
232
    {
233
        $setOption = function ($name, $type = null) use ($options) {
234 68
            if (isset($options[$name])) {
235 23
                if (!is_null($type) && (gettype($options[$name]) != $type)) {
236 4
                    throw new ConfigurationException($this->instanceId, sprintf(
237 4
                        "Option: %s is expected to be: '%s', was: '%s'",
238 4
                        $name,
239 4
                        $type,
240 4
                        gettype($options[$name])
241
                    ));
242
                }
243 19
                $this->{$name} = $options[$name];
244
            }
245 68
        };
246
247 68
        $setOption('host', 'string');
248 68
        $setOption('port');
249 68
        $setOption('namespace', 'string');
250 68
        $setOption('timeout');
251 68
        $setOption('onError', 'string');
252 68
        $setOption('dataDog', 'boolean');
253 68
        $setOption('tags', 'array');
254
255 68
        $this->port = (int) $this->port;
256 68
        if (!$this->port || !is_numeric($this->port) || $this->port > 65535) {
257 2
            throw new ConfigurationException($this->instanceId, 'Option: Port is invalid or is out of range');
258
        }
259
260 68
        if (!in_array(
261 68
            $this->onError,
262 68
            [StreamWriter::ON_ERROR_ERROR, StreamWriter::ON_ERROR_EXCEPTION, StreamWriter::ON_ERROR_IGNORE]
263
        )) {
264 1
            throw new ConfigurationException(
265 1
                $this->instanceId,
266 1
                sprintf("Option: onError '%s' is not one of: [error,exception,ignore]", $this->onError)
267
            );
268
        }
269
270 68
        return $this;
271
    }
272
273
    /**
274
     * Get Host
275
     *
276
     * @return string Host
277
     */
278 1
    public function getHost()
279
    {
280 1
        return $this->host;
281
    }
282
283
    /**
284
     * Get Port
285
     *
286
     * @return int Port
287
     */
288 3
    public function getPort()
289
    {
290 3
        return $this->port;
291
    }
292
293
    /**
294
     * Get Namespace
295
     *
296
     * @return string Namespace
297
     */
298 1
    public function getNamespace()
299
    {
300 1
        return $this->namespace;
301
    }
302
303
    /**
304
     * Get Last Message
305
     *
306
     * @return string Last message sent to server
307
     */
308 37
    public function getLastMessage()
309
    {
310 37
        return $this->message;
311
    }
312
313
    /**
314
     * Was the last write successful
315
     *
316
     * @return bool
317
     */
318 2
    public function wasSuccessful()
319
    {
320 2
        return $this->written;
321
    }
322
323
    /**
324
     * Increment a metric
325
     *
326
     * @param string|string[] $metrics    Metric(s) to increment
327
     * @param int             $delta      Value to decrement the metric by
328
     * @param float           $sampleRate Sample rate of metric
329
     * @param string[]        $tags       List of tags for this metric
330
     *
331
     * @return Client This instance
332
     */
333 18
    public function increment($metrics, $delta = 1, $sampleRate = 1.0, array $tags = [])
334
    {
335 18
        $metrics = is_array($metrics) ? $metrics : [$metrics];
336
337 18
        if ($this->isSampled($sampleRate, $postfix)) {
338 17
            $data = [];
339 17
            foreach ($metrics as $metric) {
340 17
                $data[$metric] = $delta . '|c' . $postfix;
341
            }
342 17
            return $this->send($data, $tags);
343
        }
344 1
        return $this;
345
    }
346
347
    /**
348
     * Decrement a metric
349
     *
350
     * @param string|string[] $metrics    Metric(s) to decrement
351
     * @param int             $delta      Value to increment the metric by
352
     * @param int             $sampleRate Sample rate of metric
353
     * @param string[]        $tags       List of tags for this metric
354
     *
355
     * @return Client This instance
356
     */
357 2
    public function decrement($metrics, $delta = 1, $sampleRate = 1, array $tags = [])
358
    {
359 2
        return $this->increment($metrics, 0 - $delta, $sampleRate, $tags);
360
    }
361
362
    /**
363
     * Timing
364
     *
365
     * @param string   $metric Metric to track
366
     * @param float    $time   Time in milliseconds
367
     * @param string[] $tags   List of tags for this metric
368
     *
369
     * @return Client This instance
370
     */
371 3
    public function timing($metric, $time, array $tags = [])
372
    {
373 3
        return $this->send(
374
            [
375 3
                $metric => $time . '|ms',
376
            ],
377 3
            $tags
378
        );
379
    }
380
381
    /**
382
     * Time a function
383
     *
384
     * @param string   $metric Metric to time
385
     * @param callable $func   Function to record
386
     * @param string[] $tags   List of tags for this metric
387
     *
388
     * @return Client This instance
389
     */
390 1
    public function time($metric, callable $func, array $tags = [])
391
    {
392 1
        $timerStart = microtime(true);
393 1
        $func();
394 1
        $timerEnd = microtime(true);
395 1
        $time = round(($timerEnd - $timerStart) * 1000, 4);
396 1
        return $this->timing($metric, $time, $tags);
397
    }
398
399
    /**
400
     * Gauges
401
     *
402
     * @param string   $metric Metric to gauge
403
     * @param int      $value  Set the value of the gauge
404
     * @param string[] $tags   List of tags for this metric
405
     *
406
     * @return Client This instance
407
     */
408 2
    public function gauge($metric, $value, array $tags = [])
409
    {
410 2
        return $this->send(
411
            [
412 2
                $metric => $value . '|g',
413
            ],
414 2
            $tags
415
        );
416
    }
417
418
    /**
419
     * Histogram
420
     *
421
     * @param string   $metric     Metric to send
422
     * @param float    $value      Value to send
423
     * @param float    $sampleRate Sample rate of metric
424
     * @param string[] $tags       List of tags for this metric
425
     *
426
     * @return Client This instance
427
     */
428 4
    public function histogram($metric, $value, $sampleRate = 1.0, array $tags = [])
429
    {
430 4
        if ($this->isSampled($sampleRate, $postfix)) {
431 3
            return $this->send(
432 3
                [$metric => $value . '|h' . $postfix],
433 3
                $tags
434
            );
435
        }
436 2
        return $this;
437
    }
438
439
    /**
440
     * Sets - count the number of unique elements for a group
441
     *
442
     * @param string   $metric
443
     * @param int      $value
444
     * @param string[] $tags List of tags for this metric
445
     *
446
     * @return Client This instance
447
     */
448 2
    public function set($metric, $value, array $tags = [])
449
    {
450 2
        return $this->send(
451
            [
452 2
                $metric => $value . '|s',
453
            ],
454 2
            $tags
455
        );
456
    }
457
458
    /**
459
     * Send a event notification
460
     *
461
     * @link http://docs.datadoghq.com/guides/dogstatsd/#events
462
     *
463
     * @param string   $title     Event Title
464
     * @param string   $text      Event Text
465
     * @param array    $metadata  Set of metadata for this event:
466
     *                            - time - Assign a timestamp to the event.
467
     *                            - hostname - Assign a hostname to the event
468
     *                            - key - Assign an aggregation key to th event, to group it with some others
469
     *                            - priority - Can be 'normal' or 'low'
470
     *                            - source - Assign a source type to the event
471
     *                            - alert - Can be 'error', 'warning', 'info' or 'success'
472
     * @param string[] $tags      List of tags for this event
473
     *
474
     * @return Client This instance
475
     * @throws ConnectionException If there is a connection problem with the host
476
     */
477 7
    public function event($title, $text, array $metadata = [], array $tags = [])
478
    {
479 7
        if (!$this->dataDog) {
480 1
            return $this;
481
        }
482
483 6
        $text = str_replace(["\r", "\n"], ['', "\\n"], $text);
484 6
        $metric = sprintf('_e{%d,%d}', strlen($title), strlen($text));
485 6
        $prefix = $this->namespace ? $this->namespace . '.' : '';
486 6
        $value = sprintf('%s|%s', $prefix . $title, $text);
487
488 6
        foreach ($metadata as $key => $data) {
489 2
            if (isset($this->eventMetaData[$key])) {
490 2
                $value .= sprintf('|%s:%s', $this->eventMetaData[$key], $data);
491
            }
492
        }
493
494 6
        $value .= $this->formatTags($tags);
495
496 6
        return $this->sendMessages([
497 6
            sprintf('%s:%s', $metric, $value),
498
        ]);
499
    }
500
501
    /**
502
     * Service Checks
503
     *
504
     * @link http://docs.datadoghq.com/guides/dogstatsd/#service-checks
505
     *
506
     * @param string   $name     Name of the service
507
     * @param int      $status   digit corresponding to the status you’re reporting (OK = 0, WARNING = 1, CRITICAL = 2,
508
     *                           UNKNOWN = 3)
509
     * @param array    $metadata - time - Assign a timestamp to the service check
510
     *                           - hostname - Assign a hostname to the service check
511
     * @param string[] $tags     List of tags for this event
512
     *
513
     * @return Client This instance
514
     * @throws ConnectionException If there is a connection problem with the host
515
     */
516 6
    public function serviceCheck($name, $status, array $metadata = [], array $tags = [])
517
    {
518 6
        if (!$this->dataDog) {
519 1
            return $this;
520
        }
521
522 5
        $prefix = $this->namespace ? $this->namespace . '.' : '';
523 5
        $value = sprintf('_sc|%s|%d', $prefix . $name, $status);
524
525 5
        $applyMetadata = function ($metadata, $definition) use (&$value) {
526 5
            foreach ($metadata as $key => $data) {
527 3
                if (isset($definition[$key])) {
528 3
                    $value .= sprintf('|%s:%s', $definition[$key], $data);
529
                }
530
            }
531 5
        };
532
533 5
        $applyMetadata($metadata, $this->serviceCheckMetaData);
534 5
        $value .= $this->formatTags($tags);
535 5
        $applyMetadata($metadata, $this->serviceCheckMessage);
536
537 5
        return $this->sendMessages([
538 5
            $value,
539
        ]);
540
    }
541
542
    /**
543
     * @param float  $rate
544
     * @param string $postfix
545
     *
546
     * @return bool
547
     */
548 22
    private function isSampled($rate = 1.0, &$postfix = '')
549
    {
550 22
        if ($rate == 1.0) {
551 18
            return true;
552
        }
553 4
        if ((mt_rand() / mt_getrandmax()) <= $rate) {
554 2
            $postfix = '|@' . $rate;
555 2
            return true;
556
        }
557 3
        return false;
558
    }
559
560
    /**
561
     * @param string[] $tags A list of tags to apply to each message
562
     *
563
     * @return string
564
     */
565 38
    private function formatTags(array $tags = [])
566
    {
567 38
        if (!$this->dataDog || count($tags) === 0) {
568 27
            return '';
569
        }
570
571 12
        $result = [];
572 12
        foreach ($tags as $key => $value) {
573 12
            if (is_numeric($key)) {
574 11
                $result[] = $value;
575
            } else {
576 12
                $result[] = sprintf('%s:%s', $key, $value);
577
            }
578
        }
579
580 12
        return sprintf('|#%s', implode(',', $result));
581
    }
582
583
    /**
584
     * Send Data to StatsD Server
585
     *
586
     * @param string[] $data A list of messages to send to the server
587
     * @param string[] $tags A list of tags to apply to each message
588
     *
589
     * @return Client This instance
590
     * @throws ConnectionException If there is a connection problem with the host
591
     */
592 27
    protected function send(array $data, array $tags = [])
593
    {
594 27
        $messages = [];
595 27
        $prefix = $this->namespace ? $this->namespace . '.' : '';
596 27
        $formattedTags = $this->formatTags(array_merge($this->tags, $tags));
597 27
        foreach ($data as $key => $value) {
598 27
            $messages[] = $prefix . $key . ':' . $value . $formattedTags;
599
        }
600 27
        return $this->sendMessages($messages);
601
    }
602
603
    /**
604
     * @param string[] $messages
605
     *
606
     * @return Client This instance
607
     * @throws ConnectionException If there is a connection problem with the host
608
     */
609 38
    protected function sendMessages(array $messages)
610
    {
611 38
        if (is_null($this->stream)) {
612 38
            $this->stream = new StreamWriter(
613 38
                $this->instanceId,
614 38
                $this->host,
615 38
                $this->port,
616 38
                $this->onError,
617 38
                $this->timeout
618
            );
619
        }
620 38
        $this->message = implode("\n", $messages);
621 38
        $this->written = $this->stream->write($this->message);
622
623 37
        return $this;
624
    }
625
}
626