├── .gitignore ├── LICENSE ├── README.md ├── Tideways.php └── composer.json /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | 3 | Version 2.0, January 2004 4 | 5 | http://www.apache.org/licenses/ 6 | 7 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 8 | 9 | 1. Definitions. 10 | 11 | "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 16 | 17 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. 18 | 19 | "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. 20 | 21 | "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. 22 | 23 | "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). 24 | 25 | "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. 26 | 27 | "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." 28 | 29 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 30 | 31 | 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 32 | 33 | 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 34 | 35 | 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: 36 | 37 | You must give any other recipients of the Work or Derivative Works a copy of this License; and 38 | You must cause any modified files to carry prominent notices stating that You changed the files; and 39 | You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and 40 | If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. 41 | 42 | You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 43 | 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 44 | 45 | 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 46 | 47 | 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 48 | 49 | 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 50 | 51 | 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. 52 | 53 | END OF TERMS AND CONDITIONS 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > **IMPORTANT NOTICE** This library is not needed anymore, because the `Tideways\Profiler` class is now provided directly by the `tideways` PHP extension. [Follow the Installation Guides](https://support.tideways.com/documentation/setup/index.html) to install the PHP extension. 2 | 3 | # Tideways PHP Library 4 | 5 | PHP Client Library for [Tideways PHP Profiler](https://tideways.io) platform. 6 | 7 | Not using Tideways as a service and looking for a modern XHProf compatible Profiler? 8 | See [Tideways PHP Extension](https://github.com/tideways/php-profiler-extension). 9 | 10 | This repository only contains a snapshotted, compiled version of the PHP code base. 11 | 12 | You can use it as a composer package, if you are using Tideways with HHVM, but 13 | otherwise it is not needed to interact with this repository/package. For PHP 14 | this source code is embedded into the DEB, RPM or Tarball downloads. 15 | 16 | ## License (Apache2) 17 | 18 | Copyright 2014-2016 Tideways GmbH 19 | 20 | Licensed under the Apache License, Version 2.0 (the "License"); 21 | you may not use this file except in compliance with the License. 22 | You may obtain a copy of the License at 23 | 24 | http://www.apache.org/licenses/LICENSE-2.0 25 | 26 | Unless required by applicable law or agreed to in writing, software 27 | distributed under the License is distributed on an "AS IS" BASIS, 28 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 29 | See the License for the specific language governing permissions and 30 | limitations under the License. 31 | -------------------------------------------------------------------------------- /Tideways.php: -------------------------------------------------------------------------------- 1 | 66 | */ 67 | public abstract function annotate(array $annotations); 68 | } 69 | 70 | namespace Tideways\Traces; 71 | 72 | class NullSpan extends Span 73 | { 74 | public function createSpan($name = null) 75 | { 76 | return $this; 77 | } 78 | 79 | public function getSpans() 80 | { 81 | return array(); 82 | } 83 | 84 | public function getId() 85 | { 86 | return 0; 87 | } 88 | 89 | /** 90 | * Record start of timer in microseconds. 91 | * 92 | * If timer is already running, don't record another start. 93 | */ 94 | public function startTimer() 95 | { 96 | } 97 | 98 | /** 99 | * Record stop of timer in microseconds. 100 | * 101 | * If timer is not running, don't record. 102 | */ 103 | public function stopTimer() 104 | { 105 | } 106 | 107 | /** 108 | * Annotate span with metadata. 109 | * 110 | * @param array 111 | */ 112 | public function annotate(array $annotations) 113 | { 114 | } 115 | } 116 | 117 | namespace Tideways\Traces; 118 | 119 | use Tideways\Profiler; 120 | 121 | /** 122 | * When Tideways PHP extension is not installed the span API 123 | * is handled in memory. 124 | */ 125 | class PhpSpan extends Span 126 | { 127 | const ID = 'i'; 128 | const NAME = 'n'; 129 | const STARTS = 'b'; 130 | const STOPS = 'e'; 131 | const ANNOTATIONS = 'a'; 132 | 133 | /** 134 | * @var array 135 | */ 136 | private static $spans = array(); 137 | private static $startTime = false; 138 | 139 | /** 140 | * @var bool 141 | */ 142 | private $timerRunning = false; 143 | 144 | /** 145 | * @var int 146 | */ 147 | private $idx; 148 | 149 | static public function clear() 150 | { 151 | self::$spans = array(); 152 | self::$startTime = microtime(true); 153 | } 154 | 155 | public function createSpan($name = null) 156 | { 157 | $idx = count(self::$spans); 158 | return new self($idx, $name); 159 | } 160 | 161 | /** 162 | * @return int 163 | */ 164 | private static function currentDuration() 165 | { 166 | return intval(round((microtime(true) - self::$startTime) * 1000000)); 167 | } 168 | 169 | public function getSpans() 170 | { 171 | return self::$spans; 172 | } 173 | 174 | public function __construct($idx, $name = null) 175 | { 176 | $this->idx = $idx; 177 | self::$spans[$idx] = array( 178 | self::STARTS => array(), 179 | self::STOPS => array(), 180 | self::ANNOTATIONS => array(), 181 | ); 182 | if ($name) { 183 | self::$spans[$idx][self::NAME] = $name; 184 | } 185 | } 186 | 187 | public function getId() 188 | { 189 | if (!isset(self::$spans[$this->idx][self::ID])) { 190 | self::$spans[$this->idx][self::ID] = \Tideways\Profiler::generateRandomId(); 191 | } 192 | 193 | return self::$spans[$this->idx][self::ID]; 194 | } 195 | 196 | public function startTimer() 197 | { 198 | if ($this->timerRunning) { 199 | return; 200 | } 201 | 202 | self::$spans[$this->idx][self::STARTS][] = self::currentDuration(); 203 | $this->timerRunning = true; 204 | } 205 | 206 | public function stopTimer() 207 | { 208 | if (!$this->timerRunning) { 209 | return; 210 | } 211 | 212 | self::$spans[$this->idx][self::STOPS][] = self::currentDuration(); 213 | $this->timerRunning = false; 214 | } 215 | 216 | public function annotate(array $annotations) 217 | { 218 | foreach ($annotations as $name => $value) { 219 | if (!is_scalar($value)) { 220 | continue; 221 | } 222 | 223 | self::$spans[$this->idx][self::ANNOTATIONS][$name] = (string)$value; 224 | } 225 | } 226 | } 227 | 228 | namespace Tideways\Traces; 229 | 230 | class TwExtensionSpan extends Span 231 | { 232 | /** 233 | * @var int 234 | */ 235 | private $idx; 236 | 237 | public function createSpan($name = null) 238 | { 239 | return new self(tideways_span_create($name)); 240 | } 241 | 242 | public function getSpans() 243 | { 244 | return tideways_get_spans(); 245 | } 246 | 247 | public function __construct($idx) 248 | { 249 | $this->idx = $idx; 250 | } 251 | 252 | /** 253 | * 32/64 bit random integer. 254 | * 255 | * @return int 256 | */ 257 | public function getId() 258 | { 259 | return $this->idx; 260 | } 261 | 262 | /** 263 | * Record start of timer in microseconds. 264 | * 265 | * If timer is already running, don't record another start. 266 | */ 267 | public function startTimer() 268 | { 269 | tideways_span_timer_start($this->idx); 270 | } 271 | 272 | /** 273 | * Record stop of timer in microseconds. 274 | * 275 | * If timer is not running, don't record. 276 | */ 277 | public function stopTimer() 278 | { 279 | tideways_span_timer_stop($this->idx); 280 | } 281 | 282 | /** 283 | * Annotate span with metadata. 284 | * 285 | * @param array 286 | */ 287 | public function annotate(array $annotations) 288 | { 289 | tideways_span_annotate($this->idx, $annotations); 290 | } 291 | } 292 | 293 | namespace Tideways\Profiler; 294 | 295 | /** 296 | * Low-level abstraction for storage of profiling data. 297 | */ 298 | interface Backend 299 | { 300 | public function socketStore(array $trace); 301 | public function socketStoreMeasurement(array $measurement); 302 | public function udpStore(array $trace); 303 | } 304 | 305 | namespace Tideways\Profiler; 306 | 307 | class NetworkBackend implements Backend 308 | { 309 | /** 310 | * Old v1 type profile format. 311 | */ 312 | const TYPE_PROFILE = 'profile'; 313 | /** 314 | * v2 type traces 315 | */ 316 | const TYPE_TRACE = 'trace'; 317 | /** 318 | * new format for all types 319 | */ 320 | const TYPE_T2 = 't2'; 321 | 322 | private $socketFile; 323 | private $udp; 324 | 325 | public function __construct($socketFile = "unix:///var/run/tideways/tidewaysd.sock", $udp = "127.0.0.1:8135") 326 | { 327 | $this->socketFile = $socketFile; 328 | $this->udp = $udp; 329 | } 330 | 331 | /** 332 | * To avoid user apps messing up socket errors that Tideways can produce 333 | * when the daemon is not reachable, this error handler is used 334 | * wrapped around daemons to guard user apps from erroring. 335 | */ 336 | public static function ignoreErrorsHandler($errno, $errstr, $errfile, $errline) 337 | { 338 | // ignore all errors! 339 | } 340 | 341 | public function socketStore(array $trace) 342 | { 343 | if (!function_exists('json_encode')) { 344 | \Tideways\Profiler::log(1, "ext/json must be installed and activated to use Tideways."); 345 | return; 346 | } 347 | 348 | set_error_handler(array(__CLASS__, "ignoreErrorsHandler")); 349 | $fp = stream_socket_client($this->socketFile); 350 | 351 | if ($fp == false) { 352 | \Tideways\Profiler::log(1, "Cannot connect to socket for storing trace."); 353 | restore_error_handler(); 354 | return; 355 | } 356 | 357 | $flags = 0; 358 | if (defined('JSON_PARTIAL_OUTPUT_ON_ERROR')) { 359 | $flags = constant('JSON_PARTIAL_OUTPUT_ON_ERROR'); 360 | } 361 | 362 | $payload = json_encode(array('type' => self::TYPE_TRACE, 'payload' => $trace), $flags); 363 | 364 | $timeout = (int)ini_get('tideways.timeout'); 365 | 366 | // We always enforce a timeout, even when the user configures 367 | // tideways.timeout=0 manually 368 | if (!$timeout) { 369 | $timeout = 10000; 370 | } 371 | 372 | if ($trace['keep']) { 373 | // as a dev trace we collect more data and the developer can be 374 | // waiting a little longer to make sure the socket gets everything. 375 | $timeout *= 10; 376 | } 377 | 378 | stream_set_timeout($fp, 0, $timeout); // 10 milliseconds max 379 | 380 | if (fwrite($fp, $payload) < strlen($payload)) { 381 | \Tideways\Profiler::log(1, "Could not write payload to socket."); 382 | } 383 | fclose($fp); 384 | restore_error_handler(); 385 | \Tideways\Profiler::log(3, "Sent trace to socket."); 386 | } 387 | 388 | public function socketStoreMeasurement(array $measurement) 389 | { 390 | if (!function_exists('json_encode')) { 391 | \Tideways\Profiler::log(1, "ext/json must be installed and activated to use Tideways."); 392 | return; 393 | } 394 | 395 | set_error_handler(array(__CLASS__, "ignoreErrorsHandler")); 396 | $fp = stream_socket_client($this->socketFile); 397 | 398 | if ($fp == false) { 399 | \Tideways\Profiler::log(1, "Cannot connect to socke for recording measurement."); 400 | restore_error_handler(); 401 | return; 402 | } 403 | 404 | $spans = array( 405 | array( 406 | 'ts' => $measurement['spans'][0]['b'][0], 407 | 'd' => $measurement['spans'][0]['e'][0] - $measurement['spans'][0]['b'][0], 408 | 'a' => array( 409 | 'tw.key' => $measurement['apiKey'], 410 | 'tw.s' => $measurement['service'], 411 | 'tw.tx' => $measurement['tx'], 412 | 'php.memory' => $measurement['spans'][0]['a']['mem'], 413 | ), 414 | 'n' => 'php', 415 | ) 416 | ); 417 | 418 | $payload = json_encode(array('type' => self::TYPE_T2, 'payload' => $spans)); 419 | // Golang is very strict about json types. 420 | $payload = str_replace('"a":[]', '"a":{}', $payload); 421 | 422 | stream_set_timeout($fp, 0, 200); 423 | if (fwrite($fp, $payload) < strlen($payload)) { 424 | \Tideways\Profiler::log(1, "Could not write payload to socket."); 425 | } 426 | fclose($fp); 427 | restore_error_handler(); 428 | \Tideways\Profiler::log(3, "Sent trace to socket."); 429 | } 430 | 431 | public function udpStore(array $trace) 432 | { 433 | if (!function_exists('json_encode')) { 434 | \Tideways\Profiler::log(1, "ext/json must be installed and activated to use Tideways."); 435 | return; 436 | } 437 | 438 | set_error_handler(array(__CLASS__, "ignoreErrorsHandler")); 439 | $fp = stream_socket_client("udp://" . $this->udp); 440 | 441 | if ($fp == false) { 442 | \Tideways\Profiler::log(1, "Cannot connect to UDP port for storing trace."); 443 | restore_error_handler(); 444 | return; 445 | } 446 | 447 | unset($trace['id']); 448 | 449 | $payload = json_encode($trace); 450 | // Golang is very strict about json types. 451 | $payload = str_replace('"a":[]', '"a":{}', $payload); 452 | 453 | stream_set_timeout($fp, 0, 200); 454 | if (fwrite($fp, $payload) < strlen($payload)) { 455 | \Tideways\Profiler::log(1, "Could not write payload to UDP port."); 456 | } 457 | fclose($fp); 458 | restore_error_handler(); 459 | \Tideways\Profiler::log(3, "Sent trace to UDP port."); 460 | } 461 | } 462 | 463 | namespace Tideways\Profiler; 464 | 465 | /** 466 | * Convert a Backtrace to a String like {@see Exception::getTraceAsString()} would do. 467 | */ 468 | class BacktraceConverter 469 | { 470 | static public function convertToString(array $backtrace) 471 | { 472 | $trace = ''; 473 | 474 | foreach ($backtrace as $k => $v) { 475 | if (!isset($v['function'])) { 476 | continue; 477 | } 478 | 479 | if (!isset($v['file'])) { 480 | $v['file'] = ''; 481 | } 482 | 483 | if (!isset($v['line'])) { 484 | $v['line'] = ''; 485 | } 486 | 487 | $args = ''; 488 | if (isset($v['args'])) { 489 | $args = implode(', ', array_map(function ($arg) { 490 | return (is_object($arg)) ? get_class($arg) : gettype($arg); 491 | }, $v['args'])); 492 | } 493 | 494 | $trace .= '#' . ($k) . ' '; 495 | if (isset($v['file'])) { 496 | $trace .= $v['file'] . '(' . $v['line'] . '): '; 497 | } 498 | 499 | if (isset($v['class'])) { 500 | $trace .= $v['class'] . '->'; 501 | } 502 | 503 | $trace .= $v['function'] . '(' . $args .')' . "\n"; 504 | } 505 | 506 | return $trace; 507 | } 508 | } 509 | 510 | namespace Tideways; 511 | 512 | /** 513 | * Tideways PHP API 514 | * 515 | * Contains all methods to gather measurements and profile data with 516 | * Xhprof and send to local Profiler Collector Daemon. 517 | * 518 | * This class is intentionally monolithic and static to allow 519 | * users to easily copy it into their projects or auto-prepend PHP 520 | * scripts. 521 | * 522 | * @example 523 | * 524 | * Tideways\Profiler::start($apiKey); 525 | * Tideways\Profiler::setTransactionName("my tx name"); 526 | * 527 | * Calling the {@link stop()} method is not necessary as it is 528 | * called automatically from a shutdown handler, if you are timing 529 | * worker processes however it is necessary: 530 | * 531 | * Tideways\Profiler::stop(); 532 | * 533 | * The method {@link setTransactionName} is required, failing to call 534 | * it will result in discarding of the data. 535 | */ 536 | class Profiler 537 | { 538 | const MODE_DISABLED = 0; 539 | const MODE_NONE = 0; 540 | const MODE_BASIC = 1; 541 | const MODE_PROFILING = 2; 542 | const MODE_TRACING = 4; 543 | const MODE_FULL = 6; 544 | 545 | const EXTENSION_NONE = 0; 546 | const EXTENSION_XHPROF = 1; 547 | const EXTENSION_TIDEWAYS = 2; 548 | 549 | const EXT_FATAL = 1; 550 | const EXT_EXCEPTION = 4; 551 | const EXT_TRANSACTION_NAME = 8; 552 | 553 | const FRAMEWORK_ZEND_FRAMEWORK1 = 'zend1'; 554 | const FRAMEWORK_ZEND_FRAMEWORK2 = 'zend2'; 555 | const FRAMEWORK_SYMFONY2_COMPONENT = 'symfony2c'; 556 | const FRAMEWORK_SYMFONY2_FRAMEWORK = 'symfony2'; 557 | const FRAMEWORK_OXID = 'oxid'; 558 | const FRAMEWORK_OXID6 = 'oxid6'; 559 | const FRAMEWORK_SHOPWARE = 'shopware'; 560 | const FRAMEWORK_WORDPRESS = 'wordpress'; 561 | const FRAMEWORK_LARAVEL = 'laravel'; 562 | const FRAMEWORK_MAGENTO = 'magento'; 563 | const FRAMEWORK_MAGENTO2 = 'magento2'; 564 | const FRAMEWORK_PRESTA16 = 'presta16'; 565 | const FRAMEWORK_DRUPAL8 = 'drupal8'; 566 | const FRAMEWORK_TYPO3 = 'typo3'; 567 | const FRAMEWORK_FLOW = 'flow'; 568 | const FRAMEWORK_FLOW4 = 'flow4'; 569 | const FRAMEWORK_CAKE2 = 'cake2'; 570 | const FRAMEWORK_CAKE3 = 'cake3'; 571 | const FRAMEWORK_YII = 'yii'; 572 | const FRAMEWORK_YII2 = 'yii2'; 573 | 574 | /** 575 | * Default XHProf/Tideways hierachical profiling options. 576 | */ 577 | private static $defaultOptions = array( 578 | 'ignored_functions' => array( 579 | 'call_user_func', 580 | 'call_user_func_array', 581 | 'array_filter', 582 | 'array_map', 583 | 'array_reduce', 584 | 'array_walk', 585 | 'array_walk_recursive', 586 | 'Symfony\Component\DependencyInjection\Container::get', 587 | ), 588 | 'transaction_function' => null, 589 | 'exception_function' => null, 590 | 'watches' => array(), 591 | 'callbacks' => array(), 592 | 'framework' => null, 593 | ); 594 | 595 | private static $trace; 596 | private static $currentRootSpan; 597 | private static $shutdownRegistered = false; 598 | private static $error = false; 599 | private static $mode = self::MODE_DISABLED; 600 | private static $backend; 601 | private static $extension = self::EXTENSION_NONE; 602 | private static $logLevel = 0; 603 | 604 | public static function setBackend(Profiler\Backend $backend = null) 605 | { 606 | self::$backend = $backend; 607 | } 608 | 609 | public static function detectExceptionFunction($function) 610 | { 611 | self::$defaultOptions['exception_function'] = $function; 612 | } 613 | 614 | /** 615 | * Instruct Tideways Profiler to automatically detect transaction names during profiling. 616 | * 617 | * @param string $function - A transaction function name 618 | */ 619 | public static function detectFrameworkTransaction($function) 620 | { 621 | self::detectFramework($function); 622 | } 623 | 624 | /** 625 | * Configure detecting framework transactions and ignoring unnecessary layer calls. 626 | * 627 | * If the framework is not from the list of known frameworks it is assumed to 628 | * be a function name that is the transaction function. 629 | * 630 | * @param string $framework 631 | */ 632 | public static function detectFramework($framework) 633 | { 634 | self::$defaultOptions['framework'] = $framework; 635 | $cli = (php_sapi_name() === 'cli'); 636 | 637 | switch ($framework) { 638 | case self::FRAMEWORK_ZEND_FRAMEWORK1: 639 | self::$defaultOptions['transaction_function'] = 'Zend_Controller_Action::dispatch'; 640 | self::$defaultOptions['exception_function'] = 'Zend_Controller_Response_Abstract::setException'; 641 | break; 642 | 643 | case self::FRAMEWORK_ZEND_FRAMEWORK2: 644 | self::$defaultOptions['transaction_function'] = 'Zend\\Mvc\\Controller\\ControllerManager::get'; 645 | break; 646 | 647 | case self::FRAMEWORK_SYMFONY2_COMPONENT: 648 | self::$defaultOptions['transaction_function'] = $cli 649 | ? 'Symfony\Component\Console\Application::find' 650 | : 'Symfony\Component\HttpKernel\Controller\ControllerResolver::createController'; 651 | self::$defaultOptions['exception_function'] = $cli 652 | ? 'Symfony\Component\Console\Application::renderException' 653 | : 'Symfony\Component\HttpKernel\HttpKernel::handleException'; 654 | break; 655 | 656 | case self::FRAMEWORK_SYMFONY2_FRAMEWORK: 657 | self::$defaultOptions['transaction_function'] = $cli 658 | ? 'Symfony\Component\Console\Application::find' 659 | : 'Symfony\Bundle\FrameworkBundle\Controller\ControllerResolver::createController'; 660 | self::$defaultOptions['exception_function'] = $cli 661 | ? 'Symfony\Component\Console\Application::renderException' 662 | : 'Symfony\Component\HttpKernel\HttpKernel::handleException'; 663 | break; 664 | 665 | case self::FRAMEWORK_OXID: 666 | self::$defaultOptions['transaction_function'] = 'oxView::setClassName'; 667 | self::$defaultOptions['exception_function'] = 'oxShopControl::_handleBaseException'; 668 | break; 669 | 670 | case self::FRAMEWORK_OXID6: 671 | self::$defaultOptions['transaction_function'] = 'OxidEsales\EshopCommunity\Core\Controller\BaseController::setClassKey'; 672 | self::$defaultOptions['exception_function'] = 'OxidEsales\EshopCommunity\Core\ShopControl::logException'; 673 | break; 674 | 675 | case self::FRAMEWORK_SHOPWARE: 676 | self::$defaultOptions['transaction_function'] = $cli 677 | ? 'Symfony\Component\Console\Application::find' 678 | : 'Enlight_Controller_Action::dispatch'; 679 | self::$defaultOptions['exception_function'] = $cli 680 | ? 'Symfony\Component\Console\Application::renderException' 681 | : 'Zend_Controller_Response_Abstract::setException'; 682 | break; 683 | 684 | case self::FRAMEWORK_WORDPRESS: 685 | self::$defaultOptions['transaction_function'] = 'get_query_template'; 686 | break; 687 | 688 | case self::FRAMEWORK_LARAVEL: 689 | self::$defaultOptions['transaction_function'] = $cli 690 | ? 'Symfony\Component\Console\Application::find' 691 | : 'Illuminate\Routing\Controller::callAction'; 692 | self::$defaultOptions['exception_function'] = $cli 693 | ? 'Symfony\Component\Console\Application::renderException' 694 | : 'Illuminate\Foundation\Http\Kernel::reportException'; 695 | break; 696 | 697 | case self::FRAMEWORK_MAGENTO: 698 | self::$defaultOptions['transaction_function'] = 'Mage_Core_Controller_Varien_Action::dispatch'; 699 | self::$defaultOptions['exception_function'] = 'Mage::printException'; 700 | break; 701 | 702 | case self::FRAMEWORK_MAGENTO2: 703 | self::$defaultOptions['transaction_function'] = 'Magento\Framework\App\ActionFactory::create'; 704 | self::$defaultOptions['exception_function'] = 'Magento\Framework\App\Http::catchException'; 705 | break; 706 | 707 | case self::FRAMEWORK_PRESTA16: 708 | self::$defaultOptions['transaction_function'] = 'ControllerCore::getController'; 709 | self::$defaultOptions['exception_function'] = 'PrestaShopExceptionCore::displayMessage'; 710 | break; 711 | 712 | case self::FRAMEWORK_DRUPAL8: 713 | self::$defaultOptions['transaction_function'] = 'Drupal\Core\Controller\ControllerResolver::createController'; 714 | self::$defaultOptions['exception_function'] = 'Symfony\Component\HttpKernel\HttpKernel::handleException'; 715 | break; 716 | 717 | case self::FRAMEWORK_FLOW: 718 | self::$defaultOptions['transaction_function'] = 'TYPO3\Flow\Mvc\Controller\ActionController_Original::callActionMethod'; 719 | self::$defaultOptions['exception_function'] = 'TYPO3\Flow\Error\AbstractExceptionHandler::handleException'; 720 | break; 721 | 722 | case self::FRAMEWORK_FLOW4: 723 | self::$defaultOptions['transaction_function'] = 'Neos\Flow\Mvc\Controller\ActionController_Original::callActionMethod'; 724 | self::$defaultOptions['exception_function'] = 'Neos\Flow\Error\AbstractExceptionHandler::handleException'; 725 | break; 726 | 727 | case self::FRAMEWORK_TYPO3: 728 | self::$defaultOptions['transaction_function'] = 'TYPO3\CMS\Extbase\Mvc\Controller\ActionController::callActionMethod'; 729 | self::$defaultOptions['exception_function'] = 'TYPO3\CMS\Error\AbstractExceptionHandler::handleException'; 730 | break; 731 | 732 | case self::FRAMEWORK_CAKE2: 733 | self::$defaultOptions['transaction_function'] = 'Controller::invokeAction'; 734 | self::$defaultOptions['exception_function'] = 'ExceptionRenderer::__construct'; 735 | break; 736 | 737 | case self::FRAMEWORK_CAKE3: 738 | self::$defaultOptions['transaction_function'] = 'Cake\\Controller\\Controller::invokeAction'; 739 | self::$defaultOptions['exception_function'] = 'Cake\\Error\\ExceptionRenderer::__construct'; 740 | break; 741 | 742 | case self::FRAMEWORK_YII: 743 | self::$defaultOptions['transaction_function'] = 'CController::run'; 744 | self::$defaultOptions['exception_function'] = 'CApplication::handleException'; 745 | break; 746 | 747 | case self::FRAMEWORK_YII2: 748 | self::$defaultOptions['transaction_function'] = 'yii\\base\\Module::runAction'; 749 | self::$defaultOptions['exception_function'] = 'yii\\base\\ErrorHandler::handleException'; 750 | break; 751 | 752 | default: 753 | self::$defaultOptions['transaction_function'] = $framework; 754 | break; 755 | } 756 | } 757 | 758 | /** 759 | * Add more ignore functions to profiling options. 760 | * 761 | * @param array $functionNames 762 | * @return void 763 | */ 764 | public static function addIgnoreFunctions(array $functionNames) 765 | { 766 | foreach ($functionNames as $functionName) { 767 | self::$defaultOptions['ignored_functions'][] = $functionName; 768 | } 769 | } 770 | 771 | /** 772 | * Start profiling in development mode. 773 | * 774 | * This will always generate a full profile and send it to the profiler. 775 | * It adds a correlation id that forces the profile into "developer" 776 | * traces and activates the memory profiling as well. 777 | * 778 | * WARNING: This method can cause huge performance impact on production 779 | * setups. Make sure to wrap this in your own sampling code and don't 780 | * execute it in every request. 781 | */ 782 | public static function startDevelopment($apiKey = null, array $options = array()) 783 | { 784 | if ($apiKey) { 785 | $options['api_key'] = $apiKey; 786 | } else if (!isset($options['api_key'])) { 787 | $options['api_key'] = isset($_SERVER['TIDEWAYS_APIKEY']) ? $_SERVER['TIDEWAYS_APIKEY'] : ini_get("tideways.api_key"); 788 | } 789 | 790 | $time = time() + 60; 791 | $_SERVER['TIDEWAYS_SESSION'] = 792 | "time=" . $time . "&user=&method=&hash=" . 793 | hash_hmac('sha256', 'method=&time=' . $time . '&user=', md5($options['api_key'])) 794 | ; 795 | 796 | self::start($options); 797 | } 798 | 799 | /** 800 | * Start production profiling for the application. 801 | * 802 | * There are three modes for profiling: 803 | * 804 | * 1. Wall-time only profiling of the complete request (no overhead) 805 | * 2. Full profile/trace using xhprof (depending of # function calls 806 | * significant overhead) 807 | * 3. Whitelist-profiling mode only interesting functions. 808 | * (5-40% overhead, requires custom xhprof version >= 0.95) 809 | * 810 | * Decisions to profile are made based on a sample-rate and random picks. 811 | * You can influence the sample rate and pick a value that suites your 812 | * application. Applications with lower request rates need a much higher 813 | * transaction rate (25-50%) than applications with high load (<= 1%). 814 | * 815 | * Factors that influence sample rate: 816 | * 817 | * 1. Second parameter $sampleRate to start() method. 818 | * 2. _tideways Query Parameter (string key is deprecated or array) 819 | * 3. Cookie TIDEWAYS_SESSION 820 | * 4. TIDEWAYS_SAMPLERATE environment variable. 821 | * 5. X-TIDEWAYS-PROFILER HTTP header 822 | * 823 | * start() automatically invokes a register shutdown handler that stops and 824 | * transmits the profiling data to the local daemon for further processing. 825 | * 826 | * @param array|string $options Either options array or api key (when string) 827 | * @param int $sampleRate Deprecated, use "sample_rate" key in options instead. 828 | * 829 | * @return void 830 | */ 831 | public static function start($options = array(), $sampleRate = null) 832 | { 833 | self::ignoreTransaction(); // this discards any data that was collected up to now and restarts. 834 | 835 | if (!is_array($options)) { 836 | $options = array('api_key' => $options); 837 | } 838 | if ($sampleRate !== null) { 839 | $options['sample_rate'] = $sampleRate; 840 | } 841 | 842 | $defaults = array( 843 | 'api_key' => isset($_SERVER['TIDEWAYS_APIKEY']) ? $_SERVER['TIDEWAYS_APIKEY'] : ini_get("tideways.api_key"), 844 | 'sample_rate' => isset($_SERVER['TIDEWAYS_SAMPLERATE']) ? intval($_SERVER['TIDEWAYS_SAMPLERATE']) : (ini_get("tideways.sample_rate") ?: 0), 845 | 'collect' => isset($_SERVER['TIDEWAYS_COLLECT']) ? $_SERVER['TIDEWAYS_COLLECT'] : (ini_get("tideways.collect") ?: self::MODE_PROFILING), 846 | 'monitor' => isset($_SERVER['TIDEWAYS_MONITOR']) ? $_SERVER['TIDEWAYS_MONITOR'] : (ini_get("tideways.monitor") ?: self::MODE_BASIC), 847 | 'triggered' => self::MODE_FULL, 848 | 'log_level' => ini_get("tideways.log_level") ?: 0, 849 | 'service' => isset($_SERVER['TIDEWAYS_SERVICE']) ? $_SERVER['TIDEWAYS_SERVICE'] : ini_get("tideways.service"), 850 | 'framework' => isset($_SERVER['TIDEWAYS_FRAMEWORK']) ? $_SERVER['TIDEWAYS_FRAMEWORK'] : ini_get("tideways.framework"), 851 | ); 852 | $options = array_merge($defaults, $options); 853 | 854 | if (strlen((string)$options['api_key']) === 0) { 855 | return; 856 | } 857 | 858 | self::$logLevel = $options['log_level']; 859 | self::init($options['api_key'], $options); 860 | self::decideProfiling($options['sample_rate'], $options); 861 | } 862 | 863 | /** 864 | * Enable the profiler in the given $mode. 865 | * 866 | * @param string $mode 867 | * @return void 868 | */ 869 | private static function enableProfiler($mode) 870 | { 871 | self::$mode = $mode; 872 | 873 | if (self::$extension === self::EXTENSION_TIDEWAYS && (self::$mode !== self::MODE_DISABLED)) { 874 | switch (self::$mode) { 875 | case self::MODE_FULL: 876 | $flags = 0; 877 | break; 878 | 879 | case self::MODE_PROFILING: 880 | $flags = TIDEWAYS_FLAGS_NO_SPANS; 881 | break; 882 | 883 | case self::MODE_TRACING: 884 | $flags = TIDEWAYS_FLAGS_NO_HIERACHICAL; 885 | break; 886 | 887 | default: 888 | $flags = TIDEWAYS_FLAGS_NO_COMPILE | TIDEWAYS_FLAGS_NO_USERLAND | TIDEWAYS_FLAGS_NO_BUILTINS; 889 | break; 890 | } 891 | 892 | self::$currentRootSpan = new \Tideways\Traces\TwExtensionSpan(0); 893 | tideways_enable($flags, self::$defaultOptions); 894 | 895 | if (($flags & TIDEWAYS_FLAGS_NO_SPANS) === 0) { 896 | foreach (self::$defaultOptions['watches'] as $watch => $category) { 897 | tideways_span_watch($watch, $category); 898 | } 899 | foreach (self::$defaultOptions['callbacks'] as $function => $callback) { 900 | tideways_span_callback($function, $callback); 901 | } 902 | } 903 | 904 | self::log(2, "Starting tideways extension for " . self::$trace['apiKey'] . " with mode: " . $mode); 905 | } elseif (self::$extension === self::EXTENSION_XHPROF && (self::$mode & self::MODE_PROFILING) > 0) { 906 | \Tideways\Traces\PhpSpan::clear(); 907 | self::$currentRootSpan = new \Tideways\Traces\PhpSpan(0, 'app'); 908 | self::$currentRootSpan->startTimer(); 909 | 910 | xhprof_enable(0, self::$defaultOptions); 911 | self::log(2, "Starting xhprof extension for " . self::$trace['apiKey'] . " with mode: " . $mode); 912 | } else { 913 | \Tideways\Traces\PhpSpan::clear(); 914 | self::$currentRootSpan = new \Tideways\Traces\PhpSpan(0, 'app'); 915 | self::$currentRootSpan->startTimer(); 916 | 917 | self::log(2, "Starting non-extension based tracing for " . self::$trace['apiKey'] . " with mode: " . $mode); 918 | } 919 | } 920 | 921 | /** 922 | * Check if headers, cookie or environment variables for a developer trace 923 | * are present. This method does not validate if the passed information is 924 | * actually valid for the current API Key. 925 | * 926 | * @return bool 927 | */ 928 | public static function containsDeveloperTraceRequest() 929 | { 930 | if (isset($_SERVER['HTTP_X_TIDEWAYS_PROFILER']) && is_string($_SERVER['HTTP_X_TIDEWAYS_PROFILER'])) { 931 | return true; 932 | } else if (isset($_SERVER['TIDEWAYS_SESSION']) && is_string($_SERVER['TIDEWAYS_SESSION'])) { 933 | return true; 934 | } else if (isset($_COOKIE['TIDEWAYS_SESSION']) && is_string($_COOKIE['TIDEWAYS_SESSION'])) { 935 | return true; 936 | } else if (isset($_GET['_tideways']) && is_array($_GET['_tideways'])) { 937 | return true; 938 | } 939 | 940 | return false; 941 | } 942 | 943 | /** 944 | * Decide in which mode to start collecting data. 945 | * 946 | * @param int $treshold (0-100) 947 | * @param array $options 948 | * @return int 949 | */ 950 | private static function decideProfiling($treshold, array $options = array()) 951 | { 952 | $vars = array(); 953 | $type = null; 954 | $tidewaysReferenceId = null; 955 | 956 | if (isset($_SERVER['HTTP_X_TIDEWAYS_PROFILER']) && is_string($_SERVER['HTTP_X_TIDEWAYS_PROFILER'])) { 957 | parse_str($_SERVER['HTTP_X_TIDEWAYS_PROFILER'], $vars); 958 | $type = 'header X-Tideways-Profiler'; 959 | $tidewaysReferenceId = isset($_SERVER['HTTP_X_TIDEWAYS_REF']) ? $_SERVER['HTTP_X_TIDEWAYS_REF'] : null; 960 | } else if (isset($_SERVER['TIDEWAYS_SESSION']) && is_string($_SERVER['TIDEWAYS_SESSION'])) { 961 | parse_str($_SERVER['TIDEWAYS_SESSION'], $vars); 962 | $type = 'environment variable TIDEWAYS_SESSION'; 963 | $tidewaysReferenceId = isset($_SERVER['TIDEWAYS_REF']) ? $_SERVER['TIDEWAYS_REF'] : null; 964 | } else if (isset($_COOKIE['TIDEWAYS_SESSION']) && is_string($_COOKIE['TIDEWAYS_SESSION'])) { 965 | parse_str($_COOKIE['TIDEWAYS_SESSION'], $vars); 966 | $type = 'cookie TIDEWAYS_SESSION'; 967 | $tidewaysReferenceId = isset($_COOKIE['TIDEWAYS_REF']) ? $_COOKIE['TIDEWAYS_REF'] : null; 968 | } else if (isset($_GET['_tideways']) && is_array($_GET['_tideways'])) { 969 | $vars = $_GET['_tideways']; 970 | $type = 'GET parameter'; 971 | } 972 | 973 | if (isset($_SERVER['TIDEWAYS_DISABLE_SESSIONS']) && $_SERVER['TIDEWAYS_DISABLE_SESSIONS']) { 974 | $vars = array(); 975 | } 976 | 977 | if (isset($vars['hash'], $vars['time'], $vars['user'], $vars['method'])) { 978 | $message = 'method=' . $vars['method'] . '&time=' . $vars['time'] . '&user=' . $vars['user']; 979 | self::log(3, "Found explicit trigger trace parameters in " . $type); 980 | 981 | if (hash_hmac('sha256', $message, md5(self::$trace['apiKey'])) === $vars['hash']) { 982 | if ($vars['time'] < time()) { 983 | self::log(1, "trigger trace request with " . $type . " is authenticated, but signature is too old: " . $vars['time'] . " < " . time()); 984 | self::$mode = self::MODE_DISABLED; 985 | return; 986 | } 987 | 988 | if (self::$logLevel >= 2) { 989 | $location = "unknown"; 990 | $backtrace = debug_backtrace(); 991 | 992 | for ($i = count($backtrace) - 1; $i >= 0; $i--) { // find the last call location before going into Tideways\Profiler 993 | if (isset($backtrace[$i-1]['class']) && $backtrace[$i-1]['class'] === "Tideways\\Profiler") { 994 | $location = isset($backtrace[$i]['file']) ? $backtrace[$i]['file'] : ''; 995 | $location .= (':' . (isset($backtrace[$i]['line']) ? $backtrace[$i]['line'] : '')); 996 | 997 | break; 998 | } 999 | } 1000 | 1001 | self::log(2, "Successful trigger trace request with valid hash in " . $type . " started from " . $location); 1002 | } 1003 | 1004 | self::$trace['keep'] = true; // always keep 1005 | 1006 | self::enableProfiler($options['triggered']); 1007 | self::setCustomVariable('tw.uid', $vars['user']); 1008 | 1009 | if ($tidewaysReferenceId) { 1010 | self::setCustomVariable('tw.ref', $tidewaysReferenceId); 1011 | } 1012 | return; 1013 | } else { 1014 | self::log(1, "Invalid trigger trace request with " . $type . " cannot be authenticated."); 1015 | self::$mode = self::MODE_DISABLED; 1016 | return; 1017 | } 1018 | } 1019 | 1020 | self::log(3, sprintf("Profiling decision with sample-rate: %d", $treshold)); 1021 | 1022 | $collectMode = self::convertMode($options['collect']); 1023 | $monitorMode = self::convertMode($options['monitor']) & self::MODE_BASIC; 1024 | 1025 | $rand = rand(1, 100); 1026 | $mode = ($rand <= $treshold) ? $collectMode : $monitorMode; 1027 | 1028 | self::enableProfiler($mode); 1029 | } 1030 | 1031 | /** 1032 | * Make sure provided mode is converted to a valid integer value. 1033 | * 1034 | * @return int 1035 | */ 1036 | private static function convertMode($mode) 1037 | { 1038 | if (is_string($mode)) { 1039 | $mode = defined('\Tideways\Profiler::MODE_' . strtoupper($mode)) 1040 | ? constant('\Tideways\Profiler::MODE_' . strtoupper($mode)) 1041 | : self::MODE_DISABLED; 1042 | } else if (!is_int($mode)) { 1043 | $mode = self::MODE_DISABLED; 1044 | } else if (($mode & (self::MODE_FULL|self::MODE_BASIC)) === 0) { 1045 | $mode = self::MODE_DISABLED; 1046 | } 1047 | 1048 | return $mode; 1049 | } 1050 | 1051 | /** 1052 | * Ignore this transaction and don't collect profiling or performance measurements. 1053 | * 1054 | * @return void 1055 | */ 1056 | public static function ignoreTransaction() 1057 | { 1058 | if (self::$mode !== self::MODE_DISABLED) { 1059 | self::$mode = self::MODE_DISABLED; 1060 | 1061 | if (self::$extension === self::EXTENSION_XHPROF) { 1062 | xhprof_disable(); 1063 | } else if (self::$extension === self::EXTENSION_TIDEWAYS) { 1064 | tideways_disable(); 1065 | } 1066 | } 1067 | } 1068 | 1069 | private static function init($apiKey, $options) 1070 | { 1071 | if (self::$shutdownRegistered == false) { 1072 | register_shutdown_function(array("Tideways\\Profiler", "shutdown")); 1073 | self::$shutdownRegistered = true; 1074 | } 1075 | 1076 | if (self::$backend === null) { 1077 | self::$backend = new Profiler\NetworkBackend( 1078 | ini_get('tideways.connection') ?: 'unix:///var/run/tideways/tidewaysd.sock', 1079 | ini_get('tideways.udp_connection') ?: '127.0.0.1:8135' 1080 | ); 1081 | } 1082 | 1083 | if ($options['framework']) { 1084 | self::detectFramework($options['framework']); 1085 | } 1086 | 1087 | if (function_exists('tideways_enable')) { 1088 | self::$extension = self::EXTENSION_TIDEWAYS; 1089 | } else if (function_exists('xhprof_enable')) { 1090 | self::$extension = self::EXTENSION_XHPROF; 1091 | } 1092 | 1093 | self::$mode = self::MODE_BASIC; 1094 | self::$error = false; 1095 | self::$trace = array( 1096 | 'apiKey' => $apiKey, 1097 | 'id' => self::generateRandomId(), 1098 | 'tx' => 'default', 1099 | ); 1100 | 1101 | if ($options['service']) { 1102 | self::$trace['service'] = $options['service']; 1103 | } 1104 | } 1105 | 1106 | /** 1107 | * Generates a random integer used for internal identification and correlation of traces. 1108 | * 1109 | * @return int 1110 | */ 1111 | public static function generateRandomId() 1112 | { 1113 | return mt_rand(1, PHP_INT_MAX); 1114 | } 1115 | 1116 | public static function setTransactionName($name) 1117 | { 1118 | self::$trace['tx'] = !empty($name) ? $name : 'default'; 1119 | } 1120 | 1121 | /** 1122 | * Returns the current transaction name. 1123 | * 1124 | * If you use automatic framework transaction detection, this is "default" 1125 | * until the engine detects the transaction name somewhere in the 1126 | * frameworks lifecycle. No guarantees given when this happens. 1127 | * 1128 | * @return string 1129 | */ 1130 | public static function getTransactionName() 1131 | { 1132 | if (self::$trace['tx'] === 'default' && self::$extension === self::EXTENSION_TIDEWAYS) { 1133 | return tideways_transaction_name() ?: 'default'; 1134 | } 1135 | 1136 | return self::$trace['tx']; 1137 | } 1138 | 1139 | public static function setServiceName($name) 1140 | { 1141 | self::$trace['service'] = $name; 1142 | } 1143 | 1144 | /** 1145 | * @return int 1146 | */ 1147 | public static function currentTraceId() 1148 | { 1149 | return isset(self::$trace['id']) ? self::$trace['id'] : 0; 1150 | } 1151 | 1152 | public static function rootTraceId() 1153 | { 1154 | return isset(self::$trace['rid']) 1155 | ? self::$trace['rid'] 1156 | : self::currentTraceId(); 1157 | } 1158 | 1159 | /** 1160 | * @deprecated 1161 | */ 1162 | public static function setOperationName($name) 1163 | { 1164 | self::setTransactionName($name); 1165 | } 1166 | 1167 | public static function isStarted() 1168 | { 1169 | return self::$mode !== self::MODE_DISABLED; 1170 | } 1171 | 1172 | public static function isProfiling() 1173 | { 1174 | return (self::$mode & self::MODE_PROFILING) > 0; 1175 | } 1176 | 1177 | /** 1178 | * Returns true if profiler is currently tracing spans. 1179 | * 1180 | * This can be used to check if adding X-TW-* HTTP headers makes sense. 1181 | * 1182 | * @return bool 1183 | */ 1184 | public static function isTracing() 1185 | { 1186 | return (self::$mode & self::MODE_TRACING) > 0; 1187 | } 1188 | 1189 | /** 1190 | * Add a custom variable to this profile. 1191 | * 1192 | * Examples are the Request URL, UserId, Correlation Ids and more. 1193 | * 1194 | * Please do *NOT* set private data in custom variables as this 1195 | * data is not encrypted on our servers. 1196 | * 1197 | * Only accepts scalar values. 1198 | * 1199 | * The key 'url' is a magic value and should contain the request 1200 | * url if you want to transmit it. The Profiler UI will specially 1201 | * display it. 1202 | * 1203 | * @param string $name 1204 | * @param scalar $value 1205 | * @return void 1206 | */ 1207 | public static function setCustomVariable($name, $value) 1208 | { 1209 | if ((self::$mode & self::MODE_FULL) === 0 || !is_scalar($value)) { 1210 | return; 1211 | } 1212 | 1213 | if (!self::$currentRootSpan) { 1214 | return; 1215 | } 1216 | 1217 | self::$currentRootSpan->annotate(array($name => $value)); 1218 | } 1219 | 1220 | /** 1221 | * Watch a function for calls and create timeline spans around it. 1222 | * 1223 | * @param string $function 1224 | * @param string $category 1225 | */ 1226 | public static function watch($function, $category = null) 1227 | { 1228 | if (self::$extension === self::EXTENSION_TIDEWAYS) { 1229 | self::$defaultOptions['watches'][$function] = $category; 1230 | 1231 | if ((self::$mode & self::MODE_TRACING) > 0) { 1232 | tideways_span_watch($function, $category); 1233 | } 1234 | } 1235 | } 1236 | 1237 | /** 1238 | * Watch a function and invoke a callback when its called. 1239 | * 1240 | * To start a span, call {@link \Tideways\Profiler::createSpan($category)} 1241 | * inside the callback and return {$span->getId()}: 1242 | * 1243 | * @example 1244 | * 1245 | * \Tideways\Profiler::watchCallback('mysql_query', function ($context) { 1246 | * $span = \Tideways\Profiler::createSpan('sql'); 1247 | * $span->annotate(array('title' => $context['args'][0])); 1248 | * return $span->getId(); 1249 | * }); 1250 | */ 1251 | public static function watchCallback($function, $callback) 1252 | { 1253 | if (self::$extension === self::EXTENSION_TIDEWAYS) { 1254 | self::$defaultOptions['callbacks'][$function] = $callback; 1255 | 1256 | if ((self::$mode & self::MODE_TRACING) > 0) { 1257 | tideways_span_callback($function, $callback); 1258 | } 1259 | } 1260 | } 1261 | 1262 | /** 1263 | * Create a new trace span with the given category name. 1264 | * 1265 | * @example 1266 | * 1267 | * $span = \Tideways\Profiler::createSpan('sql'); 1268 | * 1269 | * @return \Tideways\Traces\Span 1270 | */ 1271 | public static function createSpan($name) 1272 | { 1273 | return (self::$mode & self::MODE_TRACING) > 0 1274 | ? self::$currentRootSpan->createSpan($name) 1275 | : new \Tideways\Traces\NullSpan(); 1276 | } 1277 | 1278 | /** 1279 | * Stop all profiling actions and submit collected data. 1280 | */ 1281 | public static function stop() 1282 | { 1283 | if (self::$mode === self::MODE_DISABLED) { 1284 | return; 1285 | } 1286 | 1287 | $mode = self::$mode; 1288 | 1289 | if (self::$trace['tx'] === 'default' && self::$extension === self::EXTENSION_TIDEWAYS) { 1290 | self::$trace['tx'] = tideways_transaction_name() ?: 'default'; 1291 | } 1292 | 1293 | if (function_exists('tideways_last_detected_exception') && $exception = tideways_last_detected_exception()) { 1294 | self::logException($exception); 1295 | } elseif (function_exists("http_response_code") && http_response_code() >= 500) { 1296 | self::logFatal("PHP request set error HTTP response code to '" . http_response_code() . "'.", "", 0, E_USER_ERROR); 1297 | } 1298 | 1299 | $profilingData = array(); 1300 | 1301 | if (($mode & self::MODE_FULL) > 0 || self::$error) { 1302 | if (self::$extension === self::EXTENSION_TIDEWAYS) { 1303 | $profilingData = tideways_disable(); 1304 | } elseif (self::$extension === self::EXTENSION_XHPROF) { 1305 | $profilingData = xhprof_disable(); 1306 | self::$currentRootSpan->stopTimer(); 1307 | } 1308 | 1309 | $annotations = array('mem' => ceil(memory_get_peak_usage() / 1024)); 1310 | 1311 | if (self::$extension === self::EXTENSION_TIDEWAYS) { 1312 | $annotations['xhpv'] = phpversion('tideways'); 1313 | 1314 | if (self::$defaultOptions['framework']) { 1315 | $annotations['framework'] = self::$defaultOptions['framework']; 1316 | } 1317 | } elseif (self::$extension === self::EXTENSION_XHPROF) { 1318 | $annotations['xhpv'] = phpversion('xhprof'); 1319 | } 1320 | 1321 | if (extension_loaded('xdebug')) { 1322 | $annotations['xdebug'] = '1'; 1323 | } 1324 | $annotations['php'] = PHP_VERSION; 1325 | $annotations['sapi'] = php_sapi_name(); 1326 | 1327 | if (isset($_SERVER['REQUEST_URI'])) { 1328 | $annotations['title'] = ''; 1329 | if (isset($_SERVER['REQUEST_METHOD'])) { 1330 | $annotations['title'] = $_SERVER["REQUEST_METHOD"] . ' '; 1331 | } 1332 | 1333 | if (isset($_SERVER['HTTP_HOST'])) { 1334 | $annotations['title'] .= (isset($_SERVER['HTTPS']) ? 'https' : 'http') . '://' . $_SERVER['HTTP_HOST'] . self::getRequestUri(); 1335 | } elseif(isset($_SERVER['SERVER_ADDR'])) { 1336 | $annotations['title'] .= (isset($_SERVER['HTTPS']) ? 'https' : 'http') . '://' . $_SERVER['SERVER_ADDR'] . self::getRequestUri(); 1337 | } 1338 | 1339 | if (isset($_SERVER['QUERY_STRING'])) { 1340 | $annotations['query'] = $_SERVER['QUERY_STRING']; 1341 | } 1342 | 1343 | } elseif ($annotations['sapi'] === "cli") { 1344 | $annotations['title'] = basename($_SERVER['argv'][0]); 1345 | } 1346 | } else { 1347 | self::$currentRootSpan->stopTimer(); 1348 | $annotations = array('mem' => ceil(memory_get_peak_usage() / 1024)); 1349 | } 1350 | 1351 | self::$currentRootSpan->annotate($annotations); 1352 | 1353 | if (($mode & self::MODE_PROFILING) > 0) { 1354 | self::$trace['profdata'] = $profilingData ?: array(); 1355 | } 1356 | 1357 | self::$mode = self::MODE_DISABLED; 1358 | 1359 | $spans = self::$currentRootSpan->getSpans(); 1360 | 1361 | if (self::$error === true || ($mode & self::MODE_FULL) > 0) { 1362 | self::$trace['spans'] = $spans; 1363 | self::$backend->socketStore(self::$trace); 1364 | } else { 1365 | self::$trace['spans'] = isset($spans[0]) ? array($spans[0]) : array(); // prevent flooding udp by accident 1366 | self::$backend->socketStoreMeasurement(self::$trace); 1367 | } 1368 | self::$trace = null; // free memory 1369 | self::$logLevel = 0; 1370 | } 1371 | 1372 | /** 1373 | * Use Request or Script information for the transaction name. 1374 | * 1375 | * @return void 1376 | */ 1377 | public static function useRequestAsTransactionName() 1378 | { 1379 | self::setTransactionName(self::guessOperationName()); 1380 | } 1381 | 1382 | /** 1383 | * Use {@link useRequestAsTransactionName()} instead. 1384 | * 1385 | * @deprecated 1386 | */ 1387 | public static function guessOperationName() 1388 | { 1389 | if (php_sapi_name() === "cli") { 1390 | return "cli:" . basename($_SERVER["argv"][0]); 1391 | } 1392 | 1393 | return $_SERVER["REQUEST_METHOD"] . " " . self::getRequestUri(); 1394 | } 1395 | 1396 | protected static function getRequestUri() 1397 | { 1398 | return strpos($_SERVER["REQUEST_URI"], "?") 1399 | ? substr($_SERVER["REQUEST_URI"], 0, strpos($_SERVER["REQUEST_URI"], "?")) 1400 | : $_SERVER["REQUEST_URI"]; 1401 | } 1402 | 1403 | public static function logFatal($message, $file, $line, $type = null, $trace = null) 1404 | { 1405 | if (self::$error === true || !self::$currentRootSpan) { 1406 | return; 1407 | } 1408 | 1409 | if ($type === null) { 1410 | $type = E_USER_ERROR; 1411 | } 1412 | 1413 | $trace = is_array($trace) 1414 | ? \Tideways\Profiler\BacktraceConverter::convertToString($trace) 1415 | : $trace; 1416 | 1417 | self::$error = true; 1418 | self::$currentRootSpan->annotate(array( 1419 | "err_msg" => $message, 1420 | "err_source" => $file . ':' . $line, 1421 | "err_exception" => 'EngineException', // Forward compatibility with PHP7 1422 | "err_trace" => $trace, 1423 | )); 1424 | } 1425 | 1426 | public static function logException($exception) 1427 | { 1428 | if (is_string($exception)) { 1429 | $exception = new \RuntimeException($exception); 1430 | } 1431 | 1432 | if (self::$error === true || !self::$currentRootSpan || !is_object($exception)) { 1433 | return; 1434 | } 1435 | 1436 | // PHP 5 compatible way to check for !($exception instanceof Throwable) 1437 | if (!($exception instanceof \Exception) && !in_array('Throwable', class_implements($exception, false))) { 1438 | return; 1439 | } 1440 | 1441 | // We are only interested in the original exception 1442 | while ($previous = $exception->getPrevious()) { 1443 | $exception = $previous; 1444 | } 1445 | 1446 | self::$error = true; 1447 | self::$currentRootSpan->annotate(array( 1448 | "err_msg" => $exception->getMessage(), 1449 | "err_source" => $exception->getFile() . ':' . $exception->getLine(), 1450 | "err_exception" => get_class($exception), 1451 | "err_trace" => \Tideways\Profiler\BacktraceConverter::convertToString($exception->getTrace()), 1452 | )); 1453 | } 1454 | 1455 | public static function shutdown() 1456 | { 1457 | if (self::$mode === self::MODE_DISABLED) { 1458 | return; 1459 | } 1460 | 1461 | $lastError = error_get_last(); 1462 | 1463 | if ($lastError && ($lastError["type"] === E_ERROR || $lastError["type"] === E_PARSE || $lastError["type"] === E_COMPILE_ERROR)) { 1464 | $lastError['trace'] = function_exists('tideways_fatal_backtrace') ? tideways_fatal_backtrace() : null; 1465 | 1466 | self::logFatal($lastError['message'], $lastError['file'], $lastError['line'], $lastError['type'], $lastError['trace']); 1467 | } 1468 | 1469 | self::stop(); 1470 | } 1471 | 1472 | /** 1473 | * Check for auto starting the Profiler in Web and CLI (via Env Variable) 1474 | */ 1475 | public static function autoStart() 1476 | { 1477 | if (ini_get("tideways.auto_start") || isset($_SERVER["TIDEWAYS_AUTO_START"])) { 1478 | if (self::isStarted() === false) { 1479 | switch (php_sapi_name()) { 1480 | case 'cli': 1481 | if (ini_get("tideways.monitor_cli") || isset($_SERVER['TIDEWAYS_SESSION'])) { 1482 | self::start(array('service' => 'cli')); 1483 | } 1484 | break; 1485 | 1486 | default: 1487 | self::start(); 1488 | } 1489 | } 1490 | } 1491 | 1492 | if (self::requiresDelegateToOriginalPrependFile()) { 1493 | require_once ini_get("auto_prepend_file"); 1494 | } 1495 | } 1496 | 1497 | /** 1498 | * @return bool 1499 | */ 1500 | private static function requiresDelegateToOriginalPrependFile() 1501 | { 1502 | return ini_get('tideways.auto_prepend_library') && 1503 | tideways_prepend_overwritten() && 1504 | ini_get("auto_prepend_file") && 1505 | file_exists(stream_resolve_include_path(ini_get("auto_prepend_file"))); 1506 | } 1507 | 1508 | /** 1509 | * Log a message to the PHP error log when the defined log-level is higher 1510 | * or equal to the messages log-level. 1511 | * 1512 | * @param int $level Logs message level. 1 = warning, 2 = notice, 3 = debug 1513 | * @param string $message 1514 | * @return void 1515 | */ 1516 | public static function log($level, $message) 1517 | { 1518 | if ($level <= self::$logLevel) { 1519 | $level = ($level === 3) ? "debug" : (($level === 2) ? "info" : "warn"); 1520 | error_log(sprintf('[%s] tideways - %s', $level, $message), 0); 1521 | } 1522 | } 1523 | } 1524 | // auto-starts the profiler if that is configured 1525 | \Tideways\Profiler::autoStart(); 1526 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tideways/profiler", 3 | "description": "Client library that wraps Xhprof profile collection and sends to Tideways", 4 | "license": "apache-2.0", 5 | "autoload": { 6 | "psr-0": {"Tideways\\": "src/main"}, 7 | "files": ["Tideways.php"] 8 | }, 9 | "abandoned": "https://support.tideways.com/documentation/setup/troubleshooting/tideways-profiler-composer-package-abandoned.html" 10 | } 11 | --------------------------------------------------------------------------------