├── .gitattributes ├── LICENSE ├── composer.json └── src ├── BatchedDogStatsd.php ├── DogStatsd.php └── OriginDetection.php /.gitattributes: -------------------------------------------------------------------------------- 1 | /.circleci export-ignore 2 | /.github export-ignore 3 | /examples export-ignore 4 | /tests export-ignore 5 | .gitignore export-ignore 6 | .phpcs.xml export-ignore 7 | CHANGELOG.md export-ignore 8 | LICENSE-3rdparty.csv export-ignore 9 | README.md export-ignore 10 | phpunit.xml export-ignore 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Datadog 2 | Copyright (c) 2012 John Crepezzi 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | * Neither the name of Datadog nor the 13 | names of its contributors may be used to endorse or promote products 14 | derived from this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 20 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "datadog/php-datadogstatsd", 3 | "type": "library", 4 | "description": "An extremely simple PHP datadogstatsd client", 5 | "keywords": ["datadog", "monitoring", "logging", "statsd", "error-reporting", "check", "health"], 6 | "homepage": "https://www.datadoghq.com/", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Alex Corley", 11 | "email": "anthroprose@gmail.com", 12 | "role": "Developer" 13 | }, 14 | { 15 | "name": "Datadog", 16 | "email": "dev@datadoghq.com", 17 | "role": "Developer" 18 | } 19 | ], 20 | "require": { 21 | "php": ">=5.6.0", 22 | "ext-sockets": "*" 23 | }, 24 | "support": { 25 | "email": "package@datadoghq.com", 26 | "irc": "irc://irc.freenode.net/datadog", 27 | "issues": "https://github.com/DataDog/php-datadogstatsd/issues", 28 | "source": "https://github.com/DataDog/php-datadogstatsd", 29 | "chat": "https://chat.datadoghq.com/" 30 | }, 31 | "autoload": { 32 | "psr-4": { 33 | "DataDog\\": "src/" 34 | } 35 | }, 36 | "autoload-dev": { 37 | "files": [ 38 | "tests/mt_rand_function_stubs.php", 39 | "tests/socket_function_stubs.php" 40 | ], 41 | "psr-4": { 42 | "DataDog\\": "tests/" 43 | } 44 | }, 45 | "config": { 46 | "sort-packages": true 47 | }, 48 | "require-dev": { 49 | "yoast/phpunit-polyfills": "^1.0.1", 50 | "squizlabs/php_codesniffer": "^3.3", 51 | "mikey179/vfsstream": "^1.6" 52 | }, 53 | "scripts": { 54 | "fix-lint": "phpcbf", 55 | "lint": "phpcs", 56 | "test": "vendor/bin/phpunit" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/BatchedDogStatsd.php: -------------------------------------------------------------------------------- 1 | static::$maxBufferLength) { 39 | $this->flushBuffer(); 40 | } 41 | } 42 | 43 | /** 44 | * @deprecated flush_buffer will be removed in future versions in favor of flushBuffer 45 | */ 46 | public function flush_buffer() // phpcs:ignore 47 | { 48 | $this->flushBuffer(); 49 | } 50 | 51 | 52 | public function flushBuffer() 53 | { 54 | $this->flush(join("\n", static::$buffer)); 55 | static::$buffer = array(); 56 | static::$bufferLength = 0; 57 | } 58 | 59 | /** 60 | * @return int 61 | */ 62 | public static function getBufferLength() 63 | { 64 | return self::$bufferLength; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/DogStatsd.php: -------------------------------------------------------------------------------- 1 | host = isset($config['host']) 112 | ? $config['host'] : (getenv('DD_AGENT_HOST') 113 | ? getenv('DD_AGENT_HOST') : ($urlHost 114 | ? $urlHost : 'localhost')); 115 | 116 | $this->port = isset($config['port']) 117 | ? $config['port'] : (getenv('DD_DOGSTATSD_PORT') 118 | ? (int)getenv('DD_DOGSTATSD_PORT') : ($urlPort 119 | ? $urlPort : 8125)); 120 | 121 | $this->socketPath = isset($config['socket_path']) 122 | ? $config['socket_path'] : ($urlSocketPath 123 | ? $urlSocketPath : null); 124 | 125 | $this->datadogHost = isset($config['datadog_host']) ? $config['datadog_host'] : 'https://app.datadoghq.com'; 126 | 127 | $this->decimalPrecision = isset($config['decimal_precision']) ? $config['decimal_precision'] : 2; 128 | 129 | $this->globalTags = isset($config['global_tags']) ? $config['global_tags'] : array(); 130 | if (getenv('DD_ENTITY_ID')) { 131 | $this->globalTags['dd.internal.entity_id'] = getenv('DD_ENTITY_ID'); 132 | } 133 | if (getenv('DD_ENV')) { 134 | $this->globalTags['env'] = getenv('DD_ENV'); 135 | } 136 | if (getenv('DD_SERVICE')) { 137 | $this->globalTags['service'] = getenv('DD_SERVICE'); 138 | } 139 | if (getenv('DD_VERSION')) { 140 | $this->globalTags['version'] = getenv('DD_VERSION'); 141 | } 142 | // DD_EXTERNAL_ENV can be supplied by the Admission controller for origin detection. 143 | if (getenv('DD_EXTERNAL_ENV')) { 144 | $this->externalData = $this->sanitize(getenv('DD_EXTERNAL_ENV')); 145 | } 146 | 147 | $this->metricPrefix = isset($config['metric_prefix']) ? "$config[metric_prefix]." : ''; 148 | 149 | // by default the telemetry is disable 150 | $this->disable_telemetry = isset($config["disable_telemetry"]) ? $config["disable_telemetry"] : true; 151 | $transport_type = !is_null($this->socketPath) ? "uds" : "udp"; 152 | $this->telemetry_tags = $this->serializeTags( 153 | array( 154 | "client" => "php", 155 | "client_version" => self::$version, 156 | "client_transport" => $transport_type) 157 | ); 158 | 159 | $this->resetTelemetry(); 160 | 161 | $originDetection = new OriginDetection(); 162 | $originDetectionEnabled = $this->isOriginDetectionEnabled($config); 163 | $containerID = isset($config["container_id"]) ? $config["container_id"] : ""; 164 | $this->containerID = $originDetection->getContainerID($containerID, $originDetectionEnabled); 165 | } 166 | 167 | /** 168 | * For boolean environment variables if the value is 0, f or false (case insensitive) the 169 | * value is treated as false. 170 | * All other values are true. 171 | **/ 172 | private function isTrue($value) 173 | { 174 | switch (strtolower($value)) { 175 | case '0': 176 | case 'f': 177 | case 'false': 178 | return false; 179 | } 180 | 181 | return true; 182 | } 183 | 184 | private function isOriginDetectionEnabled($config) 185 | { 186 | if ((isset($config["origin_detection"]) && !$config["origin_detection"])) { 187 | return false; 188 | } 189 | 190 | if (getenv("DD_ORIGIN_DETECTION_ENABLED")) { 191 | $envVarValue = getenv("DD_ORIGIN_DETECTION_ENABLED"); 192 | return $this->isTrue($envVarValue); 193 | } 194 | 195 | // default to true 196 | return true; 197 | } 198 | 199 | /** 200 | * Sanitize the DD_EXTERNAL_ENV input to ensure it doesn't contain invalid characters 201 | * that may break the protocol. 202 | * Removing any non-printable characters and `|`. 203 | */ 204 | private function sanitize($input) 205 | { 206 | $output = ''; 207 | 208 | for ($i = 0, $len = strlen($input); $i < $len; $i++) { 209 | $char = $input[$i]; 210 | 211 | if (ctype_print($char) && $char !== '|') { 212 | $output .= $char; 213 | } 214 | } 215 | 216 | return $output; 217 | } 218 | 219 | /** 220 | * Reset the telemetry value to zero 221 | */ 222 | private function resetTelemetry() 223 | { 224 | $this->metrics_sent = 0; 225 | $this->events_sent = 0; 226 | $this->service_checks_sent = 0; 227 | $this->bytes_sent = 0; 228 | $this->bytes_dropped = 0; 229 | $this->packets_sent = 0; 230 | $this->packets_dropped = 0; 231 | } 232 | /** 233 | * Reset the telemetry value to zero 234 | */ 235 | private function flushTelemetry() 236 | { 237 | if ($this->disable_telemetry == true) { 238 | return ""; 239 | } 240 | 241 | return "\ndatadog.dogstatsd.client.metrics:{$this->metrics_sent}|c{$this->telemetry_tags}" 242 | . "\ndatadog.dogstatsd.client.events:{$this->events_sent}|c{$this->telemetry_tags}" 243 | . "\ndatadog.dogstatsd.client.service_checks:{$this->service_checks_sent}|c{$this->telemetry_tags}" 244 | . "\ndatadog.dogstatsd.client.bytes_sent:{$this->bytes_sent}|c{$this->telemetry_tags}" 245 | . "\ndatadog.dogstatsd.client.bytes_dropped:{$this->bytes_dropped}|c{$this->telemetry_tags}" 246 | . "\ndatadog.dogstatsd.client.packets_sent:{$this->packets_sent}|c{$this->telemetry_tags}" 247 | . "\ndatadog.dogstatsd.client.packets_dropped:{$this->packets_dropped}|c{$this->telemetry_tags}"; 248 | } 249 | 250 | /** 251 | * Log timing information 252 | * 253 | * @param string $stat The metric to in log timing info for. 254 | * @param float $time The elapsed time (ms) to log 255 | * @param float $sampleRate the rate (0-1) for sampling. 256 | * @param array|string $tags Key Value array of Tag => Value, or single tag as string 257 | * @return void 258 | */ 259 | public function timing($stat, $time, $sampleRate = 1.0, $tags = null) 260 | { 261 | $time = $this->normalizeValue($time); 262 | $this->send(array($stat => "$time|ms"), $sampleRate, $tags); 263 | } 264 | 265 | /** 266 | * A convenient alias for the timing function when used with micro-timing 267 | * 268 | * @param string $stat The metric name 269 | * @param float $time The elapsed time to log, IN SECONDS 270 | * @param float $sampleRate the rate (0-1) for sampling. 271 | * @param array|string $tags Key Value array of Tag => Value, or single tag as string 272 | * @return void 273 | **/ 274 | public function microtiming($stat, $time, $sampleRate = 1.0, $tags = null) 275 | { 276 | $this->timing($stat, $time * 1000, $sampleRate, $tags); 277 | } 278 | 279 | /** 280 | * Gauge 281 | * 282 | * @param string $stat The metric 283 | * @param float $value The value 284 | * @param float $sampleRate the rate (0-1) for sampling. 285 | * @param array|string $tags Key Value array of Tag => Value, or single tag as string 286 | * @return void 287 | **/ 288 | public function gauge($stat, $value, $sampleRate = 1.0, $tags = null) 289 | { 290 | $value = $this->normalizeValue($value); 291 | $this->send(array($stat => "$value|g"), $sampleRate, $tags); 292 | } 293 | 294 | /** 295 | * Histogram 296 | * 297 | * @param string $stat The metric 298 | * @param float $value The value 299 | * @param float $sampleRate the rate (0-1) for sampling. 300 | * @param array|string $tags Key Value array of Tag => Value, or single tag as string 301 | * @return void 302 | **/ 303 | public function histogram($stat, $value, $sampleRate = 1.0, $tags = null) 304 | { 305 | $value = $this->normalizeValue($value); 306 | $this->send(array($stat => "$value|h"), $sampleRate, $tags); 307 | } 308 | 309 | /** 310 | * Distribution 311 | * 312 | * @param string $stat The metric 313 | * @param float $value The value 314 | * @param float $sampleRate the rate (0-1) for sampling. 315 | * @param array|string $tags Key Value array of Tag => Value, or single tag as string 316 | * @return void 317 | **/ 318 | public function distribution($stat, $value, $sampleRate = 1.0, $tags = null) 319 | { 320 | $value = $this->normalizeValue($value); 321 | $this->send(array($stat => "$value|d"), $sampleRate, $tags); 322 | } 323 | 324 | /** 325 | * Set 326 | * 327 | * @param string $stat The metric 328 | * @param string|float $value The value 329 | * @param float $sampleRate the rate (0-1) for sampling. 330 | * @param array|string $tags Key Value array of Tag => Value, or single tag as string 331 | * @return void 332 | **/ 333 | public function set($stat, $value, $sampleRate = 1.0, $tags = null) 334 | { 335 | if (!is_string($value)) { 336 | $value = $this->normalizeValue($value); 337 | } 338 | 339 | $this->send(array($stat => "$value|s"), $sampleRate, $tags); 340 | } 341 | 342 | 343 | /** 344 | * Increments one or more stats counters 345 | * 346 | * @param string|array $stats The metric(s) to increment. 347 | * @param float $sampleRate the rate (0-1) for sampling. 348 | * @param array|string $tags Key Value array of Tag => Value, or single tag as string 349 | * @param int $value the amount to increment by (default 1) 350 | * @return void 351 | **/ 352 | public function increment($stats, $sampleRate = 1.0, $tags = null, $value = 1) 353 | { 354 | $this->updateStats($stats, $value, $sampleRate, $tags); 355 | } 356 | 357 | /** 358 | * Decrements one or more stats counters. 359 | * 360 | * @param string|array $stats The metric(s) to decrement. 361 | * @param float $sampleRate the rate (0-1) for sampling. 362 | * @param array|string $tags Key Value array of Tag => Value, or single tag as string 363 | * @param int $value the amount to decrement by (default -1) 364 | * @return void 365 | **/ 366 | public function decrement($stats, $sampleRate = 1.0, $tags = null, $value = -1) 367 | { 368 | if ($value > 0) { 369 | $value = -$value; 370 | } 371 | $this->updateStats($stats, $value, $sampleRate, $tags); 372 | } 373 | 374 | /** 375 | * Updates one or more stats counters by arbitrary amounts. 376 | * 377 | * @param string|array $stats The metric(s) to update. Should be either a string or array of metrics. 378 | * @param int $delta The amount to increment/decrement each metric by. 379 | * @param float $sampleRate the rate (0-1) for sampling. 380 | * @param array|string $tags Key Value array of Tag => Value, or single tag as string 381 | * @return void 382 | **/ 383 | public function updateStats($stats, $delta = 1, $sampleRate = 1.0, $tags = null) 384 | { 385 | $delta = $this->normalizeValue($delta); 386 | if (!is_array($stats)) { 387 | $stats = array($stats); 388 | } 389 | $data = array(); 390 | foreach ($stats as $stat) { 391 | $data[$stat] = "$delta|c"; 392 | } 393 | $this->send($data, $sampleRate, $tags); 394 | } 395 | 396 | /** 397 | * Serialize tags to StatsD protocol 398 | * 399 | * @param string|array $tags The tags to be serialize 400 | * @return string 401 | **/ 402 | private function serializeTags($tags) 403 | { 404 | $all_tags = array_merge( 405 | $this->normalizeTags($this->globalTags), 406 | $this->normalizeTags($tags) 407 | ); 408 | 409 | if (count($all_tags) === 0) { 410 | return ''; 411 | } 412 | $tag_strings = array(); 413 | foreach ($all_tags as $tag => $value) { 414 | if ($value === null) { 415 | $tag_strings[] = $tag; 416 | } elseif (is_bool($value)) { 417 | $tag_strings[] = $tag . ':' . ($value === true ? 'true' : 'false'); 418 | } else { 419 | $tag_strings[] = $tag . ':' . $value; 420 | } 421 | } 422 | return '|#' . implode(',', $tag_strings); 423 | } 424 | 425 | /** 426 | * Turns tags in any format into an array of tags 427 | * 428 | * @param mixed $tags The tags to normalize 429 | * @return array 430 | */ 431 | private function normalizeTags($tags) 432 | { 433 | if ($tags === null) { 434 | return array(); 435 | } 436 | if (is_array($tags)) { 437 | $data = array(); 438 | foreach ($tags as $tag_key => $tag_val) { 439 | if (isset($tag_val)) { 440 | $data[$tag_key] = $tag_val; 441 | } else { 442 | $data[$tag_key] = null; 443 | } 444 | } 445 | return $data; 446 | } else { 447 | $tags = explode(',', $tags); 448 | $data = array(); 449 | foreach ($tags as $tag_string) { 450 | if (false === strpos($tag_string, ':')) { 451 | $data[$tag_string] = null; 452 | } else { 453 | list($key, $value) = explode(':', $tag_string, 2); 454 | $data[$key] = $value; 455 | } 456 | } 457 | return $data; 458 | } 459 | } 460 | 461 | /** 462 | * Squirt the metrics over UDP 463 | * 464 | * @param array $data Incoming Data 465 | * @param float $sampleRate the rate (0-1) for sampling. 466 | * @param array|string $tags Key Value array of Tag => Value, or single tag as string 467 | * @return void 468 | **/ 469 | public function send($data, $sampleRate = 1.0, $tags = null) 470 | { 471 | $sampleRate = $this->normalizeValue($sampleRate); 472 | $this->metrics_sent += count($data); 473 | // sampling 474 | $sampledData = array(); 475 | if ($sampleRate < 1) { 476 | foreach ($data as $stat => $value) { 477 | if ((mt_rand() / mt_getrandmax()) <= $sampleRate) { 478 | $sampledData[$stat] = "$value|@$sampleRate"; 479 | } 480 | } 481 | } else { 482 | $sampledData = $data; 483 | } 484 | 485 | if (empty($sampledData)) { 486 | return; 487 | } 488 | 489 | foreach ($sampledData as $stat => $value) { 490 | $value .= $this->serializeTags($tags); 491 | if ($this->externalData) { 492 | $value .= "|e:{$this->externalData}"; 493 | } 494 | if ($this->containerID) { 495 | $value .= "|c:{$this->containerID}"; 496 | } 497 | $this->report("{$this->metricPrefix}$stat:$value"); 498 | } 499 | } 500 | 501 | /** 502 | * @deprecated service_check will be removed in future versions in favor of serviceCheck 503 | * 504 | * Send a custom service check status over UDP 505 | * @param string $name service check name 506 | * @param int $status service check status code (see OK, WARNING,...) 507 | * @param array|string $tags Key Value array of Tag => Value, or single tag as string 508 | * @param string $hostname hostname to associate with this service check status 509 | * @param string $message message to associate with this service check status 510 | * @param int $timestamp timestamp for the service check status (defaults to now) 511 | * @return void 512 | **/ 513 | public function service_check( // phpcs:ignore 514 | $name, 515 | $status, 516 | $tags = null, 517 | $hostname = null, 518 | $message = null, 519 | $timestamp = null 520 | ) { 521 | $this->serviceCheck($name, $status, $tags, $hostname, $message, $timestamp); 522 | } 523 | 524 | /** 525 | * Send a custom service check status over UDP 526 | * 527 | * @param string $name service check name 528 | * @param int $status service check status code (see OK, WARNING,...) 529 | * @param array|string $tags Key Value array of Tag => Value, or single tag as string 530 | * @param string $hostname hostname to associate with this service check status 531 | * @param string $message message to associate with this service check status 532 | * @param int $timestamp timestamp for the service check status (defaults to now) 533 | * @return void 534 | **/ 535 | public function serviceCheck( 536 | $name, 537 | $status, 538 | $tags = null, 539 | $hostname = null, 540 | $message = null, 541 | $timestamp = null 542 | ) { 543 | $msg = "_sc|$name|$status"; 544 | 545 | if ($timestamp !== null) { 546 | $msg .= sprintf("|d:%s", $timestamp); 547 | } 548 | if ($hostname !== null) { 549 | $msg .= sprintf("|h:%s", $hostname); 550 | } 551 | $msg .= $this->serializeTags($tags); 552 | if ($message !== null) { 553 | $msg .= sprintf('|m:%s', $this->escapeScMessage($message)); 554 | } 555 | 556 | $this->service_checks_sent += 1; 557 | $this->report($msg); 558 | } 559 | 560 | private function escapeScMessage($msg) 561 | { 562 | return str_replace("m:", "m\:", str_replace("\n", "\\n", $msg)); 563 | } 564 | 565 | public function report($message) 566 | { 567 | $this->flush($message); 568 | } 569 | 570 | public function flush($message) 571 | { 572 | $message .= $this->flushTelemetry(); 573 | 574 | // Non - Blocking UDP I/O - Use IP Addresses! 575 | if (!is_null($this->socketPath)) { 576 | $socket = socket_create(AF_UNIX, SOCK_DGRAM, 0); 577 | } elseif (filter_var($this->host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { 578 | $socket = socket_create(AF_INET6, SOCK_DGRAM, SOL_UDP); 579 | } else { 580 | $socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP); 581 | } 582 | socket_set_nonblock($socket); 583 | 584 | if (!is_null($this->socketPath)) { 585 | $res = socket_sendto($socket, $message, strlen($message), 0, $this->socketPath); 586 | } else { 587 | $res = socket_sendto($socket, $message, strlen($message), 0, $this->host, $this->port); 588 | } 589 | 590 | if ($res !== false) { 591 | $this->resetTelemetry(); 592 | $this->bytes_sent += strlen($message); 593 | $this->packets_sent += 1; 594 | } else { 595 | $this->bytes_dropped += strlen($message); 596 | $this->packets_dropped += 1; 597 | } 598 | 599 | socket_close($socket); 600 | } 601 | 602 | /** 603 | * Formats $vals array into event for submission to Datadog via UDP 604 | * 605 | * @param array $vals Optional values of the event. See 606 | * https://docs.datadoghq.com/api/?lang=bash#post-an-event for the valid keys 607 | * @return bool 608 | */ 609 | public function event($title, $vals = array()) 610 | { 611 | // Format required values title and text 612 | $text = isset($vals['text']) ? (string) $vals['text'] : ''; 613 | 614 | // Format fields into string that follows Datadog event submission via UDP standards 615 | // http://docs.datadoghq.com/guides/dogstatsd/#events 616 | $fields = ''; 617 | $fields .= ($title); 618 | $textField = ($text) ? '|' . str_replace("\n", "\\n", $text) : '|'; 619 | $fields .= $textField; 620 | $fields .= (isset($vals['date_happened'])) ? '|d:' . ((string) $vals['date_happened']) : ''; 621 | $fields .= (isset($vals['hostname'])) ? '|h:' . ((string) $vals['hostname']) : ''; 622 | $fields .= (isset($vals['aggregation_key'])) ? '|k:' . ((string) $vals['aggregation_key']) : ''; 623 | $fields .= (isset($vals['priority'])) ? '|p:' . ((string) $vals['priority']) : ''; 624 | $fields .= (isset($vals['source_type_name'])) ? '|s:' . ((string) $vals['source_type_name']) : ''; 625 | $fields .= (isset($vals['alert_type'])) ? '|t:' . ((string) $vals['alert_type']) : ''; 626 | $fields .= (isset($vals['tags'])) ? $this->serializeTags($vals['tags']) : ''; 627 | 628 | $title_length = strlen($title); 629 | $text_length = strlen($textField) - 1; 630 | 631 | $this->events_sent += 1; 632 | $this->report('_e{' . $title_length . ',' . $text_length . '}:' . $fields); 633 | 634 | return true; 635 | } 636 | 637 | /** 638 | * Normalize the value witout locale consideration before queuing the metric for sending 639 | * 640 | * @param float $value The value to normalize 641 | * 642 | * @return string Formatted value 643 | */ 644 | private function normalizeValue($value) 645 | { 646 | // Controlls the way things are converted to a string. 647 | // Otherwise localization settings impact float to string conversion (e.x 1.3 -> 1,3 and 10000 => 10,000) 648 | 649 | return rtrim(rtrim(number_format((float) $value, $this->decimalPrecision, '.', ''), "0"), "."); 650 | } 651 | } 652 | -------------------------------------------------------------------------------- /src/OriginDetection.php: -------------------------------------------------------------------------------- 1 | "/proc/self/cgroup", 23 | 24 | // selfMountinfo is the path to the mountinfo path where we can find the container id in case 25 | // cgroup namespace is preventing the use of /proc/self/cgroup 26 | "selfMountInfoPath" => "/proc/self/mountinfo", 27 | 28 | // defaultCgroupMountPath is the default path to the cgroup mount point. 29 | "defaultCgroupMountPath" => "/sys/fs/cgroup", 30 | ); 31 | } 32 | 33 | public function isHostCgroupNamespace() 34 | { 35 | // phpcs:disable 36 | $stat = @stat("/proc/self/ns/cgroup"); 37 | // phpcs:enable 38 | if (!$stat) { 39 | return false; 40 | } 41 | $inode = isset($stat['ino']) ? $stat['ino'] : null; 42 | return $inode === self::HOSTCGROUPNAMESPACEINODE; 43 | } 44 | 45 | // parseCgroupNodePath parses /proc/self/cgroup and returns a map of controller to its associated cgroup node path. 46 | public function parseCgroupNodePath($lines) 47 | { 48 | $res = []; 49 | 50 | foreach (explode("\n", $lines) as $line) { 51 | $tokens = explode(':', $line); 52 | if (count($tokens) !== 3) { 53 | continue; 54 | } 55 | 56 | if ($tokens[1] === self::CGROUPV1BASECONTROLLER || $tokens[1] === '') { 57 | $res[$tokens[1]] = $tokens[2]; 58 | } 59 | } 60 | 61 | return $res; 62 | } 63 | 64 | public function getCgroupInode($cgroupMountPath, $procSelfCgroupPath) 65 | { 66 | $cgroupControllersPaths = $this->parseCgroupNodePath(file_get_contents($procSelfCgroupPath)); 67 | 68 | foreach ([self::CGROUPV1BASECONTROLLER , ''] as $controller) { 69 | if (!isset($cgroupControllersPaths[$controller])) { 70 | continue; 71 | } 72 | 73 | $segments = array(rtrim($cgroupMountPath, '/'), 74 | trim($controller, '/'), 75 | ltrim($cgroupControllersPaths[$controller], '/')); 76 | $path = implode("/", array_filter($segments, function ($segment) { 77 | return $segment !== null && $segment !== ''; 78 | })); 79 | $inode = $this->inodeForPath($path); 80 | if ($inode !== '') { 81 | return $inode; 82 | } 83 | } 84 | 85 | return ''; 86 | } 87 | 88 | // inodeForPath returns the inode number for the file at the given path. 89 | // The number is prefixed by 'in-' so the agent can identify this as an 90 | // inode and not a container id. 91 | private function inodeForPath($path) 92 | { 93 | // phpcs:disable 94 | $stat = @stat($path); 95 | // phpcs:enable 96 | if (!$stat || !isset($stat['ino'])) { 97 | return ""; 98 | } 99 | 100 | return 'in-' . $stat['ino']; 101 | } 102 | 103 | // parseContainerID finds the first container ID reading from $handle and returns it. 104 | private function parseContainerID($handle) 105 | { 106 | $expLine = '/^\d+:[^:]*:(.+)$/'; 107 | $uuidSource = "[0-9a-f]{8}[-_][0-9a-f]{4}[-_][0-9a-f]{4}[-_][0-9a-f]{4}[-_][0-9a-f]{12}"; 108 | $containerSource = "[0-9a-f]{64}"; 109 | $taskSource = "[0-9a-f]{32}-\\d+"; 110 | 111 | $expContainerID = '/(' . $uuidSource . '|' . $containerSource . '|' . $taskSource . ')(?:.scope)?$/'; 112 | 113 | while (($line = fgets($handle)) !== false) { 114 | if (preg_match($expLine, $line, $matches)) { 115 | if (count($matches) != 2) { 116 | continue; 117 | } 118 | 119 | if (preg_match($expContainerID, $matches[1], $idMatch) && count($idMatch) == 2) { 120 | return $idMatch[1]; 121 | } 122 | } 123 | } 124 | 125 | return ""; 126 | } 127 | 128 | // readContainerID attempts to return the container ID from the provided file path or empty on failure. 129 | public function readContainerID($fpath) 130 | { 131 | // phpcs:disable 132 | $handle = @fopen($fpath, 'r'); 133 | // phpcs:enable 134 | if (!$handle) { 135 | return ""; 136 | } 137 | 138 | $id = $this->parseContainerID($handle); 139 | 140 | fclose($handle); 141 | 142 | return $id; 143 | } 144 | 145 | // Parsing /proc/self/mountinfo is not always reliable in Kubernetes+containerd (at least) 146 | // We're still trying to use it as it may help in some cgroupv2 configurations (Docker, ECS, raw containerd) 147 | private function parseMountInfo($handle) 148 | { 149 | $containerRegexpStr = '([0-9a-f]{64})|([0-9a-f]{32}-\\d+)|([0-9a-f]{8}(-[0-9a-f]{4}){4}$)'; 150 | $cIDMountInfoRegexp = '#.*/([^\s/]+)/(' . $containerRegexpStr . ')/[\S]*hostname#'; 151 | 152 | while (($line = fgets($handle)) !== false) { 153 | preg_match_all($cIDMountInfoRegexp, $line, $allMatches, PREG_SET_ORDER); 154 | if (empty($allMatches)) { 155 | continue; 156 | } 157 | 158 | // Get the rightmost match 159 | $matches = $allMatches[count($allMatches) - 1]; 160 | 161 | // Ensure the first capture group isn't the sandbox prefix 162 | $containerdSandboxPrefix = "sandboxes"; 163 | if (count($matches) > 0 && $matches[1] !== $containerdSandboxPrefix) { 164 | return $matches[2]; 165 | } 166 | } 167 | 168 | return ""; 169 | } 170 | 171 | public function readMountInfo($path) 172 | { 173 | // phpcs:disable 174 | $handle = @fopen($path, 'r'); 175 | // phpcs:enable 176 | if (!$handle) { 177 | return ""; 178 | } 179 | 180 | $info = $this->parseMountInfo($handle); 181 | 182 | fclose($handle); 183 | 184 | return $info; 185 | } 186 | 187 | // getContainerID attempts to retrieve the container Id in the following order: 188 | // 1. If the user provides a container ID via the configuration, this is used. 189 | // 2. Reading the container ID from /proc/self/cgroup. Works with cgroup v1. 190 | // 3. Read the container Id from /proc/self/mountinfo. Sometimes, depending on container runtimes or 191 | // mount settings this can contain a container id. 192 | // 4. Read the inode from /proc/self/cgroup. 193 | public function getContainerID($userProvidedId, $cgroupFallback) 194 | { 195 | if ($userProvidedId != "") { 196 | return $userProvidedId; 197 | } 198 | 199 | if ($cgroupFallback) { 200 | $paths = $this->getFilepaths(); 201 | $containerID = $this->readContainerID($paths["cgroupPath"]); 202 | if ($containerID != "") { 203 | return $containerID; 204 | } 205 | 206 | $containerID = $this->readMountInfo($paths["selfMountInfoPath"]); 207 | if ($containerID != "") { 208 | return $containerID; 209 | } 210 | 211 | if ($this->isHostCgroupNamespace()) { 212 | return ""; 213 | } 214 | 215 | $containerID = $this->getCgroupInode($paths["defaultCgroupMountPath"], $paths["cgroupPath"]); 216 | return $containerID; 217 | } 218 | 219 | return ""; 220 | } 221 | } 222 | --------------------------------------------------------------------------------