Completed
Pull Request — master (#12)
by Harry
02:19
created

Client::write()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4.128

Importance

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