├── .gitignore ├── src ├── Blackfire │ ├── Bridge │ │ ├── Symfony │ │ │ ├── MonitorableCommandInterface.php │ │ │ ├── MonitoredMiddleware.php │ │ │ ├── BlackfiredHttpResponse.php │ │ │ ├── AutoProfilingBlackfiredClient.php │ │ │ ├── Event │ │ │ │ └── MonitoredCommandSubscriber.php │ │ │ ├── BlackfiredKernelBrowser.php │ │ │ ├── BlackfiredHttpBrowser.php │ │ │ └── BlackfiredHttpClient.php │ │ ├── Behat │ │ │ ├── BlackfireExtension │ │ │ │ ├── ServiceContainer │ │ │ │ │ ├── Driver │ │ │ │ │ │ ├── BlackfireDriver.php │ │ │ │ │ │ ├── BlackfireKernelBrowserDriver.php │ │ │ │ │ │ ├── BlackfiredHttpBrowserFactory.php │ │ │ │ │ │ └── BlackfiredKernelBrowserFactory.php │ │ │ │ │ └── BlackfireExtension.php │ │ │ │ └── Event │ │ │ │ │ ├── ScenarioSubscriber.php │ │ │ │ │ └── BuildSubscriber.php │ │ │ └── Context │ │ │ │ └── BlackfireContextTrait.php │ │ ├── PhpUnit │ │ │ ├── TestConstraint5x.php │ │ │ ├── BlackfireTestCase.php │ │ │ ├── TestConstraint71.php │ │ │ ├── TestConstraint11x.php │ │ │ ├── Laravel │ │ │ │ └── BlackfireBuildExtension.php │ │ │ ├── TestCaseTrait.php │ │ │ ├── TestConstraint.php │ │ │ ├── BlackfireTestCaseTrait.php │ │ │ └── BlackfireBuildExtension.php │ │ ├── Laravel │ │ │ ├── BlackfireTestHttpRequestsTrait.php │ │ │ ├── LoadBlackfireEnvironmentVariables.php │ │ │ ├── Listeners │ │ │ │ ├── OctaneRequestIntrumentationStop.php │ │ │ │ └── OctaneRequestIntrumentationStart.php │ │ │ ├── OctaneProfilerMiddleware.php │ │ │ ├── OctaneServiceProvider.php │ │ │ ├── InstrumentedTestRequests.php │ │ │ ├── ObservableJobProvider.php │ │ │ ├── BlackfireTestArtisanCommandsTrait.php │ │ │ ├── ObservableCommandProvider.php │ │ │ ├── OctaneProfiler.php │ │ │ └── BlackfireTestCase.php │ │ ├── ReactHttpServerHelper.php │ │ └── Guzzle │ │ │ └── Middleware.php │ ├── Exception │ │ ├── ExceptionInterface.php │ │ ├── OfflineException.php │ │ ├── ConfigErrorException.php │ │ ├── EnvNotFoundException.php │ │ ├── NotAvailableException.php │ │ ├── ConfigNotFoundException.php │ │ ├── InvalidArgumentException.php │ │ ├── ProfileNotReadyException.php │ │ ├── LogicException.php │ │ ├── RuntimeException.php │ │ └── ApiException.php │ ├── Profile │ │ ├── MetricLayer.php │ │ ├── MetricMatcher.php │ │ ├── Cost.php │ │ ├── Test.php │ │ ├── Metric.php │ │ ├── Request.php │ │ └── Configuration.php │ ├── Report.php │ ├── Probe.php │ ├── Build │ │ ├── Build.php │ │ ├── Scenario.php │ │ └── BuildHelper.php │ ├── LoopClient.php │ ├── Util │ │ └── NoProxyPattern.php │ ├── ClientConfiguration.php │ ├── Profile.php │ └── Client.php └── BlackfireSpan.php ├── LICENSE ├── README.md ├── composer.json ├── UPGRADE.md ├── bin └── blackfire-io-proxy.php ├── stubs └── BlackfireProbe.php └── CHANGELOG /.gitignore: -------------------------------------------------------------------------------- 1 | /composer.lock 2 | /vendor/ 3 | -------------------------------------------------------------------------------- /src/Blackfire/Bridge/Symfony/MonitorableCommandInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Exception; 13 | 14 | interface ExceptionInterface 15 | { 16 | } 17 | -------------------------------------------------------------------------------- /src/Blackfire/Exception/OfflineException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Exception; 13 | 14 | class OfflineException extends RuntimeException 15 | { 16 | } 17 | -------------------------------------------------------------------------------- /src/Blackfire/Exception/ConfigErrorException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Exception; 13 | 14 | class ConfigErrorException extends RuntimeException 15 | { 16 | } 17 | -------------------------------------------------------------------------------- /src/Blackfire/Exception/EnvNotFoundException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Exception; 13 | 14 | class EnvNotFoundException extends RuntimeException 15 | { 16 | } 17 | -------------------------------------------------------------------------------- /src/Blackfire/Exception/NotAvailableException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Exception; 13 | 14 | class NotAvailableException extends RuntimeException 15 | { 16 | } 17 | -------------------------------------------------------------------------------- /src/Blackfire/Exception/ConfigNotFoundException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Exception; 13 | 14 | class ConfigNotFoundException extends RuntimeException 15 | { 16 | } 17 | -------------------------------------------------------------------------------- /src/Blackfire/Exception/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Exception; 13 | 14 | class InvalidArgumentException extends LogicException 15 | { 16 | } 17 | -------------------------------------------------------------------------------- /src/Blackfire/Exception/ProfileNotReadyException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Exception; 13 | 14 | class ProfileNotReadyException extends ApiException 15 | { 16 | } 17 | -------------------------------------------------------------------------------- /src/Blackfire/Exception/LogicException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Exception; 13 | 14 | class LogicException extends \LogicException implements ExceptionInterface 15 | { 16 | } 17 | -------------------------------------------------------------------------------- /src/Blackfire/Exception/RuntimeException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Exception; 13 | 14 | class RuntimeException extends \RuntimeException implements ExceptionInterface 15 | { 16 | } 17 | -------------------------------------------------------------------------------- /src/Blackfire/Bridge/Behat/BlackfireExtension/ServiceContainer/Driver/BlackfireDriver.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Bridge\Behat\BlackfireExtension\ServiceContainer\Driver; 13 | 14 | use Behat\Mink\Driver\BrowserKitDriver; 15 | 16 | class BlackfireDriver extends BrowserKitDriver 17 | { 18 | } 19 | -------------------------------------------------------------------------------- /src/Blackfire/Bridge/PhpUnit/TestConstraint5x.php: -------------------------------------------------------------------------------- 1 | isSuccessful(); 15 | } 16 | 17 | protected function fail($profile, $description, ?ComparisonFailure $comparisonFailure = null) 18 | { 19 | $this->doFail($profile, $description, $comparisonFailure); 20 | } 21 | 22 | public function toString() 23 | { 24 | return ''; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Blackfire/Bridge/PhpUnit/BlackfireTestCase.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Bridge\PhpUnit; 13 | 14 | use Symfony\Component\Panther\PantherTestCase; 15 | 16 | class BlackfireTestCase extends PantherTestCase 17 | { 18 | use BlackfireTestCaseTrait; 19 | 20 | public function tearDown(): void 21 | { 22 | // Enforce to "quit" the browser session. 23 | self::$httpBrowserClient = null; 24 | parent::tearDown(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Blackfire/Bridge/PhpUnit/TestConstraint71.php: -------------------------------------------------------------------------------- 1 | isSuccessful(); 15 | } 16 | 17 | protected function fail($profile, $description, ?ComparisonFailure $comparisonFailure = null): void 18 | { 19 | $this->doFail($profile, $description, $comparisonFailure); 20 | } 21 | 22 | public function toString(): string 23 | { 24 | return ''; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Blackfire/Bridge/PhpUnit/TestConstraint11x.php: -------------------------------------------------------------------------------- 1 | isSuccessful(); 15 | } 16 | 17 | protected function fail(mixed $other, string $description, ?ComparisonFailure $comparisonFailure = null): never 18 | { 19 | $this->doFail($other, $description, $comparisonFailure); 20 | } 21 | 22 | public function toString(): string 23 | { 24 | return ''; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Blackfire/Profile/MetricLayer.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Profile; 13 | 14 | class MetricLayer extends Metric 15 | { 16 | public function __construct($name, $label = null) 17 | { 18 | parent::__construct($name); 19 | $this->setLayer($name); 20 | $this->setLabel($label ?: $name); 21 | } 22 | 23 | public function addCallee($selector) 24 | { 25 | throw new \LogicException('A layer cannot have callees.'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Blackfire/Bridge/Laravel/BlackfireTestHttpRequestsTrait.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Bridge\Laravel; 13 | 14 | use Illuminate\Contracts\Foundation\Application; 15 | use Illuminate\Routing\Router; 16 | 17 | trait BlackfireTestHttpRequestsTrait 18 | { 19 | /** 20 | * Create a new HTTP kernel instance. 21 | * 22 | * @return void 23 | */ 24 | public function __construct(Application $app, Router $router) 25 | { 26 | array_unshift($this->bootstrappers, LoadBlackfireEnvironmentVariables::class); 27 | 28 | parent::__construct($app, $router); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Blackfire/Bridge/PhpUnit/Laravel/BlackfireBuildExtension.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Bridge\PhpUnit\Laravel; 13 | 14 | use Blackfire\Bridge\PhpUnit\BlackfireBuildExtension as DefaultBlackfireBuildExtension; 15 | use Blackfire\Build\BuildHelper; 16 | 17 | final class BlackfireBuildExtension extends DefaultBlackfireBuildExtension 18 | { 19 | public function __construct( 20 | string $blackfireEnvironmentId, 21 | string $buildTitle = 'Laravel Tests', 22 | ?BuildHelper $buildHelper = null, 23 | ) { 24 | if (!$buildHelper) { 25 | $buildHelper = BuildHelper::getInstance(); 26 | } 27 | 28 | parent::__construct($blackfireEnvironmentId, $buildTitle, $buildHelper); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Blackfire/Bridge/Behat/BlackfireExtension/ServiceContainer/Driver/BlackfireKernelBrowserDriver.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Bridge\Behat\BlackfireExtension\ServiceContainer\Driver; 13 | 14 | use Blackfire\Bridge\Symfony\BlackfiredKernelBrowser; 15 | 16 | class BlackfireKernelBrowserDriver extends BlackfireDriver 17 | { 18 | private $client; 19 | 20 | private $baseUrl; 21 | 22 | public function __construct(BlackfiredKernelBrowser $client, ?string $baseUrl = null) 23 | { 24 | parent::__construct($client, $baseUrl); 25 | 26 | $client->enableBlackfire(); 27 | $this->baseUrl = $baseUrl; 28 | $this->client = $client; 29 | } 30 | 31 | public function reset() 32 | { 33 | parent::reset(); 34 | 35 | parent::__construct($this->client, $this->baseUrl); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2023 Platform.sh 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /src/Blackfire/Bridge/Laravel/LoadBlackfireEnvironmentVariables.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Bridge\Laravel; 13 | 14 | use Dotenv\Dotenv; 15 | use Illuminate\Contracts\Foundation\Application; 16 | use Illuminate\Http\Request; 17 | 18 | class LoadBlackfireEnvironmentVariables 19 | { 20 | /** 21 | * Bootstrap the given application. 22 | * 23 | * @return void 24 | */ 25 | public function bootstrap(Application $app) 26 | { 27 | $request = Request::capture(); 28 | if (!($request->headers->has('X-BLACKFIRE-LARAVEL-TESTS') && $request->headers->has('X-BLACKFIRE-QUERY'))) { 29 | return; 30 | } 31 | 32 | $dotenv = Dotenv::createImmutable(base_path(), '.env.testing'); 33 | $dotenv->load(); 34 | 35 | if ('testing' !== env('APP_ENV')) { 36 | throw new \RuntimeException('The .env.testing file should contain APP_ENV=testing'); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Blackfire/Profile/MetricMatcher.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Profile; 13 | 14 | class MetricMatcher 15 | { 16 | private $selector; 17 | private $argument; 18 | 19 | public function __construct($selector) 20 | { 21 | $this->selector = $selector; 22 | } 23 | 24 | public function selectArgument($indice, $matcher) 25 | { 26 | $this->argument = array($indice, $matcher); 27 | 28 | return $this; 29 | } 30 | 31 | /** 32 | * @internal 33 | */ 34 | public function toYaml($indent) 35 | { 36 | $indent = str_repeat(' ', $indent); 37 | 38 | $yaml = sprintf("%s- callee:\n", $indent); 39 | $yaml .= sprintf("%s selector: '%s'\n", $indent, $this->selector); 40 | 41 | if (null !== $this->argument) { 42 | $yaml .= sprintf("%s argument: { %d: '%s' }\n", $indent, $this->argument[0], $this->argument[1]); 43 | } 44 | 45 | return $yaml; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Blackfire/Bridge/Laravel/Listeners/OctaneRequestIntrumentationStop.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Bridge\Laravel\Listeners; 13 | 14 | use Illuminate\Routing\Route; 15 | 16 | class OctaneRequestIntrumentationStop 17 | { 18 | /** 19 | * Handle the event. 20 | */ 21 | public function handle($event): void 22 | { 23 | if (!class_exists(\BlackfireProbe::class, false)) { 24 | return; 25 | } 26 | 27 | $request = $event->request; 28 | if (!$request) { 29 | \BlackfireProbe::stopTransaction(); 30 | 31 | return; 32 | } 33 | 34 | $transactionName = $request->path(); 35 | $route = $request->route(); 36 | if ($route instanceof Route && is_string($route->getAction('uses'))) { 37 | $transactionName = $route->getActionName(); 38 | } 39 | \BlackfireProbe::setTransactionName($transactionName); 40 | 41 | \BlackfireProbe::stopTransaction(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Blackfire/Exception/ApiException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Exception; 13 | 14 | class ApiException extends RuntimeException 15 | { 16 | private $headers; 17 | 18 | public function __construct($message = '', $code = 0, ?\Exception $previous = null, $headers = array()) 19 | { 20 | parent::__construct($message, $code, $previous); 21 | 22 | $this->headers = $headers; 23 | } 24 | 25 | public static function fromStatusCode($message, $code, ?\Exception $previous = null) 26 | { 27 | return new static(sprintf('%s: %s', $code, $message), $code, $previous); 28 | } 29 | 30 | public static function fromURL($method, $url, $message, $code, $context, $headers, ?\Exception $previous = null) 31 | { 32 | return new static(sprintf('%s: %s while calling %s %s [context: %s]', $code, $message, $method, $url, var_export($context, true)), $code, $previous, $headers); 33 | } 34 | 35 | public function getHeaders() 36 | { 37 | return $this->headers; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Blackfire/Bridge/Laravel/Listeners/OctaneRequestIntrumentationStart.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Bridge\Laravel\Listeners; 13 | 14 | class OctaneRequestIntrumentationStart 15 | { 16 | /** 17 | * Handle the event. 18 | */ 19 | public function handle($event): void 20 | { 21 | if (!method_exists(\BlackfireProbe::class, 'startTransaction') || !method_exists(\BlackfireProbe::class, 'setAttribute')) { 22 | return; 23 | } 24 | 25 | \BlackfireProbe::startTransaction(); 26 | 27 | $request = $event->request; 28 | if (!$request) { 29 | return; 30 | } 31 | \BlackfireProbe::setAttribute('http.target', $request->path()); 32 | \BlackfireProbe::setAttribute('http.url', $request->url()); 33 | \BlackfireProbe::setAttribute('http.method', $request->method()); 34 | \BlackfireProbe::setAttribute('http.host', $request->getHost()); 35 | \BlackfireProbe::setAttribute('host', $request->getHost()); 36 | \BlackfireProbe::setAttribute('framework', 'Laravel'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Blackfire/Bridge/Laravel/OctaneProfilerMiddleware.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Bridge\Laravel; 13 | 14 | use Illuminate\Http\Request; 15 | 16 | class OctaneProfilerMiddleware 17 | { 18 | private $profiler; 19 | 20 | public function __construct() 21 | { 22 | $this->profiler = new OctaneProfiler(); 23 | } 24 | 25 | /** 26 | * Handle an incoming request. 27 | * 28 | * @param \Closure(Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next 29 | * 30 | * @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse 31 | */ 32 | public function handle(Request $request, \Closure $next) 33 | { 34 | if (!method_exists(\BlackfireProbe::class, 'setAttribute')) { 35 | return; 36 | } 37 | 38 | try { 39 | $this->profiler->start($request); 40 | $response = $next($request); 41 | \BlackfireProbe::setAttribute('http.status_code', $response->status()); 42 | } finally { 43 | $this->profiler->stop($request, $response ?? null); 44 | } 45 | 46 | return $response; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Blackfire/Profile/Cost.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Profile; 13 | 14 | /** 15 | * Represents a Blackfire Profile Cost. 16 | * 17 | * Instances of this class should never be created directly. 18 | */ 19 | class Cost 20 | { 21 | private $envelope; 22 | 23 | /** 24 | * @internal 25 | */ 26 | public function __construct($envelope) 27 | { 28 | $this->envelope = $envelope; 29 | } 30 | 31 | public function getCount() 32 | { 33 | return $this->envelope['ct']; 34 | } 35 | 36 | public function getWallTime() 37 | { 38 | return $this->envelope['wt']; 39 | } 40 | 41 | public function getCpu() 42 | { 43 | return $this->envelope['cpu']; 44 | } 45 | 46 | public function getIo() 47 | { 48 | return $this->envelope['io']; 49 | } 50 | 51 | public function getNetwork() 52 | { 53 | return $this->envelope['nw']; 54 | } 55 | 56 | public function getPeakMemoryUsage() 57 | { 58 | return $this->envelope['pmu']; 59 | } 60 | 61 | public function getMemoryUsage() 62 | { 63 | return $this->envelope['mu']; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Blackfire/Bridge/Symfony/MonitoredMiddleware.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Bridge\Symfony; 13 | 14 | use Symfony\Component\Messenger\Envelope; 15 | use Symfony\Component\Messenger\Middleware\MiddlewareInterface; 16 | use Symfony\Component\Messenger\Middleware\StackInterface; 17 | use Symfony\Component\Messenger\Stamp\ConsumedByWorkerStamp; 18 | 19 | class MonitoredMiddleware implements MiddlewareInterface 20 | { 21 | public function handle(Envelope $envelope, StackInterface $stack): Envelope 22 | { 23 | if (null === $envelope->last(ConsumedByWorkerStamp::class)) { 24 | return $stack->next()->handle($envelope, $stack); 25 | } 26 | 27 | $txName = \get_class($envelope->getMessage()); 28 | 29 | if (version_compare(phpversion('blackfire'), '1.78.0', '>=')) { 30 | \BlackfireProbe::startTransaction($txName); 31 | } else { 32 | \BlackfireProbe::startTransaction(); 33 | \BlackfireProbe::setTransactionName($txName); 34 | } 35 | 36 | try { 37 | return $stack->next()->handle($envelope, $stack); 38 | } finally { 39 | \BlackfireProbe::stopTransaction(); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Blackfire/Bridge/Laravel/OctaneServiceProvider.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Bridge\Laravel; 13 | 14 | use Blackfire\Bridge\Laravel\Listeners\OctaneRequestIntrumentationStart; 15 | use Blackfire\Bridge\Laravel\Listeners\OctaneRequestIntrumentationStop; 16 | use Illuminate\Support\Facades\Event; 17 | use Illuminate\Support\ServiceProvider; 18 | use Laravel\Octane\Events\RequestHandled; 19 | use Laravel\Octane\Events\RequestReceived; 20 | use Laravel\Octane\Events\RequestTerminated; 21 | 22 | class OctaneServiceProvider extends ServiceProvider 23 | { 24 | /** 25 | * Bootstrap services. 26 | * 27 | * @return void 28 | */ 29 | public function boot() 30 | { 31 | if (!class_exists(\BlackfireProbe::class, false)) { 32 | return; 33 | } 34 | 35 | Event::listen( 36 | array( 37 | RequestReceived::class, 38 | ), 39 | OctaneRequestIntrumentationStart::class 40 | ); 41 | 42 | Event::listen( 43 | array( 44 | RequestHandled::class, 45 | RequestTerminated::class, 46 | ), 47 | OctaneRequestIntrumentationStop::class 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Blackfire/Bridge/Laravel/InstrumentedTestRequests.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Bridge\Laravel; 13 | 14 | use Illuminate\Http\Request; 15 | use Symfony\Component\HttpClient\HttpClient; 16 | 17 | class InstrumentedTestRequests 18 | { 19 | /** 20 | * Handle an incoming request. 21 | */ 22 | public function handle(Request $request, \Closure $next) 23 | { 24 | if ('testing' !== env('APP_ENV')) { 25 | return $next($request); 26 | } 27 | 28 | if (!$request->headers->has('X-BLACKFIRE-QUERY')) { 29 | return $next($request); 30 | } 31 | 32 | if ($request->headers->has('X-BLACKFIRE-LARAVEL-TESTS')) { 33 | return $next($request); 34 | } 35 | 36 | $headers = $request->headers->all() ?? array(); 37 | $headers['X-BLACKFIRE-LARAVEL-TESTS'] = array(true); 38 | 39 | $httpClient = HttpClient::create(); 40 | 41 | $httpClient->request( 42 | $request->getMethod(), 43 | $request->getUri(), 44 | array( 45 | 'headers' => $headers, 46 | 'body' => $request->request->all(), 47 | ), 48 | ); 49 | 50 | return $next($request); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Blackfire/Profile/Test.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Profile; 13 | 14 | /** 15 | * Represents a Blackfire Profile Test. 16 | * 17 | * Instances of this class should never be created directly. 18 | */ 19 | class Test 20 | { 21 | private $name; 22 | private $state; 23 | private $failures; 24 | 25 | /** 26 | * @internal 27 | */ 28 | public function __construct($name, $state, array $failures) 29 | { 30 | $this->name = $name; 31 | $this->state = $state; 32 | $this->failures = $failures; 33 | } 34 | 35 | /** 36 | * @return string 37 | */ 38 | public function getName() 39 | { 40 | return $this->name; 41 | } 42 | 43 | /** 44 | * @return string 45 | */ 46 | public function getState() 47 | { 48 | return $this->state; 49 | } 50 | 51 | /** 52 | * @return bool 53 | */ 54 | public function isSuccessful() 55 | { 56 | return 'successful' === $this->state; 57 | } 58 | 59 | /** 60 | * @return bool 61 | */ 62 | public function isErrored() 63 | { 64 | return 'errored' === $this->state; 65 | } 66 | 67 | /** 68 | * @return array 69 | */ 70 | public function getFailures() 71 | { 72 | return $this->failures; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Blackfire PHP SDK 2 | ================= 3 | 4 | Install the Blackfire PHP SDK via Composer: 5 | 6 | $ composer require blackfire/php-sdk 7 | 8 | Blackfire Client 9 | ---------------- 10 | 11 | See https://docs.blackfire.io/php/integrations/sdk 12 | 13 | PhpUnit Integration 14 | ------------------- 15 | 16 | See https://docs.blackfire.io/php/integrations/phpunit 17 | 18 | Proxy 19 | ----- 20 | 21 | If you want to inspect the traffic between profiled servers and blackfire's 22 | servers, you can use a small proxy script provided in this repository. Please 23 | read the instructions in `./bin/blackfire-io-proxy.php` to do so. 24 | 25 | PHP Probe 26 | --------- 27 | 28 | **WARNING**: This code should only be used when installing the Blackfire PHP 29 | extension is not possible. 30 | 31 | This repository provides a [Blackfire](https://blackfire.io/) PHP Probe 32 | implementation that should only be used under the following circumstances: 33 | 34 | * You already have XHProf installed and cannot install the Blackfire PHP 35 | extension (think PHP 5.2); 36 | 37 | * You want a fallback in case the Blackfire PHP extension is not installed on 38 | some machines (manual instrumentation will be converted to noops). 39 | 40 | [Read more](https://blog.blackfire.io/blackfire-for-xhprof-users.html) about 41 | how to use this feature on Blackfire's blog. 42 | 43 | Blackfire Support 44 | ----------------- 45 | 46 | If you are facing any issue with using the Blackfire PHP SDK, please check 47 | [our support site](https://support.blackfire.platform.sh) or reach out to [support@blackfire.io](mailto:support@blackfire.io). 48 | -------------------------------------------------------------------------------- /src/Blackfire/Bridge/Behat/BlackfireExtension/ServiceContainer/Driver/BlackfiredHttpBrowserFactory.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Bridge\Behat\BlackfireExtension\ServiceContainer\Driver; 13 | 14 | use Behat\Mink\Driver\BrowserKitDriver; 15 | use Behat\MinkExtension\ServiceContainer\Driver\DriverFactory; 16 | use Blackfire\Bridge\Symfony\BlackfiredHttpBrowser; 17 | use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; 18 | use Symfony\Component\DependencyInjection\Definition; 19 | use Symfony\Component\DependencyInjection\Reference; 20 | 21 | class BlackfiredHttpBrowserFactory implements DriverFactory 22 | { 23 | public function getDriverName() 24 | { 25 | return 'blackfire'; 26 | } 27 | 28 | public function supportsJavascript() 29 | { 30 | return false; 31 | } 32 | 33 | public function configure(ArrayNodeDefinition $builder) 34 | { 35 | } 36 | 37 | public function buildDriver(array $config) 38 | { 39 | if (!class_exists(BrowserKitDriver::class)) { 40 | throw new \RuntimeException('Install "friends-of-behat/mink-browserkit-driver" (drop-in replacement for "behat/mink-browserkit-driver") in order to use the "blackfire" driver.'); 41 | } 42 | 43 | return new Definition(BlackfireDriver::class, array( 44 | new Reference(BlackfiredHttpBrowser::class), 45 | '%mink.base_url%', 46 | )); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Blackfire/Bridge/Laravel/ObservableJobProvider.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Bridge\Laravel; 13 | 14 | use Illuminate\Queue\Events\JobExceptionOccurred; 15 | use Illuminate\Queue\Events\JobProcessed; 16 | use Illuminate\Queue\Events\JobProcessing; 17 | use Illuminate\Support\Facades\Queue; 18 | use Illuminate\Support\ServiceProvider; 19 | 20 | class ObservableJobProvider extends ServiceProvider 21 | { 22 | /** 23 | * Bootstrap services. 24 | * 25 | * @return void 26 | */ 27 | public function boot() 28 | { 29 | if (!class_exists(\BlackfireProbe::class, false)) { 30 | return; 31 | } 32 | 33 | Queue::before(function (JobProcessing $event) { 34 | $transactionName = $event->job->payload()['displayName'] ?? 'Job'; 35 | if (version_compare(phpversion('blackfire'), '1.78.0', '>=')) { 36 | \BlackfireProbe::startTransaction($transactionName); 37 | } else { 38 | \BlackfireProbe::startTransaction(); 39 | \BlackfireProbe::setTransactionName($transactionName); 40 | } 41 | }); 42 | 43 | Queue::after(function (JobProcessed $event) { 44 | \BlackfireProbe::stopTransaction(); 45 | }); 46 | 47 | Queue::exceptionOccurred(function (JobExceptionOccurred $event) { 48 | \BlackfireProbe::stopTransaction(); 49 | }); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Blackfire/Bridge/Symfony/BlackfiredHttpResponse.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Bridge\Symfony; 13 | 14 | use Blackfire\Profile\Request; 15 | use Symfony\Contracts\HttpClient\ResponseInterface; 16 | 17 | class BlackfiredHttpResponse implements ResponseInterface 18 | { 19 | private ResponseInterface $response; 20 | 21 | /** @var Request */ 22 | private $request; 23 | 24 | public function __construct(ResponseInterface $response, ?Request $request = null) 25 | { 26 | $this->response = $response; 27 | $this->request = $request; 28 | } 29 | 30 | public function getStatusCode(): int 31 | { 32 | return $this->response->getStatusCode(); 33 | } 34 | 35 | public function getHeaders(bool $throw = true): array 36 | { 37 | $headers = $this->response->getHeaders($throw); 38 | 39 | if (null !== $this->request) { 40 | $headers['X-Blackfire-Profile-Uuid'] = array($this->request->getUuid()); 41 | } 42 | 43 | return $headers; 44 | } 45 | 46 | public function getContent(bool $throw = true): string 47 | { 48 | return $this->response->getContent($throw); 49 | } 50 | 51 | public function toArray(bool $throw = true): array 52 | { 53 | return $this->response->toArray($throw); 54 | } 55 | 56 | public function cancel(): void 57 | { 58 | $this->response->cancel(); 59 | } 60 | 61 | public function getInfo(?string $type = null): mixed 62 | { 63 | return $this->response->getInfo($type); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Blackfire/Bridge/Behat/Context/BlackfireContextTrait.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Bridge\Behat\Context; 13 | 14 | use Behat\Behat\Hook\Scope\StepScope; 15 | use Behat\Mink\Driver\DriverInterface; 16 | use Blackfire\Bridge\Behat\BlackfireExtension\ServiceContainer\Driver\BlackfireDriver; 17 | use Blackfire\Bridge\Symfony\BlackfiredHttpBrowser; 18 | 19 | trait BlackfireContextTrait 20 | { 21 | protected function disableProfiling() 22 | { 23 | if (!$this->isBlackfireDriver()) { 24 | return; 25 | } 26 | 27 | $this->getCurrentDriver()->getClient()->disableProfiling(); 28 | } 29 | 30 | protected function enableProfiling() 31 | { 32 | if (!$this->isBlackfireDriver()) { 33 | return; 34 | } 35 | 36 | $this->getCurrentDriver()->getClient()->enableProfiling(); 37 | } 38 | 39 | private function getCurrentDriver(): DriverInterface 40 | { 41 | return $this->getSession()->getDriver(); 42 | } 43 | 44 | private function isBlackfireDriver() 45 | { 46 | return $this->getCurrentDriver() instanceof BlackfireDriver; 47 | } 48 | 49 | /** 50 | * @AfterStep 51 | */ 52 | public function afterStep(StepScope $scope) 53 | { 54 | if (!$this->isBlackfireDriver()) { 55 | return; 56 | } 57 | 58 | /** @var BlackfiredHttpBrowser $client */ 59 | $client = $this->getCurrentDriver()->getClient(); 60 | if (!$client->isProfilingEnabled()) { 61 | $client->enableProfiling(); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Blackfire/Bridge/Symfony/AutoProfilingBlackfiredClient.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Bridge\Symfony; 13 | 14 | use Blackfire\Profile\Configuration; 15 | use Symfony\Contracts\HttpClient\ResponseInterface; 16 | 17 | /** 18 | * Decorates BlackfiredHttpClient in order to auto-enable it. 19 | * Every HTTP requests going through AutoProfilingBlackfiredClient will trigger 20 | * a profile, except if profiling is explicitly disabled by calling disableProfiling(). 21 | */ 22 | class AutoProfilingBlackfiredClient extends BlackfiredHttpClient 23 | { 24 | private $profilingEnabled = true; 25 | 26 | /** 27 | * @var Configuration 28 | */ 29 | private $profilingConfig; 30 | 31 | public function isProfilingEnabled(): bool 32 | { 33 | return $this->profilingEnabled; 34 | } 35 | 36 | public function enableProfiling(?Configuration $profilingConfig = null): self 37 | { 38 | $this->profilingEnabled = true; 39 | $this->profilingConfig = $profilingConfig; 40 | 41 | return $this; 42 | } 43 | 44 | public function disableProfiling(): self 45 | { 46 | $this->profilingEnabled = false; 47 | $this->profilingConfig = null; 48 | 49 | return $this; 50 | } 51 | 52 | public function request(string $method, string $url, array $options = array()): ResponseInterface 53 | { 54 | $options['extra']['blackfire'] = $this->profilingConfig ?? $this->profilingEnabled; 55 | 56 | $response = parent::request($method, $url, $options); 57 | $this->profilingConfig = null; 58 | 59 | return $response; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Blackfire/Profile/Metric.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Profile; 13 | 14 | class Metric 15 | { 16 | private $name; 17 | private $layer; 18 | private $label; 19 | private $matchers = array(); 20 | 21 | public function __construct($name, $selectors = null) 22 | { 23 | $this->name = $this->label = $name; 24 | 25 | foreach ((array) $selectors as $selector) { 26 | $this->addCallee($selector); 27 | } 28 | } 29 | 30 | /** 31 | * @return $this 32 | */ 33 | public function setLayer($layer) 34 | { 35 | $this->layer = $layer; 36 | 37 | return $this; 38 | } 39 | 40 | /** 41 | * @return $this 42 | */ 43 | public function setLabel($label) 44 | { 45 | $this->label = $label; 46 | 47 | return $this; 48 | } 49 | 50 | /** 51 | * @return MetricMatcher 52 | */ 53 | public function addCallee($selector) 54 | { 55 | $this->matchers[] = $matcher = new MetricMatcher($selector); 56 | 57 | return $matcher; 58 | } 59 | 60 | /** 61 | * @internal 62 | */ 63 | public function toYaml() 64 | { 65 | $yaml = " \"$this->name\":\n"; 66 | $yaml .= " label: \"$this->label\"\n"; 67 | if (null !== $this->layer) { 68 | $yaml .= " layer: \"$this->layer\"\n"; 69 | } 70 | 71 | if ($this->matchers) { 72 | $yaml .= " matching_calls:\n"; 73 | $yaml .= " php:\n"; 74 | 75 | foreach ($this->matchers as $matcher) { 76 | $yaml .= $matcher->toYaml(8); 77 | } 78 | } 79 | 80 | return $yaml; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blackfire/php-sdk", 3 | "description": "Blackfire.io PHP SDK", 4 | "keywords": ["profiler", "xhprof", "performance", "uprofiler"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Blackfire.io", 9 | "email": "support@blackfire.io" 10 | } 11 | ], 12 | "require": { 13 | "php": ">=5.3.0", 14 | "composer/ca-bundle": "^1.0" 15 | }, 16 | "suggest": { 17 | "ext-blackfire": "The C version of the Blackfire probe", 18 | "ext-zlib": "To push config to remote profiling targets", 19 | "symfony/panther": "To use Symfony web test cases with Blackfire" 20 | }, 21 | "autoload": { 22 | "psr-4": { 23 | "Blackfire\\": "src/Blackfire" 24 | } 25 | }, 26 | "extra": { 27 | "branch-alias": { 28 | "dev-master": "2.5-dev" 29 | } 30 | }, 31 | "require-dev": { 32 | "behat/behat": "^3.8", 33 | "friends-of-behat/mink-browserkit-driver": "^1.4", 34 | "friends-of-behat/mink-extension": "^2.5", 35 | "friends-of-behat/symfony-extension": "^2", 36 | "guzzlehttp/psr7": "^1.6 || ^2.0", 37 | "illuminate/console": "^8.81 || ^9.0 || ^10.0 || ^11.0", 38 | "illuminate/queue": "^8.81 || ^9.0 || ^10.0 || ^11.0", 39 | "illuminate/support": "^8.81 || ^9.0 || ^10.0 || ^11.0", 40 | "laravel/octane": "^1.2 || ^2.0", 41 | "phpunit/phpunit": "^9.5", 42 | "psr/http-message": "^1.0 || ^2.0", 43 | "symfony/browser-kit": "^5.1 || ^6.0 || ^7.0", 44 | "symfony/framework-bundle": "^5.1 || ^6.0 || ^7.0", 45 | "symfony/http-client": "^5.1 || ^6.0 || ^7.0", 46 | "symfony/messenger": "^5.1 || ^6.0 || ^7.0", 47 | "symfony/panther": "^1.0 || ^2.0", 48 | "symfony/phpunit-bridge": "^5.2 || ^6.0 || ^7.0" 49 | }, 50 | "config": { 51 | "audit": { 52 | "block-insecure": false 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Blackfire/Bridge/PhpUnit/TestCaseTrait.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Bridge\PhpUnit; 13 | 14 | use Blackfire\Client; 15 | use Blackfire\ClientConfiguration; 16 | use Blackfire\Exception\ExceptionInterface; 17 | use Blackfire\Profile; 18 | use Blackfire\Profile\Configuration as ProfileConfiguration; 19 | use PHPUnit\Framework\Attributes\Before; 20 | 21 | trait TestCaseTrait 22 | { 23 | private static $blackfire; 24 | 25 | /** 26 | * use both doc and attribute to support older versions of php. 27 | * 28 | * @before 29 | */ 30 | #[Before] 31 | protected function createBlackfire() 32 | { 33 | if (!self::$blackfire) { 34 | self::$blackfire = new Client($this->getBlackfireClientConfiguration()); 35 | } 36 | } 37 | 38 | /** 39 | * @param callable $callback The code to profile 40 | * 41 | * @return Profile 42 | */ 43 | public function assertBlackfire(ProfileConfiguration $config, $callback) 44 | { 45 | if (!$config->hasMetadata('skip_timeline')) { 46 | $config->setMetadata('skip_timeline', 'true'); 47 | } 48 | 49 | try { 50 | $probe = self::$blackfire->createProbe($config); 51 | 52 | $callback(); 53 | 54 | $profile = self::$blackfire->endProbe($probe); 55 | 56 | if ($config->hasAssertions()) { 57 | $this->assertThat($profile, new TestConstraint()); 58 | } 59 | } catch (ExceptionInterface $e) { 60 | $this->markTestSkipped($e->getMessage()); 61 | } 62 | 63 | return $profile; 64 | } 65 | 66 | protected function getBlackfireClientConfiguration() 67 | { 68 | return new ClientConfiguration(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Blackfire/Bridge/Behat/BlackfireExtension/ServiceContainer/Driver/BlackfiredKernelBrowserFactory.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Bridge\Behat\BlackfireExtension\ServiceContainer\Driver; 13 | 14 | use Behat\Mink\Driver\BrowserKitDriver; 15 | use Behat\MinkExtension\ServiceContainer\Driver\DriverFactory; 16 | use Blackfire\Bridge\Symfony\BlackfiredKernelBrowser; 17 | use FriendsOfBehat\SymfonyExtension\Driver\SymfonyDriver; 18 | use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; 19 | use Symfony\Component\DependencyInjection\Definition; 20 | use Symfony\Component\DependencyInjection\Reference; 21 | 22 | class BlackfiredKernelBrowserFactory implements DriverFactory 23 | { 24 | public function getDriverName() 25 | { 26 | return 'blackfire_symfony'; 27 | } 28 | 29 | public function supportsJavascript() 30 | { 31 | return false; 32 | } 33 | 34 | public function configure(ArrayNodeDefinition $builder) 35 | { 36 | } 37 | 38 | public function buildDriver(array $config) 39 | { 40 | if (!class_exists(BrowserKitDriver::class)) { 41 | throw new \RuntimeException('Install "friends-of-behat/mink-browserkit-driver" (drop-in replacement for "behat/mink-browserkit-driver") in order to use the "blackfire_symfony" driver.'); 42 | } 43 | if (!class_exists(SymfonyDriver::class)) { 44 | throw new \RuntimeException('Install "friends-of-behat/symfony-extension" (drop-in replacement for "behat/symfony2-extension") in order to use the "blackfire_symfony" driver.'); 45 | } 46 | 47 | return new Definition(BlackfireKernelBrowserDriver::class, array( 48 | new Reference(BlackfiredKernelBrowser::class), 49 | '%mink.base_url%', 50 | )); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Blackfire/Report.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire; 13 | 14 | class Report 15 | { 16 | private $callable; 17 | private $data; 18 | 19 | /** 20 | * @internal 21 | */ 22 | public function __construct($callable) 23 | { 24 | $this->callable = $callable; 25 | } 26 | 27 | /** 28 | * Returns the Build URL on Blackfire.io. 29 | * 30 | * @return string 31 | */ 32 | public function getUrl() 33 | { 34 | if (null === $this->data) { 35 | $this->initializeReport(); 36 | } 37 | 38 | return $this->data['_links']['report']['href']; 39 | } 40 | 41 | /** 42 | * Returns true if the tests executed without any errors. 43 | * 44 | * Errors are different from failures. An error occurs when there is 45 | * a syntax error in an assertion for instance. 46 | * 47 | * @return bool 48 | */ 49 | public function isErrored() 50 | { 51 | if (null === $this->data) { 52 | $this->initializeReport(); 53 | } 54 | 55 | return isset($this->data['report']['state']) && 'errored' === $this->data['report']['state']; 56 | } 57 | 58 | /** 59 | * Returns true if the tests pass, false otherwise. 60 | * 61 | * You should also check isErrored() in case your tests generated an error. 62 | * 63 | * @return bool 64 | */ 65 | public function isSuccessful() 66 | { 67 | if (null === $this->data) { 68 | $this->initializeReport(); 69 | } 70 | 71 | return isset($this->data['report']['state']) && 'successful' === $this->data['report']['state']; 72 | } 73 | 74 | private function initializeReport() 75 | { 76 | $this->data = call_user_func($this->callable); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Blackfire/Probe.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire; 13 | 14 | use Blackfire\Exception\ApiException; 15 | use Blackfire\Profile\Request; 16 | 17 | /** 18 | * Represents a Blackfire Probe. 19 | * 20 | * Instances of this class should never be created directly. 21 | * Use Blackfire\Client instead. 22 | */ 23 | class Probe 24 | { 25 | private $probe; 26 | private $request; 27 | 28 | /** 29 | * @internal 30 | */ 31 | public function __construct(Request $request) 32 | { 33 | $this->request = $request; 34 | $this->probe = new \BlackfireProbe($request->getToken()); 35 | 36 | if ($yaml = $request->getYaml()) { 37 | $this->probe->setConfiguration($yaml); 38 | } 39 | } 40 | 41 | public function getRequest() 42 | { 43 | return $this->request; 44 | } 45 | 46 | public function discard() 47 | { 48 | return $this->probe->discard(); 49 | } 50 | 51 | /** 52 | * @throw ApiException if the probe cannot be enabled 53 | */ 54 | public function enable() 55 | { 56 | $ret = $this->probe->enable(); 57 | 58 | $this->checkError(); 59 | 60 | return $ret; 61 | } 62 | 63 | public function disable() 64 | { 65 | return $this->probe->disable(); 66 | } 67 | 68 | public function close() 69 | { 70 | return $this->probe->close(); 71 | } 72 | 73 | private function checkError() 74 | { 75 | $response = $this->probe->getResponseLine(); 76 | $errorPrefix = 'Blackfire-Error: '; 77 | if (0 !== strpos($response, $errorPrefix)) { 78 | return; 79 | } 80 | 81 | // 4 is the length of the error code + one space 82 | $error = substr($response, strlen($errorPrefix) + 4); 83 | $code = substr($response, strlen($errorPrefix), 3); 84 | 85 | throw ApiException::fromStatusCode($error, $code); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Blackfire/Build/Build.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Build; 13 | 14 | use http\Exception\InvalidArgumentException; 15 | 16 | class Build 17 | { 18 | private $env; 19 | private $data; 20 | private $scenarioCount; 21 | private $scenarios; 22 | private $status; 23 | private $version; 24 | 25 | public function __construct($env, $data) 26 | { 27 | $this->env = $env; 28 | $this->data = $data; 29 | $this->scenarioCount = 0; 30 | $this->scenarios = array(); 31 | $this->status = 'in_progress'; 32 | $this->version = 1; 33 | } 34 | 35 | public function getEnv() 36 | { 37 | return $this->env; 38 | } 39 | 40 | public function getUuid() 41 | { 42 | return $this->data['uuid']; 43 | } 44 | 45 | public function incScenario() 46 | { 47 | @trigger_error(sprintf('The method "%s" is deprecated since blackfire/php-sdk 2.3 and will be removed in 3.0.', __METHOD__), E_USER_DEPRECATED); 48 | 49 | ++$this->scenarioCount; 50 | } 51 | 52 | public function getScenarioCount() 53 | { 54 | return $this->scenarioCount + count($this->scenarios); 55 | } 56 | 57 | public function getUrl() 58 | { 59 | return isset($this->data['_links']['report']['href']) ? $this->data['_links']['report']['href'] : null; 60 | } 61 | 62 | public function addScenario(Scenario $scenario) 63 | { 64 | $this->scenarios[] = $scenario; 65 | } 66 | 67 | /** 68 | * @return Scenario[] 69 | */ 70 | public function getScenarios() 71 | { 72 | return $this->scenarios; 73 | } 74 | 75 | public function getStatus() 76 | { 77 | return $this->status; 78 | } 79 | 80 | public function setStatus($status) 81 | { 82 | if (!in_array($status, array('todo', 'in_progress', 'done'), true)) { 83 | throw new InvalidArgumentException(); 84 | } 85 | 86 | $this->status = $status; 87 | } 88 | 89 | /** 90 | * @internal 91 | */ 92 | public function getNextVersion() 93 | { 94 | ++$this->version; 95 | 96 | return $this->version; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/BlackfireSpan.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | /** 13 | * This is a PHP 5.2 compatible fallback implementation of the BlackfireSpan provided by the extension. 14 | * The interfaces and behavior are the same, or as close as possible. 15 | * 16 | * A general rule of design is that this fallback (as the extension) does not generate any exception. 17 | */ 18 | class BlackfireSpan 19 | { 20 | private $id; 21 | private $name; 22 | private $category; 23 | private $meta; 24 | private $finished = false; 25 | 26 | public function __construct($name = null, $category = null, array $meta = array()) 27 | { 28 | $this->id = microtime(true).mt_rand(0, 9999); 29 | $this->name = $name; 30 | $this->category = $category; 31 | $this->meta = $meta; 32 | 33 | $this->addEntry(http_build_query(array_merge($meta, array( 34 | '__type__' => 'start', 35 | '__id__' => $this->id, 36 | '__name__' => $name, 37 | '__category__' => $category, 38 | )), '', '&')); 39 | } 40 | 41 | public function __destruct() 42 | { 43 | if (!$this->finished) { 44 | $this->stop(); 45 | } 46 | } 47 | 48 | public function stop(array $meta = array()) 49 | { 50 | if ($this->finished) { 51 | trigger_error('Attempt to stop an already stopped BlackfireSpan', E_USER_WARNING); 52 | 53 | return; 54 | } 55 | 56 | $this->addEntry(http_build_query(array_merge($meta, array( 57 | '__type__' => 'stop', 58 | '__id__' => $this->id, 59 | )), '', '&')); 60 | $this->finished = true; 61 | } 62 | 63 | public function addEvent($description, array $meta = array()) 64 | { 65 | $this->addEntry(http_build_query(array_merge($meta, array( 66 | '__type__' => 'event', 67 | '__description__' => $description, 68 | '__id__' => $this->id, 69 | )), '', '&')); 70 | } 71 | 72 | public function lap() 73 | { 74 | $this->stop(); 75 | 76 | return new self($this->name, $this->category, $this->meta); 77 | } 78 | 79 | private function addEntry($entry) 80 | { 81 | $entry = ''; // prevent OPcache optimization 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Blackfire/Bridge/ReactHttpServerHelper.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Bridge; 13 | 14 | use Psr\Http\Message\ServerRequestInterface; 15 | 16 | /** 17 | * Helper used to profile React Http Server requests. 18 | * Please refer to the sample below for usage. 19 | * 20 | * $loop = Factory::create(); 21 | * $blackfire = new \Blackfire\Bridge\ReactHttpServerHelper(); 22 | * 23 | * $server = new Server(function (ServerRequestInterface $request) use ($blackfire) { 24 | * $blackfire->start($request); 25 | * 26 | * // The business logic goes here... 27 | * 28 | * return new Response( 29 | * 200, 30 | * array_merge(array( 31 | * 'Content-Type' => 'text/plain' 32 | * ), $blackfire->stop()), 33 | * "Hello world\n" 34 | * ); 35 | * }); 36 | * 37 | * $socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop); 38 | * $server->listen($socket); 39 | * 40 | * echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; 41 | * 42 | * $loop->run(); 43 | */ 44 | class ReactHttpServerHelper 45 | { 46 | private $probe; 47 | 48 | public function start(ServerRequestInterface $request) 49 | { 50 | $headers = array_change_key_case($request->getHeaders(), CASE_LOWER); 51 | 52 | // Only enable when the X-Blackfire-Query header is present 53 | if (!isset($headers['x-blackfire-query'])) { 54 | return false; 55 | } 56 | 57 | if (null !== $this->probe) { 58 | return false; 59 | } 60 | 61 | $this->probe = new \BlackfireProbe($headers['x-blackfire-query'][0]); 62 | 63 | // Stop if it failed 64 | if (!$this->probe->enable()) { 65 | return false; 66 | } 67 | 68 | return true; 69 | } 70 | 71 | public function stop() 72 | { 73 | if (null === $this->probe) { 74 | return array(); 75 | } 76 | 77 | if (!$this->probe->isEnabled()) { 78 | return array(); 79 | } 80 | 81 | $this->probe->close(); 82 | 83 | $header = explode(':', $this->probe->getResponseLine(), 2); 84 | $this->probe = null; 85 | 86 | return array('x-'.$header[0] => $header[1]); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Blackfire/Bridge/Behat/BlackfireExtension/Event/ScenarioSubscriber.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Bridge\Behat\BlackfireExtension\Event; 13 | 14 | use Behat\Behat\EventDispatcher\Event\ScenarioLikeTested; 15 | use Behat\Behat\EventDispatcher\Event\ScenarioTested; 16 | use Behat\Gherkin\Node\FeatureNode; 17 | use Behat\Gherkin\Node\ScenarioInterface; 18 | use Behat\Mink\Mink; 19 | use Blackfire\Bridge\Behat\BlackfireExtension\ServiceContainer\Driver\BlackfireDriver; 20 | use Blackfire\Build\BuildHelper; 21 | use Blackfire\Exception\ApiException; 22 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; 23 | 24 | class ScenarioSubscriber implements EventSubscriberInterface 25 | { 26 | private $buildHelper; 27 | private $mink; 28 | 29 | public function __construct(BuildHelper $buildHelper, Mink $mink) 30 | { 31 | $this->buildHelper = $buildHelper; 32 | $this->mink = $mink; 33 | } 34 | 35 | public static function getSubscribedEvents(): array 36 | { 37 | return array( 38 | ScenarioTested::BEFORE => 'beforeScenario', 39 | ScenarioTested::AFTER => 'afterScenario', 40 | ); 41 | } 42 | 43 | public function beforeScenario(ScenarioLikeTested $event) 44 | { 45 | if (!$this->buildHelper->isEnabled()) { 46 | return; 47 | } 48 | 49 | if (!$this->isBlackfiredScenario($event->getFeature(), $event->getScenario())) { 50 | return; 51 | } 52 | 53 | $this->buildHelper->createScenario($event->getScenario()->getTitle()); 54 | } 55 | 56 | public function afterScenario(ScenarioLikeTested $event) 57 | { 58 | if (!$this->buildHelper->hasCurrentScenario()) { 59 | return; 60 | } 61 | 62 | try { 63 | $this->buildHelper->endCurrentScenario(); 64 | } catch (ApiException $e) { 65 | $this->buildHelper->endCurrentBuild(); 66 | throw new \RuntimeException("Blackfire: an error occurred with your scenario.\nDid you disable profiling in all its context steps?"); 67 | } 68 | } 69 | 70 | private function isBlackfiredScenario(FeatureNode $feature, ScenarioInterface $scenario) 71 | { 72 | return $this->mink->getSession()->getDriver() instanceof BlackfireDriver; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Blackfire/Bridge/Symfony/Event/MonitoredCommandSubscriber.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Bridge\Symfony\Event; 13 | 14 | use Blackfire\Bridge\Symfony\MonitorableCommandInterface; 15 | use Symfony\Component\Console\Command\Command; 16 | use Symfony\Component\Console\Event\ConsoleCommandEvent; 17 | use Symfony\Component\Console\Event\ConsoleTerminateEvent; 18 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; 19 | 20 | /** 21 | * This subscriber automatically collects traces to Blackfire Monitoring for 22 | * commands implementing MonitorableCommandInterface. Transactions are named after the command name. 23 | * 24 | * To be eligible, commands need to implement MonitorableCommandInterface. 25 | */ 26 | class MonitoredCommandSubscriber implements EventSubscriberInterface 27 | { 28 | private $enabled; 29 | 30 | public function __construct() 31 | { 32 | $this->enabled = extension_loaded('blackfire') && method_exists(\BlackfireProbe::class, 'startTransaction'); 33 | } 34 | 35 | public function onConsoleCommand(ConsoleCommandEvent $event) 36 | { 37 | if (!$this->enabled) { 38 | return; 39 | } 40 | 41 | $command = $event->getCommand(); 42 | if (!$this->isTracingEnabled($command)) { 43 | return; 44 | } 45 | 46 | if (version_compare(phpversion('blackfire'), '1.78.0', '>=')) { 47 | \BlackfireProbe::startTransaction($command->getName()); 48 | } else { 49 | \BlackfireProbe::startTransaction(); 50 | \BlackfireProbe::setTransactionName($command->getName()); 51 | } 52 | } 53 | 54 | public function onConsoleTerminate(ConsoleTerminateEvent $event) 55 | { 56 | if (!$this->enabled) { 57 | return; 58 | } 59 | 60 | if (!$this->isTracingEnabled($event->getCommand())) { 61 | return; 62 | } 63 | 64 | \BlackfireProbe::stopTransaction(); 65 | } 66 | 67 | private function isTracingEnabled(Command $command) 68 | { 69 | return $command instanceof MonitorableCommandInterface; 70 | } 71 | 72 | public static function getSubscribedEvents(): array 73 | { 74 | return array( 75 | ConsoleCommandEvent::class => 'onConsoleCommand', 76 | ConsoleTerminateEvent::class => 'onConsoleTerminate', 77 | ); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Blackfire/Bridge/Laravel/BlackfireTestArtisanCommandsTrait.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Bridge\Laravel; 13 | 14 | use Blackfire\Build\BuildHelper; 15 | use Symfony\Component\Process\Process; 16 | 17 | trait BlackfireTestArtisanCommandsTrait 18 | { 19 | /** 20 | * Run an Artisan console command by name. 21 | * 22 | * @param string $command 23 | * @param \Symfony\Component\Console\Output\OutputInterface|null $outputBuffer 24 | * 25 | * @return int 26 | * 27 | * @throws \Symfony\Component\Console\Exception\CommandNotFoundException 28 | */ 29 | public function call($command, array $parameters = array(), $outputBuffer = null) 30 | { 31 | if ('testing' === env('APP_ENV') && array_key_exists('blackfire-laravel-tests', $parameters)) { 32 | $buildHelper = BuildHelper::getInstance(); 33 | 34 | $process = new Process( 35 | array_merge(array( 36 | 'blackfire', 37 | '--json', 38 | 'run', 39 | '--env='.$buildHelper->getBlackfireEnvironmentId(), 40 | './artisan', 41 | ), explode(' ', $command)), 42 | null, 43 | array( 44 | 'APP_ENV' => 'testing', 45 | ) 46 | ); 47 | 48 | $process->run(); 49 | $output = @json_decode($process->getOutput(), true); 50 | 51 | if (JSON_ERROR_NONE === json_last_error()) { 52 | $statusCode = $output['status']['code'] ?? null; 53 | if (64 !== $statusCode) { 54 | $graphUrl = $output['_links']['graph_url']['href'] ?? ''; 55 | throw new \PHPUnit\Framework\AssertionFailedError("Profile on error:\n$graphUrl"); 56 | } 57 | 58 | $reportState = $output['report']['state'] ?? null; 59 | if ('failed' === $reportState) { 60 | $graphUrl = $output['_links']['graph_url']['href'] ?? ''; 61 | throw new \PHPUnit\Framework\AssertionFailedError("Blackfire assertions on failure:\n$graphUrl"); 62 | } 63 | } 64 | 65 | unset($parameters['blackfire-laravel-tests']); 66 | } 67 | 68 | return parent::call($command, $parameters, $outputBuffer); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Blackfire/Bridge/Laravel/ObservableCommandProvider.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Bridge\Laravel; 13 | 14 | use Illuminate\Console\Events\CommandFinished; 15 | use Illuminate\Console\Events\CommandStarting; 16 | use Illuminate\Console\Events\ScheduledTaskFailed; 17 | use Illuminate\Console\Events\ScheduledTaskFinished; 18 | use Illuminate\Console\Events\ScheduledTaskStarting; 19 | use Illuminate\Support\Facades\Event; 20 | use Illuminate\Support\ServiceProvider; 21 | 22 | class ObservableCommandProvider extends ServiceProvider 23 | { 24 | /** 25 | * Bootstrap services. 26 | * 27 | * @return void 28 | */ 29 | public function boot() 30 | { 31 | if (!class_exists(\BlackfireProbe::class, false)) { 32 | return; 33 | } 34 | 35 | Event::listen( 36 | array( 37 | CommandStarting::class, 38 | ), 39 | function ($event) { 40 | $transactionName = 'artisan '.($event->input->__toString() ?? 'Unnamed Command'); 41 | if (version_compare(phpversion('blackfire'), '1.78.0', '>=')) { 42 | \BlackfireProbe::startTransaction($transactionName); 43 | } else { 44 | \BlackfireProbe::startTransaction(); 45 | \BlackfireProbe::setTransactionName($transactionName); 46 | } 47 | } 48 | ); 49 | 50 | Event::listen( 51 | array( 52 | ScheduledTaskStarting::class, 53 | ), 54 | function ($event) { 55 | $task = $event->task; 56 | $transactionName = $task->expression.' '.$task->command; 57 | if (version_compare(phpversion('blackfire'), '1.78.0', '>=')) { 58 | \BlackfireProbe::startTransaction($transactionName); 59 | } else { 60 | \BlackfireProbe::startTransaction(); 61 | \BlackfireProbe::setTransactionName($transactionName); 62 | } 63 | } 64 | ); 65 | 66 | Event::listen( 67 | array( 68 | CommandFinished::class, 69 | ScheduledTaskFinished::class, 70 | ScheduledTaskFailed::class, 71 | ), 72 | function () { 73 | \BlackfireProbe::stopTransaction(); 74 | } 75 | ); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Blackfire/Bridge/Laravel/OctaneProfiler.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Bridge\Laravel; 13 | 14 | use Illuminate\Http\RedirectResponse; 15 | use Illuminate\Http\Request; 16 | use Illuminate\Http\Response; 17 | 18 | class OctaneProfiler 19 | { 20 | /** @var \BlackfireProbe */ 21 | protected $probe; 22 | 23 | /** @var Request */ 24 | protected $request; 25 | 26 | public function start(Request $request): bool 27 | { 28 | if (!method_exists(\BlackfireProbe::class, 'setAttribute')) { 29 | return false; 30 | } 31 | 32 | if (!$request->headers->has('x-blackfire-query')) { 33 | return false; 34 | } 35 | 36 | if ($this->probe) { 37 | // profiling may have been activated on a concurrent thread 38 | return false; 39 | } 40 | 41 | $this->probe = new \BlackfireProbe($request->headers->get('x-blackfire-query')); 42 | 43 | $this->request = $request; 44 | if (!$this->probe->enable()) { 45 | \BlackfireProbe::setAttribute('profileTitle', $request->url()); 46 | $this->reset(); 47 | throw new \UnexpectedValueException('Cannot enable Blackfire profiler'); 48 | } 49 | 50 | return true; 51 | } 52 | 53 | /** 54 | * @param \Swoole\Http\Request $request 55 | * @param ?Response|RedirectResponse $response 56 | */ 57 | public function stop(Request $request, $response = null): bool 58 | { 59 | if (!class_exists(\BlackfireProbe::class, false)) { 60 | return false; 61 | } 62 | 63 | if (!$this->probe) { 64 | return false; 65 | } 66 | 67 | if (!$this->probe->isEnabled()) { 68 | return false; 69 | } 70 | 71 | if ($this->request !== $request) { 72 | return false; 73 | } 74 | 75 | $this->probe->close(); 76 | if ($response) { 77 | list($probeHeaderName, $probeHeaderValue) = explode(':', $this->probe->getResponseLine(), 2); 78 | $response->header(strtolower("x-$probeHeaderName"), trim($probeHeaderValue)); 79 | } 80 | $this->reset(); 81 | 82 | return true; 83 | } 84 | 85 | public function reset(): void 86 | { 87 | if ($this->probe && $this->probe->isEnabled()) { 88 | $this->probe->close(); 89 | } 90 | $this->probe = null; 91 | $this->request = null; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Blackfire/Bridge/Symfony/BlackfiredKernelBrowser.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Bridge\Symfony; 13 | 14 | use Blackfire\Build\BuildHelper; 15 | use Blackfire\Client as BlackfireClient; 16 | use Blackfire\Profile\Configuration; 17 | use Symfony\Bundle\FrameworkBundle\KernelBrowser; 18 | use Symfony\Component\BrowserKit\CookieJar; 19 | use Symfony\Component\BrowserKit\History; 20 | use Symfony\Component\HttpFoundation\Response; 21 | use Symfony\Component\HttpKernel\KernelInterface; 22 | 23 | class BlackfiredKernelBrowser extends KernelBrowser 24 | { 25 | /** 26 | * @var BuildHelper 27 | */ 28 | private $buildHelper; 29 | 30 | /** 31 | * @var BlackfireClient 32 | */ 33 | private $blackfire; 34 | 35 | private $blackfireEnabled = false; 36 | 37 | public function __construct(KernelInterface $kernel, array $server = array(), ?History $history = null, ?CookieJar $cookieJar = null) 38 | { 39 | parent::__construct($kernel, $server, $history, $cookieJar); 40 | 41 | $this->buildHelper = BuildHelper::getInstance(); 42 | $this->blackfire = $this->buildHelper->getBlackfireClient(); 43 | } 44 | 45 | public function enableBlackfire(): void 46 | { 47 | $this->blackfireEnabled = true; 48 | } 49 | 50 | public function disableBlackfire(): void 51 | { 52 | $this->blackfireEnabled = false; 53 | } 54 | 55 | public function isBlackfireEnabled(): bool 56 | { 57 | return $this->blackfireEnabled; 58 | } 59 | 60 | protected function doRequest($request): Response 61 | { 62 | if ($this->blackfireEnabled) { 63 | $profileConfig = (new Configuration()) 64 | ->setMetadata('skip_timeline', 'false') 65 | ->setTitle(sprintf('%s - %s', $request->getPathInfo(), $request->getMethod())); 66 | if ($this->buildHelper->hasCurrentScenario()) { 67 | $profileConfig->setScenario($this->buildHelper->getCurrentScenario()); 68 | } 69 | 70 | $_SERVER += array( 71 | 'HTTP_HOST' => 'localhost', 72 | 'REQUEST_URI' => $request->getPathInfo(), 73 | 'HTTP_USER_AGENT' => 'BlackfireKernelBrowser', 74 | 'REQUEST_METHOD' => $request->getMethod(), 75 | ); 76 | $probe = $this->blackfire->createProbe($profileConfig); 77 | } 78 | 79 | try { 80 | return parent::doRequest($request); 81 | } finally { 82 | if (isset($probe)) { 83 | $this->blackfire->endProbe($probe); 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Blackfire/Bridge/PhpUnit/TestConstraint.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Bridge\PhpUnit; 13 | 14 | use PHPUnit\Framework\Constraint\Constraint; 15 | use PHPUnit\Framework\ExpectationFailedException; 16 | use SebastianBergmann\Comparator\ComparisonFailure; 17 | 18 | if (!class_exists(Constraint::class) && class_exists(\PHPUnit_Framework_Constraint::class)) { 19 | class_alias(\PHPUnit_Framework_Constraint::class, Constraint::class); 20 | } 21 | 22 | if (!class_exists(ExpectationFailedException::class) && class_exists(\PHPUnit_Framework_ExpectationFailedException::class)) { 23 | class_alias(\PHPUnit_Framework_ExpectationFailedException::class, ExpectationFailedException::class); 24 | } 25 | 26 | trait BlackfireTestContraintTrait 27 | { 28 | protected function doFail($profile, $description, ?ComparisonFailure $comparisonFailure = null) 29 | { 30 | $failureDescription = sprintf('An error occurred when profiling the test. More information at %s', $profile->getUrl().'?settings%5BtabPane%5D=assertions'); 31 | 32 | if (!$profile->isErrored()) { 33 | $tests = $profile->getTests(); 34 | 35 | $failures = 0; 36 | $details = ''; 37 | foreach ($tests as $test) { 38 | if ($test->isSuccessful()) { 39 | continue; 40 | } 41 | 42 | ++$failures; 43 | $details .= sprintf(" %s: %s\n", $test->getState(), $test->getName()); 44 | foreach ($test->getFailures() as $assertion) { 45 | $details .= sprintf(" - %s\n", $assertion); 46 | } 47 | } 48 | $details .= sprintf("\nMore information at %s.", $profile->getUrl().'?settings%5BtabPane%5D=assertions'); 49 | 50 | $failureDescription = "Failed asserting that Blackfire tests pass.\n"; 51 | $failureDescription .= sprintf("%d tests failures out of %d.\n\n", $failures, \count($tests)); 52 | $failureDescription .= $details; 53 | } 54 | 55 | // not used 56 | if (!empty($description)) { 57 | $failureDescription = $description."\n".$failureDescription; 58 | } 59 | 60 | throw new ExpectationFailedException($failureDescription, $comparisonFailure); 61 | } 62 | } 63 | 64 | if (class_exists('PHPUnit\Runner\Version') && version_compare(\PHPUnit\Runner\Version::id(), '11.0.0', '>=')) { 65 | class_alias('Blackfire\Bridge\PhpUnit\TestConstraint11x', 'Blackfire\Bridge\PhpUnit\TestConstraint'); 66 | } elseif (\PHP_VERSION_ID > 70100) { 67 | class_alias('Blackfire\Bridge\PhpUnit\TestConstraint71', 'Blackfire\Bridge\PhpUnit\TestConstraint'); 68 | } else { 69 | class_alias('Blackfire\Bridge\PhpUnit\TestConstraint5x', 'Blackfire\Bridge\PhpUnit\TestConstraint'); 70 | } 71 | 72 | if (false) { 73 | class TestConstraint 74 | { 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Blackfire/Bridge/PhpUnit/BlackfireTestCaseTrait.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Bridge\PhpUnit; 13 | 14 | use Blackfire\Bridge\Symfony\BlackfiredHttpBrowser; 15 | use Blackfire\Build\BuildHelper; 16 | use Symfony\Component\Panther\WebTestAssertionsTrait; 17 | 18 | trait BlackfireTestCaseTrait 19 | { 20 | use WebTestAssertionsTrait; 21 | 22 | // Define this constant in your test case to control the scenario auto-start. 23 | // By default a scenario is created for each test case. 24 | // protected const BLACKFIRE_SCENARIO_AUTO_START = true; 25 | 26 | // Define this constant to give a title to the auto-started scenario. 27 | // protected const BLACKFIRE_SCENARIO_TITLE = null; 28 | 29 | public static function isBlackfireScenarioAutoStart(): bool 30 | { 31 | $autoStartConstant = static::class.'::BLACKFIRE_SCENARIO_AUTO_START'; 32 | 33 | return defined($autoStartConstant) ? constant($autoStartConstant) : true; 34 | } 35 | 36 | public static function setUpBeforeClass(): void 37 | { 38 | parent::setUpBeforeClass(); 39 | $buildHelper = BuildHelper::getInstance(); 40 | if (self::isBlackfireScenarioAutoStart() && $buildHelper->isEnabled()) { 41 | $scenarioConstantName = static::class.'::BLACKFIRE_SCENARIO_TITLE'; 42 | $buildHelper->createScenario( 43 | defined($scenarioConstantName) ? constant($scenarioConstantName) : null 44 | ); 45 | } 46 | } 47 | 48 | public static function tearDownAfterClass(): void 49 | { 50 | $buildHelper = BuildHelper::getInstance(); 51 | if (static::isBlackfireScenarioAutoStart() && $buildHelper->isEnabled()) { 52 | $buildHelper->endCurrentScenario(); 53 | } 54 | parent::tearDownAfterClass(); 55 | } 56 | 57 | /** 58 | * Copycat of PanterTestCaseTrait::createHttpBrowserClient(), but using BlackfiredHttpClient. 59 | */ 60 | protected static function createBlackfiredHttpBrowserClient(): BlackfiredHttpBrowser 61 | { 62 | $callGetClient = \is_callable(array(self::class, 'getClient')) && (new \ReflectionMethod(self::class, 'getClient'))->isStatic(); 63 | 64 | static::startWebServer(); 65 | 66 | if (null === self::$httpBrowserClient) { 67 | self::$httpBrowserClient = new BlackfiredHttpBrowser(BuildHelper::getInstance()); 68 | } 69 | 70 | $urlComponents = parse_url(self::$baseUri); 71 | self::$httpBrowserClient->setServerParameter('HTTP_HOST', sprintf('%s:%s', $urlComponents['host'], $urlComponents['port'])); 72 | if ('https' === $urlComponents['scheme']) { 73 | self::$httpBrowserClient->setServerParameter('HTTPS', 'true'); 74 | } 75 | 76 | // Calling getClient() is mandatory in order to use assertions from BrowserKitAssertionsTrait, as this is the 77 | // only way to give it the created browser which will provide the testable responses. 78 | return $callGetClient ? self::getClient(self::$httpBrowserClient) : self::$httpBrowserClient; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Blackfire/Profile/Request.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Profile; 13 | 14 | /** 15 | * Represents a Blackfire Profile Request. 16 | * 17 | * Instances of this class should never be created directly. 18 | * Use Blackfire\Client instead. 19 | */ 20 | class Request 21 | { 22 | private $configuration; 23 | private $data; 24 | 25 | /** 26 | * @internal 27 | */ 28 | public function __construct(Configuration $configuration, $data) 29 | { 30 | $this->configuration = $configuration; 31 | 32 | if (!isset($data['query_string'])) { 33 | throw new \RuntimeException('The data returned by the signing API are not valid.'); 34 | } 35 | 36 | if (!isset($data['options'])) { 37 | $data['options'] = array(); 38 | } 39 | 40 | $data['options']['aggreg_samples'] = 1; 41 | if ($configuration->getTitle()) { 42 | $data['options']['profile_title'] = $configuration->getTitle(); 43 | } 44 | $data['user_metadata'] = $configuration->getAllMetadata(); 45 | $data['yaml'] = $configuration->toYaml(); 46 | 47 | if (null !== $data['yaml']) { 48 | if (function_exists('gzencode')) { 49 | $data['options']['config_yml'] = base64_encode(gzencode($data['yaml'])); 50 | } else { 51 | // $data['options']['config_yml'] = base64_encode(gzencode(<<isDebug()) { 63 | // and user has actually access to the debug profile 64 | if (isset($data['options']['no_pruning'])) { 65 | $data['options']['no_pruning'] = 1; 66 | } 67 | if (isset($data['options']['no_anon'])) { 68 | $data['options']['no_anon'] = 1; 69 | } 70 | } 71 | 72 | $this->data = $data; 73 | } 74 | 75 | public function getToken() 76 | { 77 | return $this->data['query_string'].'&'.http_build_query($this->data['options'], '', '&'); 78 | } 79 | 80 | public function getProfileUrl() 81 | { 82 | return $this->data['_links']['profile']['href']; 83 | } 84 | 85 | public function getStoreUrl() 86 | { 87 | return $this->data['_links']['store']['href']; 88 | } 89 | 90 | public function getUserMetadata() 91 | { 92 | return $this->data['user_metadata']; 93 | } 94 | 95 | public function getUuid() 96 | { 97 | return $this->data['uuid']; 98 | } 99 | 100 | public function getYaml() 101 | { 102 | return $this->data['yaml']; 103 | } 104 | 105 | /** 106 | * @internal 107 | */ 108 | public function getConfiguration() 109 | { 110 | return $this->configuration; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Blackfire/Bridge/Symfony/BlackfiredHttpBrowser.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Bridge\Symfony; 13 | 14 | use Blackfire\Build\BuildHelper; 15 | use Blackfire\Profile\Configuration; 16 | use Symfony\Component\BrowserKit\HttpBrowser; 17 | use Symfony\Component\DomCrawler\Crawler; 18 | use Symfony\Component\HttpClient\HttpClient; 19 | use Symfony\Component\HttpFoundation\Response; 20 | 21 | class BlackfiredHttpBrowser extends HttpBrowser 22 | { 23 | /** 24 | * @var BuildHelper 25 | */ 26 | private $buildHelper; 27 | 28 | /** 29 | * @var \Blackfire\Client 30 | */ 31 | private $blackfire; 32 | 33 | /** 34 | * @var AutoProfilingBlackfiredClient 35 | */ 36 | private $blackfiredHttpClient; 37 | 38 | private $profilingEnabled; 39 | 40 | private $profileTitle; 41 | 42 | public function __construct(BuildHelper $buildHelper) 43 | { 44 | $this->buildHelper = $buildHelper; 45 | $this->profilingEnabled = $buildHelper->isEnabled(); 46 | $this->blackfire = $buildHelper->getBlackfireClient(); 47 | 48 | if (!class_exists(HttpClient::class)) { 49 | throw new \RuntimeException('symfony/http-client is required to use the BlackfiredHttpBrowser, please add it to your composer dependencies.'); 50 | } 51 | $this->blackfiredHttpClient = new AutoProfilingBlackfiredClient(HttpClient::create(), $this->blackfire); 52 | 53 | parent::__construct($this->blackfiredHttpClient); 54 | } 55 | 56 | public function isProfilingEnabled(): bool 57 | { 58 | return $this->profilingEnabled; 59 | } 60 | 61 | public function enableProfiling(?string $title = null): self 62 | { 63 | $this->profilingEnabled = true; 64 | $this->profileTitle = $title; 65 | 66 | return $this; 67 | } 68 | 69 | public function disableProfiling(): self 70 | { 71 | $this->profilingEnabled = false; 72 | $this->profileTitle = null; 73 | 74 | return $this; 75 | } 76 | 77 | public function request(string $method, string $uri, array $parameters = array(), array $files = array(), array $server = array(), ?string $content = null, bool $changeHistory = true): Crawler 78 | { 79 | if ($this->isProfilingEnabled()) { 80 | $profileConfig = (new Configuration())->setTitle($this->profileTitle ?? sprintf('%s - %s', $uri, $method)); 81 | if ($this->buildHelper->hasCurrentScenario()) { 82 | $profileConfig->setScenario($this->buildHelper->getCurrentScenario()); 83 | } 84 | 85 | $this->blackfiredHttpClient->enableProfiling($profileConfig); 86 | } else { 87 | $this->blackfiredHttpClient->disableProfiling(); 88 | } 89 | 90 | try { 91 | $crawler = parent::request($method, $uri, $parameters, $files, $server, $content, $changeHistory); 92 | } catch (\Throwable $e) { 93 | throw $e; 94 | } finally { 95 | $this->profileTitle = null; 96 | } 97 | 98 | return $crawler; 99 | } 100 | 101 | public function getResponse(): object 102 | { 103 | // Transform BrowserKit\Response into HttpFoundation\Response 104 | $response = parent::getResponse(); 105 | 106 | return new Response($response->getContent(), $response->getStatusCode(), $response->getHeaders()); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /UPGRADE.md: -------------------------------------------------------------------------------- 1 | PHP-SDK UPGRADE 2 | =============== 3 | 4 | To v2.3.0 5 | --------- 6 | 7 | * Methods `Blackfire\Build::incScenario`, `Blackfire\Scenario::incJob` are 8 | deprecated without replacement. 9 | * Method `Blackfire\Client::getScenarioReport` is deprecated. Use 10 | `Blackfire\Client::getBuildReport` instead. 11 | * Method `Blackfire\Client::addJobInScenario` is deprecated. Set the scenario 12 | with `Blackfire\Profile\Configuration::setScenario` then call `Blackfire\Client::endProbe` 13 | 14 | To v1.18.0 15 | ---------- 16 | 17 | * Methods `Blackfire\LoopClient::promoteReferenceSignal`, `Blackfire\LoopClient::attachReference`, 18 | `Blackfire\Profile\Configuration::getReference`, `Blackfire\Profile\Configuration::setReference`, 19 | `Blackfire\Profile\Configuration::isNewReference`, `Blackfire\Profile\Configuration::setAsReference` 20 | are deprecated. 21 | * Class `\Blackfire\Exception\ReferenceNotFoundException` is deprecated. 22 | 23 | To v1.16.0 24 | ---------- 25 | 26 | * Method `getReport($scenarioUuid)` is deprecated. Use `getScenarioReport($scenarioUuid)` instead, 27 | or `getBuildReport($buildUuid)` for a full Build Report. 28 | * Method `closeBuild()` return was void. It now returns a `Blackfire\Report`. 29 | 30 | To v1.14.0 31 | ---------- 32 | 33 | Before this release, a build was only one scenario with one report. 34 | Now a build can have many scenarios. 35 | 36 | Changes are: 37 | * deprecate class `\Blackfire\Build` (replaced by `\Blackfire\Build\Scenario`) 38 | * deprecate methods `createBuild`, `endBuild` and `addJobInBuild` (replaced by `startScenario`, `closeScenario` and `addJobInScenario`) 39 | * add new classes `\Blackfire\Build\Build` and `\Blackfire\Build\Scenario` 40 | * add new methods `startBuild`, `closeBuild`, `startScenario`, `closeScenario` and `addJobInScenario` 41 | 42 | The previous code: 43 | 44 | ```php 45 | createBuild('Blackfire dev', array( 53 | 'title' => 'Legacy build', 54 | 'trigger_name' => 'PHP', 55 | 'external_id' => 'c:my-scenario', 56 | 'external_parent_id' => 'b:my-scenario', 57 | )); 58 | 59 | // create a configuration 60 | $config = new \Blackfire\Profile\Configuration(); 61 | $config->setBuild($build); 62 | 63 | // create as many profiles as you need 64 | $probe = $blackfire->createProbe($config); 65 | 66 | // some PHP code you want to profile 67 | echo strlen('Hello !'); 68 | 69 | $blackfire->endProbe($probe); 70 | 71 | // end the build and fetch the report 72 | $report = $blackfire->endBuild($build); 73 | ``` 74 | 75 | should now be written: 76 | 77 | ```php 78 | startBuild('Blackfire dev', array( 86 | 'title' => 'My build', 87 | 'trigger_name' => 'PHP', 88 | )); 89 | 90 | // create a scenario 91 | $scenario = $blackfire->startScenario($build, array( 92 | 'title' => 'Test documentation', 93 | 'external_id' => 'c:my-scenario', 94 | 'external_parent_id' => 'b:my-scenario', 95 | )); 96 | 97 | // create a configuration 98 | $config = new \Blackfire\Profile\Configuration(); 99 | $config->setScenario($scenario); 100 | 101 | // create as many profiles as you need 102 | $probe = $blackfire->createProbe($config); 103 | 104 | // some PHP code you want to profile 105 | echo strlen('Hello !'); 106 | 107 | $blackfire->endProbe($probe); 108 | 109 | // end the build and fetch the report 110 | $report = $blackfire->closeScenario($scenario); 111 | 112 | $blackfire->closeBuild($build); 113 | ``` 114 | -------------------------------------------------------------------------------- /src/Blackfire/Build/Scenario.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Build; 13 | 14 | class Scenario 15 | { 16 | private $uuid; 17 | private $build; 18 | private $data; 19 | private $jobCount; 20 | private $status; 21 | private $steps; 22 | private $errors; 23 | 24 | public function __construct(Build $build, array $data = array()) 25 | { 26 | $this->uuid = self::generateUuid(); 27 | $this->build = $build; 28 | $this->data = $data + array('title' => null); 29 | $this->jobCount = 0; 30 | $this->status = 'in_progress'; 31 | $this->errors = array(); 32 | $this->steps = array(); 33 | } 34 | 35 | public function getEnv() 36 | { 37 | return $this->build->getEnv(); 38 | } 39 | 40 | public function getBuild() 41 | { 42 | return $this->build; 43 | } 44 | 45 | public function getUuid() 46 | { 47 | return $this->uuid; 48 | } 49 | 50 | public function getUrl() 51 | { 52 | return $this->build->getUrl(); 53 | } 54 | 55 | public function incJob() 56 | { 57 | @trigger_error(sprintf('The method "%s" is deprecated since blackfire/php-sdk 2.3 and will be removed in 3.0.', __METHOD__), E_USER_DEPRECATED); 58 | 59 | ++$this->jobCount; 60 | } 61 | 62 | public function getJobCount() 63 | { 64 | return $this->jobCount + count($this->steps); 65 | } 66 | 67 | public function addStep(array $step) 68 | { 69 | if (!isset($step['uuid'])) { 70 | $step['uuid'] = self::generateUuid(); 71 | } 72 | 73 | if (!isset($step['name'])) { 74 | $step['name'] = 'undefined'; 75 | } 76 | 77 | $this->steps[] = $step; 78 | } 79 | 80 | /** 81 | * @return array[] 82 | */ 83 | public function getSteps() 84 | { 85 | return $this->steps; 86 | } 87 | 88 | public function getStatus() 89 | { 90 | return $this->status; 91 | } 92 | 93 | public function getName() 94 | { 95 | return $this->data['title']; 96 | } 97 | 98 | public function setStatus($status) 99 | { 100 | $this->status = $status; 101 | } 102 | 103 | /** 104 | * @return array 105 | */ 106 | public function getErrors() 107 | { 108 | return $this->errors; 109 | } 110 | 111 | public function addErrors(array $errors) 112 | { 113 | $this->errors = array_merge($this->errors, $errors); 114 | } 115 | 116 | private static function generateUuid() 117 | { 118 | if (function_exists('uuid_create')) { 119 | return uuid_create(\UUID_TYPE_RANDOM); 120 | } 121 | 122 | // Polyfill inspired by https://github.com/symfony/polyfill-uuid/blob/9c44518a5aff8da565c8a55dbe85d2769e6f630e/Uuid.php 123 | // We don't requires the polyfill for compatibility with php 5.x 124 | if (function_exists('random_bytes')) { 125 | $uuid = bin2hex(random_bytes(16)); 126 | } else { 127 | $uuid = substr(sha1(uniqid('', true)), 0, 32); 128 | } 129 | 130 | return sprintf('%08s-%04s-4%03s-%04x-%012s', 131 | // 32 bits for "time_low" 132 | substr($uuid, 0, 8), 133 | // 16 bits for "time_mid" 134 | substr($uuid, 8, 4), 135 | // 16 bits for "time_hi_and_version", 136 | // four most significant bits holds version number 4 137 | substr($uuid, 13, 3), 138 | // 16 bits: 139 | // * 8 bits for "clk_seq_hi_res", 140 | // * 8 bits for "clk_seq_low", 141 | // two most significant bits holds zero and one for variant DCE1.1 142 | hexdec(substr($uuid, 16, 4)) & 0x3FFF | 0x8000, 143 | // 48 bits for "node" 144 | substr($uuid, 20, 12) 145 | ); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /bin/blackfire-io-proxy.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | \033[0m "; 16 | 17 | $log = fopen('php://stdout', 'wb'); 18 | $proxy = stream_socket_server('tcp://'.$upstreamHost, $errno, $errstr); 19 | 20 | $backendInfo = parse_url($backendEndpoint); 21 | if (false === $backendInfo) { 22 | fwrite($log, "The backendEndpoint is not valid ($backendEndpoint)\n"); 23 | exit(1); 24 | } 25 | if ('https' === $backendInfo['scheme']) { 26 | $backendSocketUrl = sprintf('ssl://%s:%s', $backendInfo['host'], isset($backendInfo['port']) ? $backendInfo['port'] : 443); 27 | } else { 28 | $backendSocketUrl = sprintf('%s:%s', $backendInfo['host'], isset($backendInfo['port']) ? $backendInfo['port'] : 80); 29 | } 30 | 31 | $rewrite = array( 32 | 'HTTP/1.1' => 'HTTP/1.0', 33 | $upstreamHost => $backendInfo['host'], 34 | ); 35 | 36 | fwrite($log, "Listening on $upstreamHost\n"); 37 | fwrite($log, "Forwarding to $backendSocketUrl\n"); 38 | 39 | while ($upstream = stream_socket_accept($proxy, -1)) { 40 | $backend = @stream_socket_client($backendSocketUrl, $errno, $errstr); 41 | if (false === $backend) { 42 | fwrite($log, "Could not connect to the backend ($errstr)\n"); 43 | exit(1); 44 | } 45 | 46 | stream_set_timeout($upstream, 1); 47 | stream_set_timeout($backend, 1); 48 | 49 | foreach ($rewrite as $k => $v) { 50 | $line = str_replace($k, $v, fgets($upstream)); 51 | fwrite($log, $OUT.$line); 52 | fwrite($backend, $line, strlen($line)); 53 | } 54 | 55 | $write = array(); 56 | 57 | while (true) { 58 | $read = array($upstream, $backend); 59 | if (!stream_select($read, $write, $write, 5)) { 60 | break; 61 | } 62 | foreach ($read as $socket) { 63 | if (false === $line = fgets($socket)) { 64 | break 2; 65 | } 66 | if ("\x1F" === $line[0] && "\x8B" === $line[1]) { 67 | for ($zip = $line;;) { 68 | fwrite($upstream === $socket ? $backend : $upstream, $line, strlen($line)); 69 | $line = fgets($socket); 70 | if ('-' === $line[0] && '-' === $line[0] && "--\r\n" === substr($line, -4)) { 71 | break; 72 | } 73 | $zip .= $line; 74 | } 75 | if (extension_loaded('zlib')) { 76 | if (function_exists('zlib_decode')) { 77 | $zip = zlib_decode($zip); 78 | } else { 79 | $zip = file_get_contents('compress.zlib://data:application/octet-stream;base64,'.base64_encode($zip)); 80 | } 81 | 82 | foreach (explode("\n", $zip) as $zip) { 83 | fwrite($log, ($upstream === $socket ? $OUT : $IN).rtrim($zip, "\r\n").PHP_EOL); 84 | } 85 | } else { 86 | fwrite($log, ($upstream === $socket ? $OUT : $IN).'COMPRESSED DATA ('.strlen($zip).' bytes). Enable zlib extension to inflate.'.PHP_EOL); 87 | } 88 | } elseif (PHP_VERSION_ID >= 50400 && is_array($json = @json_decode($line, true))) { 89 | foreach (explode("\n", json_encode($json, JSON_PRETTY_PRINT)) as $json) { 90 | fwrite($log, ($upstream === $socket ? $OUT : $IN).rtrim($json, "\r").PHP_EOL); 91 | } 92 | } else { 93 | fwrite($log, ($upstream === $socket ? $OUT : $IN).rtrim($line, "\r\n").PHP_EOL); 94 | } 95 | 96 | fwrite($upstream === $socket ? $backend : $upstream, $line, strlen($line)); 97 | } 98 | } 99 | 100 | @fclose($backend); 101 | @fclose($upstream); 102 | } 103 | -------------------------------------------------------------------------------- /src/Blackfire/LoopClient.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire; 13 | 14 | use Blackfire\Exception\LogicException; 15 | use Blackfire\Exception\RuntimeException; 16 | use Blackfire\Profile\Configuration as ProfileConfiguration; 17 | 18 | class LoopClient 19 | { 20 | private $client; 21 | private $maxIterations; 22 | private $currentIteration = 0; 23 | private $probe; 24 | private $signal = false; 25 | private $enabled = true; 26 | private $running = false; 27 | private $build; 28 | private $scenario; 29 | private $buildFactory; 30 | private $env = false; 31 | 32 | /** 33 | * @param int $maxIterations The number of iterations 34 | */ 35 | public function __construct(Client $client, $maxIterations) 36 | { 37 | $this->client = $client; 38 | $this->maxIterations = $maxIterations; 39 | } 40 | 41 | /** 42 | * @param int $signal A signal that triggers profiling (like SIGUSR1) 43 | */ 44 | public function setSignal($signal) 45 | { 46 | if (!extension_loaded('pcntl')) { 47 | throw new RuntimeException('pcntl must be available to use signals.'); 48 | } 49 | 50 | $enabled = &$this->enabled; 51 | pcntl_signal($signal, function ($signo) use (&$enabled) { 52 | $enabled = true; 53 | }); 54 | 55 | $this->signal = true; 56 | $this->enabled = false; 57 | } 58 | 59 | /** 60 | * @param string|null $env The environment name (or null to use the one configured on the client) 61 | * @param callable|null $buildFactory An optional factory callable that creates build instances 62 | */ 63 | public function generateBuilds($env = null, $buildFactory = null) 64 | { 65 | $this->env = $env; 66 | $this->buildFactory = $buildFactory; 67 | } 68 | 69 | public function startLoop(?ProfileConfiguration $config = null) 70 | { 71 | if ($this->signal) { 72 | pcntl_signal_dispatch(); 73 | } 74 | 75 | if (!$this->enabled) { 76 | return; 77 | } 78 | 79 | if ($this->running) { 80 | throw new LogicException('Unable to start a loop as one is already running.'); 81 | } 82 | 83 | $this->running = true; 84 | 85 | if (0 === $this->currentIteration) { 86 | $this->probe = $this->createProbe($config); 87 | } 88 | 89 | $this->probe->enable(); 90 | } 91 | 92 | /** 93 | * @return Profile|null 94 | */ 95 | public function endLoop() 96 | { 97 | if (!$this->enabled) { 98 | return; 99 | } 100 | 101 | if (null === $this->probe) { 102 | return; 103 | } 104 | 105 | if (!$this->running) { 106 | throw new LogicException('Unable to stop a loop as none is running.'); 107 | } 108 | 109 | $this->running = false; 110 | 111 | $this->probe->close(); 112 | 113 | ++$this->currentIteration; 114 | if ($this->currentIteration === $this->maxIterations) { 115 | return $this->endProbe(); 116 | } 117 | } 118 | 119 | private function createProbe($config) 120 | { 121 | if (null === $config) { 122 | $config = new ProfileConfiguration(); 123 | } else { 124 | $config = clone $config; 125 | } 126 | 127 | $config->setSamples($this->maxIterations); 128 | 129 | if (false !== $this->env) { 130 | $this->scenario = $this->client->startScenario(); 131 | $config->setScenario($this->scenario); 132 | } 133 | 134 | return $this->client->createProbe($config, false); 135 | } 136 | 137 | private function endProbe() 138 | { 139 | $this->currentIteration = 0; 140 | 141 | if ($this->signal) { 142 | $this->enabled = false; 143 | } 144 | 145 | $profile = $this->client->endProbe($this->probe); 146 | 147 | if (null !== $this->scenario) { 148 | $this->client->closeScenario($this->scenario); 149 | $this->client->closeBuild($this->build); 150 | 151 | $this->scenario = null; 152 | $this->build = null; 153 | } 154 | 155 | if (null !== $this->build) { 156 | $this->client->endBuild($this->build); 157 | 158 | $this->build = null; 159 | } 160 | 161 | return $profile; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /stubs/BlackfireProbe.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace { 13 | final class BlackfireProbe 14 | { 15 | /** 16 | * Returns a global singleton and enables it by default. 17 | * 18 | * @return self 19 | */ 20 | public static function getMainInstance() 21 | { 22 | } 23 | 24 | /** 25 | * Tells whether any probes are currently profiling or not. 26 | * 27 | * @return bool 28 | */ 29 | public static function isEnabled() 30 | { 31 | } 32 | 33 | /** 34 | * Instantiate a probe object. 35 | */ 36 | public function __construct($query, $envId = null, $envToken = null, $agentSocket = null) 37 | { 38 | } 39 | 40 | /** 41 | * Tells if the probe is cryptographically verified, i.e. if the signature in $query is valid. 42 | * 43 | * @return bool 44 | */ 45 | public function isVerified() 46 | { 47 | } 48 | 49 | /** 50 | * Gets the response message/status/line. 51 | * 52 | * This lines gives details about the status of the probe. That can be: 53 | * - an error: `Blackfire-Error: $errNumber $urlEncodedErrorMessage` 54 | * - or not: `Blackfire-Response: $rfc1738EncodedMessage` 55 | * 56 | * @return string The response line 57 | */ 58 | public function getResponseLine() 59 | { 60 | } 61 | 62 | /** 63 | * Enables profiling instrumentation and data aggregation. 64 | * 65 | * One and only one probe can be enabled at the same time. 66 | * 67 | * @return bool false if enabling failed 68 | * @see getResponseLine() for error/status reporting 69 | * 70 | */ 71 | public function enable() 72 | { 73 | } 74 | 75 | /** 76 | * Discard collected data and disables instrumentation. 77 | * 78 | * Does not close the profile payload, allowing to re-enable the probe and aggregate data in the same profile. 79 | * 80 | * @return bool false if the probe was not enabled 81 | */ 82 | public function discard() 83 | { 84 | } 85 | 86 | /** 87 | * Disables profiling instrumentation and data aggregation. 88 | * 89 | * Does not close the profile payload, allowing to re-enable the probe and aggregate data in the same profile. 90 | * As a side-effect, flushes the collected profile to the output. 91 | * 92 | * @return bool false if the probe was not enabled 93 | */ 94 | public function disable() 95 | { 96 | } 97 | 98 | /** 99 | * Disables and closes profiling instrumentation and data aggregation. 100 | * 101 | * Closing means that a later enable() will create a new profile on the output. 102 | * As a side-effect, flushes the collected profile to the output. 103 | * 104 | * @return bool false if the probe was not enabled 105 | */ 106 | public function close() 107 | { 108 | } 109 | 110 | /** 111 | * Adds a marker for the Timeline View. 112 | * Production safe. Operates a no-op if no profile is requested. 113 | * 114 | * @param string $markerName 115 | */ 116 | public static function addMarker($label = '') 117 | { 118 | } 119 | 120 | /** 121 | * Creates a sub-query string to create a new profile linked to the current one. 122 | * This query must be set in the X-Blackire-Query HTTP header or in the BLACKFIRE_QUERY environment variable. 123 | * 124 | * @return string|null the sub-query or null if profiling is disabled 125 | */ 126 | public function createSubProfileQuery() 127 | { 128 | } 129 | 130 | /** 131 | * Set the transaction name. 132 | */ 133 | public static function setTransactionName($transactionName) 134 | { 135 | } 136 | 137 | public static function startTransaction($transactionName = null) 138 | { 139 | } 140 | 141 | public static function stopTransaction() 142 | { 143 | } 144 | 145 | public static function ignoreTransaction() 146 | { 147 | } 148 | 149 | public static function getBrowserProbe($withTags = true) 150 | { 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/Blackfire/Bridge/Behat/BlackfireExtension/ServiceContainer/BlackfireExtension.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Bridge\Behat\BlackfireExtension\ServiceContainer; 13 | 14 | use Behat\Behat\EventDispatcher\ServiceContainer\EventDispatcherExtension; 15 | use Behat\MinkExtension\ServiceContainer\MinkExtension; 16 | use Behat\Testwork\Output\ServiceContainer\OutputExtension; 17 | use Behat\Testwork\ServiceContainer\Extension as ExtensionInterface; 18 | use Behat\Testwork\ServiceContainer\ExtensionManager; 19 | use Blackfire\Bridge\Behat\BlackfireExtension\Event\BuildSubscriber; 20 | use Blackfire\Bridge\Behat\BlackfireExtension\Event\ScenarioSubscriber; 21 | use Blackfire\Bridge\Behat\BlackfireExtension\ServiceContainer\Driver\BlackfiredHttpBrowserFactory; 22 | use Blackfire\Bridge\Behat\BlackfireExtension\ServiceContainer\Driver\BlackfiredKernelBrowserFactory; 23 | use Blackfire\Bridge\Symfony\BlackfiredHttpBrowser; 24 | use Blackfire\Bridge\Symfony\BlackfiredKernelBrowser; 25 | use Blackfire\Build\BuildHelper; 26 | use FriendsOfBehat\SymfonyExtension\Driver\SymfonyDriver; 27 | use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; 28 | use Symfony\Component\DependencyInjection\ContainerBuilder; 29 | use Symfony\Component\DependencyInjection\Definition; 30 | use Symfony\Component\DependencyInjection\Reference; 31 | 32 | class BlackfireExtension implements ExtensionInterface 33 | { 34 | public function process(ContainerBuilder $container) 35 | { 36 | } 37 | 38 | public function getConfigKey() 39 | { 40 | return 'blackfire'; 41 | } 42 | 43 | public function initialize(ExtensionManager $extensionManager) 44 | { 45 | /** @var MinkExtension $minkExtension */ 46 | $minkExtension = $extensionManager->getExtension('mink'); 47 | if (null === $minkExtension) { 48 | return; 49 | } 50 | 51 | $minkExtension->registerDriverFactory(new BlackfiredHttpBrowserFactory()); 52 | $minkExtension->registerDriverFactory(new BlackfiredKernelBrowserFactory()); 53 | } 54 | 55 | public function configure(ArrayNodeDefinition $builder) 56 | { 57 | $builder 58 | ->children() 59 | ->scalarNode('blackfire_environment') 60 | ->isRequired() 61 | ->info('The Blackfire environment name or its UUID.') 62 | ->end() 63 | ->scalarNode('build_name') 64 | ->defaultValue('Behat Build') 65 | ->info('Name for the build, as it appears in the Blackfire Build Dashboard.') 66 | ->end() 67 | ->end() 68 | ->end(); 69 | } 70 | 71 | public function load(ContainerBuilder $container, array $config) 72 | { 73 | $container->setDefinition( 74 | BuildHelper::class, 75 | (new Definition(BuildHelper::class)) 76 | ->setFactory(BuildHelper::class.'::getInstance') 77 | ); 78 | $container->setDefinition( 79 | BlackfiredHttpBrowser::class, 80 | new Definition(BlackfiredHttpBrowser::class, array(new Reference(BuildHelper::class))) 81 | ); 82 | if (class_exists(SymfonyDriver::class)) { 83 | $container->setDefinition( 84 | BlackfiredKernelBrowser::class, 85 | new Definition(BlackfiredKernelBrowser::class, array(new Reference('fob_symfony.kernel'))) 86 | ); 87 | } 88 | 89 | $container->setParameter('blackfire.environment', $config['blackfire_environment']); 90 | $container->setParameter('blackfire.build_name', $config['build_name']); 91 | 92 | $this->registerSubscribers($container); 93 | } 94 | 95 | private function registerSubscribers(ContainerBuilder $container) 96 | { 97 | $buildSubscriberDef = new Definition(BuildSubscriber::class, array( 98 | new Reference(OutputExtension::FORMATTER_TAG.'.pretty'), 99 | new Reference(BuildHelper::class), 100 | '%blackfire.environment%', 101 | '%blackfire.build_name%', 102 | )); 103 | $buildSubscriberDef->addTag(EventDispatcherExtension::SUBSCRIBER_TAG); 104 | $container->setDefinition( 105 | BuildSubscriber::class, 106 | $buildSubscriberDef 107 | ); 108 | 109 | $scenarioSubscriberDef = new Definition(ScenarioSubscriber::class, array( 110 | new Reference(BuildHelper::class), 111 | new Reference(MinkExtension::MINK_ID), 112 | )); 113 | $scenarioSubscriberDef->addTag(EventDispatcherExtension::SUBSCRIBER_TAG); 114 | $container->setDefinition(ScenarioSubscriber::class, $scenarioSubscriberDef); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/Blackfire/Bridge/Laravel/BlackfireTestCase.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Bridge\Laravel; 13 | 14 | use Blackfire\Build\BuildHelper; 15 | use Illuminate\Testing\TestResponse; 16 | use Symfony\Component\HttpFoundation\Request; 17 | use Tests\TestCase; 18 | 19 | abstract class BlackfireTestCase extends TestCase 20 | { 21 | /** @var string */ 22 | protected $blackfireScenarioTitle; 23 | 24 | /** @var bool */ 25 | protected $profileAllRequests = true; 26 | 27 | /** @var bool */ 28 | protected $profileNextRequest = true; 29 | 30 | /** @var Request */ 31 | private $request; 32 | 33 | /** @var ?string */ 34 | private $nextProfileTitle; 35 | 36 | /** @var ?BuildHelper */ 37 | private $buildHelper; 38 | 39 | public static function tearDownAfterClass(): void 40 | { 41 | $buildHelper = BuildHelper::getInstance(); 42 | $scenarioKey = debug_backtrace()[1]['object']->toString(); 43 | if ($buildHelper->hasScenario($scenarioKey)) { 44 | $buildHelper->endScenario($scenarioKey); 45 | } 46 | } 47 | 48 | /** 49 | * Call artisan command and return code. 50 | * 51 | * @param string $command 52 | * @param array $parameters 53 | * 54 | * @return \Illuminate\Testing\PendingCommand|int 55 | */ 56 | public function artisan($command, $parameters = array()) 57 | { 58 | if ($this->profileNextRequest) { 59 | $parameters['blackfire-laravel-tests'] = true; 60 | } 61 | 62 | return parent::artisan($command, $parameters); 63 | } 64 | 65 | protected function initializeTestEnvironment(): void 66 | { 67 | $scenarioKey = get_class(debug_backtrace()[1]['object']); 68 | if (!$this->buildHelper->hasScenario($scenarioKey)) { 69 | $this->buildHelper->createScenario( 70 | $this->blackfireScenarioTitle ?? $scenarioKey, 71 | $scenarioKey 72 | ); 73 | } 74 | } 75 | 76 | /** 77 | * Call the given URI and return the Response. 78 | * 79 | * @param string $method 80 | * @param string $uri 81 | * @param array $parameters 82 | * @param array $cookies 83 | * @param array $files 84 | * @param array $server 85 | * @param string|null $content 86 | * 87 | * @return TestResponse 88 | */ 89 | public function call($method, $uri, $parameters = array(), $cookies = array(), $files = array(), $server = array(), $content = null) 90 | { 91 | if ($this->profileNextRequest) { 92 | $this->initializeTestEnvironment(); 93 | 94 | $scenarioKey = get_class(debug_backtrace()[1]['object']); 95 | 96 | $stepTitle = $this->nextProfileTitle ?? $method.' '.$uri; 97 | $this->blackfireStepTitle = null; 98 | $this->request = $this->buildHelper->createRequest($scenarioKey, $stepTitle); 99 | 100 | $server[$this->formatServerHeaderKey('X-Blackfire-Query')] = $this->request->getToken(); 101 | } 102 | 103 | return parent::call($method, $uri, $parameters, $cookies, $files, $server, $content); 104 | } 105 | 106 | protected function createTestResponse($response): TestResponse 107 | { 108 | $response = parent::createTestResponse($response); 109 | 110 | if ($this->request) { 111 | $this->printProfileLink($this->request->getUuid()); 112 | } 113 | 114 | return $response; 115 | } 116 | 117 | protected function enableProfiling(): self 118 | { 119 | $this->profileNextRequest = true; 120 | 121 | return $this; 122 | } 123 | 124 | protected function disableProfiling(): self 125 | { 126 | $this->profileNextRequest = false; 127 | 128 | return $this; 129 | } 130 | 131 | protected function setUp(): void 132 | { 133 | parent::setUp(); 134 | 135 | $this->profileNextRequest = $this->profileAllRequests; 136 | $this->nextProfileTitle = null; 137 | $this->buildHelper = BuildHelper::getInstance(); 138 | 139 | if (!$this->buildHelper->isEnabled()) { 140 | $this->profileNextRequest = false; 141 | } 142 | } 143 | 144 | protected function tearDown(): void 145 | { 146 | $this->request = null; 147 | $this->nextProfileTitle = null; 148 | 149 | parent::tearDown(); 150 | } 151 | 152 | protected function setProfileTitle(?string $profileTitle): self 153 | { 154 | $this->nextProfileTitle = $profileTitle; 155 | 156 | return $this; 157 | } 158 | 159 | private function printProfileLink(string $profileUuid): void 160 | { 161 | echo "\033[01;36m https://blackfire.io/profiles/{$profileUuid}/graph \033[0m\n"; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/Blackfire/Bridge/Behat/BlackfireExtension/Event/BuildSubscriber.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Bridge\Behat\BlackfireExtension\Event; 13 | 14 | use Behat\Testwork\EventDispatcher\Event\SuiteTested; 15 | use Behat\Testwork\Output\Formatter; 16 | use Blackfire\Build\BuildHelper; 17 | use Blackfire\Exception\ApiException; 18 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; 19 | 20 | class BuildSubscriber implements EventSubscriberInterface 21 | { 22 | private $printer; 23 | private $buildHelper; 24 | private $blackfireEnvironmentId; 25 | private $buildTitle; 26 | private $externalId; 27 | private $externalParentId; 28 | 29 | public function __construct(Formatter $formatter, BuildHelper $buildHelper, string $blackfireEnvironmentId, string $buildTitle = 'Build from Behat') 30 | { 31 | $this->printer = $formatter->getOutputPrinter(); 32 | $this->blackfireEnvironmentId = $blackfireEnvironmentId; 33 | $this->buildTitle = $buildTitle; 34 | $this->externalId = $this->getEnv('BLACKFIRE_EXTERNAL_ID'); 35 | $this->externalParentId = $this->getEnv('BLACKFIRE_EXTERNAL_PARENT_ID'); 36 | $this->buildHelper = $buildHelper; 37 | $this->buildHelper->setEnabled($this->isGloballyEnabled()); 38 | } 39 | 40 | private function getEnv(string $envVarName): ?string 41 | { 42 | $value = getenv($envVarName); 43 | 44 | return false === $value ? null : $value; 45 | } 46 | 47 | private function isGloballyEnabled(): bool 48 | { 49 | $buildDisabled = $this->getEnv('BLACKFIRE_BUILD_DISABLED'); 50 | 51 | return \is_null($buildDisabled) || '0' === $buildDisabled; 52 | } 53 | 54 | public static function getSubscribedEvents(): array 55 | { 56 | return array( 57 | SuiteTested::BEFORE => 'prepareBuild', 58 | SuiteTested::BEFORE_TEARDOWN => 'endUpBuild', 59 | ); 60 | } 61 | 62 | public function prepareBuild(SuiteTested $event) 63 | { 64 | if (!$this->buildHelper->isEnabled()) { 65 | return; 66 | } 67 | 68 | $this->buildHelper->deferBuild( 69 | $this->blackfireEnvironmentId, 70 | sprintf('%s (%s)', $this->buildTitle, $event->getSuite()->getName()), 71 | $this->externalId, 72 | $this->externalParentId, 73 | 'Behat' 74 | ); 75 | } 76 | 77 | public function endUpBuild(SuiteTested $event) 78 | { 79 | if (!$this->buildHelper->hasCurrentBuild()) { 80 | return; 81 | } 82 | 83 | $build = $this->buildHelper->getCurrentBuild(); 84 | if (0 === $build->getScenarioCount()) { 85 | $this->printer->writeln($this->formatMessage('Blackfire: No scenario was created for the current build.', 'skipped')); 86 | $this->buildHelper->endCurrentBuild(); 87 | 88 | return; 89 | } 90 | 91 | if ($this->buildHelper->hasCurrentScenario()) { 92 | $this->printer->writeln($this->formatMessage('The last scenario was not ended.', 'pending_param', true)); 93 | 94 | $this->buildHelper->endCurrentScenario(); 95 | } 96 | 97 | $report = $this->buildHelper->endCurrentBuild(); 98 | 99 | try { 100 | $this->printer->writeln($this->formatMessage('Blackfire Report:', 'keyword', true)); 101 | $style = 'failed'; 102 | if ($report->isErrored()) { 103 | $this->printer->writeln($this->formatMessage('The build has errored', $style)); 104 | } else { 105 | if ($report->isSuccessful()) { 106 | $style = 'passed'; 107 | $this->printer->writeln($this->formatMessage('The build was successful', $style)); 108 | } else { 109 | $this->printer->writeln($this->formatMessage('The build has failed', $style)); 110 | } 111 | } 112 | 113 | $this->printer->writeln($this->formatMessage('Full report is available here:', $style)); 114 | $this->printer->writeln($this->formatMessage($report->getUrl(), 'info')); 115 | } catch (ApiException $e) { 116 | $this->printer->writeln($this->formatMessage('An error has occured while communicating with Blackfire.io', 'error')); 117 | $this->printer->writeln($this->formatMessage($e->getMessage(), 'exception')); 118 | } finally { 119 | $this->printer->writeln(); 120 | } 121 | } 122 | 123 | private function formatMessage(string $message, string $style, bool $isHeader = false): string 124 | { 125 | return sprintf( 126 | '%s{+%s}%s{-%s}%s', 127 | $isHeader ? '' : ' ', 128 | $style, 129 | $message, 130 | $style, 131 | $isHeader ? PHP_EOL : '' 132 | ); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/Blackfire/Bridge/PhpUnit/BlackfireBuildExtension.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Bridge\PhpUnit; 13 | 14 | use Blackfire\Build\BuildHelper; 15 | use Blackfire\Exception\ApiException; 16 | use PHPUnit\Runner\AfterLastTestHook; 17 | use PHPUnit\Runner\AfterTestHook; 18 | use PHPUnit\Runner\BeforeFirstTestHook; 19 | use PHPUnit\Util\Color; 20 | 21 | class BlackfireBuildExtension implements BeforeFirstTestHook, AfterLastTestHook, AfterTestHook 22 | { 23 | /** @var BuildHelper */ 24 | private $buildHelper; 25 | 26 | /** @var string */ 27 | private $blackfireEnvironmentId; 28 | 29 | /** @var string */ 30 | private $buildTitle; 31 | 32 | /** @var ?string */ 33 | private $externalId; 34 | 35 | /** @var ?string */ 36 | private $externalParentId; 37 | 38 | public function __construct( 39 | string $blackfireEnvironmentId, 40 | string $buildTitle = 'Build from PHPUnit', 41 | ?BuildHelper $buildHelper = null, 42 | ) { 43 | $this->blackfireEnvironmentId = $blackfireEnvironmentId; 44 | $this->buildTitle = $buildTitle; 45 | $this->externalId = $this->getEnv('BLACKFIRE_EXTERNAL_ID'); 46 | $this->externalParentId = $this->getEnv('BLACKFIRE_EXTERNAL_PARENT_ID'); 47 | $this->buildHelper = $buildHelper ?? BuildHelper::getInstance(); 48 | $this->buildHelper->setEnabled($this->isGloballyEnabled()); 49 | $this->buildHelper->setBlackfireEnvironmentId($blackfireEnvironmentId); 50 | } 51 | 52 | private function getEnv(string $envVarName): ?string 53 | { 54 | $value = getenv($envVarName); 55 | 56 | return false === $value ? null : $value; 57 | } 58 | 59 | private function isGloballyEnabled(): bool 60 | { 61 | $buildDisabled = $this->getEnv('BLACKFIRE_BUILD_DISABLED'); 62 | 63 | return \is_null($buildDisabled) || '0' === $buildDisabled; 64 | } 65 | 66 | public function executeBeforeFirstTest(): void 67 | { 68 | if (!$this->buildHelper->isEnabled()) { 69 | return; 70 | } 71 | 72 | $this->buildHelper->deferBuild($this->blackfireEnvironmentId, $this->buildTitle, $this->externalId, $this->externalParentId, 'PHPUnit'); 73 | } 74 | 75 | public function executeAfterLastTest(): void 76 | { 77 | if (!$this->buildHelper->hasCurrentBuild()) { 78 | return; 79 | } 80 | 81 | $build = $this->buildHelper->getCurrentBuild(); 82 | if (0 === $build->getScenarioCount()) { 83 | echo "\n"; 84 | echo Color::colorize('bg-yellow', 'Blackfire: No scenario was created for the current build.'); 85 | echo "\n"; 86 | 87 | $this->buildHelper->endCurrentBuild(); 88 | 89 | return; 90 | } 91 | 92 | if ($this->buildHelper->hasAnyScenario()) { 93 | echo "\n"; 94 | echo Color::colorize('bg-yellow', 'Blackfire: The last scenario was not ended.'); 95 | echo "\n"; 96 | 97 | $this->buildHelper->endAllScenarios(); 98 | } 99 | 100 | $report = $this->buildHelper->endCurrentBuild(); 101 | 102 | try { 103 | if ($report->isErrored()) { 104 | echo "\n"; 105 | echo Color::colorize('bg-red,fg-white', 'Blackfire: The build has errored'); 106 | echo "\n"; 107 | } else { 108 | if ($report->isSuccessful()) { 109 | echo "\n"; 110 | echo Color::colorize('bg-green', 'Blackfire: The build was successful'); 111 | echo "\n"; 112 | } else { 113 | echo "\n"; 114 | echo Color::colorize('bg-red,fg-white', 'Blackfire: The build has failed'); 115 | echo "\n"; 116 | } 117 | } 118 | 119 | echo "Full report is available here:\n"; 120 | echo "{$report->getUrl()}\n"; 121 | } catch (ApiException $e) { 122 | echo "\n"; 123 | echo Color::colorize('bg-red,fg-white', 'Blackfire: The build has errored'); 124 | echo "\n"; 125 | } 126 | } 127 | 128 | public function executeAfterTest(string $test, float $time): void 129 | { 130 | list($class) = explode('::', $test); 131 | if (!method_exists($class, 'isBlackfireScenarioAutoStart')) { 132 | return; 133 | } 134 | 135 | if (true === call_user_func("$class::isBlackfireScenarioAutoStart")) { 136 | return; 137 | } 138 | 139 | // If scenario is not automatically started, it should at least be ended 140 | // at the end of each test. 141 | // This is to avoid an exception when creating a new scenario. 142 | if ($this->buildHelper->hasCurrentScenario()) { 143 | $this->buildHelper->endCurrentScenario(); 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/Blackfire/Util/NoProxyPattern.php: -------------------------------------------------------------------------------- 1 | 27 | * Jordi Boggiano 28 | * 29 | * For the full copyright and license information, please view the LICENSE 30 | * file that was distributed with this source code. 31 | */ 32 | 33 | namespace Blackfire\Util; 34 | 35 | /** 36 | * Tests URLs against no_proxy patterns. 37 | */ 38 | class NoProxyPattern 39 | { 40 | /** 41 | * @var string[] 42 | */ 43 | protected $rules = array(); 44 | 45 | /** 46 | * @param string $pattern no_proxy pattern 47 | */ 48 | public function __construct($pattern) 49 | { 50 | $this->rules = preg_split("/[\s,]+/", $pattern); 51 | } 52 | 53 | /** 54 | * Test a URL against the stored pattern. 55 | * 56 | * @param string $url 57 | * 58 | * @return true if the URL matches one of the rules 59 | */ 60 | public function test($url) 61 | { 62 | $host = parse_url($url, PHP_URL_HOST); 63 | $port = parse_url($url, PHP_URL_PORT); 64 | 65 | if (empty($port)) { 66 | switch (parse_url($url, PHP_URL_SCHEME)) { 67 | case 'http': 68 | $port = 80; 69 | break; 70 | case 'https': 71 | $port = 443; 72 | break; 73 | } 74 | } 75 | 76 | foreach ($this->rules as $rule) { 77 | if ('*' == $rule) { 78 | return true; 79 | } 80 | 81 | $match = false; 82 | list($ruleHost) = explode(':', $rule); 83 | list($base) = explode('/', $ruleHost); 84 | 85 | if (filter_var($base, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { 86 | // ip or cidr match 87 | if (!isset($ip)) { 88 | $ip = gethostbyname($host); 89 | } 90 | if (false === strpos($ruleHost, '/')) { 91 | $match = $ip === $ruleHost; 92 | } else { 93 | // gethostbyname() failed to resolve $host to an ip, so we assume 94 | // it must be proxied to let the proxy's DNS resolve it 95 | if ($ip === $host) { 96 | $match = false; 97 | } else { 98 | // match resolved IP against the rule 99 | $match = self::inCIDRBlock($ruleHost, $ip); 100 | } 101 | } 102 | } else { 103 | // match end of domain 104 | $haystack = '.'.trim($host, '.').'.'; 105 | $needle = '.'.trim($ruleHost, '.').'.'; 106 | $match = 0 === stripos(strrev($haystack), strrev($needle)); 107 | } 108 | 109 | // final port check 110 | if ($match && false !== strpos($rule, ':')) { 111 | list(, $rulePort) = explode(':', $rule); 112 | if (!empty($rulePort) && $port != $rulePort) { 113 | $match = false; 114 | } 115 | } 116 | 117 | if ($match) { 118 | return true; 119 | } 120 | } 121 | 122 | return false; 123 | } 124 | 125 | /** 126 | * Check an IP address against a CIDR. 127 | * 128 | * http://framework.zend.com/svn/framework/extras/incubator/library/ZendX/Whois/Adapter/Cidr.php 129 | * 130 | * @param string $cidr IPv4 block in CIDR notation 131 | * @param string $ip IPv4 address 132 | * 133 | * @return bool 134 | */ 135 | private static function inCIDRBlock($cidr, $ip) 136 | { 137 | // Get the base and the bits from the CIDR 138 | list($base, $bits) = explode('/', $cidr); 139 | // Now split it up into it's classes 140 | list($a, $b, $c, $d) = explode('.', $base); 141 | // Now do some bit shifting/switching to convert to ints 142 | $i = ($a << 24) + ($b << 16) + ($c << 8) + $d; 143 | $mask = 0 == $bits ? 0 : (~0 << (32 - $bits)); 144 | // Here's our lowest int 145 | $low = $i & $mask; 146 | // Here's our highest int 147 | $high = $i | (~$mask & 0xFFFFFFFF); 148 | // Now split the ip we're checking against up into classes 149 | list($a, $b, $c, $d) = explode('.', $ip); 150 | // Now convert the ip we're checking against to an int 151 | $check = ($a << 24) + ($b << 16) + ($c << 8) + $d; 152 | 153 | // If the ip is within the range, including highest/lowest values, 154 | // then it's within the CIDR range 155 | return $check >= $low && $check <= $high; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/Blackfire/Bridge/Guzzle/Middleware.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | use Blackfire\Client as BlackfireClient; 15 | use Blackfire\Profile\Configuration as ProfileConfiguration; 16 | use GuzzleHttp\Promise\PromiseInterface; 17 | use GuzzleHttp\Psr7; 18 | use Psr\Http\Message\RequestInterface; 19 | use Psr\Http\Message\ResponseInterface; 20 | use Psr\Log\LoggerInterface; 21 | 22 | /** 23 | * Blackfire middleware for Guzzle. 24 | * 25 | * @author Fabien Potencier 26 | */ 27 | class Middleware 28 | { 29 | private $handler; 30 | private $blackfire; 31 | private $logger; 32 | private $autoEnable; 33 | 34 | public function __construct(BlackfireClient $blackfire, callable $handler, ?LoggerInterface $logger = null, $autoEnable = true) 35 | { 36 | $this->blackfire = $blackfire; 37 | $this->handler = $handler; 38 | $this->logger = $logger; 39 | $this->autoEnable = (bool) $autoEnable; 40 | } 41 | 42 | public static function create(BlackfireClient $blackfire, ?LoggerInterface $logger = null, $autoEnable = true) 43 | { 44 | return function (callable $handler) use ($blackfire, $logger, $autoEnable) { 45 | return new self($blackfire, $handler, $logger, $autoEnable); 46 | }; 47 | } 48 | 49 | /** 50 | * @return PromiseInterface 51 | */ 52 | public function __invoke(RequestInterface $request, array $options) 53 | { 54 | $fn = $this->handler; 55 | 56 | if ($this->shouldAutoEnable() && !array_key_exists('blackfire', $options)) { 57 | $options['blackfire'] = new ProfileConfiguration(); 58 | } 59 | 60 | if (!$request->hasHeader('X-Blackfire-Query') && (!isset($options['blackfire']) || false === $options['blackfire'])) { 61 | return $fn($request, $options); 62 | } 63 | 64 | if (!$request->hasHeader('X-Blackfire-Query')) { 65 | if (\BlackfireProbe::isEnabled()) { 66 | $probe = \BlackfireProbe::getMainInstance(); 67 | $probe->disable(); 68 | } 69 | 70 | if (true === $options['blackfire']) { 71 | $options['blackfire'] = new ProfileConfiguration(); 72 | } elseif (!$options['blackfire'] instanceof ProfileConfiguration) { 73 | throw new \InvalidArgumentException('blackfire must be true or an instance of \Blackfire\Profile\Configuration.'); 74 | } 75 | 76 | $profileRequest = $this->blackfire->createRequest($options['blackfire']); 77 | 78 | if (isset($probe)) { 79 | $probe->enable(); 80 | } 81 | 82 | $request = $request 83 | ->withHeader('X-Blackfire-Query', $profileRequest->getToken()) 84 | ->withHeader('X-Blackfire-Profile-Url', $profileRequest->getProfileUrl()) 85 | ->withHeader('X-Blackfire-Profile-Uuid', $profileRequest->getUuid()) 86 | ; 87 | } 88 | 89 | return $fn($request, $options) 90 | ->then(function (ResponseInterface $response) use ($request, $options) { 91 | return $this->processResponse($request, $options, $response); 92 | }); 93 | } 94 | 95 | /** 96 | * @param ResponseInterface|PromiseInterface $response 97 | * 98 | * @return ResponseInterface|PromiseInterface 99 | */ 100 | public function processResponse(RequestInterface $request, array $options, ResponseInterface $response) 101 | { 102 | $response = $response 103 | ->withHeader('X-Blackfire-Profile-Uuid', $request->getHeader('X-Blackfire-Profile-Uuid')) 104 | ->withHeader('X-Blackfire-Profile-Url', $request->getHeader('X-Blackfire-Profile-Url')) 105 | ; 106 | 107 | if (!$response->hasHeader('X-Blackfire-Response')) { 108 | if (null !== $this->logger) { 109 | $this->logger->warning('Profile request failed.', array( 110 | 'profile-uuid' => $request->getHeader('X-Blackfire-Profile-Uuid'), 111 | 'profile-url' => $request->getHeader('X-Blackfire-Profile-Url'), 112 | )); 113 | } 114 | 115 | return $response; 116 | } 117 | 118 | parse_str($response->getHeader('X-Blackfire-Response')[0], $values); 119 | 120 | if (!isset($values['continue']) || 'true' !== $values['continue']) { 121 | if (null !== $this->logger) { 122 | $this->logger->debug('Profile request succeeded.', array( 123 | 'profile-uuid' => $request->getHeader('X-Blackfire-Profile-Uuid'), 124 | 'profile-url' => $request->getHeader('X-Blackfire-Profile-Url'), 125 | )); 126 | } 127 | 128 | return $response; 129 | } 130 | 131 | if (method_exists(Psr7\Message::class, 'rewindBody')) { 132 | Psr7\Message::rewindBody($request); 133 | } elseif (function_exists('\GuzzleHttp\Psr7\rewind_body')) { 134 | Psr7\rewind_body($request); 135 | } 136 | 137 | /* @var PromiseInterface|ResponseInterface $promise */ 138 | return $this($request, $options); 139 | } 140 | 141 | private function shouldAutoEnable() 142 | { 143 | if (\BlackfireProbe::isEnabled() && $this->autoEnable) { 144 | if (isset($_SERVER['HTTP_X_BLACKFIRE_QUERY'])) { 145 | // Let's disable subrequest profiling if aggregation is enabled 146 | if (preg_match('/aggreg_samples=(\d+)/', $_SERVER['HTTP_X_BLACKFIRE_QUERY'], $matches)) { 147 | return '1' === $matches[1]; 148 | } 149 | } 150 | } 151 | 152 | return false; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/Blackfire/Bridge/Symfony/BlackfiredHttpClient.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Bridge\Symfony; 13 | 14 | use Blackfire\Client as BlackfireClient; 15 | use Blackfire\Profile\Configuration as ProfileConfiguration; 16 | use Psr\Log\LoggerInterface; 17 | use Symfony\Component\HttpClient\HttpClientTrait; 18 | use Symfony\Contracts\HttpClient\HttpClientInterface; 19 | use Symfony\Contracts\HttpClient\ResponseInterface; 20 | use Symfony\Contracts\HttpClient\ResponseStreamInterface; 21 | 22 | class BlackfiredHttpClient implements HttpClientInterface 23 | { 24 | use HttpClientTrait; 25 | 26 | private $client; 27 | private $blackfire; 28 | private $logger; 29 | private $autoEnable; 30 | private $defaultOptions = self::OPTIONS_DEFAULTS; 31 | 32 | public function __construct(HttpClientInterface $client, BlackfireClient $blackfire, ?LoggerInterface $logger = null, bool $autoEnable = true) 33 | { 34 | $this->client = $client; 35 | $this->blackfire = $blackfire; 36 | $this->logger = $logger; 37 | $this->autoEnable = $autoEnable; 38 | } 39 | 40 | public function request(string $method, string $url, array $options = array()): ResponseInterface 41 | { 42 | // this normalizes HTTP headers and allows direct access to $options['headers']['x-blackfire-query'] 43 | // without checking the header name case sensitivity 44 | $options = self::mergeDefaultOptions($options, $this->defaultOptions, true); 45 | 46 | if (!isset($options['extra'])) { 47 | $options['extra'] = array(); 48 | } 49 | 50 | $profileRequest = $options['extra']['profile_request'] ?? null; 51 | 52 | if ($this->shouldAutoEnable() && !isset($options['extra']['blackfire'])) { 53 | $options['extra']['blackfire'] = new ProfileConfiguration(); 54 | } 55 | 56 | if (!isset($options['normalized_headers']['x-blackfire-query']) && (!isset($options['extra']['blackfire']) || false === $options['extra']['blackfire'])) { 57 | return $this->client->request($method, $url, $options); 58 | } 59 | 60 | if (!isset($options['normalized_headers']['x-blackfire-query'])) { 61 | if (\BlackfireProbe::isEnabled()) { 62 | $probe = \BlackfireProbe::getMainInstance(); 63 | $probe->disable(); 64 | } 65 | 66 | if (isset($options['extra']['blackfire']) && true === $options['extra']['blackfire']) { 67 | $options['extra']['blackfire'] = new ProfileConfiguration(); 68 | } elseif (!(($options['extra']['blackfire'] ?? null) instanceof ProfileConfiguration)) { 69 | throw new \InvalidArgumentException('blackfire must be true or an instance of \Blackfire\Profile\Configuration.'); 70 | } 71 | 72 | if (!$profileRequest) { 73 | $options['extra']['profile_request'] = $this->blackfire->createRequest($options['extra']['blackfire']); 74 | } 75 | 76 | if (isset($probe)) { 77 | $probe->enable(); 78 | } 79 | 80 | $options['headers']['X-Blackfire-Query'] = $options['extra']['profile_request']->getToken(); 81 | $options['headers']['X-Blackfire-Profile-Url'] = $options['extra']['profile_request']->getProfileUrl(); 82 | $options['headers']['X-Blackfire-Profile-Uuid'] = $options['extra']['profile_request']->getUuid(); 83 | } 84 | 85 | $response = $this->client->request($method, $url, $options); 86 | 87 | return $this->processResponse($method, $url, $options, $response); 88 | } 89 | 90 | public function stream($responses, ?float $timeout = null): ResponseStreamInterface 91 | { 92 | return $this->client->stream($responses, $timeout); 93 | } 94 | 95 | private function processResponse($method, $url, array $options, ResponseInterface $response) 96 | { 97 | $headers = $response->getHeaders(false); 98 | $request = $options['extra']['profile_request'] ?? null; 99 | 100 | if (!isset($headers['x-blackfire-response'])) { 101 | if (null !== $this->logger) { 102 | $this->logger->warning('Profile request failed.', array( 103 | 'profile-uuid' => $request->getUuid() ?? null, 104 | 'profile-url' => $request->getProfileUrl() ?? null, 105 | )); 106 | } 107 | 108 | return new BlackfiredHttpResponse($response); 109 | } 110 | 111 | parse_str($headers['x-blackfire-response'][0], $values); 112 | 113 | if (!isset($values['continue']) || 'true' !== $values['continue']) { 114 | if (null !== $this->logger) { 115 | $this->logger->debug('Profile request succeeded.', array( 116 | 'profile-uuid' => $request->getUuid() ?? null, 117 | 'profile-url' => $request->getProfileUrl() ?? null, 118 | )); 119 | } 120 | 121 | if ($request) { 122 | $this->blackfire->addStep($request); 123 | } 124 | 125 | return new BlackfiredHttpResponse($response, $request); 126 | } 127 | 128 | $options['extra']['profile_request'] = $request; 129 | 130 | return $this->request($method, $url, $options); 131 | } 132 | 133 | private function shouldAutoEnable(): bool 134 | { 135 | if (\BlackfireProbe::isEnabled() && $this->autoEnable) { 136 | if (isset($_SERVER['HTTP_X_BLACKFIRE_QUERY'])) { 137 | // Let's disable subrequest profiling if aggregation is enabled 138 | if (preg_match('/aggreg_samples=(\d+)/', $_SERVER['HTTP_X_BLACKFIRE_QUERY'], $matches)) { 139 | return '1' === $matches[1]; 140 | } 141 | } 142 | } 143 | 144 | return false; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/Blackfire/Profile/Configuration.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Profile; 13 | 14 | use Blackfire\Build; 15 | 16 | /** 17 | * Configures a Blackfire profile. 18 | */ 19 | class Configuration 20 | { 21 | private $uuid; 22 | private $assertions; 23 | private $metrics; 24 | private $title = ''; 25 | private $metadata = array(); 26 | private $layers = array(); 27 | private $scenario; 28 | private $requestInfo = array(); 29 | private $intention; 30 | private $buildUuid; 31 | private $debug = false; 32 | 33 | public function getUuid() 34 | { 35 | return $this->uuid; 36 | } 37 | 38 | /** 39 | * Sets the UUID of the profile to an existing one. 40 | * 41 | * @param string $uuid 42 | */ 43 | public function setUuid($uuid) 44 | { 45 | $this->uuid = $uuid; 46 | } 47 | 48 | public function getTitle() 49 | { 50 | return $this->title; 51 | } 52 | 53 | /** 54 | * @return $this 55 | */ 56 | public function setTitle($title) 57 | { 58 | $this->title = $title; 59 | 60 | return $this; 61 | } 62 | 63 | public function getScenario() 64 | { 65 | return $this->scenario; 66 | } 67 | 68 | /** 69 | * @return $this 70 | */ 71 | public function setScenario(Build\Scenario $scenario) 72 | { 73 | $this->scenario = $scenario; 74 | $this->intention = 'build'; 75 | $this->buildUuid = $scenario->getBuild()->getUuid(); 76 | 77 | return $this; 78 | } 79 | 80 | public function getRequestInfo() 81 | { 82 | return $this->requestInfo; 83 | } 84 | 85 | /** 86 | * @return $this 87 | */ 88 | public function setRequestInfo(array $info) 89 | { 90 | $this->requestInfo = $info; 91 | 92 | return $this; 93 | } 94 | 95 | public function hasMetadata($key) 96 | { 97 | return array_key_exists($key, $this->metadata); 98 | } 99 | 100 | public function getMetadata($key) 101 | { 102 | if (!array_key_exists($key, $this->metadata)) { 103 | throw new \LogicException(sprintf('Metadata "%s" is not set.', $key)); 104 | } 105 | 106 | return $this->metadata[$key]; 107 | } 108 | 109 | public function getAllMetadata() 110 | { 111 | return $this->metadata; 112 | } 113 | 114 | /** 115 | * @return $this 116 | */ 117 | public function setMetadata($key, $value) 118 | { 119 | if (!is_string($value)) { 120 | throw new \LogicException(sprintf('Metadata values must be strings ("%s" given).', is_object($value) ? get_class($value) : gettype($value))); 121 | } 122 | 123 | $this->metadata[$key] = $value; 124 | 125 | return $this; 126 | } 127 | 128 | /** 129 | * @return $this 130 | */ 131 | public function assert($assertion, $name = '') 132 | { 133 | static $counter = 0; 134 | 135 | if (!$name) { 136 | $name = '_assertion_'.(++$counter); 137 | } 138 | 139 | $key = $name; 140 | $i = 0; 141 | while (isset($this->assertions[$key])) { 142 | $key = $name.' ('.(++$i).')'; 143 | } 144 | 145 | $this->assertions[$key] = $assertion; 146 | 147 | return $this; 148 | } 149 | 150 | /** 151 | * @return bool 152 | */ 153 | public function hasAssertions() 154 | { 155 | return (bool) $this->assertions; 156 | } 157 | 158 | /** 159 | * @return $this 160 | */ 161 | public function defineLayer(MetricLayer $layer) 162 | { 163 | $this->layers[] = $layer; 164 | 165 | return $this; 166 | } 167 | 168 | /** 169 | * @return $this 170 | */ 171 | public function defineMetric(Metric $metric) 172 | { 173 | $this->metrics[] = $metric; 174 | 175 | return $this; 176 | } 177 | 178 | public function getSamples() 179 | { 180 | @trigger_error(sprintf('The method "%s" is deprecated since blackfire/php-sdk 2.5 and will be removed in 3.0.', __METHOD__), E_USER_DEPRECATED); 181 | 182 | return 1; 183 | } 184 | 185 | /** 186 | * @return $this 187 | */ 188 | public function setSamples($samples) 189 | { 190 | @trigger_error(sprintf('The method "%s" is deprecated since blackfire/php-sdk 2.5 and will be removed in 3.0.', __METHOD__), E_USER_DEPRECATED); 191 | 192 | return $this; 193 | } 194 | 195 | public function getBuildUuid() 196 | { 197 | return $this->buildUuid; 198 | } 199 | 200 | public function setBuildUuid($buildUuid) 201 | { 202 | $this->buildUuid = $buildUuid; 203 | 204 | return $this; 205 | } 206 | 207 | public function getIntention() 208 | { 209 | return $this->intention; 210 | } 211 | 212 | /** 213 | * @return $this 214 | */ 215 | public function setIntention($intention) 216 | { 217 | $this->intention = (string) $intention; 218 | 219 | return $this; 220 | } 221 | 222 | public function isDebug() 223 | { 224 | return $this->debug; 225 | } 226 | 227 | /** 228 | * @return $this 229 | */ 230 | public function setDebug($debug) 231 | { 232 | $this->debug = (bool) $debug; 233 | 234 | return $this; 235 | } 236 | 237 | /** 238 | * @internal 239 | */ 240 | public function toYaml() 241 | { 242 | if (!$this->assertions && !$this->metrics) { 243 | return; 244 | } 245 | 246 | $yaml = ''; 247 | 248 | if ($this->metrics) { 249 | $yaml .= "metrics:\n"; 250 | foreach ($this->layers as $layer) { 251 | $yaml .= $layer->toYaml(); 252 | } 253 | 254 | foreach ($this->metrics as $metric) { 255 | $yaml .= $metric->toYaml(); 256 | } 257 | } 258 | 259 | if ($this->assertions) { 260 | $yaml .= "tests:\n"; 261 | foreach ($this->assertions as $name => $assertion) { 262 | $yaml .= " \"$name\":\n"; 263 | $yaml .= " assertions: [\"$assertion\"]\n\n"; 264 | } 265 | } 266 | 267 | return $yaml; 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /src/Blackfire/ClientConfiguration.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire; 13 | 14 | use Blackfire\Exception\ConfigErrorException; 15 | use Blackfire\Exception\ConfigNotFoundException; 16 | 17 | class ClientConfiguration 18 | { 19 | private $configResolved = false; 20 | private $config; 21 | private $clientId; 22 | private $clientToken; 23 | private $env; 24 | private $userAgentSuffix; 25 | private $endpoint; 26 | 27 | /** 28 | * @param string|null $clientId 29 | * @param string|null $clientToken 30 | * @param string|null $env 31 | * @param string $userAgentSuffix 32 | */ 33 | public function __construct($clientId = null, $clientToken = null, $env = null, $userAgentSuffix = '') 34 | { 35 | $this->clientId = $clientId; 36 | $this->clientToken = $clientToken; 37 | $this->env = $env; 38 | $this->userAgentSuffix = (string) $userAgentSuffix; 39 | } 40 | 41 | public static function createFromFile($file) 42 | { 43 | if (!file_exists($file)) { 44 | throw new ConfigNotFoundException(sprintf('Configuration file "%s" does not exist.', $file)); 45 | } 46 | 47 | $config = new self(); 48 | $config->config = $file; 49 | 50 | return $config; 51 | } 52 | 53 | /** 54 | * @param string|null $env 55 | * 56 | * @return $this 57 | */ 58 | public function setEnv($env) 59 | { 60 | $this->env = $env; 61 | 62 | return $this; 63 | } 64 | 65 | /** 66 | * @return string|null 67 | */ 68 | public function getEnv() 69 | { 70 | return $this->env; 71 | } 72 | 73 | /** 74 | * @param string $userAgentSuffix 75 | * 76 | * @return $this 77 | */ 78 | public function setUserAgentSuffix($userAgentSuffix) 79 | { 80 | $this->userAgentSuffix = (string) $userAgentSuffix; 81 | 82 | return $this; 83 | } 84 | 85 | /** 86 | * @return string 87 | */ 88 | public function getUserAgentSuffix() 89 | { 90 | return $this->userAgentSuffix; 91 | } 92 | 93 | /** 94 | * @param string|null $clientId 95 | * 96 | * @return $this 97 | */ 98 | public function setClientId($clientId) 99 | { 100 | $this->clientId = $clientId; 101 | $this->configResolved = false; 102 | 103 | return $this; 104 | } 105 | 106 | /** 107 | * @return string|null 108 | */ 109 | public function getClientId() 110 | { 111 | if (!$this->configResolved) { 112 | $this->resolveConfig(); 113 | } 114 | 115 | return $this->clientId; 116 | } 117 | 118 | /** 119 | * @return $this 120 | */ 121 | public function setClientToken($clientToken) 122 | { 123 | $this->clientToken = $clientToken; 124 | $this->configResolved = false; 125 | 126 | return $this; 127 | } 128 | 129 | /** 130 | * @return string|null 131 | */ 132 | public function getClientToken() 133 | { 134 | if (!$this->configResolved) { 135 | $this->resolveConfig(); 136 | } 137 | 138 | return $this->clientToken; 139 | } 140 | 141 | /** 142 | * @param string $endPoint 143 | * 144 | * @return $this 145 | */ 146 | public function setEndPoint($endPoint) 147 | { 148 | $this->endpoint = $endPoint; 149 | $this->configResolved = false; 150 | 151 | return $this; 152 | } 153 | 154 | /** 155 | * @return string 156 | */ 157 | public function getEndPoint() 158 | { 159 | if (!$this->configResolved) { 160 | $this->resolveConfig(); 161 | } 162 | 163 | return $this->endpoint; 164 | } 165 | 166 | private function resolveConfig() 167 | { 168 | $this->configResolved = true; 169 | 170 | $config = null; 171 | if ($this->config) { 172 | $config = $this->parseConfigFile($this->config); 173 | } else { 174 | $home = $this->getHomeDir(); 175 | if ($home && file_exists($home.'/.blackfire.ini')) { 176 | $config = $this->parseConfigFile($home.'/.blackfire.ini'); 177 | } 178 | } 179 | 180 | if (null !== $config) { 181 | if (null === $this->clientId && isset($config['client-id'])) { 182 | $this->clientId = $config['client-id']; 183 | } 184 | if (null === $this->clientToken && isset($config['client-token'])) { 185 | $this->clientToken = $config['client-token']; 186 | } 187 | if (null === $this->endpoint && isset($config['endpoint'])) { 188 | $this->endpoint = rtrim($config['endpoint'], '/'); 189 | } 190 | } 191 | 192 | if (isset($_SERVER['BLACKFIRE_CLIENT_ID'])) { 193 | $this->clientId = $_SERVER['BLACKFIRE_CLIENT_ID']; 194 | } 195 | if (isset($_SERVER['BLACKFIRE_CLIENT_TOKEN'])) { 196 | $this->clientToken = $_SERVER['BLACKFIRE_CLIENT_TOKEN']; 197 | } 198 | if (isset($_SERVER['BLACKFIRE_ENDPOINT'])) { 199 | $this->endpoint = rtrim($_SERVER['BLACKFIRE_ENDPOINT'], '/'); 200 | } 201 | 202 | if (!$this->endpoint) { 203 | $this->endpoint = 'https://blackfire.io'; 204 | } 205 | } 206 | 207 | private function getHomeDir() 208 | { 209 | if ($home = getenv('HOME')) { 210 | return $home; 211 | } 212 | 213 | if (!empty($_SERVER['HOMEDRIVE']) && !empty($_SERVER['HOMEPATH'])) { 214 | // home on windows 215 | return $_SERVER['HOMEDRIVE'].$_SERVER['HOMEPATH']; 216 | } 217 | 218 | if ($home = getenv('USERPROFILE')) { 219 | // home on windows 220 | return $home; 221 | } 222 | } 223 | 224 | private function parseConfigFile($file) 225 | { 226 | if (!is_readable($file)) { 227 | throw new ConfigErrorException(sprintf('Unable to parse configuration file "%s": file is not readable.', $file)); 228 | } 229 | 230 | if (false === $config = @parse_ini_file($file)) { 231 | throw new ConfigErrorException(sprintf('Unable to parse configuration file "%s".', $file)); 232 | } 233 | 234 | return $config; 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/Blackfire/Profile.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire; 13 | 14 | use Blackfire\Profile\Cost; 15 | use Blackfire\Profile\Test; 16 | 17 | /** 18 | * Represents a Blackfire Profile. 19 | * 20 | * Instances of this class should never be created directly. 21 | * Use Blackfire\Client instead. 22 | */ 23 | class Profile 24 | { 25 | private $uuid; 26 | private $initializeProfileCallback; 27 | private $data; 28 | private $tests; 29 | private $recommendations; 30 | 31 | /** 32 | * @internal 33 | */ 34 | public function __construct($initializeProfileCallback, $uuid = null) 35 | { 36 | $this->uuid = $uuid; 37 | $this->initializeProfileCallback = $initializeProfileCallback; 38 | } 39 | 40 | /** 41 | * @return string 42 | */ 43 | public function getUuid() 44 | { 45 | if (null === $this->uuid) { 46 | $this->initializeProfile(); 47 | $this->uuid = $this->data['uuid']; 48 | } 49 | 50 | return $this->uuid; 51 | } 52 | 53 | /** 54 | * Returns the Profile URL on Blackfire.io. 55 | * 56 | * @return string 57 | */ 58 | public function getUrl() 59 | { 60 | if (null === $this->data) { 61 | $this->initializeProfile(); 62 | } 63 | 64 | return $this->data['_links']['graph_url']['href']; 65 | } 66 | 67 | /** 68 | * Returns true if the tests executed without any errors. 69 | * 70 | * Errors are different from failures. An error occurs when there is 71 | * a syntax error in an assertion for instance. 72 | * 73 | * @return bool 74 | */ 75 | public function isErrored() 76 | { 77 | if (null === $this->data) { 78 | $this->initializeProfile(); 79 | } 80 | 81 | return isset($this->data['report']['state']) && 'errored' === $this->data['report']['state']; 82 | } 83 | 84 | /** 85 | * Returns true if the tests pass, false otherwise. 86 | * 87 | * You should also check isErrored() in case your tests generated an error. 88 | * 89 | * @return bool 90 | */ 91 | public function isSuccessful() 92 | { 93 | if (null === $this->data) { 94 | $this->initializeProfile(); 95 | } 96 | 97 | return isset($this->data['report']['state']) && 'successful' === $this->data['report']['state']; 98 | } 99 | 100 | /** 101 | * Returns tests associated with this profile. 102 | * 103 | * @return Test[] 104 | */ 105 | public function getTests() 106 | { 107 | if (null !== $this->tests) { 108 | return $this->tests; 109 | } 110 | 111 | if (null === $this->data) { 112 | $this->initializeProfile(); 113 | } 114 | 115 | if (!isset($this->data['report']['tests'])) { 116 | return $this->tests = array(); 117 | } 118 | 119 | $this->tests = array(); 120 | foreach ($this->data['report']['tests'] as $test) { 121 | $this->tests[] = new Test($test['name'], $test['state'], isset($test['failures']) ? $test['failures'] : array()); 122 | } 123 | 124 | return $this->tests; 125 | } 126 | 127 | /** 128 | * Returns recommendations associated with this profile. 129 | * 130 | * @return Test[] 131 | */ 132 | public function getRecommendations() 133 | { 134 | if (null !== $this->recommendations) { 135 | return $this->recommendations; 136 | } 137 | 138 | if (null === $this->data) { 139 | $this->initializeProfile(); 140 | } 141 | 142 | if (!isset($this->data['recommendations']['tests'])) { 143 | return $this->recommendations = array(); 144 | } 145 | 146 | $this->recommendations = array(); 147 | foreach ($this->data['recommendations']['tests'] as $test) { 148 | $this->recommendations[] = new Test($test['name'], $test['state'], isset($test['failures']) ? $test['failures'] : array()); 149 | } 150 | 151 | return $this->recommendations; 152 | } 153 | 154 | /** 155 | * Returns the main costs associated with the profile. 156 | * 157 | * @return Cost 158 | */ 159 | public function getMainCost() 160 | { 161 | if (null === $this->data) { 162 | $this->initializeProfile(); 163 | } 164 | 165 | return new Cost($this->data['envelope']); 166 | } 167 | 168 | /** 169 | * Returns the SQL queries executed during the profile. 170 | * 171 | * @return array An array where keys are SQL queries and values are Cost instances 172 | */ 173 | public function getSqls() 174 | { 175 | return $this->getLayer('sql.queries'); 176 | } 177 | 178 | /** 179 | * Returns the HTTP requests executed during the profile. 180 | * 181 | * @return array An array where keys are HTTP requests and values are Cost instances 182 | */ 183 | public function getHttpRequests() 184 | { 185 | return $this->getLayer('http.requests'); 186 | } 187 | 188 | /** 189 | * Returns the arguments for the given layer. 190 | * 191 | * @param string $name 192 | * 193 | * @return array An array where keys are the argument values and values are Cost instances 194 | */ 195 | public function getLayer($name) 196 | { 197 | if (null === $this->data) { 198 | $this->initializeProfile(); 199 | } 200 | 201 | if (!is_array($this->data['layers'])) { 202 | return array(); 203 | } 204 | 205 | $arguments = array(); 206 | foreach ($this->data['layers'] as $key => $layer) { 207 | if ($name !== $layer) { 208 | continue; 209 | } 210 | 211 | foreach ($this->data['arguments'][$key] as $value => $cost) { 212 | $arguments[$value] = new Cost($cost); 213 | } 214 | } 215 | 216 | return $arguments; 217 | } 218 | 219 | /** 220 | * Returns the arguments for the given metric name. 221 | * 222 | * @param string $name 223 | * 224 | * @return array An array where keys are the argument values and values are Cost instances 225 | */ 226 | public function getArguments($name) 227 | { 228 | if (null === $this->data) { 229 | $this->initializeProfile(); 230 | } 231 | 232 | if (!isset($this->data['arguments'][$name])) { 233 | return array(); 234 | } 235 | 236 | $arguments = array(); 237 | foreach ($this->data['arguments'][$name] as $argument => $cost) { 238 | $arguments[$argument] = new Cost($cost); 239 | } 240 | 241 | return $arguments; 242 | } 243 | 244 | private function initializeProfile() 245 | { 246 | $this->data = call_user_func($this->initializeProfileCallback); 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | * v2.5.10 (2025-11-21) 5 | 6 | - Internal releasing changes 7 | 8 | * v2.5.9 (2025-10-15) 9 | 10 | - Prevent committing debug code 11 | 12 | * v2.5.8 (2025-08-13) 13 | 14 | - Fix Client's retry logic 15 | 16 | * v2.5.7 (2025-01-28) 17 | 18 | - Fix deprecations in return type declaration 19 | 20 | * v2.5.6 (2025-01-10) 21 | 22 | - [sdk] implement ProfileNotReadyException 23 | 24 | * v2.5.5 (2024-12-23) 25 | 26 | - Upgrade to PHP 8.4 to run functional tests 27 | - Fix CS 28 | 29 | * v2.5.4 (2024-09-16) 30 | 31 | - Fix CS 32 | 33 | * v2.5.3 (2024-07-26) 34 | 35 | - Minor perf improvment in uniqId generator 36 | 37 | * v2.5.2 (2024-06-11) 38 | 39 | - Add required defaultOption in BlackfiredHttpClient 40 | - Fix HttpClient when calling a relative URL 41 | 42 | * v2.5.1 (2024-05-13) 43 | 44 | - Remove documentation about samples feature 45 | 46 | * v2.5.0 (2024-05-03) 47 | 48 | - Deprecate the "samples" option 49 | 50 | * v2.4.1 (2024-04-29) 51 | 52 | - Fix deprecation message formatting 53 | 54 | * v2.4.0 (2024-04-09) 55 | 56 | - Add Behat support for the Symfony kernel browser 57 | 58 | * v2.3.9 (2024-03-05) 59 | 60 | - Fix CS 61 | 62 | * 2.3.8 (2024-02-19) 63 | 64 | - Fix CS 65 | 66 | * 2.3.7 (2024-01-08) 67 | 68 | - Fix CS 69 | 70 | * 2.3.6 (2023-12-18) 71 | 72 | - Send SDK version as part of the User Agent 73 | 74 | * 2.3.5 (2023-11-03) 75 | 76 | - enforce undefined step name when none is provided 77 | - fix `BlackfiredHttpClient` sampling 78 | 79 | * 2.3.4 (2023-10-30) 80 | 81 | - Ensure profiles processing is complete before binding them to a build 82 | 83 | * 2.3.3 (2023-10-30) 84 | 85 | - provide the `X-Blackfire-Profile-Uuid` response header when using `BlackfiredHttpClient` 86 | 87 | * 2.3.2 (2023-10-26) 88 | 89 | - Fix undefined array key error on bad Blackfire API response 90 | 91 | * 2.3.1 (2023-10-24) 92 | 93 | - Fix profile to build association when using a `BlackfiredHttpClient` instance 94 | 95 | * 2.3.0 (2023-08-29) 96 | 97 | - Add compatibility with version 2 of Blackfire builds, using "Json View" 98 | 99 | * 2.2.0 (2023-08-01) 100 | 101 | - Add maxRetries option to `getProfile()` 102 | 103 | * 2.1.0 (2023-06-27) 104 | 105 | - Add build uuid to Configuration 106 | 107 | * 2.0.1 (2023-04-26) 108 | 109 | - Remove call to removed method 110 | 111 | * 2.0.0 (2023-04-13) 112 | 113 | - Drop support of PHP 5.2 fallback 114 | - Remove deprecated methods of the version 1: 115 | - Blackfire/Client::createBuild, replaced by Blackfire/Client::startScenario 116 | - Blackfire/Client::endBuild, replaced by Blackfire/Client::closeScenario 117 | - Blackfire/Client::assertPhpUnit 118 | - Blackfire/Client::addJobInBuild, replaced by Blackfire/Client::addJobInScenario 119 | - Blackfire/Client::getReport, replaced by Blackfire/Client::getScenarioReport 120 | - Blackfire/LoopClient::promoteReferenceSignal 121 | - Blackfire/LoopClient::attachReference 122 | - Blackfire/Profile/Configuration::getBuild, replaced by Blackfire/Profile/Configuration::getScenario 123 | - Blackfire/Profile/Configuration::setBuild, replaced by Blackfire/Profile/Configuration::setScenario 124 | - Blackfire/Profile/Configuration::getReference 125 | - Blackfire/Profile/Configuration::setReference 126 | - Blackfire/Profile/Configuration::isNewReference 127 | - Blackfire/Profile/Configuration::setAsReference 128 | 129 | * 1.35.0 (2023-04-06) 130 | 131 | - Proper check of the probe feature for Octane integration 132 | 133 | * 1.34.0 (2023-04-05) 134 | 135 | - Update support URL 136 | - Add BlackfireProbe::setAttribute signature 137 | 138 | * 1.33.0 (2023-01-26) 139 | 140 | - Allow creating debug profile using PHP SDK 141 | 142 | * 1.32.0 (2023-01-16) 143 | 144 | - Use new intention feature when creating builds 145 | - Fix profile_title encoding 146 | - Update dependencies 147 | 148 | * 1.31.0 (2022-06-14) 149 | 150 | - Name transaction using latest API 151 | - Upgrade to PHP 8.1 to run functional tests 152 | 153 | * 1.30.0 (2022-05-19) 154 | 155 | - Add Laravel Octane integration 156 | - Add an optional argument 'transactionName' to the 'startTransaction' method 157 | 158 | * 1.29.0 (2022-02-04) 159 | 160 | - Add the Laravel Tests integration 161 | 162 | * 1.28.0 (2021-12-17) 163 | 164 | - Provide support for monitoring Symfony commands 165 | 166 | * 1.27.2 (2021-11-17) 167 | 168 | - Tweak exception messages 169 | 170 | * 1.27.1 (2021-11-02) 171 | 172 | - Fix Laravel integration 173 | 174 | * 1.27.0 (2021-09-23) 175 | 176 | - Add a Symfony Messenger integration 177 | - Add Monitoring integration for Laravel Artisan commands and Consumers 178 | - Update default agent socket on arm64 179 | 180 | * 1.26.0 (2021-07-06) 181 | 182 | - Retry HTTP request on specific cases 183 | - Scenarios should be closed after each test in manual start mode 184 | 185 | * 1.25.1 (2021-03-18) 186 | 187 | - Behat Support bug fix: start a deferred build only if there is no current build running 188 | 189 | * 1.25.0 (2021-02-26) 190 | 191 | - Add Behat support 192 | 193 | * 1.24.0 (2021-02-05) 194 | 195 | - Enable the use of Symfony web test cases with Blackfire 196 | 197 | * 1.23.0 (2020-05-29) 198 | 199 | - Add Blackfire\Profile::getUuid() method 200 | - Fix some docblocks 201 | 202 | * 1.22.0 (2020-03-13) 203 | 204 | - Add "dot-blackfire" protocol feature support in the probe 205 | - Remove duplicated data from BlackfireSpan 206 | - Better unsuccessful profiles detection 207 | - Add Profile return to help with auto-complete in TestCaseTrait::assertBlackfire 208 | 209 | * 1.21.0 (2019-12-05) 210 | 211 | - Replace the "_server" header in fallback probe by "context" 212 | 213 | * 1.20.0 (2019-11-18) 214 | 215 | - Add BlackfireSpan 216 | 217 | * 1.19.2 (2019-10-09) 218 | 219 | - Fix PHP 5.3 compatibility 220 | 221 | * 1.19.1 (2019-08-14) 222 | 223 | - Fix invalid header name breaking the Guzzle middleware with recent guzzle/psr7 224 | - Modify subprofile ID generator so that it's only ever composed of alphanumerics 225 | 226 | * 1.19.0 (2019-08-02) 227 | 228 | - Add Symfony HttpClient bridge 229 | - Fix compatibility with PHP 5.3 230 | - Fix sub-profile id generation to be compatible with url encoding 231 | 232 | * 1.18.0 (2019-01-21) 233 | 234 | - Deprecate methods and classes related to references 235 | 236 | * 1.17.6 (2018-12-17) 237 | 238 | - Fix PHPUnit bridge 239 | 240 | * 1.17.5 (2018-12-14) 241 | 242 | - Fix PHPUnit bridge 243 | 244 | * 1.17.4 (2018-12-14) 245 | 246 | - Fix profiling with Hoster environment 247 | 248 | * 1.17.3 (2018-11-30) 249 | 250 | - [PHPunit Bridge] Fix compatibility with PHP 5.x 251 | 252 | * 1.17.3 (2018-11-30) 253 | 254 | - [PHPunit Bridge] Fix compatibility with PHP 5.x 255 | 256 | * 1.17.2 (2018-10-22) 257 | 258 | - Remove headers from Blackfire\Exception\ApiException's message 259 | - Add method "getHeaders()" to Blackfire\Exception\ApiException 260 | 261 | * 1.17.1 (2018-07-16) 262 | 263 | - Fix usage of PHPUnit Bridge without assertions 264 | 265 | * 1.17.0 (2018-07-11) 266 | 267 | - Add proper version in the User-Agent header 268 | - Add User-Agent header as well as the X-Blackfire-User-Agent one 269 | - Add possibility to suffix the User Agent header 270 | 271 | * 1.16.0 (2018-07-04) 272 | 273 | - Update to use the Build API version 2 274 | - Add a new method `getBuildReport()` to get the Report of a full Build 275 | -------------------------------------------------------------------------------- /src/Blackfire/Build/BuildHelper.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire\Build; 13 | 14 | use Blackfire\Client; 15 | use Blackfire\Probe; 16 | use Blackfire\Profile\Configuration; 17 | use Blackfire\Profile\Request; 18 | use Blackfire\Report; 19 | 20 | class BuildHelper 21 | { 22 | /** @var BuildHelper */ 23 | private static $instance; 24 | 25 | /** @var Client */ 26 | private $blackfire; 27 | 28 | /** @var Build */ 29 | private $currentBuild; 30 | 31 | /** @var bool */ 32 | private $buildDeferred = false; 33 | 34 | /** @var array */ 35 | private $buildOptions = array(); 36 | 37 | /** @var array */ 38 | private $scenarios = array(); 39 | 40 | /** @var bool */ 41 | private $enabled = true; 42 | 43 | /** @var ?string */ 44 | private $blackfireEnvironmentId; 45 | 46 | public function __construct() 47 | { 48 | $this->blackfire = new Client(); 49 | } 50 | 51 | /** 52 | * @return BuildHelper 53 | */ 54 | public static function getInstance() 55 | { 56 | if (!isset(self::$instance)) { 57 | self::$instance = new self(); 58 | } 59 | 60 | return self::$instance; 61 | } 62 | 63 | /** 64 | * @return Client 65 | */ 66 | public function getBlackfireClient() 67 | { 68 | return $this->blackfire; 69 | } 70 | 71 | /** 72 | * @param string $blackfireEnvironment The Blackfire environment name or UUID 73 | * @param string $buildTitle The build title 74 | * @param string|null $externalId Reference for this build 75 | * @param string|null $externalParentId Reference to compare this build to 76 | * @param string $triggerName Name of the build trigger 77 | * 78 | * @return Build 79 | * 80 | * @throws \RuntimeException 81 | */ 82 | public function startBuild($blackfireEnvironment, $buildTitle, $externalId = null, $externalParentId = null, $triggerName = 'Build SDK') 83 | { 84 | if ($this->hasCurrentBuild()) { 85 | throw new \RuntimeException('A Blackfire build was already started.'); 86 | } 87 | 88 | if (!$this->enabled) { 89 | throw new \RuntimeException('Cannot start a build because Blackfire builds are globally disabled.'); 90 | } 91 | 92 | $options = array( 93 | 'trigger_name' => $triggerName, 94 | 'title' => $buildTitle, 95 | ); 96 | if (null !== $externalId) { 97 | $options['external_id'] = $externalId; 98 | } 99 | if (null !== $externalParentId) { 100 | $options['external_parent_id'] = $externalParentId; 101 | } 102 | 103 | $this->currentBuild = $this->blackfire->startBuild($blackfireEnvironment, $options); 104 | 105 | return $this->currentBuild; 106 | } 107 | 108 | /** 109 | * Defers the build start at the first scenario. 110 | * 111 | * @param string $blackfireEnvironment The Blackfire environment name or UUID 112 | * @param string $buildTitle The build title 113 | * @param string|null $externalId Reference for this build 114 | * @param string|null $externalParentId Reference to compare this build to 115 | * @param string $triggerName Name of the build trigger 116 | * 117 | * @throws \RuntimeException 118 | */ 119 | public function deferBuild($blackfireEnvironment, $buildTitle, $externalId = null, $externalParentId = null, $triggerName = 'Build SDK') 120 | { 121 | if ($this->hasCurrentBuild()) { 122 | throw new \RuntimeException('A Blackfire build was already started.'); 123 | } 124 | 125 | if (!$this->enabled) { 126 | throw new \RuntimeException('Cannot start a build because Blackfire builds are globally disabled.'); 127 | } 128 | 129 | $this->buildDeferred = true; 130 | $this->buildOptions = array( 131 | 'environment' => $blackfireEnvironment, 132 | 'trigger_name' => $triggerName, 133 | 'title' => $buildTitle, 134 | 'external_id' => $externalId, 135 | 'external_parent_id' => $externalParentId, 136 | ); 137 | } 138 | 139 | /** 140 | * Starts a build that has been deferred. 141 | * 142 | * @return Build 143 | * 144 | * @throws \RuntimeException 145 | */ 146 | public function startDeferredBuild() 147 | { 148 | if (!$this->buildDeferred) { 149 | throw new \RuntimeException('There is no deferred build to start.'); 150 | } 151 | 152 | return $this->startBuild( 153 | $this->buildOptions['environment'], 154 | $this->buildOptions['title'], 155 | $this->buildOptions['external_id'], 156 | $this->buildOptions['external_parent_id'], 157 | $this->buildOptions['trigger_name'] 158 | ); 159 | } 160 | 161 | public function isBuildDeferred() 162 | { 163 | return $this->buildDeferred; 164 | } 165 | 166 | /** 167 | * @return Report 168 | */ 169 | public function endCurrentBuild() 170 | { 171 | if (!$this->hasCurrentBuild()) { 172 | throw new \RuntimeException('A Blackfire build must be started to be able to end it.'); 173 | } 174 | 175 | $report = $this->blackfire->closeBuild($this->currentBuild); 176 | unset($this->currentBuild); 177 | 178 | return $report; 179 | } 180 | 181 | /** 182 | * @return bool 183 | */ 184 | public function hasCurrentBuild() 185 | { 186 | return isset($this->currentBuild); 187 | } 188 | 189 | /** 190 | * @return Build|null 191 | */ 192 | public function getCurrentBuild() 193 | { 194 | return $this->currentBuild; 195 | } 196 | 197 | public function createScenario(?string $scenarioTitle = null, ?string $scenarioKey = 'current'): Scenario 198 | { 199 | if (!$this->enabled) { 200 | throw new \RuntimeException('Unable to create a Scenario because Blackfire build is globally disabled.'); 201 | } 202 | 203 | if ($this->isBuildDeferred() && !$this->hasCurrentBuild()) { 204 | $this->startDeferredBuild(); 205 | } 206 | 207 | if (!$this->hasCurrentBuild()) { 208 | throw new \RuntimeException('A Blackfire build must be started to be able create a scenario.'); 209 | } 210 | 211 | if ($this->hasCurrentScenario()) { 212 | throw new \RuntimeException('A Blackfire scenario is already running.'); 213 | } 214 | 215 | $options = array(); 216 | if (null !== $scenarioTitle) { 217 | $options['title'] = $scenarioTitle; 218 | } 219 | 220 | return $this->scenarios[$scenarioKey] = $this->blackfire->startScenario($this->currentBuild, $options); 221 | } 222 | 223 | public function endCurrentScenario() 224 | { 225 | if (!$this->hasCurrentScenario()) { 226 | throw new \RuntimeException('A Blackfire scenario must be started to be able to end it.'); 227 | } 228 | 229 | $this->endScenario('current'); 230 | } 231 | 232 | /** 233 | * @return bool 234 | */ 235 | public function hasCurrentScenario() 236 | { 237 | return $this->hasScenario('current'); 238 | } 239 | 240 | /** 241 | * @return Scenario|null 242 | */ 243 | public function getCurrentScenario() 244 | { 245 | return $this->scenarios['current'] ?? null; 246 | } 247 | 248 | /** 249 | * @param bool $enabled 250 | * 251 | * @return BuildHelper 252 | */ 253 | public function setEnabled($enabled) 254 | { 255 | $this->enabled = (bool) $enabled; 256 | 257 | return $this; 258 | } 259 | 260 | /** 261 | * @return bool 262 | */ 263 | public function isEnabled() 264 | { 265 | return $this->enabled; 266 | } 267 | 268 | public function hasScenario(string $scenarioKey = 'current'): bool 269 | { 270 | return array_key_exists($scenarioKey, $this->scenarios); 271 | } 272 | 273 | public function getScenario(string $scenarioKey): Scenario 274 | { 275 | if (!$this->hasScenario($scenarioKey)) { 276 | throw new \RuntimeException('No Scenario registered with that key'); 277 | } 278 | 279 | return $this->scenarios[$scenarioKey]; 280 | } 281 | 282 | public function endScenario(string $scenarioKey): void 283 | { 284 | $scenario = $this->getScenario($scenarioKey); 285 | $this->getBlackfireClient()->closeScenario($scenario); 286 | 287 | unset($this->scenarios[$scenarioKey]); 288 | } 289 | 290 | public function createRequest(string $scenarioKey, ?string $title = null): Request 291 | { 292 | return $this->getBlackfireClient()->createRequest( 293 | $this->getConfigurationForScenario($scenarioKey, $title) 294 | ); 295 | } 296 | 297 | public function createProbe(string $scenarioKey, ?string $title = null): Probe 298 | { 299 | return $this->getBlackfireClient()->createProbe( 300 | $this->getConfigurationForScenario($scenarioKey, $title) 301 | ); 302 | } 303 | 304 | public function endProbe(Probe $probe): void 305 | { 306 | $this->getBlackfireClient()->endProbe($probe); 307 | } 308 | 309 | public function getConfigurationForScenario(string $scenarioKey, $title = null): Configuration 310 | { 311 | return (new Configuration()) 312 | ->setScenario($this->getScenario($scenarioKey)) 313 | ->setMetadata('skip_timeline', 'true') 314 | ->setTitle($title) 315 | ; 316 | } 317 | 318 | public function hasAnyScenario(): bool 319 | { 320 | return !empty($this->scenarios); 321 | } 322 | 323 | public function endAllScenarios(): void 324 | { 325 | foreach (array_keys($this->scenarios) as $scenarioKey) { 326 | $this->endScenario($scenarioKey); 327 | } 328 | } 329 | 330 | public function getBlackfireEnvironmentId(): ?string 331 | { 332 | return $this->blackfireEnvironmentId; 333 | } 334 | 335 | public function setBlackfireEnvironmentId(string $blackfireEnvironmentId): self 336 | { 337 | $this->blackfireEnvironmentId = $blackfireEnvironmentId; 338 | 339 | return $this; 340 | } 341 | } 342 | -------------------------------------------------------------------------------- /src/Blackfire/Client.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Blackfire; 13 | 14 | use Blackfire\Build\Scenario; 15 | use Blackfire\Exception\ApiException; 16 | use Blackfire\Exception\EnvNotFoundException; 17 | use Blackfire\Exception\OfflineException; 18 | use Blackfire\Exception\ProfileNotReadyException; 19 | use Blackfire\Profile\Configuration as ProfileConfiguration; 20 | use Blackfire\Profile\Request; 21 | use Blackfire\Util\NoProxyPattern; 22 | use Composer\CaBundle\CaBundle; 23 | 24 | /** 25 | * The Blackfire Client. 26 | */ 27 | class Client 28 | { 29 | const MAX_RETRY = 60; 30 | const NO_REFERENCE_ID = '00000000-0000-0000-0000-000000000000'; 31 | const VERSION = '2.5.10'; 32 | 33 | private $config; 34 | private $collabTokens; 35 | 36 | public function __construct(?ClientConfiguration $config = null) 37 | { 38 | if (null === $config) { 39 | $config = new ClientConfiguration(); 40 | } 41 | 42 | $this->config = $config; 43 | } 44 | 45 | public function getConfiguration() 46 | { 47 | return $this->config; 48 | } 49 | 50 | /** 51 | * Creates a Blackfire probe. 52 | * 53 | * @return Probe 54 | */ 55 | public function createProbe(?ProfileConfiguration $config = null, $enable = true) 56 | { 57 | if (null === $config) { 58 | $config = new ProfileConfiguration(); 59 | } 60 | 61 | $probe = new Probe($this->doCreateRequest($config)); 62 | 63 | if ($enable) { 64 | $probe->enable(); 65 | } 66 | 67 | return $probe; 68 | } 69 | 70 | /** 71 | * Ends a Blackfire probe. 72 | * 73 | * @return Profile 74 | */ 75 | public function endProbe(Probe $probe) 76 | { 77 | $probe->close(); 78 | 79 | $profile = $this->getProfile($probe->getRequest()->getUuid()); 80 | 81 | $request = $probe->getRequest(); 82 | 83 | if ($request->getUserMetadata()) { 84 | $this->storeMetadata($request->getUuid(), $request->getUserMetadata()); 85 | } 86 | 87 | $config = $request->getConfiguration(); 88 | $scenario = $config->getScenario(); 89 | if ($scenario) { 90 | // call getUrl to trigger the `initializeProfile` method and wait for the profile to be finished 91 | $profile->getUrl(); 92 | 93 | $scenario->addStep(array( 94 | 'type' => 'request', 95 | 'status' => 'done', 96 | 'name' => $config->getTitle(), 97 | 'blackfire_profile_uuid' => $profile->getUuid(), 98 | )); 99 | 100 | $this->updateJsonView($scenario->getBuild()); 101 | } 102 | 103 | return $profile; 104 | } 105 | 106 | public function addStep(Request $request) 107 | { 108 | $config = $request->getConfiguration(); 109 | $scenario = $config->getScenario(); 110 | if ($scenario) { 111 | $profile = $this->getProfile($request->getUuid()); 112 | // call getUrl to trigger the `initializeProfile` method and wait for the profile to be finished 113 | $profile->getUrl(); 114 | 115 | $scenario->addStep(array( 116 | 'type' => 'request', 117 | 'status' => 'done', 118 | 'name' => $config->getTitle(), 119 | 'blackfire_profile_uuid' => $request->getUuid(), 120 | )); 121 | 122 | $this->updateJsonView($scenario->getBuild()); 123 | } 124 | } 125 | 126 | /** 127 | * Creates a Blackfire Build. 128 | * 129 | * @param string|null $env The environment name (or null to use the one configured on the client) 130 | * @param array $options An array of Build options 131 | * (title, metadata, trigger_name, external_id, external_parent_id) 132 | * 133 | * @return Build\Build 134 | */ 135 | public function startBuild($env = null, $options = array()) 136 | { 137 | $env = $this->getEnvUuid(null === $env ? $this->config->getEnv() : $env); 138 | $content = json_encode($options); 139 | $data = json_decode($this->sendHttpRequest($this->config->getEndpoint().'/api/v2/builds/env/'.$env, 'POST', array('content' => $content), array('Content-Type: application/json')), true); 140 | 141 | return new Build\Build($env, $data); 142 | } 143 | 144 | /** 145 | * Closes a build. 146 | * 147 | * @return Report 148 | */ 149 | public function closeBuild(Build\Build $build) 150 | { 151 | $uuid = $build->getUuid(); 152 | 153 | $build->setStatus('done'); 154 | $this->updateJsonView($build); 155 | 156 | return $this->getBuildReport($uuid); 157 | } 158 | 159 | /** 160 | * Creates a Blackfire Scenario. 161 | */ 162 | public function startScenario(?Build\Build $build = null, $options = array()) 163 | { 164 | if (null === $build) { 165 | $build = $this->startBuild(); 166 | } 167 | 168 | $scenario = new Scenario($build, $options); 169 | $build->addScenario($scenario); 170 | 171 | $this->updateJsonView($build); 172 | 173 | return $scenario; 174 | } 175 | 176 | /** 177 | * Closes a Blackfire Scenario. 178 | * 179 | * @return Report 180 | */ 181 | public function closeScenario(Scenario $scenario, array $errors = array()) 182 | { 183 | $scenario->setStatus('done'); 184 | $scenario->addErrors($errors); 185 | 186 | $this->updateJsonView($scenario->getBuild()); 187 | 188 | return $this->getBuildReport($scenario->getBuild()->getUuid()); 189 | } 190 | 191 | /** 192 | * Returns a profile request. 193 | * 194 | * Retrieve the X-Blackfire-Query value with Request::getToken(). 195 | * 196 | * @param ProfileConfiguration|string $config The profile title or a ProfileConfiguration instance 197 | * 198 | * @return Request 199 | */ 200 | public function createRequest($config = null) 201 | { 202 | if (\is_string($config)) { 203 | $cfg = new ProfileConfiguration(); 204 | $config = $cfg->setTitle($config); 205 | } elseif (null === $config) { 206 | $config = new ProfileConfiguration(); 207 | } elseif (!$config instanceof ProfileConfiguration) { 208 | throw new \InvalidArgumentException(sprintf('The "%s" method takes a string or a Blackfire\Profile\Configuration instance.', __METHOD__)); 209 | } 210 | 211 | return $this->doCreateRequest($config); 212 | } 213 | 214 | /** 215 | * @return bool True if the profile was successfully updated 216 | */ 217 | public function updateProfile($uuid, $title = null, ?array $metadata = null) 218 | { 219 | try { 220 | // be sure that the profile exist first 221 | $this->getProfile($uuid)->getUrl(); 222 | 223 | if (null !== $title) { 224 | $this->sendHttpRequest($this->config->getEndpoint().'/api/v1/profiles/'.$uuid, 'PUT', array('content' => http_build_query(array('label' => $title), '', '&')), array('Content-Type: application/x-www-form-urlencoded')); 225 | } 226 | 227 | if (null !== $metadata) { 228 | $this->storeMetadata($uuid, $metadata); 229 | } 230 | 231 | return true; 232 | } catch (ApiException $e) { 233 | return false; 234 | } 235 | } 236 | 237 | /** 238 | * @param string $uuid A Profile UUID 239 | * 240 | * @return Profile 241 | */ 242 | public function getProfile($uuid, $retryCount = self::MAX_RETRY) 243 | { 244 | $self = $this; 245 | 246 | return new Profile(function () use ($self, $uuid, $retryCount) { 247 | return $self->doGetProfile($uuid, $retryCount); 248 | }, $uuid); 249 | } 250 | 251 | public function addJobInScenario(ProfileConfiguration $config, Scenario $scenario) 252 | { 253 | @trigger_error(sprintf('The method "%s" is deprecated since blackfire/php-sdk 2.3 and will be removed in 3.0.', __METHOD__), E_USER_DEPRECATED); 254 | 255 | $step = array(); 256 | $step['type'] = 'request'; 257 | $step['status'] = 'done'; 258 | $step['name'] = $config->getTitle(); 259 | 260 | if ($config->getUuid()) { 261 | $step['blackfire_profile_uuid'] = $config->getUuid(); 262 | } 263 | 264 | $scenario->addStep($step); 265 | 266 | $this->updateJsonView($scenario->getBuild()); 267 | 268 | return $step; 269 | } 270 | 271 | /** 272 | * @internal 273 | */ 274 | public function doGetProfile($uuid, $maxRetries) 275 | { 276 | if ($maxRetries < 0) { 277 | throw new \InvalidArgumentException('Max retries must be a positive integer'); 278 | } 279 | 280 | $retry = 0; 281 | $url = $this->config->getEndpoint().'/api/v1/profiles/'.$uuid; 282 | while (true) { 283 | $e = null; 284 | try { 285 | $data = json_decode($this->sendHttpRequest($url), true); 286 | 287 | if ($data['status']['code'] > 0) { 288 | if ('finished' == $data['status']['name']) { 289 | return $data; 290 | } 291 | 292 | throw new ProfileNotReadyException($data['status']['failure_reason'] ?? 'Failed to fetch profile', $data['status']['code'] ?? 0); 293 | } 294 | } catch (ApiException $e) { 295 | $code = $e->getCode(); 296 | $canBeRetried = \in_array($code, array(0, 404, 405), true) || $code >= 500; 297 | 298 | if (!$canBeRetried || $retry > $maxRetries) { 299 | throw $e; 300 | } 301 | } 302 | 303 | usleep(++$retry * 50000); 304 | 305 | if ($retry > $maxRetries) { 306 | if (null === $e) { 307 | throw new ApiException('Profile is still in the queue.'); 308 | } 309 | 310 | if ($e instanceof ProfileNotReadyException) { 311 | throw $e; 312 | } 313 | 314 | throw ApiException::fromStatusCode(sprintf('Error while fetching profile from the API at "%s" using client "%s".', $url, $this->config->getClientId()), $e->getCode(), $e); 315 | } 316 | } 317 | } 318 | 319 | /** 320 | * @param string $uuid A Scenario Report UUID 321 | * 322 | * @return Report 323 | */ 324 | public function getScenarioReport($uuid) 325 | { 326 | @trigger_error(sprintf('The method "%s" is deprecated since blackfire/php-sdk 2.3 and will be removed in 3.0.', __METHOD__), E_USER_DEPRECATED); 327 | 328 | $self = $this; 329 | 330 | return new Report(function () use ($self, $uuid) { 331 | return $self->doGetReport($uuid, 'scenario'); 332 | }); 333 | } 334 | 335 | /** 336 | * @param string $uuid A Build Report UUID 337 | * 338 | * @return Report 339 | */ 340 | public function getBuildReport($uuid) 341 | { 342 | $self = $this; 343 | 344 | return new Report(function () use ($self, $uuid) { 345 | return $self->doGetReport($uuid); 346 | }); 347 | } 348 | 349 | /** 350 | * @internal 351 | */ 352 | private function doGetReport($uuid, $type = 'build') 353 | { 354 | $retry = 0; 355 | $e = null; 356 | $path = 'build' === $type ? '/api/v2/builds/'.$uuid : '/api/v2/scenarios/'.$uuid; 357 | $url = $this->config->getEndpoint().$path; 358 | 359 | while (true) { 360 | try { 361 | $data = json_decode($this->sendHttpRequest($url), true); 362 | 363 | if ('finished' === $data['status']['name']) { 364 | return $data; 365 | } 366 | 367 | if ('errored' === $data['status']['name']) { 368 | throw new ApiException(isset($data['status']['failure_reason']) ? $data['status']['failure_reason'] : 'Build errored.'); 369 | } 370 | } catch (ApiException $e) { 371 | if (404 != $e->getCode() || $retry > self::MAX_RETRY) { 372 | throw $e; 373 | } 374 | } 375 | 376 | usleep(++$retry * 50000); 377 | 378 | if ($retry > self::MAX_RETRY) { 379 | if (null === $e) { 380 | throw new ApiException('Report is still in the queue.'); 381 | } 382 | 383 | throw ApiException::fromStatusCode(sprintf('Error while fetching report from the API at "%s" using client "%s".', $url, $this->config->getClientId()), $e->getCode(), $e); 384 | } 385 | } 386 | } 387 | 388 | private function doCreateRequest(ProfileConfiguration $config) 389 | { 390 | $content = json_encode($this->getRequestDetails($config)); 391 | $data = json_decode($this->sendHttpRequest($this->config->getEndpoint().'/api/v1/signing', 'POST', array('content' => $content), array('Content-Type: application/json')), true); 392 | 393 | return new Request($config, $data); 394 | } 395 | 396 | private function getCollabTokens() 397 | { 398 | if (null === $this->collabTokens) { 399 | $this->collabTokens = json_decode($this->sendHttpRequest($this->config->getEndpoint().'/api/v1/collab-tokens'), true); 400 | } 401 | 402 | return $this->collabTokens; 403 | } 404 | 405 | private function getEnvUuid($env) 406 | { 407 | $env = $this->getEnvDetails($env); 408 | 409 | return $env['collabToken']; 410 | } 411 | 412 | private function getPersonalCollabToken() 413 | { 414 | $collabTokens = $this->getCollabTokens(); 415 | 416 | foreach ($collabTokens['collabTokens'] as $collabToken) { 417 | if ('personal' === $collabToken['type']) { 418 | return $collabToken; 419 | } 420 | } 421 | 422 | throw new EnvNotFoundException('No personal collab token found.'); 423 | } 424 | 425 | private function getEnvDetails($env) 426 | { 427 | if (!$env) { 428 | return $this->getPersonalCollabToken(); 429 | } 430 | 431 | $collabTokens = $this->getCollabTokens(); 432 | 433 | foreach ($collabTokens['collabTokens'] as $i => $collabToken) { 434 | if (isset($collabToken['search_identifiers']) && \in_array($env, $collabToken['search_identifiers'], true)) { 435 | return $collabToken; 436 | } 437 | 438 | if (isset($collabToken['collabToken']) && $collabToken['collabToken'] == $env) { 439 | return $collabToken; 440 | } 441 | 442 | if (isset($collabToken['name']) && false !== stripos($collabToken['name'], $env)) { 443 | return $collabToken; 444 | } 445 | } 446 | 447 | throw new EnvNotFoundException(sprintf('Environment "%s" does not exist.', $env)); 448 | } 449 | 450 | private function getRequestDetails(ProfileConfiguration $config) 451 | { 452 | $details = array(); 453 | $scenario = $config->getScenario(); 454 | $this->getEnvDetails($scenario ? $scenario->getEnv() : $this->config->getEnv()); 455 | 456 | if (null !== $config->getUuid()) { 457 | $details['requestId'] = $config->getUuid(); 458 | } 459 | 460 | if ($intention = $config->getIntention()) { 461 | $details['intention'] = $intention; 462 | } 463 | if ($buildUuid = $config->getBuildUuid()) { 464 | $details['build'] = $buildUuid; 465 | } 466 | 467 | if ($debug = $config->isDebug()) { 468 | $details['debug'] = $debug; 469 | } 470 | 471 | $personalCollabToken = $this->getPersonalCollabToken(); 472 | $details['collabToken'] = $personalCollabToken['collabToken']; 473 | 474 | $details['profileSlot'] = self::NO_REFERENCE_ID; 475 | 476 | return $details; 477 | } 478 | 479 | private function storeMetadata($uuid, array $metadata) 480 | { 481 | return json_decode($this->sendHttpRequest($this->config->getEndpoint().'/api/v1/profiles/'.$uuid.'/store', 'PUT', array('content' => json_encode($metadata)), array('Content-Type: application/json')), true); 482 | } 483 | 484 | private function sendHttpRequest($url, $method = 'GET', $context = array(), $headers = array()) 485 | { 486 | $userAgent = sprintf('Blackfire PHP SDK/%s%s%s', self::VERSION, ' - PHP/'.phpversion(), $this->config->getUserAgentSuffix() ? ' - '.$this->config->getUserAgentSuffix() : ''); 487 | 488 | $headers[] = 'Authorization: Basic '.base64_encode($this->config->getClientId().':'.$this->config->getClientToken()); 489 | $headers[] = 'X-Blackfire-User-Agent: '.$userAgent; 490 | $headers[] = 'User-Agent: '.$userAgent; 491 | 492 | $caPath = CaBundle::getSystemCaRootBundlePath(); 493 | $sslOpts = array( 494 | 'verify_peer' => 1, 495 | 'verify_host' => 2, 496 | ); 497 | 498 | if (is_dir($caPath)) { 499 | $sslOpts['capath'] = $caPath; 500 | } else { 501 | $sslOpts['cafile'] = $caPath; 502 | } 503 | 504 | $context = self::getContext($url, array( 505 | 'http' => array_replace(array( 506 | 'method' => $method, 507 | 'header' => implode("\r\n", $headers), 508 | 'ignore_errors' => true, 509 | 'follow_location' => true, 510 | 'max_redirects' => 3, 511 | 'timeout' => 60, 512 | ), $context), 513 | 'ssl' => $sslOpts, 514 | )); 515 | 516 | set_error_handler(function ($type, $message) { 517 | throw new OfflineException(sprintf('An error occurred: "%s".', $message)); 518 | }); 519 | try { 520 | $body = file_get_contents($url, 0, $context); 521 | } catch (\Exception $e) { 522 | restore_error_handler(); 523 | 524 | throw $e; 525 | } 526 | restore_error_handler(); 527 | 528 | if (!$data = @json_decode($body, true)) { 529 | $data = array('message' => ''); 530 | } 531 | 532 | $error = isset($data['message']) ? $data['message'] : 'Unknown error'; 533 | 534 | // status code 535 | if (!preg_match('{HTTP/\d\.\d (\d+) }i', $http_response_header[0], $match)) { 536 | throw ApiException::fromURL($method, $url, sprintf('An unknown API error occurred (%s).', $error), null, $context, $headers); 537 | } 538 | 539 | $statusCode = $match[1]; 540 | 541 | if ($statusCode >= 401) { 542 | throw ApiException::fromURL($method, $url, $error, $statusCode, $context, $headers); 543 | } 544 | 545 | if ($statusCode >= 300) { 546 | throw ApiException::fromURL($method, $url, sprintf('The API call failed for an unknown reason (HTTP %d: %s).', $statusCode, $error), $statusCode, $context, $headers); 547 | } 548 | 549 | return $body; 550 | } 551 | 552 | /** 553 | * Creates a context supporting HTTP proxies. 554 | * 555 | * The following method is copy/pasted from Composer v1.5.5 556 | * 557 | * Copyright (c) Nils Adermann, Jordi Boggiano 558 | * 559 | * Permission is hereby granted, free of charge, to any person obtaining a copy 560 | * of this software and associated documentation files (the "Software"), to deal 561 | * in the Software without restriction, including without limitation the rights 562 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 563 | * copies of the Software, and to permit persons to whom the Software is furnished 564 | * to do so, subject to the following conditions: 565 | * 566 | * The above copyright notice and this permission notice shall be included in all 567 | * copies or substantial portions of the Software. 568 | * 569 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 570 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 571 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 572 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 573 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 574 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 575 | * THE SOFTWARE. 576 | * 577 | * @param string $url URL the context is to be used for 578 | * @param array $defaultOptions Options to merge with the default 579 | * @param array $defaultParams Parameters to specify on the context 580 | * 581 | * @return resource Default context 582 | * 583 | * @throws \RuntimeException if HTTPS proxy required and OpenSSL uninstalled 584 | * 585 | * @author Jordan Alliot 586 | * @author Markus Tacker 587 | */ 588 | private static function getContext($url, array $defaultOptions = array(), array $defaultParams = array()) 589 | { 590 | $options = array('http' => array( 591 | // specify defaults again to try and work better with curlwrappers enabled 592 | 'follow_location' => 1, 593 | 'max_redirects' => 20, 594 | )); 595 | 596 | // Handle HTTP_PROXY/http_proxy on CLI only for security reasons 597 | if (\PHP_SAPI === 'cli' && (!empty($_SERVER['HTTP_PROXY']) || !empty($_SERVER['http_proxy']))) { 598 | $proxy = parse_url(!empty($_SERVER['http_proxy']) ? $_SERVER['http_proxy'] : $_SERVER['HTTP_PROXY']); 599 | } 600 | 601 | // Prefer CGI_HTTP_PROXY if available 602 | if (!empty($_SERVER['CGI_HTTP_PROXY'])) { 603 | $proxy = parse_url($_SERVER['CGI_HTTP_PROXY']); 604 | } 605 | 606 | // Override with HTTPS proxy if present and URL is https 607 | if (preg_match('{^https://}i', $url) && (!empty($_SERVER['HTTPS_PROXY']) || !empty($_SERVER['https_proxy']))) { 608 | $proxy = parse_url(!empty($_SERVER['https_proxy']) ? $_SERVER['https_proxy'] : $_SERVER['HTTPS_PROXY']); 609 | } 610 | 611 | // Remove proxy if URL matches no_proxy directive 612 | if (!empty($_SERVER['no_proxy']) && parse_url($url, PHP_URL_HOST)) { 613 | $pattern = new NoProxyPattern($_SERVER['no_proxy']); 614 | if ($pattern->test($url)) { 615 | unset($proxy); 616 | } 617 | } 618 | 619 | if (!empty($proxy)) { 620 | $proxyURL = isset($proxy['scheme']) ? $proxy['scheme'].'://' : ''; 621 | $proxyURL .= isset($proxy['host']) ? $proxy['host'] : ''; 622 | 623 | if (isset($proxy['port'])) { 624 | $proxyURL .= ':'.$proxy['port']; 625 | } elseif ('http://' == substr($proxyURL, 0, 7)) { 626 | $proxyURL .= ':80'; 627 | } elseif ('https://' == substr($proxyURL, 0, 8)) { 628 | $proxyURL .= ':443'; 629 | } 630 | 631 | // http(s):// is not supported in proxy 632 | $proxyURL = str_replace(array('http://', 'https://'), array('tcp://', 'ssl://'), $proxyURL); 633 | 634 | if (0 === strpos($proxyURL, 'ssl:') && !\extension_loaded('openssl')) { 635 | throw new \RuntimeException('You must enable the openssl extension to use a proxy over https.'); 636 | } 637 | 638 | $options['http']['proxy'] = $proxyURL; 639 | 640 | // enabled request_fulluri unless it is explicitly disabled 641 | switch (parse_url($url, PHP_URL_SCHEME)) { 642 | case 'http': // default request_fulluri to true 643 | $reqFullUriEnv = getenv('HTTP_PROXY_REQUEST_FULLURI'); 644 | if (false === $reqFullUriEnv || '' === $reqFullUriEnv || ('false' !== strtolower($reqFullUriEnv) && (bool) $reqFullUriEnv)) { 645 | $options['http']['request_fulluri'] = true; 646 | } 647 | break; 648 | case 'https': // default request_fulluri to true 649 | $reqFullUriEnv = getenv('HTTPS_PROXY_REQUEST_FULLURI'); 650 | if (false === $reqFullUriEnv || '' === $reqFullUriEnv || ('false' !== strtolower($reqFullUriEnv) && (bool) $reqFullUriEnv)) { 651 | $options['http']['request_fulluri'] = true; 652 | } 653 | break; 654 | } 655 | 656 | // add SNI opts for HTTPS URLs 657 | if ('https' === parse_url($url, PHP_URL_SCHEME)) { 658 | $options['ssl']['SNI_enabled'] = true; 659 | if (\PHP_VERSION_ID < 50600) { 660 | $options['ssl']['SNI_server_name'] = parse_url($url, PHP_URL_HOST); 661 | } 662 | } 663 | 664 | // handle proxy auth if present 665 | if (isset($proxy['user'])) { 666 | $auth = urldecode($proxy['user']); 667 | if (isset($proxy['pass'])) { 668 | $auth .= ':'.urldecode($proxy['pass']); 669 | } 670 | $auth = base64_encode($auth); 671 | 672 | // Preserve headers if already set in default options 673 | if (isset($defaultOptions['http']['header'])) { 674 | if (\is_string($defaultOptions['http']['header'])) { 675 | $defaultOptions['http']['header'] = array($defaultOptions['http']['header']); 676 | } 677 | $defaultOptions['http']['header'][] = "Proxy-Authorization: Basic {$auth}"; 678 | } else { 679 | $options['http']['header'] = array("Proxy-Authorization: Basic {$auth}"); 680 | } 681 | } 682 | } 683 | 684 | $options = array_replace_recursive($options, $defaultOptions); 685 | 686 | if (isset($options['http']['header'])) { 687 | if (!\is_array($header = $options['http']['header'])) { 688 | $header = explode("\r\n", $header); 689 | } 690 | uasort($header, function ($el) { 691 | return preg_match('{^content-type}i', $el) ? 1 : -1; 692 | }); 693 | 694 | $options['http']['header'] = $header; 695 | } 696 | 697 | return stream_context_create($options, $defaultParams); 698 | } 699 | 700 | private function updateJsonView(Build\Build $build) 701 | { 702 | $uuid = $build->getUuid(); 703 | 704 | $data = array( 705 | 'version' => $build->getNextVersion(), 706 | 'status' => $build->getStatus(), 707 | 'scenarios' => array(), 708 | ); 709 | foreach ($build->getScenarios() as $scenario) { 710 | $scenarioData = array( 711 | 'uuid' => $scenario->getUuid(), 712 | 'status' => $scenario->getStatus(), 713 | 'name' => $scenario->getName(), 714 | 'errors' => $scenario->getErrors(), 715 | 'steps' => $scenario->getSteps(), 716 | ); 717 | $data['scenarios'][] = $scenarioData; 718 | } 719 | 720 | $content = json_encode($data); 721 | $this->sendHttpRequest($this->config->getEndpoint().'/api/v3/builds/'.$uuid.'/update', 'POST', array('content' => $content), array('Content-Type: application/json')); 722 | 723 | return $this->getBuildReport($uuid); 724 | } 725 | } 726 | --------------------------------------------------------------------------------