Completed
Pull Request — master (#13)
by Harry
02:43
created

Client::increment()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 4

Importance

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