├── LICENSE ├── README.md ├── composer.json └── src ├── Configuration.php ├── Exceptions └── InspectorException.php ├── Inspector.php ├── Models ├── Arrayable.php ├── Error.php ├── HasContext.php ├── Partials │ ├── Host.php │ ├── Http.php │ ├── Request.php │ ├── Socket.php │ ├── Url.php │ └── User.php ├── PerformanceModel.php ├── Segment.php └── Transaction.php ├── OS.php └── Transports ├── AbstractApiTransport.php ├── AsyncTransport.php ├── CurlTransport.php └── TransportInterface.php /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Aventure 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Inspector | Code Execution Monitoring Tool 2 | 3 | [![Total Downloads](https://poser.pugx.org/inspector-apm/inspector-php/downloads)](//packagist.org/packages/inspector-apm/inspector-php) 4 | [![Latest Stable Version](https://poser.pugx.org/inspector-apm/inspector-php/v/stable)](https://packagist.org/packages/inspector-apm/inspector-php) 5 | [![License](https://poser.pugx.org/inspector-apm/inspector-php/license)](//packagist.org/packages/inspector-apm/inspector-php) 6 | 7 | > Before moving on, please consider giving us a GitHub star ⭐️. Thank you! 8 | 9 | Code Execution Monitoring, built for PHP developers. 10 | 11 | ## Requirements 12 | 13 | - PHP >= 7.2.0 14 | 15 | ## Install 16 | Install the latest version by: 17 | 18 | ```shell 19 | composer require inspector-apm/inspector-php 20 | ``` 21 | 22 | ## Use 23 | 24 | To start sending data to Inspector you need an Ingestion Key to create an instance of the `Configuration` class. 25 | You can obtain `INSPECTOR_API_KEY` creating a new project in your [Inspector](https://www.inspector.dev) dashboard. 26 | 27 | ```php 28 | use Inspector\Inspector; 29 | use Inspector\Configuration; 30 | 31 | $configuration = new Configuration('YOUR_INGESTION_KEY'); 32 | $inspector = new Inspector($configuration); 33 | ``` 34 | 35 | All start with a `transaction`. Transaction represent an execution cycle and it can contains one or hundred of segments: 36 | 37 | ```php 38 | // Start an execution cycle with a transaction 39 | $inspector->startTransaction($_SERVER['PATH_INFO']); 40 | ``` 41 | 42 | Use `addSegment` method to monitor a code block in your transaction: 43 | 44 | ```php 45 | $result = $inspector->addSegment(function ($segment) { 46 | // Do something here... 47 | return "Hello World!"; 48 | }, 'my-process'); 49 | 50 | echo $result; // this will print "Hello World!" 51 | ``` 52 | 53 | Inspector will monitor your code execution in real time alerting you if something goes wrong. 54 | 55 | ## Custom Transport 56 | You can also set up custom transport class to transfer monitoring data from your server to Inspector 57 | in a personalized way. 58 | 59 | The transport class needs to implement `\Inspector\Transports\TransportInterface`: 60 | 61 | ```php 62 | class CustomTransport implements \Inspector\Transports\TransportInterface 63 | { 64 | protected $configuration; 65 | 66 | protected $queue = []; 67 | 68 | public function __constructor($configuration) 69 | { 70 | $this->configuration = $configuration; 71 | } 72 | 73 | public function addEntry(\Inspector\Models\Arrayable $entry) 74 | { 75 | // Add an \Inspector\Models\Arrayable entry in the queue. 76 | $this->queue[] = $entry; 77 | } 78 | 79 | public function flush() 80 | { 81 | // Performs data transfer. 82 | $handle = curl_init('https://ingest.inspector.dev'); 83 | curl_setopt($handle, CURLOPT_POST, 1); 84 | curl_setopt($handle, CURLOPT_HTTPHEADER, [ 85 | 'X-Inspector-Key: xxxxxxxxxxxx', 86 | 'Content-Type: application/json', 87 | 'Accept: application/json', 88 | ]); 89 | curl_setopt($handle, CURLOPT_POSTFIELDS, json_encode($this->queue)); 90 | curl_exec($handle); 91 | curl_close($handle); 92 | } 93 | } 94 | ``` 95 | 96 | Then you can set the new transport in the `Inspector` instance 97 | using a callback the will receive the current configuration state as parameter. 98 | 99 | ```php 100 | $inspector->setTransport(function ($configuration) { 101 | return new CustomTransport($configuration); 102 | }); 103 | ``` 104 | 105 | **[Chek out the official documentation](https://docs.inspector.dev/php)** 106 | 107 | ## Contributing 108 | 109 | We encourage you to contribute to Inspector! Please check out the [Contribution Guidelines](CONTRIBUTING.md) about how to proceed. Join us! 110 | 111 | ## LICENSE 112 | 113 | This package is licensed under the [MIT](LICENSE) license. 114 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "inspector-apm/inspector-php", 3 | "description": "Inspector for PHP applications.", 4 | "keywords": ["monitoring", "php", "inspector"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Valerio Barbera", 9 | "email": "valerio@inspector.dev" 10 | } 11 | ], 12 | "require": { 13 | "php": ">=7.2" 14 | }, 15 | "autoload": { 16 | "psr-4": { 17 | "Inspector\\": "src/" 18 | } 19 | }, 20 | "config": { 21 | "sort-packages": true, 22 | "preferred-install": "dist" 23 | }, 24 | "minimum-stability": "dev", 25 | "prefer-stable": true, 26 | "require-dev": { 27 | "phpunit/phpunit": "^9.0", 28 | "inspector-apm/neuron-ai": "^1.2.22" 29 | }, 30 | "autoload-dev": { 31 | "psr-4": { 32 | "Inspector\\Tests\\": "tests/" 33 | } 34 | }, 35 | "scripts": { 36 | "test:unit": "phpunit --colors=always", 37 | "test": [ 38 | "@test:unit" 39 | ] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Configuration.php: -------------------------------------------------------------------------------- 1 | setIngestionKey($ingestionKey); 61 | } 62 | } 63 | 64 | /** 65 | * Max size of a POST request content. 66 | */ 67 | public function getMaxPostSize(): int 68 | { 69 | return /*OS::isWin() ? 8000 :*/ 65536; 70 | } 71 | 72 | /** 73 | * Set the remote url. 74 | * 75 | * @param string $value 76 | * @return $this 77 | * @throws \InvalidArgumentException 78 | */ 79 | public function setUrl($value): Configuration 80 | { 81 | $value = \trim($value); 82 | 83 | if (empty($value)) { 84 | throw new \InvalidArgumentException('URL can not be empty'); 85 | } 86 | 87 | if (filter_var($value, FILTER_VALIDATE_URL) === false) { 88 | throw new \InvalidArgumentException('URL is invalid'); 89 | } 90 | 91 | $this->url = $value; 92 | return $this; 93 | } 94 | 95 | /** 96 | * Get the remote url. 97 | */ 98 | public function getUrl(): string 99 | { 100 | return $this->url; 101 | } 102 | 103 | /** 104 | * Verify if api key is well formed. 105 | * 106 | * @param string $value 107 | * @return $this 108 | * @throws \InvalidArgumentException 109 | */ 110 | public function setIngestionKey($value): Configuration 111 | { 112 | $value = \trim($value); 113 | 114 | if (empty($value)) { 115 | throw new \InvalidArgumentException('Ingestion key cannot be empty'); 116 | } 117 | 118 | $this->ingestionKey = $value; 119 | return $this; 120 | } 121 | 122 | /** 123 | * Get the current API key. 124 | */ 125 | public function getIngestionKey(): string 126 | { 127 | return $this->ingestionKey; 128 | } 129 | 130 | public function getMaxItems(): int 131 | { 132 | return $this->maxItems; 133 | } 134 | 135 | /** 136 | * @param int $maxItems 137 | * @return $this 138 | */ 139 | public function setMaxItems(int $maxItems): Configuration 140 | { 141 | $this->maxItems = $maxItems; 142 | return $this; 143 | } 144 | 145 | public function getOptions(): array 146 | { 147 | return $this->options; 148 | } 149 | 150 | /** 151 | * Add a key-value pair to the options list. 152 | * 153 | * @param string $key 154 | * @param mixed $value 155 | * @return $this 156 | */ 157 | public function addOption($key, $value): Configuration 158 | { 159 | $this->options[$key] = $value; 160 | return $this; 161 | } 162 | 163 | /** 164 | * Override the entire options. 165 | * 166 | * @param array $options 167 | * @return $this 168 | */ 169 | public function setOptions(array $options): Configuration 170 | { 171 | $this->options = $options; 172 | return $this; 173 | } 174 | 175 | /** 176 | * Check if data transfer is enabled. 177 | */ 178 | public function isEnabled(): bool 179 | { 180 | return isset($this->ingestionKey) && \is_string($this->ingestionKey) && $this->enabled; 181 | } 182 | 183 | /** 184 | * Enable/Disable data transfer. 185 | * 186 | * @param bool $enabled 187 | * @return $this 188 | */ 189 | public function setEnabled(bool $enabled): Configuration 190 | { 191 | $this->enabled = $enabled; 192 | return $this; 193 | } 194 | 195 | /** 196 | * Get current transport method. 197 | */ 198 | public function getTransport(): string 199 | { 200 | return $this->transport; 201 | } 202 | 203 | /** 204 | * Set the preferred transport method. 205 | * 206 | * @param string $transport 207 | * @return $this 208 | */ 209 | public function setTransport(string $transport): Configuration 210 | { 211 | $this->transport = $transport; 212 | return $this; 213 | } 214 | 215 | /** 216 | * Get the package version. 217 | */ 218 | public function getVersion(): string 219 | { 220 | return $this->version; 221 | } 222 | 223 | /** 224 | * Set the package version. 225 | * 226 | * @param string $value 227 | * @return $this 228 | */ 229 | public function setVersion($value): Configuration 230 | { 231 | $this->version = $value; 232 | return $this; 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /src/Exceptions/InspectorException.php: -------------------------------------------------------------------------------- 1 | getTransport()) { 55 | case 'async': 56 | $this->transport = new AsyncTransport($configuration); 57 | break; 58 | default: 59 | $this->transport = new CurlTransport($configuration); 60 | } 61 | 62 | $this->configuration = $configuration; 63 | \register_shutdown_function(array($this, 'flush')); 64 | } 65 | 66 | /** 67 | * Set custom transport. 68 | * 69 | * @param TransportInterface|callable $transport 70 | * @return $this 71 | * @throws InspectorException 72 | */ 73 | public function setTransport($resolver) 74 | { 75 | if (\is_callable($resolver)) { 76 | $this->transport = $resolver($this->configuration); 77 | } elseif ($resolver instanceof TransportInterface) { 78 | $this->transport = $resolver; 79 | } else { 80 | throw new InspectorException('Invalid transport resolver.'); 81 | } 82 | 83 | return $this; 84 | } 85 | 86 | /** 87 | * Create and start new Transaction. 88 | * 89 | * @param string $name 90 | * @return Transaction 91 | * @throws \Exception 92 | */ 93 | public function startTransaction($name): Transaction 94 | { 95 | $this->transaction = new Transaction($name); 96 | $this->transaction->start(); 97 | 98 | $this->addEntries($this->transaction); 99 | return $this->transaction; 100 | } 101 | 102 | /** 103 | * Get current transaction instance. 104 | * 105 | * @deprecated 106 | * @return null|Transaction 107 | */ 108 | public function currentTransaction(): ?Transaction 109 | { 110 | return $this->transaction; 111 | } 112 | 113 | /** 114 | * Get current transaction instance. 115 | * 116 | * @return null|Transaction 117 | */ 118 | public function transaction(): ?Transaction 119 | { 120 | return $this->transaction; 121 | } 122 | 123 | /** 124 | * Determine if an active transaction exists. 125 | * 126 | * @return bool 127 | */ 128 | public function hasTransaction(): bool 129 | { 130 | return isset($this->transaction); 131 | } 132 | 133 | /** 134 | * Determine if the current cycle hasn't started its transaction yet. 135 | * 136 | * @return bool 137 | */ 138 | public function needTransaction(): bool 139 | { 140 | return $this->isRecording() && !$this->hasTransaction(); 141 | } 142 | 143 | /** 144 | * Determine if a new segment can be added. 145 | * 146 | * @return bool 147 | */ 148 | public function canAddSegments(): bool 149 | { 150 | return $this->isRecording() && $this->hasTransaction(); 151 | } 152 | 153 | /** 154 | * Check if the monitoring is enabled. 155 | * 156 | * @return bool 157 | */ 158 | public function isRecording(): bool 159 | { 160 | return $this->configuration->isEnabled(); 161 | } 162 | 163 | /** 164 | * Enable recording. 165 | * 166 | * @return Inspector 167 | */ 168 | public function startRecording() 169 | { 170 | $this->configuration->setEnabled(true); 171 | return $this; 172 | } 173 | 174 | /** 175 | * Stop recording. 176 | * 177 | * @return Inspector 178 | */ 179 | public function stopRecording() 180 | { 181 | $this->configuration->setEnabled(false); 182 | return $this; 183 | } 184 | 185 | /** 186 | * Add a new segment to the queue. 187 | * 188 | * @param string $type 189 | * @param null|string $label 190 | * @return Segment 191 | */ 192 | public function startSegment($type, $label = null) 193 | { 194 | $segment = new Segment($this->transaction, addslashes($type), $label); 195 | $segment->start(); 196 | 197 | $this->addEntries($segment); 198 | return $segment; 199 | } 200 | 201 | /** 202 | * Monitor the execution of a code block. 203 | * 204 | * @param callable $callback 205 | * @param string $type 206 | * @param null|string $label 207 | * @param bool $throw 208 | * @return mixed|void 209 | * @throws \Throwable 210 | */ 211 | public function addSegment(callable $callback, string $type, $label = null, $throw = true) 212 | { 213 | if (!$this->hasTransaction()) { 214 | return $callback(); 215 | } 216 | 217 | try { 218 | $segment = $this->startSegment($type, $label); 219 | return $callback($segment); 220 | } catch (\Throwable $exception) { 221 | if ($throw === true) { 222 | throw $exception; 223 | } 224 | 225 | $this->reportException($exception); 226 | } finally { 227 | $segment->end(); 228 | } 229 | } 230 | 231 | /** 232 | * Error reporting. 233 | * 234 | * @param \Throwable $exception 235 | * @param bool $handled 236 | * @return Error 237 | * @throws \Exception 238 | */ 239 | public function reportException(\Throwable $exception, $handled = true) 240 | { 241 | if (!$this->hasTransaction()) { 242 | $this->startTransaction(get_class($exception))->setType('error'); 243 | } 244 | 245 | $segment = $this->startSegment('exception', $exception->getMessage()); 246 | 247 | $error = (new Error($exception, $this->transaction)) 248 | ->setHandled($handled); 249 | 250 | $this->addEntries($error); 251 | 252 | $segment->addContext('Error', $error)->end(); 253 | 254 | return $error; 255 | } 256 | 257 | /** 258 | * Add an entry to the queue. 259 | * 260 | * @param Arrayable[]|Arrayable $entries 261 | * @return Inspector 262 | */ 263 | public function addEntries($entries) 264 | { 265 | if ($this->isRecording()) { 266 | $entries = \is_array($entries) ? $entries : [$entries]; 267 | foreach ($entries as $entry) { 268 | $this->transport->addEntry($entry); 269 | } 270 | } 271 | return $this; 272 | } 273 | 274 | /** 275 | * Define a callback to run before flushing data to the remote platform. 276 | * 277 | * @param callable $callback 278 | */ 279 | public static function beforeFlush(callable $callback) 280 | { 281 | static::$beforeCallbacks[] = $callback; 282 | } 283 | 284 | /** 285 | * Flush data to the remote platform. 286 | * 287 | * @throws \Exception 288 | */ 289 | public function flush() 290 | { 291 | if (!$this->isRecording() || !$this->hasTransaction()) { 292 | $this->reset(); 293 | return; 294 | } 295 | 296 | if (!$this->transaction->isEnded()) { 297 | $this->transaction->end(); 298 | } 299 | 300 | foreach (static::$beforeCallbacks as $callback) { 301 | if (\call_user_func($callback, $this) === false) { 302 | $this->reset(); 303 | return; 304 | } 305 | } 306 | 307 | $this->transport->flush(); 308 | unset($this->transaction); 309 | } 310 | 311 | /** 312 | * Cancel the current transaction, segments, and errors. 313 | * 314 | * @return Inspector 315 | */ 316 | public function reset() 317 | { 318 | $this->transport->resetQueue(); 319 | unset($this->transaction); 320 | return $this; 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /src/Models/Arrayable.php: -------------------------------------------------------------------------------- 1 | $key; 28 | } 29 | return $arr; 30 | } 31 | 32 | /** 33 | * Make it compatible to work with php native array functions. 34 | * 35 | * @return array 36 | */ 37 | public function &__invoke() 38 | { 39 | return $this->data; 40 | } 41 | 42 | /** 43 | * Get a data by key 44 | * 45 | * @param string $key The key data to retrieve 46 | * @return mixed 47 | */ 48 | public function &__get($key) 49 | { 50 | return $this->data[$key]; 51 | } 52 | 53 | /** 54 | * Assigns a value to the specified data 55 | * 56 | * @param string $key The data key to assign the value to 57 | * @param mixed $value The value to set 58 | * @access public 59 | */ 60 | public function __set($key, $value) 61 | { 62 | $this->data[$key] = $value; 63 | } 64 | 65 | /** 66 | * Whether or not data exists by key 67 | * 68 | * @param string $key An data key to check for 69 | * @access public 70 | * @return boolean 71 | * @abstracting ArrayAccess 72 | */ 73 | public function __isset($key) 74 | { 75 | return isset($this->data[$key]); 76 | } 77 | 78 | /** 79 | * Unsets a data by key 80 | * 81 | * @param string $key The key to unset 82 | * @access public 83 | */ 84 | public function __unset($key) 85 | { 86 | unset($this->data[$key]); 87 | } 88 | 89 | /** 90 | * Assigns a value to the specified offset. 91 | * 92 | * @param string $offset The offset to assign the value to 93 | * @param mixed $value The value to set 94 | * @abstracting ArrayAccess 95 | */ 96 | #[\ReturnTypeWillChange] 97 | public function offsetSet($offset, $value) 98 | { 99 | if (\is_null($offset)) { 100 | $this->data[] = $value; 101 | } else { 102 | $this->data[$offset] = $value; 103 | } 104 | } 105 | 106 | /** 107 | * Whether an offset exists. 108 | * 109 | * @param string $offset An offset to check for 110 | * @return boolean 111 | * @abstracting ArrayAccess 112 | */ 113 | #[\ReturnTypeWillChange] 114 | public function offsetExists($offset) 115 | { 116 | return isset($this->data[$offset]); 117 | } 118 | 119 | /** 120 | * Unsets an offset. 121 | * 122 | * @param string $offset The offset to unset 123 | * @abstracting ArrayAccess 124 | */ 125 | #[\ReturnTypeWillChange] 126 | public function offsetUnset($offset) 127 | { 128 | if ($this->offsetExists($offset)) { 129 | unset($this->data[$offset]); 130 | } 131 | } 132 | 133 | /** 134 | * Returns the value at specified offset. 135 | * 136 | * @param string $offset The offset to retrieve 137 | * @return mixed 138 | * @abstracting ArrayAccess 139 | */ 140 | #[\ReturnTypeWillChange] 141 | public function offsetGet($offset) 142 | { 143 | return $this->offsetExists($offset) ? $this->data[$offset] : null; 144 | } 145 | 146 | /** 147 | * Json String representation of the object. 148 | * 149 | * @return false|string 150 | */ 151 | public function __toString() 152 | { 153 | return \json_encode($this->jsonSerialize()); 154 | } 155 | 156 | /** 157 | * Specify data which should be serialized to JSON. 158 | * 159 | * @link https://php.net/manual/en/jsonserializable.jsonserialize.php 160 | * 161 | * @return array data which can be serialized by json_encode, 162 | * which is a value of any type other than a resource. 163 | * @since 5.4 164 | */ 165 | public function jsonSerialize(): array 166 | { 167 | return \array_filter($this->toArray(), function ($value) { 168 | // remove NULL, FALSE, empty strings and empty arrays, but keep values of 0 (zero) 169 | return \is_array($value) ? !empty($value) : \strlen($value??''); 170 | }); 171 | } 172 | 173 | /** 174 | * Array representation of the object. 175 | * 176 | * @return array 177 | */ 178 | public function toArray(): array 179 | { 180 | return $this->data; 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/Models/Error.php: -------------------------------------------------------------------------------- 1 | model = self::MODEL_NAME; 26 | $this->timestamp = microtime(true); 27 | 28 | $this->host = new Host(); 29 | 30 | $this->message = $throwable->getMessage() 31 | ? $throwable->getMessage() 32 | : get_class($throwable); 33 | 34 | $this->class = get_class($throwable); 35 | $this->file = $throwable->getFile(); 36 | $this->line = $throwable->getLine(); 37 | $this->code = $throwable->getCode(); 38 | 39 | $this->stack = $this->stackTraceToArray($throwable); 40 | 41 | $this->transaction = $transaction->only(['name', 'hash']); 42 | } 43 | 44 | /** 45 | * Determine if the exception is handled/unhandled. 46 | * 47 | * @param bool $value 48 | * @return $this 49 | */ 50 | public function setHandled(bool $value) 51 | { 52 | $this->handled = $value; 53 | return $this; 54 | } 55 | 56 | /** 57 | * Serialize stack trace to array 58 | * 59 | * @param \Throwable $throwable 60 | * @return array 61 | */ 62 | public function stackTraceToArray(\Throwable $throwable) 63 | { 64 | $stack = []; 65 | $counter = 0; 66 | 67 | // Exception object `getTrace` does not return file and line number for the first line 68 | // http://php.net/manual/en/exception.gettrace.php#107563 69 | 70 | $inApp = function ($file) { 71 | return \strpos($file, 'vendor') === false && 72 | \strpos($file, 'index.php') === false && 73 | \strpos($file, 'web/core') === false; // Drupal 74 | }; 75 | 76 | $stack[] = [ 77 | 'file' => $throwable->getFile(), 78 | 'line' => $throwable->getLine(), 79 | 'in_app' => $inApp($throwable->getFile()), 80 | 'code' => $this->getCode($throwable->getFile(), $throwable->getLine(), $inApp($throwable->getFile()) ? 15 : 5), 81 | ]; 82 | 83 | foreach ($throwable->getTrace() as $trace) { 84 | $stack[] = [ 85 | 'class' => isset($trace['class']) ? $trace['class'] : null, 86 | 'function' => isset($trace['function']) ? $trace['function'] : null, 87 | 'args' => $this->stackTraceArgsToArray($trace), 88 | 'type' => $trace['type'] ?? 'function', 89 | 'file' => $trace['file'] ?? '[internal]', 90 | 'line' => $trace['line'] ?? '0', 91 | 'code' => isset($trace['file']) 92 | ? $this->getCode($trace['file'], $trace['line'] ?? '0', $inApp($trace['file']) ? 15 : 5) 93 | : [], 94 | 'in_app' => isset($trace['file']) && $inApp($trace['file']), 95 | ]; 96 | 97 | // Reporting limit 98 | if (++$counter >= 50) { 99 | break; 100 | } 101 | } 102 | 103 | return $stack; 104 | } 105 | 106 | /** 107 | * Serialize stack trace function arguments 108 | * 109 | * @param array $trace 110 | * @return array 111 | */ 112 | protected function stackTraceArgsToArray(array $trace) 113 | { 114 | $params = []; 115 | 116 | if (!isset($trace['args'])) { 117 | return $params; 118 | } 119 | 120 | foreach ($trace['args'] as $arg) { 121 | if (\is_array($arg)) { 122 | $params[] = 'array(' . \count($arg) . ')'; 123 | } else if (\is_object($arg)) { 124 | $params[] = \get_class($arg); 125 | } else if (\is_string($arg)) { 126 | $params[] = 'string(' . $arg . ')'; 127 | } else if (\is_int($arg)) { 128 | $params[] = 'int(' . $arg . ')'; 129 | } else if (\is_float($arg)) { 130 | $params[] = 'float(' . $arg . ')'; 131 | } else if (\is_bool($arg)) { 132 | $params[] = 'bool(' . ($arg ? 'true' : 'false') . ')'; 133 | } else if ($arg instanceof \__PHP_Incomplete_Class) { 134 | $params[] = 'object(__PHP_Incomplete_Class)'; 135 | } else { 136 | $params[] = \gettype($arg); 137 | } 138 | } 139 | 140 | return $params; 141 | } 142 | 143 | /** 144 | * Extract a code source from file. 145 | * 146 | * @param $filePath 147 | * @param $line 148 | * @param int $linesAround 149 | * @return mixed 150 | */ 151 | public function getCode($filePath, $line, $linesAround = 5) 152 | { 153 | try { 154 | $file = new \SplFileObject($filePath); 155 | $file->setMaxLineLen(250); 156 | $file->seek(PHP_INT_MAX); 157 | 158 | $codeLines = []; 159 | 160 | $from = \max(0, $line - $linesAround); 161 | $to = \min($line + $linesAround, $file->key()); 162 | 163 | $file->seek($from); 164 | 165 | while ($file->key() <= $to && !$file->eof()) { 166 | $codeLines[] = [ 167 | 'line' => $file->key()+1, 168 | 'code' => \rtrim($file->current()), 169 | ]; 170 | $file->next(); 171 | } 172 | 173 | return $codeLines; 174 | } catch (\Exception $e) { 175 | return null; 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/Models/HasContext.php: -------------------------------------------------------------------------------- 1 | context[$key] = $data; 20 | 21 | return $this; 22 | } 23 | 24 | /** 25 | * Set the entire context bag. 26 | * 27 | * @param array $data 28 | * @return $this 29 | */ 30 | public function setContext(array $data) 31 | { 32 | $this->context = $data; 33 | return $this; 34 | } 35 | 36 | /** 37 | * Get context items. 38 | * 39 | * @param string|null $label 40 | * @return mixed 41 | */ 42 | public function getContext(?string $label = null) 43 | { 44 | if (\is_string($label)) { 45 | return $this->context[$label]; 46 | } 47 | 48 | return $this->context; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Models/Partials/Host.php: -------------------------------------------------------------------------------- 1 | hostname = \gethostname(); 18 | $this->ip = \gethostbyname(\gethostname()); 19 | $this->os = PHP_OS_FAMILY; 20 | } 21 | 22 | /** 23 | * Collect server status information. 24 | * 25 | * @deprecated It's not used anymore but it's interesting to take this script in mind for future use cases. 26 | * 27 | * @return $this 28 | */ 29 | public function withServerStatus() 30 | { 31 | if (OS::isLinux() && \function_exists('shell_exec')) { 32 | try { 33 | $status = \shell_exec('echo "`LC_ALL=C top -bn1 | grep "Cpu(s)" | sed "s/.*, *\([0-9.]*\)%* id.*/\1/" | awk \'{print 100 - $1}\'`%;`free -m | awk \'/Mem:/ { printf("%3.1f%%", $3/$2*100) }\'`;`df -h / | awk \'/\// {print $(NF-1)}\'`"'); 34 | $status = \str_replace('%', '', $status); 35 | $status = \str_replace("\n", '', $status); 36 | 37 | $status = \explode(';', $status); 38 | 39 | $this->cpu = $status[0]; 40 | $this->ram = $status[1]; 41 | $this->hdd = $status[2]; 42 | } catch (\Throwable $exception) { 43 | // do nothing 44 | } 45 | } 46 | 47 | return $this; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Models/Partials/Http.php: -------------------------------------------------------------------------------- 1 | request = new Request(); 17 | $this->url = new Url(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Models/Partials/Request.php: -------------------------------------------------------------------------------- 1 | method = $_SERVER['REQUEST_METHOD']??null; 17 | 18 | $this->version = isset($_SERVER['SERVER_PROTOCOL']) 19 | ? \substr($_SERVER['SERVER_PROTOCOL'], \strpos($_SERVER['SERVER_PROTOCOL'], '/')) 20 | : 'unknown'; 21 | 22 | $this->socket = new Socket(); 23 | 24 | $this->cookies = $_COOKIE; 25 | 26 | if (\function_exists('apache_request_headers')) { 27 | $h = \apache_request_headers(); 28 | 29 | if (\array_key_exists('sec-ch-ua', $h)) { 30 | unset($h['sec-ch-ua']); 31 | } 32 | 33 | $this->headers = $h; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Models/Partials/Socket.php: -------------------------------------------------------------------------------- 1 | remote_address = $_SERVER['REMOTE_ADDR'] ?? ''; 17 | 18 | if (\array_key_exists('HTTP_X_FORWARDED_FOR', $_SERVER) === true) { 19 | $this->remote_address = $_SERVER['HTTP_X_FORWARDED_FOR']; 20 | } 21 | 22 | $this->encrypted = isset($_SERVER['HTTPS']); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Models/Partials/Url.php: -------------------------------------------------------------------------------- 1 | protocol = isset($_SERVER['HTTPS']) ? 'https' : 'http'; 17 | $this->port = $_SERVER['SERVER_PORT'] ?? ''; 18 | $this->path = $_SERVER['SCRIPT_NAME'] ?? ''; 19 | $this->search = '?' . (($_SERVER['QUERY_STRING'] ?? '') ?? ''); 20 | $this->full = isset($_SERVER['HTTP_HOST']) ? $this->protocol . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] : ''; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Models/Partials/User.php: -------------------------------------------------------------------------------- 1 | id = $id; 21 | $this->name = $name; 22 | $this->email = $email; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Models/PerformanceModel.php: -------------------------------------------------------------------------------- 1 | timestamp = \is_null($timestamp) ? \microtime(true) : $timestamp; 20 | return $this; 21 | } 22 | 23 | /** 24 | * Stop the timer and calculate duration. 25 | * 26 | * @param float|null $duration milliseconds 27 | * @return PerformanceModel 28 | */ 29 | public function end($duration = null) 30 | { 31 | $this->duration = $duration ?? \round((\microtime(true) - $this->timestamp)*1000, 2); // milliseconds 32 | return $this; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Models/Segment.php: -------------------------------------------------------------------------------- 1 | model = self::MODEL_NAME; 22 | $this->type = $type; 23 | $this->label = $label; 24 | $this->host = new Host(); 25 | $this->transaction = $transaction->only(['name', 'hash', 'timestamp']); 26 | } 27 | 28 | /** 29 | * Start the timer. 30 | * 31 | * @param null|float $time 32 | * @return $this 33 | */ 34 | public function start($time = null) 35 | { 36 | $initial = \is_null($time) ? \microtime(true) : $time; 37 | 38 | $this->start = \round(($initial - $this->transaction['timestamp'])*1000, 2); 39 | return parent::start($time); 40 | } 41 | 42 | public function setColor(string $color): Segment 43 | { 44 | $this->color = $color; 45 | return $this; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Models/Transaction.php: -------------------------------------------------------------------------------- 1 | model = self::MODEL_NAME; 26 | $this->name = $name; 27 | $this->type = 'transaction'; 28 | $this->hash = $this->generateUniqueHash(); 29 | $this->host = new Host(); 30 | } 31 | 32 | /** 33 | * Mark the current transaction as an HTTP request. 34 | * 35 | * @return $this 36 | */ 37 | public function markAsRequest() 38 | { 39 | $this->setType('request'); 40 | $this->http = new Http(); 41 | return $this; 42 | } 43 | 44 | /** 45 | * Set the type to categorize the transaction. 46 | * 47 | * @param string $type 48 | * @return $this 49 | */ 50 | public function setType(string $type) 51 | { 52 | $this->type = $type; 53 | return $this; 54 | } 55 | 56 | /** 57 | * Attach user information. 58 | * 59 | * @param integer|string $id 60 | * @param null|string $name 61 | * @param null|string $email 62 | * @return $this 63 | */ 64 | public function withUser($id, $name = null, $email = null) 65 | { 66 | $this->user = new User($id, $name, $email); 67 | return $this; 68 | } 69 | 70 | /** 71 | * Set a string representation of a transaction result (e.g. 'error', 'success', 'ok', '200', etc...). 72 | * 73 | * @param string $result 74 | * @return Transaction 75 | */ 76 | public function setResult(string $result): Transaction 77 | { 78 | $this->result = $result; 79 | return $this; 80 | } 81 | 82 | public function end($duration = null) 83 | { 84 | // Sample memory peak at the end of execution. 85 | $this->memory_peak = $this->getMemoryPeak(); 86 | return parent::end($duration); 87 | } 88 | 89 | public function isEnded() 90 | { 91 | return isset($this->duration) && $this->duration > 0; 92 | } 93 | 94 | public function getMemoryPeak(): float 95 | { 96 | return \round((\memory_get_peak_usage()/1024/1024), 2); // MB 97 | } 98 | 99 | /** 100 | * Generate a unique transaction hash. 101 | * 102 | * http://www.php.net/manual/en/function.uniqid.php 103 | * 104 | * @param int $length 105 | * @return string 106 | * @throws \Exception 107 | */ 108 | public function generateUniqueHash($length = 32) 109 | { 110 | if (!isset($length) || \intval($length) <= 8) { 111 | $length = 32; 112 | } 113 | 114 | if (\function_exists('random_bytes')) { 115 | return \bin2hex(random_bytes($length)); 116 | } elseif (\function_exists('openssl_random_pseudo_bytes')) { 117 | return \bin2hex(\openssl_random_pseudo_bytes($length)); 118 | } 119 | 120 | throw new InspectorException('Can\'t create unique transaction hash.'); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/OS.php: -------------------------------------------------------------------------------- 1 | config = $configuration; 37 | $this->verifyOptions($configuration->getOptions()); 38 | } 39 | 40 | /** 41 | * Verify if given options match constraints. 42 | * 43 | * @param $options 44 | * @throws InspectorException 45 | */ 46 | protected function verifyOptions($options) 47 | { 48 | foreach ($this->getAllowedOptions() as $name => $regex) { 49 | if (isset($options[$name])) { 50 | $value = $options[$name]; 51 | if (!\preg_match($regex, $value)) { 52 | throw new InspectorException("Option '$name' has invalid value"); 53 | } 54 | } 55 | } 56 | } 57 | 58 | /** 59 | * Get the current queue. 60 | * 61 | * @return array 62 | */ 63 | public function getQueue(): array 64 | { 65 | return $this->queue; 66 | } 67 | 68 | /** 69 | * Empty the queue. 70 | * 71 | * @return $this 72 | */ 73 | public function resetQueue() 74 | { 75 | $this->queue = []; 76 | return $this; 77 | } 78 | 79 | /** 80 | * Add a message to the queue. 81 | * 82 | * @param array|Arrayable $item 83 | * @return TransportInterface 84 | */ 85 | public function addEntry($item): TransportInterface 86 | { 87 | // Force insert when dealing with errors. 88 | if($item['model'] === Error::MODEL_NAME || \count($this->queue) <= $this->config->getMaxItems()) { 89 | $this->queue[] = $item; 90 | } 91 | return $this; 92 | } 93 | 94 | /** 95 | * Deliver everything on the queue to LOG Engine. 96 | * 97 | * @return void 98 | */ 99 | public function flush() 100 | { 101 | if (empty($this->queue)) { 102 | return; 103 | } 104 | 105 | $this->send($this->queue); 106 | 107 | $this->resetQueue(); 108 | } 109 | 110 | /** 111 | * Send data chunks based on MAX_POST_LENGTH. 112 | * 113 | * @param array $items 114 | * @return void 115 | */ 116 | public function send($items) 117 | { 118 | $json = \json_encode($items); 119 | $jsonLength = \strlen($json); 120 | $count = \count($items); 121 | 122 | if ($jsonLength > $this->config->getMaxPostSize()) { 123 | if ($count === 1) { 124 | // It makes no sense to divide into chunks, just try to send data via file 125 | return $this->sendViaFile(\base64_encode($json)); 126 | } 127 | 128 | $chunkSize = \floor($count / \ceil($jsonLength / $this->config->getMaxPostSize())); 129 | $chunks = \array_chunk($items, $chunkSize > 0 ? $chunkSize : 1); 130 | 131 | foreach ($chunks as $chunk) { 132 | $this->send($chunk); 133 | } 134 | } else { 135 | $this->sendChunk(\base64_encode($json)); 136 | } 137 | } 138 | 139 | /** 140 | * Put data into a file and provide CURL with the file path. 141 | * 142 | * @param string $data 143 | * @return void 144 | */ 145 | protected function sendViaFile($data) 146 | { 147 | $tmpfile = tempnam(sys_get_temp_dir(), 'inspector'); 148 | 149 | file_put_contents($tmpfile, $data, LOCK_EX); 150 | 151 | $this->sendChunk('@'.$tmpfile); 152 | } 153 | 154 | /** 155 | * Send a portion of the load to the remote service. 156 | * 157 | * @param string $data 158 | * @return void 159 | */ 160 | abstract protected function sendChunk($data); 161 | 162 | /** 163 | * List of available transport's options with validation regex. 164 | * 165 | * ['param-name' => 'regex'] 166 | * 167 | * @return mixed 168 | */ 169 | protected function getAllowedOptions() 170 | { 171 | return [ 172 | 'proxy' => '/.+/', // Custom url for 173 | 'debug' => '/^(0|1)?$/', // boolean 174 | ]; 175 | } 176 | 177 | /** 178 | * @return array 179 | */ 180 | protected function getApiHeaders() 181 | { 182 | return [ 183 | 'Content-Type' => 'application/json', 184 | 'Accept' => 'application/json', 185 | 'X-Inspector-Key' => $this->config->getIngestionKey(), 186 | 'X-Inspector-Version' => $this->config->getVersion(), 187 | ]; 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/Transports/AsyncTransport.php: -------------------------------------------------------------------------------- 1 | 'regex'] 32 | * 33 | * Override to introduce "curlPath". 34 | * 35 | * @return array 36 | */ 37 | protected function getAllowedOptions() 38 | { 39 | return \array_merge(parent::getAllowedOptions(), [ 40 | 'curlPath' => '/.+/', 41 | ]); 42 | } 43 | 44 | /** 45 | * Send a portion of the load to the remote service. 46 | * 47 | * @param string $data 48 | * @return void|mixed 49 | */ 50 | public function sendChunk($data) 51 | { 52 | $curl = $this->buildCurlCommand($data); 53 | 54 | if (OS::isWin()) { 55 | $cmd = "start /B {$curl} > NUL"; 56 | } else { 57 | $cmd = "({$curl} > /dev/null 2>&1"; 58 | 59 | // Delete temporary file after data transfer 60 | if (\substr($data, 0, 1) === '@') { 61 | $cmd.= '; rm ' . \str_replace('@', '', $data); 62 | } 63 | 64 | $cmd .= ')&'; 65 | } 66 | 67 | \proc_close(\proc_open($cmd, [], $pipes)); 68 | } 69 | 70 | /** 71 | * Carl command is agnostic between Win and Unix. 72 | * 73 | * @param $data 74 | * @return string 75 | */ 76 | protected function buildCurlCommand($data): string 77 | { 78 | $curl = $this->config->getOptions()['curlPath'] ?? 'curl'; 79 | 80 | $curl .= " -X POST --ipv4 --max-time 5"; 81 | 82 | foreach ($this->getApiHeaders() as $name => $value) { 83 | $curl .= " --header \"$name: $value\""; 84 | } 85 | 86 | $curl .= " --data {$data} {$this->config->getUrl()}"; 87 | 88 | if (\array_key_exists('proxy', $this->config->getOptions())) { 89 | $curl .= " --proxy \"{$this->config->getOptions()['proxy']}\""; 90 | } 91 | 92 | return $curl; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Transports/CurlTransport.php: -------------------------------------------------------------------------------- 1 | getApiHeaders() as $name => $value) { 37 | $headers[] = "$name: $value"; 38 | } 39 | 40 | $handle = \curl_init($this->config->getUrl()); 41 | 42 | \curl_setopt($handle, CURLOPT_POST, 1); 43 | 44 | // Tell cURL that it should only spend 10 seconds trying to connect to the URL in question. 45 | \curl_setopt($handle, CURLOPT_CONNECTTIMEOUT, 5); 46 | // A given cURL operation should only take 30 seconds max. 47 | curl_setopt($handle, CURLOPT_TIMEOUT, 10); 48 | 49 | \curl_setopt($handle, CURLOPT_HTTPHEADER, $headers); 50 | \curl_setopt($handle, CURLOPT_POSTFIELDS, $data); 51 | \curl_setopt($handle, CURLOPT_RETURNTRANSFER, true); 52 | \curl_setopt($handle, CURLOPT_SSL_VERIFYHOST, false); 53 | \curl_setopt($handle, CURLOPT_SSL_VERIFYPEER, true); 54 | 55 | if (\array_key_exists('proxy', $this->config->getOptions())) { 56 | \curl_setopt($handle, CURLOPT_PROXY, $this->config->getOptions()['proxy']); 57 | } 58 | 59 | $response = \curl_exec($handle); 60 | $errorNo = \curl_errno($handle); 61 | $code = \curl_getinfo($handle, CURLINFO_HTTP_CODE); 62 | $error = \curl_error($handle); 63 | 64 | // 200 OK 65 | // 403 Account has reached no. transactions limit 66 | if (0 !== $errorNo || (200 !== $code && 201 !== $code && 403 !== $code)) { 67 | \error_log(\date('Y-m-d H:i:s') . " - [Warning] [" . \get_class($this) . "] $error - $code $errorNo"); 68 | } 69 | 70 | \curl_close($handle); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Transports/TransportInterface.php: -------------------------------------------------------------------------------- 1 |