├── .gitignore ├── src ├── Exception │ ├── ZabbixNetworkException.php │ └── ZabbixResponseException.php ├── Request │ ├── Packet.php │ └── Metric.php ├── Response.php └── ZabbixSender.php ├── composer.json ├── composer.lock ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | tags 2 | .php_cs.cache 3 | vendor/ 4 | 5 | -------------------------------------------------------------------------------- /src/Exception/ZabbixNetworkException.php: -------------------------------------------------------------------------------- 1 | packet['request'] = $request; 20 | } 21 | 22 | public function addMetric(ZabbixMetric $metric): void 23 | { 24 | $this->packet['data'][] = $metric; 25 | } 26 | 27 | public function getPacket(): array 28 | { 29 | return $this->packet; 30 | } 31 | 32 | #[\ReturnTypeWillChange] 33 | public function jsonSerialize(): array 34 | { 35 | return $this->packet; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Зарплата.ру 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Request/Metric.php: -------------------------------------------------------------------------------- 1 | itemKey = $itemKey; 35 | $this->itemValue = $itemValue; 36 | $this->hostname = gethostname(); 37 | $this->timestamp = time(); 38 | } 39 | 40 | /** 41 | * Add custom hostname to metric 42 | * 43 | * @param string $hostname 44 | */ 45 | public function withHostname(string $hostname): static 46 | { 47 | $this->hostname = $hostname; 48 | return $this; 49 | } 50 | 51 | /** 52 | * Add custom timestamp to metric 53 | * 54 | * @param int $timestamp 55 | */ 56 | public function withTimestamp(int $timestamp): static 57 | { 58 | $this->timestamp = $timestamp; 59 | return $this; 60 | } 61 | 62 | #[\ReturnTypeWillChange] 63 | public function jsonSerialize(): array 64 | { 65 | return [ 66 | 'host' => $this->hostname, 67 | 'key' => $this->itemKey, 68 | 'value' => $this->itemValue, 69 | 'clock' => $this->timestamp 70 | ]; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zabbix sender 2 | 3 | Zabbix sender it's a PHP implementation of Zabbix sender protocol. 4 | With this library you can send any metric to Zabbix server. 5 | Additional information about Zabbix sender protocol and request/response 6 | you can be found in official documentation: 7 | - https://www.zabbix.com/documentation/6.4/manual/appendix/items/trapper 8 | - https://www.zabbix.com/documentation/6.4/manual/appendix/protocols/header_datalen 9 | 10 | ### Installation 11 | ```sh 12 | composer require zarplata/zabbix-sender 13 | ``` 14 | 15 | ### Usage 16 | ```php 17 | addMetric(new ZabbixMetric('my.super.text.item.key', 'OK')); 38 | $packet->addMetric(new ZabbixMetric('my.super.int.item.key', 1)); 39 | 40 | // And finally send to Zabbix Server 41 | $sender->send($packet); 42 | ``` 43 | 44 | ### Advanced usage options 45 | 46 | Sometimes it may be necessary to provide hostname and/or timestamp 47 | of metric. By default construction: 48 | ```php 49 | withHostname('my_non_local_hostname') 61 | ->withTimestamp(662637600); //Timestamp in past 62 | ``` 63 | 64 | ### License 65 | 66 | MIT. 67 | -------------------------------------------------------------------------------- /src/Response.php: -------------------------------------------------------------------------------- 1 | parseZabbixResponse($response); 43 | } 44 | 45 | public function isSuccess(): bool 46 | { 47 | return $this->responceStatus === self::SUCCESS_RESPONSE; 48 | } 49 | 50 | public function getProcessedCount(): int 51 | { 52 | return $this->processedItems; 53 | } 54 | 55 | public function getFailedCount(): int 56 | { 57 | return $this->failedItems; 58 | } 59 | 60 | public function getTotalCount(): int 61 | { 62 | return $this->totalItems; 63 | } 64 | 65 | /** 66 | * Parse array to Response class properties 67 | * 68 | * This method takes array of values through argument 69 | * check required fields - `response` and `info` and 70 | * trying to find information about processed items 71 | * to zabbix server through reqular expression. 72 | * 73 | * @param array $response 74 | * 75 | * @return void 76 | * 77 | * @throws ZabbixResponseException 78 | */ 79 | private function parseZabbixResponse(array $response) 80 | { 81 | if (!isset($response['response'])) { 82 | throw new ZabbixResponseException( 83 | 'invalid zabbix server response, missing `response` field' 84 | ); 85 | } 86 | 87 | $this->responceStatus = $response['response']; 88 | 89 | if (!isset($response['info'])) { 90 | throw new ZabbixResponseException( 91 | 'invalid zabbix server response, missing `info` field' 92 | ); 93 | } 94 | 95 | $pattern = '/\w+: (\d+); \w+: (\d+); \w+: (\d+); [a-z ]+: (\d+\.\d+)/'; 96 | $matches = []; 97 | 98 | $matched = preg_match( 99 | $pattern, 100 | $response['info'], 101 | $matches 102 | ); 103 | 104 | switch (true) { 105 | case $matched === false: 106 | throw new ZabbixResponseException( 107 | sprintf( 108 | "can't decode info into values, preg_match error: %d", 109 | preg_last_error() 110 | ) 111 | ); 112 | 113 | case $matched === 0: 114 | throw new ZabbixResponseException( 115 | sprintf( 116 | "pattern '%s' didn't satisfy to subject '%s'", 117 | $pattern, 118 | $response['info'] 119 | ) 120 | ); 121 | 122 | default: 123 | break; 124 | } 125 | 126 | /* 127 | * $matches must contains the following values: 128 | * 129 | * $matches[0] - whole matched string for example: 130 | * processed: 2; failed: 0; total: 2; seconds spent: 0.000059 131 | * 132 | * $matches[1] - 2 (processed) 133 | * $matches[2] - 0 (failed) 134 | * $matches[3] - 2 (total) 135 | * $matches[4] - 0.000059 (secods spent) 136 | */ 137 | $this->processedItems = intval($matches[1]); 138 | $this->failedItems = intval($matches[2]); 139 | $this->totalItems = intval($matches[3]); 140 | $this->secondSpent = floatval($matches[4]); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/ZabbixSender.php: -------------------------------------------------------------------------------- 1 | serverAddress = $serverAddress; 82 | $this->serverPort = $serverPort; 83 | } 84 | 85 | /** 86 | * Configure connection parameters to Zabbix server 87 | * 88 | * @param array $options Configuration options 89 | * 90 | * @return Configurated instance 91 | */ 92 | public function configure(array $options = array()) 93 | { 94 | if (isset($options['server_address'])) { 95 | $this->serverAddress = $options['server_address']; 96 | } 97 | 98 | if (isset($options['server_port'])) { 99 | $this->serverPort = intval($options['server_port']); 100 | } 101 | 102 | if (isset($options['disable'])) { 103 | $this->disable = boolval($options['disable']); 104 | } 105 | 106 | return $this; 107 | } 108 | 109 | /** 110 | * Disable sender functionality. It may be necessary if you want 111 | * switch off send metrics but you don't want remove the code 112 | * from your project. 113 | * 114 | * @return void 115 | */ 116 | public function disable() { 117 | $this->disable = true; 118 | } 119 | 120 | /** 121 | * Enable sender functionality. This is reverse operation of `disable()` 122 | * 123 | * @return void 124 | */ 125 | public function enable() { 126 | $this->disable = false; 127 | } 128 | 129 | /** 130 | * Send packet of metrics to Zabbix server through network socket 131 | * 132 | * 133 | * @param ZabbixPacket $packet 134 | * 135 | * @return void 136 | * 137 | * @throws Exception 138 | * @throws ZabbixNetworkException 139 | */ 140 | public function send(ZabbixPacket $packet) 141 | { 142 | if ($this->disable) { 143 | return; 144 | } 145 | 146 | $payload = $this->makePayload($packet); 147 | $payloadLength = strlen($payload); 148 | 149 | $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); 150 | 151 | if (!$socket) { 152 | throw new \Exception("can't create TCP socket"); 153 | } 154 | 155 | $socketConnected = socket_connect( 156 | $socket, 157 | $this->serverAddress, 158 | $this->serverPort 159 | ); 160 | 161 | if (!$socketConnected) { 162 | throw new ZabbixNetworkException( 163 | sprintf( 164 | "can't connect to %s:%d", 165 | $this->serverAddress, 166 | $this->serverPort 167 | ) 168 | ); 169 | } 170 | 171 | $bytesCount= socket_send( 172 | $socket, 173 | $payload, 174 | $payloadLength, 175 | 0 176 | ); 177 | 178 | switch (true) { 179 | case !$bytesCount: 180 | throw new ZabbixNetworkException( 181 | sprintf( 182 | "can't send %d bytes to zabbix server %s:%d", 183 | $payloadLength, 184 | $this->serverAddress, 185 | $this->serverPort 186 | ) 187 | ); 188 | 189 | case $bytesCount != $payloadLength: 190 | throw new ZabbixNetworkException( 191 | sprintf( 192 | "incorrect count of bytes %s sended, expected: %d", 193 | $bytesCount, 194 | $payloadLength 195 | ) 196 | ); 197 | 198 | default: 199 | break; 200 | } 201 | 202 | $this->checkResponse($socket); 203 | } 204 | 205 | /** 206 | * Make payload for Zabbix server with special Zabbix header 207 | * and datalen 208 | * 209 | * https://www.zabbix.com/documentation/current/en/manual/appendix/protocols/header_datalen 210 | */ 211 | private function makePayload(ZabbixPacket $packet): string 212 | { 213 | $encodedPacket = json_encode($packet); 214 | return self::zbxCreateHeader(strlen($encodedPacket)) . $encodedPacket; 215 | } 216 | 217 | /** 218 | * Zabbix Packet Header 219 | * 220 | */ 221 | public static function zbxCreateHeader(int $plain_data_size, int|null $compressed_data_size = null): string 222 | { 223 | $flags = self::VERSION; 224 | if ($compressed_data_size === null) { 225 | $datalen = $plain_data_size; 226 | $reserved = 0; 227 | } else { 228 | $flags |= 0x02; 229 | $datalen = $compressed_data_size; 230 | $reserved = $plain_data_size; 231 | } 232 | return self::HEADER . chr($flags) . pack("VV", $datalen, $reserved); 233 | } 234 | 235 | /** 236 | * Check response from Zabbix server 237 | * 238 | * @param resource $socket 239 | * 240 | * @return void 241 | * 242 | * @throws ZabbixResponseException 243 | * @throws ZabbixNetworkException 244 | */ 245 | private function checkResponse($socket) 246 | { 247 | $responseBuffer = ""; 248 | $responseBufferLength = 2048; 249 | 250 | $bytesCount = socket_recv( 251 | $socket, 252 | $responseBuffer, 253 | $responseBufferLength, 254 | 0 255 | ); 256 | 257 | if (!$bytesCount) { 258 | throw new ZabbixNetworkException( 259 | "can't receive response from socket" 260 | ); 261 | } 262 | 263 | $responseWithoutHeader = substr( 264 | $responseBuffer, 265 | self::RESPONSE_HEADER_LENGTH 266 | ); 267 | $response = json_decode( 268 | $responseWithoutHeader, 269 | true 270 | ); 271 | 272 | switch (true) { 273 | case $response === null: 274 | case $response === false: 275 | throw new ZabbixResponseException( 276 | sprintf( 277 | "can't decode zabbix server response %s, reason: %s", 278 | $responseWithoutHeader, 279 | json_last_error_msg() 280 | ) 281 | ); 282 | 283 | default: 284 | break; 285 | } 286 | 287 | $zabbixResponse = new ZabbixResponse($response); 288 | 289 | if (!$zabbixResponse->isSuccess()) { 290 | throw new ZabbixResponseException( 291 | 'zabbix server returned non-successfull response' 292 | ); 293 | } 294 | } 295 | } 296 | --------------------------------------------------------------------------------