├── LICENSE ├── README.md ├── composer.json └── src ├── Configuration.php ├── Exceptions └── InspectorException.php ├── Inspector.php ├── Models ├── Error.php ├── HasContext.php ├── Model.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 >= ^8.1 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 represents an execution cycle, and it can contain 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 a 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\Model $entry) 74 | { 75 | // Add an \Inspector\Models\Model 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 that will receive the current configuration state as parameter. 98 | 99 | ```php 100 | $inspector->setTransport(function (\Inspector\Configuration $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 monitoring for PHP applications.", 4 | "keywords": ["monitoring", "php", "inspector", "observability", "telemetry"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Valerio Barbera", 9 | "email": "valerio@inspector.dev" 10 | } 11 | ], 12 | "require": { 13 | "php": "^8.1" 14 | }, 15 | "autoload": { 16 | "psr-4": { 17 | "Inspector\\": "src/" 18 | } 19 | }, 20 | "config": { 21 | "sort-packages": true, 22 | "preferred-install": "dist", 23 | "platform": { 24 | "php": "8.1" 25 | } 26 | }, 27 | "minimum-stability": "stable", 28 | "require-dev": { 29 | "ext-curl": "*", 30 | "friendsofphp/php-cs-fixer": "^3.75", 31 | "inspector-apm/neuron-ai": "^1.2.22", 32 | "phpunit/phpunit": "^9.0", 33 | "phpstan/phpstan": "^2.1", 34 | "rector/rector": "^2.0", 35 | "tomasvotruba/type-coverage": "^2.0" 36 | }, 37 | "autoload-dev": { 38 | "psr-4": { 39 | "Inspector\\Tests\\": "tests/" 40 | } 41 | }, 42 | "scripts": { 43 | "analyse": [ 44 | "vendor/bin/phpstan analyse --memory-limit=1G -v" 45 | ], 46 | "format": [ 47 | "php-cs-fixer fix --allow-risky=yes" 48 | ], 49 | "test": [ 50 | "vendor/bin/phpunit --colors=always" 51 | ] 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Configuration.php: -------------------------------------------------------------------------------- 1 | setIngestionKey($ingestionKey); 39 | } 40 | } 41 | 42 | /** 43 | * Max size of a POST request content. 44 | */ 45 | public function getMaxPostSize(): int 46 | { 47 | return /*OS::isWin() ? 8000 :*/ 65536; 48 | } 49 | 50 | /** 51 | * Set the remote url. 52 | * 53 | * @throws \InvalidArgumentException 54 | */ 55 | public function setUrl(string $value): Configuration 56 | { 57 | $value = \trim($value); 58 | 59 | if (empty($value)) { 60 | throw new \InvalidArgumentException('URL can not be empty'); 61 | } 62 | 63 | if (\filter_var($value, \FILTER_VALIDATE_URL) === false) { 64 | throw new \InvalidArgumentException('URL is invalid'); 65 | } 66 | 67 | $this->url = $value; 68 | return $this; 69 | } 70 | 71 | /** 72 | * Get the remote url. 73 | */ 74 | public function getUrl(): string 75 | { 76 | return $this->url; 77 | } 78 | 79 | /** 80 | * Verify if api key is well-formed. 81 | * 82 | * @param string $value 83 | * @return $this 84 | * @throws \InvalidArgumentException 85 | */ 86 | public function setIngestionKey(string $value): Configuration 87 | { 88 | $value = \trim($value); 89 | 90 | if (empty($value)) { 91 | throw new \InvalidArgumentException('Ingestion key cannot be empty'); 92 | } 93 | 94 | $this->ingestionKey = $value; 95 | return $this; 96 | } 97 | 98 | /** 99 | * Get the current API key. 100 | */ 101 | public function getIngestionKey(): string 102 | { 103 | return $this->ingestionKey; 104 | } 105 | 106 | public function getMaxItems(): int 107 | { 108 | return $this->maxItems; 109 | } 110 | 111 | /** 112 | * @param int $maxItems 113 | * @return $this 114 | */ 115 | public function setMaxItems(int $maxItems): Configuration 116 | { 117 | $this->maxItems = $maxItems; 118 | return $this; 119 | } 120 | 121 | public function getOptions(): array 122 | { 123 | return $this->options; 124 | } 125 | 126 | /** 127 | * Add a key-value pair to the options list. 128 | * 129 | * @param string $key 130 | * @param mixed $value 131 | * @return $this 132 | */ 133 | public function addOption(string $key, mixed $value): Configuration 134 | { 135 | $this->options[$key] = $value; 136 | return $this; 137 | } 138 | 139 | /** 140 | * Override the entire options. 141 | * 142 | * @param array $options 143 | * @return $this 144 | */ 145 | public function setOptions(array $options): Configuration 146 | { 147 | $this->options = $options; 148 | return $this; 149 | } 150 | 151 | /** 152 | * Check if data transfer is enabled. 153 | */ 154 | public function isEnabled(): bool 155 | { 156 | return isset($this->ingestionKey) && $this->enabled; 157 | } 158 | 159 | /** 160 | * Enable/Disable data transfer. 161 | */ 162 | public function setEnabled(bool $enabled): Configuration 163 | { 164 | $this->enabled = $enabled; 165 | return $this; 166 | } 167 | 168 | /** 169 | * Get current transport method. 170 | */ 171 | public function getTransport(): string 172 | { 173 | return $this->transport; 174 | } 175 | 176 | /** 177 | * Set the preferred transport method. 178 | */ 179 | public function setTransport(string $transport): Configuration 180 | { 181 | $this->transport = $transport; 182 | return $this; 183 | } 184 | 185 | /** 186 | * Get the package version. 187 | */ 188 | public function getVersion(): ?string 189 | { 190 | return $this->version; 191 | } 192 | 193 | /** 194 | * Set the package version. 195 | */ 196 | public function setVersion(?string $value): Configuration 197 | { 198 | $this->version = $value; 199 | return $this; 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/Exceptions/InspectorException.php: -------------------------------------------------------------------------------- 1 | transport = match ($configuration->getTransport()) { 71 | 'async' => new AsyncTransport($configuration), 72 | default => new CurlTransport($configuration), 73 | }; 74 | 75 | $this->configuration = $configuration; 76 | \register_shutdown_function(array($this, 'flush')); 77 | } 78 | 79 | /** 80 | * Change the configuration instance. 81 | */ 82 | public function configure(callable $callback): Inspector 83 | { 84 | $callback($this->configuration, $this); 85 | 86 | return $this; 87 | } 88 | 89 | /** 90 | * Set custom transport. 91 | * 92 | * @throws InspectorException 93 | */ 94 | public function setTransport(TransportInterface|callable $resolver): Inspector 95 | { 96 | if (\is_callable($resolver)) { 97 | $this->transport = $resolver($this->configuration); 98 | } else { 99 | $this->transport = $resolver; 100 | } 101 | 102 | return $this; 103 | } 104 | 105 | /** 106 | * Create and start new Transaction. 107 | * 108 | * @throws \Exception 109 | */ 110 | public function startTransaction(string $name): Transaction 111 | { 112 | $this->transaction = new Transaction($name); 113 | $this->transaction->start(); 114 | 115 | // Clear any open segments from the previous transaction 116 | $this->openSegments = []; 117 | 118 | $this->addEntries($this->transaction); 119 | return $this->transaction; 120 | } 121 | 122 | /** 123 | * Get current transaction instance. 124 | * 125 | * @deprecated 126 | * @return null|Transaction 127 | */ 128 | public function currentTransaction(): ?Transaction 129 | { 130 | return $this->transaction; 131 | } 132 | 133 | /** 134 | * Get current transaction instance. 135 | * 136 | * @return null|Transaction 137 | */ 138 | public function transaction(): ?Transaction 139 | { 140 | return $this->transaction; 141 | } 142 | 143 | /** 144 | * Determine if an active transaction exists. 145 | * 146 | * @return bool 147 | */ 148 | public function hasTransaction(): bool 149 | { 150 | return isset($this->transaction); 151 | } 152 | 153 | /** 154 | * Determine if the current cycle hasn't started its transaction yet. 155 | * 156 | * @return bool 157 | */ 158 | public function needTransaction(): bool 159 | { 160 | return $this->isRecording() && !$this->hasTransaction(); 161 | } 162 | 163 | /** 164 | * Determine if a new segment can be added. 165 | * 166 | * @return bool 167 | */ 168 | public function canAddSegments(): bool 169 | { 170 | return $this->isRecording() && $this->hasTransaction(); 171 | } 172 | 173 | /** 174 | * Check if the monitoring is enabled. 175 | * 176 | * @return bool 177 | */ 178 | public function isRecording(): bool 179 | { 180 | return $this->configuration->isEnabled(); 181 | } 182 | 183 | /** 184 | * Enable recording. 185 | */ 186 | public function startRecording(): Inspector 187 | { 188 | $this->configuration->setEnabled(true); 189 | return $this; 190 | } 191 | 192 | /** 193 | * Stop recording. 194 | */ 195 | public function stopRecording(): Inspector 196 | { 197 | $this->configuration->setEnabled(false); 198 | return $this; 199 | } 200 | 201 | /** 202 | * Get the currently open parent segment, if any. 203 | */ 204 | protected function getCurrentParentSegment(): ?Segment 205 | { 206 | return $this->openSegments === [] ? null : \end($this->openSegments); 207 | } 208 | 209 | /** 210 | * Add a new segment to the queue. 211 | */ 212 | public function startSegment(string $type, ?string $label = null): Segment 213 | { 214 | $segment = new Segment($this->transaction, \addslashes($type), $label); 215 | 216 | // Set Inspector reference for lifecycle management 217 | $segment->setInspector($this); 218 | 219 | // Set a parent relationship if there's an open segment 220 | $parentSegment = $this->getCurrentParentSegment(); 221 | if ($parentSegment) { 222 | $segment->setParent($parentSegment->getHash()); 223 | } 224 | 225 | $segment->start(); 226 | 227 | // Add to open segments stack 228 | $this->openSegments[] = $segment; 229 | 230 | $this->addEntries($segment); 231 | return $segment; 232 | } 233 | 234 | /** 235 | * Monitor the execution of a code block. 236 | * 237 | * @throws \Throwable 238 | */ 239 | public function addSegment(callable $callback, string $type, ?string $label = null, bool $throw = true): mixed 240 | { 241 | if (!$this->hasTransaction()) { 242 | return $callback(); 243 | } 244 | 245 | $segment = $this->startSegment($type, $label); 246 | try { 247 | return $callback($segment); 248 | } catch (\Throwable $exception) { 249 | if ($throw === true) { 250 | throw $exception; 251 | } 252 | 253 | $this->reportException($exception); 254 | } finally { 255 | $segment->end(); 256 | } 257 | return null; 258 | } 259 | 260 | /** 261 | * Called by Segment when it ends to remove from open segments stack. 262 | * This maintains the parent-child relationship hierarchy. 263 | */ 264 | public function endSegment(Segment $segment): void 265 | { 266 | // Remove the segment from the open segments stack 267 | foreach ($this->openSegments as $index => $openSegment) { 268 | if ($openSegment === $segment) { 269 | unset($this->openSegments[$index]); 270 | // Re-index array to maintain proper stack behavior 271 | $this->openSegments = \array_values($this->openSegments); 272 | break; 273 | } 274 | } 275 | } 276 | 277 | /** 278 | * Error reporting. 279 | * 280 | * @throws \Exception 281 | */ 282 | public function reportException(\Throwable $exception, bool $handled = true): Error 283 | { 284 | if (!$this->hasTransaction()) { 285 | $this->startTransaction(\get_class($exception))->setType('error'); 286 | } 287 | 288 | $segment = $this->startSegment('exception', $exception->getMessage()); 289 | 290 | $error = (new Error($exception, $this->transaction)) 291 | ->setHandled($handled); 292 | 293 | $this->addEntries($error); 294 | 295 | $segment->addContext('Error', $error); 296 | $segment->end(); 297 | 298 | return $error; 299 | } 300 | 301 | /** 302 | * Add an entry to the queue. 303 | */ 304 | public function addEntries(array|Model $entries): Inspector 305 | { 306 | if ($this->isRecording()) { 307 | $entries = \is_array($entries) ? $entries : [$entries]; 308 | foreach ($entries as $entry) { 309 | $this->transport->addEntry($entry); 310 | } 311 | } 312 | return $this; 313 | } 314 | 315 | /** 316 | * Define a callback to run before flushing data to the remote platform. 317 | */ 318 | public static function beforeFlush(callable $callback): void 319 | { 320 | static::$beforeCallbacks[] = $callback; 321 | } 322 | 323 | /** 324 | * Flush data to the remote platform. 325 | * 326 | * @throws \Exception 327 | */ 328 | public function flush(): void 329 | { 330 | if (!$this->isRecording() || !$this->hasTransaction()) { 331 | $this->reset(); 332 | return; 333 | } 334 | 335 | if (!$this->transaction->isEnded()) { 336 | $this->transaction->end(); 337 | } 338 | 339 | foreach (static::$beforeCallbacks as $callback) { 340 | if (\call_user_func($callback, $this) === false) { 341 | $this->reset(); 342 | return; 343 | } 344 | } 345 | 346 | $this->transport->flush(); 347 | unset($this->transaction); 348 | 349 | // Clear open segments when flushing 350 | $this->openSegments = []; 351 | } 352 | 353 | /** 354 | * Cancel the current transaction, segments, and errors. 355 | */ 356 | public function reset(): Inspector 357 | { 358 | $this->transport->resetQueue(); 359 | unset($this->transaction); 360 | $this->openSegments = []; 361 | return $this; 362 | } 363 | 364 | /** 365 | * Get information about currently open segments (useful for debugging). 366 | * Returns an array of segment types and labels. 367 | */ 368 | public function getOpenSegments(): array 369 | { 370 | return \array_map(function (Segment $segment) { 371 | return [ 372 | 'type' => $segment->type, 373 | 'label' => $segment->label, 374 | 'hash' => $segment->getHash() 375 | ]; 376 | }, $this->openSegments); 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /src/Models/Error.php: -------------------------------------------------------------------------------- 1 | timestamp = \microtime(true); 40 | 41 | $this->host = new Host(); 42 | 43 | $this->message = $throwable->getMessage() 44 | ? $throwable->getMessage() 45 | : \get_class($throwable); 46 | 47 | $this->class = \get_class($throwable); 48 | $this->file = $throwable->getFile(); 49 | $this->line = $throwable->getLine(); 50 | $this->code = $throwable->getCode(); 51 | 52 | $this->stack = $this->stackTraceToArray($throwable); 53 | 54 | $this->transaction = $transaction->only(['name', 'hash']); 55 | } 56 | 57 | /** 58 | * Determine if the exception is handled/unhandled. 59 | */ 60 | public function setHandled(bool $value): Error 61 | { 62 | $this->handled = $value; 63 | return $this; 64 | } 65 | 66 | /** 67 | * Serialize stack trace to array 68 | */ 69 | public function stackTraceToArray(\Throwable $throwable): array 70 | { 71 | $stack = []; 72 | $counter = 0; 73 | 74 | // Exception object `getTrace` does not return file and line number for the first line 75 | // http://php.net/manual/en/exception.gettrace.php#107563 76 | 77 | $inApp = function ($file) { 78 | return !\str_contains($file, 'vendor') && 79 | !\str_contains($file, 'index.php') && 80 | !\str_contains($file, 'web/core'); // Drupal 81 | }; 82 | 83 | $stack[] = [ 84 | 'file' => $throwable->getFile(), 85 | 'line' => $throwable->getLine(), 86 | 'in_app' => $inApp($throwable->getFile()), 87 | 'code' => $this->getCode($throwable->getFile(), $throwable->getLine(), $inApp($throwable->getFile()) ? 15 : 5), 88 | ]; 89 | 90 | foreach ($throwable->getTrace() as $trace) { 91 | $stack[] = [ 92 | 'class' => $trace['class'] ?? null, 93 | 'function' => $trace['function'], 94 | 'args' => $this->stackTraceArgsToArray($trace), 95 | 'type' => $trace['type'] ?? 'function', 96 | 'file' => $trace['file'] ?? '[internal]', 97 | 'line' => $trace['line'] ?? '0', 98 | 'code' => isset($trace['file']) 99 | ? $this->getCode($trace['file'], $trace['line'] ?? '0', $inApp($trace['file']) ? 15 : 5) 100 | : [], 101 | 'in_app' => isset($trace['file']) && $inApp($trace['file']), 102 | ]; 103 | 104 | // Reporting limit 105 | if (++$counter >= 50) { 106 | break; 107 | } 108 | } 109 | 110 | return $stack; 111 | } 112 | 113 | /** 114 | * Serialize stack trace function arguments 115 | */ 116 | protected function stackTraceArgsToArray(array $trace): array 117 | { 118 | $params = []; 119 | 120 | if (!isset($trace['args'])) { 121 | return $params; 122 | } 123 | 124 | foreach ($trace['args'] as $arg) { 125 | if (\is_array($arg)) { 126 | $params[] = 'array(' . \count($arg) . ')'; 127 | } elseif (\is_object($arg)) { 128 | $params[] = \get_class($arg); 129 | } elseif (\is_string($arg)) { 130 | $params[] = 'string(' . $arg . ')'; 131 | } elseif (\is_int($arg)) { 132 | $params[] = 'int(' . $arg . ')'; 133 | } elseif (\is_float($arg)) { 134 | $params[] = 'float(' . $arg . ')'; 135 | } elseif (\is_bool($arg)) { 136 | $params[] = 'bool(' . ($arg ? 'true' : 'false') . ')'; 137 | } else { 138 | $params[] = \gettype($arg); 139 | } 140 | } 141 | 142 | return $params; 143 | } 144 | 145 | /** 146 | * Extract a code source from file. 147 | */ 148 | public function getCode(string $filePath, int $line, int $linesAround = 5): ?array 149 | { 150 | try { 151 | $file = new \SplFileObject($filePath); 152 | $file->setMaxLineLen(250); 153 | $file->seek(\PHP_INT_MAX); 154 | 155 | $codeLines = []; 156 | 157 | $from = \max(0, $line - $linesAround); 158 | $to = \min($line + $linesAround, $file->key()); 159 | 160 | $file->seek($from); 161 | 162 | while ($file->key() <= $to && !$file->eof()) { 163 | $codeLines[] = [ 164 | 'line' => $file->key() + 1, 165 | 'code' => \rtrim($file->current()), 166 | ]; 167 | $file->next(); 168 | } 169 | 170 | return $codeLines; 171 | } catch (\Exception $e) { 172 | return null; 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/Models/HasContext.php: -------------------------------------------------------------------------------- 1 | context[$key] = $data; 16 | 17 | return $this; 18 | } 19 | 20 | /** 21 | * Set the entire context bag. 22 | */ 23 | public function setContext(array $data): Model 24 | { 25 | $this->context = $data; 26 | return $this; 27 | } 28 | 29 | /** 30 | * Get context items. 31 | */ 32 | public function getContext(?string $label = null): array 33 | { 34 | if (\is_string($label)) { 35 | return $this->context[$label]; 36 | } 37 | 38 | return $this->context; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Models/Model.php: -------------------------------------------------------------------------------- 1 | jsonSerialize(); 22 | return \array_intersect_key($properties, \array_flip($keys)); 23 | } 24 | 25 | public function jsonSerialize(): array 26 | { 27 | return \array_filter($this->getProperties(), function ($value) { 28 | // remove NULL, FALSE, empty strings and empty arrays, but keep values of 0 (zero) 29 | return \is_array($value) || \is_object($value) ? !empty($value) : \strlen($value ?? ''); 30 | }); 31 | } 32 | 33 | protected function getProperties(): array 34 | { 35 | $properties = []; 36 | 37 | $reflect = new ReflectionClass($this); 38 | do { 39 | foreach ($reflect->getProperties(ReflectionProperty::IS_PUBLIC) as $property) { 40 | $properties[$property->getName()] = $property->getValue($this); 41 | } 42 | } while ($reflect = $reflect->getParentClass()); 43 | 44 | return $properties; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Models/Partials/Host.php: -------------------------------------------------------------------------------- 1 | hostname = \gethostname(); 23 | $this->ip = \gethostbyname(\gethostname()); 24 | $this->os = \PHP_OS_FAMILY; 25 | } 26 | 27 | /** 28 | * Collect server status information. 29 | * 30 | * @deprecated It's not used anymore, but it's interesting to take this script in mind for future use cases. 31 | */ 32 | public function withServerStatus(): Host 33 | { 34 | if (OS::isLinux() && \function_exists('shell_exec')) { 35 | try { 36 | $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)}\'`"'); 37 | $status = \str_replace('%', '', $status); 38 | $status = \str_replace("\n", '', $status); 39 | 40 | $status = \explode(';', $status); 41 | 42 | $this->cpu = $status[0]; 43 | $this->ram = $status[1]; 44 | $this->hdd = $status[2]; 45 | } catch (\Throwable $exception) { 46 | // do nothing 47 | } 48 | } 49 | 50 | return $this; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Models/Partials/Http.php: -------------------------------------------------------------------------------- 1 | method = $_SERVER['REQUEST_METHOD'] ?? null; 21 | 22 | $this->version = isset($_SERVER['SERVER_PROTOCOL']) 23 | ? \substr($_SERVER['SERVER_PROTOCOL'], \strpos($_SERVER['SERVER_PROTOCOL'], '/')) 24 | : 'unknown'; 25 | 26 | $this->socket = new Socket(); 27 | 28 | $this->cookies = $_COOKIE; 29 | 30 | if (\function_exists('apache_request_headers')) { 31 | $h = apache_request_headers(); 32 | 33 | if (\array_key_exists('sec-ch-ua', $h)) { 34 | unset($h['sec-ch-ua']); 35 | } 36 | 37 | $this->headers = $h; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Models/Partials/Socket.php: -------------------------------------------------------------------------------- 1 | remote_address = $_SERVER['REMOTE_ADDR'] ?? ''; 18 | 19 | if (\array_key_exists('HTTP_X_FORWARDED_FOR', $_SERVER) === true) { 20 | $this->remote_address = $_SERVER['HTTP_X_FORWARDED_FOR']; 21 | } 22 | 23 | $this->encrypted = isset($_SERVER['HTTPS']); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Models/Partials/Url.php: -------------------------------------------------------------------------------- 1 | protocol = isset($_SERVER['HTTPS']) ? 'https' : 'http'; 21 | $this->port = $_SERVER['SERVER_PORT'] ?? ''; 22 | $this->path = $_SERVER['SCRIPT_NAME'] ?? ''; 23 | $this->search = '?' . ($_SERVER['QUERY_STRING'] ?? ''); 24 | $this->full = isset($_SERVER['HTTP_HOST']) ? $this->protocol . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] : ''; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Models/Partials/User.php: -------------------------------------------------------------------------------- 1 | timestamp = \is_null($timestamp) ? \microtime(true) : $timestamp; 16 | return $this; 17 | } 18 | 19 | /** 20 | * Stop the timer and calculate the duration. 21 | */ 22 | public function end(int|float|null $duration = null): PerformanceModel 23 | { 24 | $this->duration = $duration ?? \round((\microtime(true) - $this->timestamp) * 1000, 2); // milliseconds 25 | return $this; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Models/Segment.php: -------------------------------------------------------------------------------- 1 | host = new Host(); 32 | $this->transaction = $transaction->only(['name', 'hash', 'timestamp']); 33 | $this->hash = $this->generateHash(); 34 | } 35 | 36 | /** 37 | * Set the Inspector instance for managing segment lifecycle. 38 | */ 39 | public function setInspector(Inspector $inspector): Segment 40 | { 41 | $this->inspector = $inspector; 42 | return $this; 43 | } 44 | 45 | /** 46 | * Set the parent segment hash. 47 | */ 48 | public function setParent(?string $parentHash): Segment 49 | { 50 | $this->parent_hash = $parentHash; 51 | return $this; 52 | } 53 | 54 | /** 55 | * Start the timer. 56 | */ 57 | public function start(int|float|null $timestamp = null): Segment 58 | { 59 | $initial = \is_null($timestamp) ? \microtime(true) : $timestamp; 60 | 61 | $this->start = \round(($initial - $this->transaction['timestamp']) * 1000, 2); 62 | parent::start($timestamp); 63 | return $this; 64 | } 65 | 66 | /** 67 | * End the segment and notify Inspector to remove from open segments. 68 | */ 69 | public function end(int|float|null $duration = null): Segment 70 | { 71 | parent::end($duration); 72 | 73 | // Notify Inspector that this segment has ended 74 | $this->inspector?->endSegment($this); 75 | 76 | return $this; 77 | } 78 | 79 | public function setColor(string $color): Segment 80 | { 81 | $this->color = $color; 82 | return $this; 83 | } 84 | 85 | /** 86 | * Generate a unique hash for this segment. 87 | */ 88 | protected function generateHash(): string 89 | { 90 | return \hash('sha256', $this->type . $this->label . \microtime(true) . \random_int(1000, 9999)); 91 | } 92 | 93 | /** 94 | * Get the segment hash. 95 | */ 96 | public function getHash(): string 97 | { 98 | return $this->hash; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Models/Transaction.php: -------------------------------------------------------------------------------- 1 | name = $name; 32 | $this->hash = $this->generateUniqueHash(); 33 | $this->host = new Host(); 34 | } 35 | 36 | /** 37 | * Mark the current transaction as an HTTP request. 38 | * 39 | * @return $this 40 | */ 41 | public function markAsRequest(): Transaction 42 | { 43 | $this->setType('request'); 44 | $this->http = new Http(); 45 | return $this; 46 | } 47 | 48 | /** 49 | * Set the type to categorize the transaction. 50 | * 51 | * @param string $type 52 | * @return $this 53 | */ 54 | public function setType(string $type): Transaction 55 | { 56 | $this->type = $type; 57 | return $this; 58 | } 59 | 60 | /** 61 | * Attach user information. 62 | * 63 | * @param integer|string $id 64 | * @param null|string $name 65 | * @param null|string $email 66 | * @return $this 67 | */ 68 | public function withUser($id, ?string $name = null, ?string $email = null): Transaction 69 | { 70 | $this->user = new User($id, $name, $email); 71 | return $this; 72 | } 73 | 74 | /** 75 | * Set a string representation of a transaction result (e.g. 'error', 'success', 'ok', '200', etc...). 76 | * 77 | * @param string $result 78 | * @return Transaction 79 | */ 80 | public function setResult(string $result): Transaction 81 | { 82 | $this->result = $result; 83 | return $this; 84 | } 85 | 86 | public function end(int|float|null $duration = null): Transaction 87 | { 88 | // Sample memory peak at the end of execution. 89 | $this->memory_peak = $this->getMemoryPeak(); 90 | parent::end($duration); 91 | return $this; 92 | } 93 | 94 | public function isEnded(): bool 95 | { 96 | return isset($this->duration) && $this->duration > 0; 97 | } 98 | 99 | public function getMemoryPeak(): float 100 | { 101 | return \round((\memory_get_peak_usage() / 1024 / 1024), 2); // MB 102 | } 103 | 104 | /** 105 | * Generate a unique transaction hash. 106 | * 107 | * http://www.php.net/manual/en/function.uniqid.php 108 | * 109 | * @throws \Exception 110 | */ 111 | public function generateUniqueHash(int $length = 32): string 112 | { 113 | if ($length <= 8) { 114 | $length = 32; 115 | } 116 | 117 | if (\function_exists('random_bytes')) { 118 | return \bin2hex(\random_bytes($length)); 119 | } elseif (\function_exists('openssl_random_pseudo_bytes')) { 120 | return \bin2hex(\openssl_random_pseudo_bytes($length)); 121 | } 122 | 123 | throw new InspectorException('Can\'t create unique transaction hash.'); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/OS.php: -------------------------------------------------------------------------------- 1 | config = $configuration; 34 | $this->verifyOptions($configuration->getOptions()); 35 | } 36 | 37 | /** 38 | * Verify if given options match constraints. 39 | * 40 | * @throws InspectorException 41 | */ 42 | protected function verifyOptions(array $options): void 43 | { 44 | foreach ($this->getAllowedOptions() as $name => $regex) { 45 | if (isset($options[$name])) { 46 | $value = $options[$name]; 47 | if (!\preg_match($regex, $value)) { 48 | throw new InspectorException("Option '$name' has invalid value"); 49 | } 50 | } 51 | } 52 | } 53 | 54 | /** 55 | * Get the current queue. 56 | */ 57 | public function getQueue(): array 58 | { 59 | return $this->queue; 60 | } 61 | 62 | /** 63 | * Empty the queue. 64 | */ 65 | public function resetQueue(): TransportInterface 66 | { 67 | $this->queue = []; 68 | return $this; 69 | } 70 | 71 | /** 72 | * Add a message to the queue. 73 | */ 74 | public function addEntry(Model $model): TransportInterface 75 | { 76 | // Force insert when dealing with errors. 77 | if ($model->model === 'error' || \count($this->queue) <= $this->config->getMaxItems()) { 78 | $this->queue[] = $model; 79 | } 80 | return $this; 81 | } 82 | 83 | /** 84 | * Deliver everything on the queue to LOG Engine. 85 | */ 86 | public function flush(): TransportInterface 87 | { 88 | if (empty($this->queue)) { 89 | return $this; 90 | } 91 | 92 | $this->send($this->queue); 93 | 94 | $this->resetQueue(); 95 | return $this; 96 | } 97 | 98 | /** 99 | * Send data chunks based on MAX_POST_LENGTH. 100 | */ 101 | public function send(array $items): void 102 | { 103 | $json = \json_encode($items); 104 | $jsonLength = \strlen($json); 105 | $count = \count($items); 106 | 107 | if ($jsonLength > $this->config->getMaxPostSize()) { 108 | if ($count === 1) { 109 | // It makes no sense to divide into chunks, just try to send data via file 110 | $this->sendViaFile(\base64_encode($json)); 111 | return; 112 | } 113 | 114 | $chunkSize = \floor($count / \ceil($jsonLength / $this->config->getMaxPostSize())); 115 | $chunks = \array_chunk($items, $chunkSize > 0 ? $chunkSize : 1); 116 | 117 | foreach ($chunks as $chunk) { 118 | $this->send($chunk); 119 | } 120 | } else { 121 | $this->sendChunk(\base64_encode($json)); 122 | } 123 | } 124 | 125 | /** 126 | * Put data into a file and provide CURL with the file path. 127 | */ 128 | protected function sendViaFile(string $data): void 129 | { 130 | $tmpfile = \tempnam(\sys_get_temp_dir(), 'inspector'); 131 | 132 | \file_put_contents($tmpfile, $data, \LOCK_EX); 133 | 134 | $this->sendChunk('@'.$tmpfile); 135 | } 136 | 137 | /** 138 | * Send a portion of the load to the remote service. 139 | */ 140 | abstract protected function sendChunk(string $data): void; 141 | 142 | /** 143 | * List of available transport options with validation regex. 144 | * 145 | * ['param-name' => 'regex'] 146 | */ 147 | protected function getAllowedOptions(): array 148 | { 149 | return [ 150 | 'proxy' => '/.+/', // Custom url for 151 | 'debug' => '/^(0|1)?$/', // boolean 152 | ]; 153 | } 154 | 155 | protected function getApiHeaders(): array 156 | { 157 | return [ 158 | 'Content-Type' => 'application/json', 159 | 'Accept' => 'application/json', 160 | 'X-Inspector-Key' => $this->config->getIngestionKey(), 161 | 'X-Inspector-Version' => $this->config->getVersion(), 162 | ]; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/Transports/AsyncTransport.php: -------------------------------------------------------------------------------- 1 | 'regex'] 30 | * 31 | * Override to introduce "curlPath". 32 | */ 33 | protected function getAllowedOptions(): array 34 | { 35 | return \array_merge(parent::getAllowedOptions(), [ 36 | 'curlPath' => '/.+/', 37 | ]); 38 | } 39 | 40 | /** 41 | * Send a portion of the load to the remote service. 42 | */ 43 | public function sendChunk(string $data): void 44 | { 45 | $curl = $this->buildCurlCommand($data); 46 | 47 | if (OS::isWin()) { 48 | $cmd = "start /B {$curl} > NUL"; 49 | } else { 50 | $cmd = "({$curl} > /dev/null 2>&1"; 51 | 52 | // Delete temporary file after data transfer 53 | if (\str_starts_with($data, '@')) { 54 | $cmd .= '; rm ' . \str_replace('@', '', $data); 55 | } 56 | 57 | $cmd .= ')&'; 58 | } 59 | 60 | \proc_close(\proc_open($cmd, [], $pipes)); 61 | } 62 | 63 | /** 64 | * Carl command is agnostic between Win and Unix. 65 | */ 66 | protected function buildCurlCommand(string $data): string 67 | { 68 | $curl = $this->config->getOptions()['curlPath'] ?? 'curl'; 69 | 70 | $curl .= " -X POST --ipv4 --max-time 5"; 71 | 72 | foreach ($this->getApiHeaders() as $name => $value) { 73 | $curl .= " --header \"$name: $value\""; 74 | } 75 | 76 | $curl .= " --data {$data} {$this->config->getUrl()}"; 77 | 78 | if (\array_key_exists('proxy', $this->config->getOptions())) { 79 | $curl .= " --proxy \"{$this->config->getOptions()['proxy']}\""; 80 | } 81 | 82 | return $curl; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Transports/CurlTransport.php: -------------------------------------------------------------------------------- 1 | getApiHeaders() as $name => $value) { 36 | $headers[] = "$name: $value"; 37 | } 38 | 39 | $handle = \curl_init($this->config->getUrl()); 40 | 41 | \curl_setopt($handle, \CURLOPT_POST, true); 42 | 43 | // Tell cURL that it should only spend 10 seconds trying to connect to the URL in question. 44 | \curl_setopt($handle, \CURLOPT_CONNECTTIMEOUT, 5); 45 | // A given cURL operation should only take 30 seconds max. 46 | \curl_setopt($handle, \CURLOPT_TIMEOUT, 10); 47 | 48 | \curl_setopt($handle, \CURLOPT_HTTPHEADER, $headers); 49 | \curl_setopt($handle, \CURLOPT_POSTFIELDS, $data); 50 | \curl_setopt($handle, \CURLOPT_RETURNTRANSFER, true); 51 | \curl_setopt($handle, \CURLOPT_SSL_VERIFYHOST, 0); 52 | \curl_setopt($handle, \CURLOPT_SSL_VERIFYPEER, true); 53 | 54 | if (\array_key_exists('proxy', $this->config->getOptions())) { 55 | \curl_setopt($handle, \CURLOPT_PROXY, $this->config->getOptions()['proxy']); 56 | } 57 | 58 | $response = \curl_exec($handle); 59 | $errorNo = \curl_errno($handle); 60 | $code = \curl_getinfo($handle, \CURLINFO_HTTP_CODE); 61 | $error = \curl_error($handle); 62 | 63 | // 200 OK 64 | // 403 Account has reached no. transactions limit 65 | if (0 !== $errorNo || (200 !== $code && 201 !== $code && 403 !== $code)) { 66 | \error_log(\date('Y-m-d H:i:s') . " - [Warning] [" . \get_class($this) . "] $error - $code $errorNo"); 67 | } 68 | 69 | \curl_close($handle); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Transports/TransportInterface.php: -------------------------------------------------------------------------------- 1 |