UdpTransport   A
last analyzed

Complexity

Total Complexity 11

Size/Duplication

Total Lines 99
Duplicated Lines 0 %

Test Coverage

Coverage 88%

Importance

Changes 4
Bugs 1 Features 0
Metric Value
eloc 37
c 4
b 1
f 0
dl 0
loc 99
ccs 22
cts 25
cp 0.88
rs 10
wmc 11

3 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 16 5
A send() 0 15 3
A sendMessageInChunks() 0 35 3
1
<?php
2
declare(strict_types=1);
3
4
/*
5
 * This file is part of the php-gelf package.
6
 *
7
 * (c) Benjamin Zikarsky <http://benjamin-zikarsky.de>
8
 *
9
 * For the full copyright and license information, please view the LICENSE
10
 * file that was distributed with this source code.
11
 */
12
13
namespace Gelf\Transport;
14
15
use Gelf\MessageInterface as Message;
16
use Gelf\Encoder\CompressedJsonEncoder as DefaultEncoder;
17
use InvalidArgumentException;
18
use LogicException;
19
use RuntimeException;
20
21
/**
22
 * UdpTransport allows the transfer of GELF-messages to an compatible
23
 * GELF-UDP-backend as described in
24
 * https://github.com/Graylog2/graylog2-docs/wiki/GELF
25
 *
26
 * It can also act as a direct publisher
27
 *
28
 * @author Benjamin Zikarsky <[email protected]>
29
 */
30
class UdpTransport extends AbstractTransport
31
{
32
    private const CHUNK_GELF_ID = "\x1e\x0f";
33
    private const CHUNK_MAX_COUNT = 128; // as per GELF spec
34
    private const CHUNK_HEADER_LENGTH = 12; // GELF ID (2b), id (8b) , sequence (2b)
35
    
36
    public const CHUNK_SIZE_LAN = 8154;
37
    public const CHUNK_SIZE_WAN = 1420;
38
39
    private const DEFAULT_HOST = "127.0.0.1";
40
    private const DEFAULT_PORT = 12201;
41
    
42
    private StreamSocketClient $socketClient;
43
44
    /**
45
     * Class constructor
46
     *
47
     * @param string $host when NULL or empty DEFAULT_HOST is used
48
     * @param int $port when NULL or empty DEFAULT_PORT is used
49
     * @param int $chunkSize defaults to CHUNK_SIZE_WAN,
50
     *                          0 disables chunks completely
51
     */
52
    public function __construct(
53
        string $host = self::DEFAULT_HOST,
54
        int $port = self::DEFAULT_PORT,
55
        private int $chunkSize = self::CHUNK_SIZE_WAN
56
    ) {
57 5
        parent::__construct();
58
59
        // allow NULL-like values for fallback on default
60
        $host = $host ?: self::DEFAULT_HOST;
61
        $port = $port ?: self::DEFAULT_PORT;
62
63 5
        $this->socketClient = new StreamSocketClient('udp', $host, $port);
64 5
65
        if ($chunkSize > 0 && $chunkSize <= self::CHUNK_HEADER_LENGTH) {
66 5
            throw new InvalidArgumentException('Chunk-size has to exceed ' . self::CHUNK_HEADER_LENGTH
67 5
                . ' which is the number of bytes reserved for the chunk-header');
68
        }
69 5
    }
70
71 5
    /**
72
     * @inheritDoc
73
     */
74
    public function send(Message $message): int
75 5
    {
76
        $rawMessage = $this->getMessageEncoder()->encode($message);
77
78
        // test if we need to split the message to multiple chunks
79
        // chunkSize == 0 allows for an unlimited packet-size, and therefore
80
        // disables chunking
81
        if ($this->chunkSize && strlen($rawMessage) > $this->chunkSize) {
82
            return $this->sendMessageInChunks($rawMessage);
83
        }
84 3
85
        // send message in one packet
86 3
        $this->socketClient->write($rawMessage);
87
88
        return 1;
89
    }
90
91 3
    /**
92 2
     * Sends given string in multiple chunks
93
     */
94
    private function sendMessageInChunks(string $rawMessage): int
95
    {
96 1
        /** @var int<1, max> $length */
97
        $length = $this->chunkSize - self::CHUNK_HEADER_LENGTH;
98 1
99
        // split to chunks
100
        $chunks = str_split($rawMessage, $length);
101
102
        $numChunks = count($chunks);
0 ignored issues
show
Bug introduced by
It seems like $chunks can also be of type true; however, parameter $value of count() does only seem to accept Countable|array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

102
        $numChunks = count(/** @scrutinizer ignore-type */ $chunks);
Loading history...
103
104
        if ($numChunks > self::CHUNK_MAX_COUNT) {
105
            throw new RuntimeException(
106
                sprintf(
107
                    "Message is too big. Chunk count exceeds %d",
108
                    self::CHUNK_MAX_COUNT
109
                )
110 2
            );
111
        }
112
113 2
        // generate a random 8byte-message-id
114 2
        $messageId = substr(md5(uniqid("", true), true), 0, 8);
115
116 2
        // send chunks with a correct chunk-header
117 1
        // @link http://graylog2.org/gelf#specs
118
        foreach ($chunks as $idx => $chunk) {
119 1
            $data = self::CHUNK_GELF_ID            // GELF chunk magic bytes
120 1
                . $messageId                       // unique message id
121
                . pack('CC', $idx, $numChunks)     // sequence information
122
                . $chunk                           // chunk-data
123
            ;
124
125
            $this->socketClient->write($data);
126 1
        }
127
128
        return $numChunks;
129
    }
130
}
131