feature #33015 [HttpClient] Added TraceableHttpClient and WebProfiler panel (jeremyFreeAgent)
This PR was merged into the 4.4 branch.
Discussion
----------
[HttpClient] Added TraceableHttpClient and WebProfiler panel
| Q | A
| ------------- | ---
| Branch? | 4.4
| Bug fix? | no
| New feature? | yes <!-- please update src/**/CHANGELOG.md files -->
| BC breaks? | no <!-- see https://symfony.com/bc -->
| Deprecations? | no <!-- please update UPGRADE-*.md and src/**/CHANGELOG.md files -->
| Tests pass? | yes
| Fixed tickets |
| License | MIT
| Doc PR |
Replace #30494
I added :
- tests
- move debug services declaration in dedicated `http_client_debug.xml` file
- rename stuff to follow messenger data collector stuff
- add CompilerPass to allow bundle to trace their own http client
I didn't add all @nicolas-grekas requests on UI profiler. I will continue to make more PR after this one.
IMO everything looks fine to make a first merge except one strange behavior that I am not sure to get :
When making a sub request :
- we still have the http_client parent data. (like messenger, but currently I did not see anything in code that could avoid that, so different topic I guess).
- The data collected are already "converted" to VarDumper data, so I have errors when trying to do all the unset stuff in the TraceableHttpClient.
Is it for this reason, some collector use `lateCollect` ? Should we also move to lateCollect in the `HttpClientDataCollector` ?
But I'm still new on this subject but glad to help, so feel free to request more changes !
Commits
-------
51640012f1
[HttpClient] Added TraceableHttpClient and WebProfiler panel
This commit is contained in:
commit
123418e125
@ -35,6 +35,7 @@ class UnusedTagsPass implements CompilerPassInterface
|
||||
'form.type',
|
||||
'form.type_extension',
|
||||
'form.type_guesser',
|
||||
'http_client.client',
|
||||
'kernel.cache_clearer',
|
||||
'kernel.cache_warmer',
|
||||
'kernel.event_listener',
|
||||
|
@ -150,6 +150,7 @@ class FrameworkExtension extends Extension
|
||||
private $validatorConfigEnabled = false;
|
||||
private $messengerConfigEnabled = false;
|
||||
private $mailerConfigEnabled = false;
|
||||
private $httpClientConfigEnabled = false;
|
||||
|
||||
/**
|
||||
* Responds to the app.config configuration parameter.
|
||||
@ -311,6 +312,10 @@ class FrameworkExtension extends Extension
|
||||
$container->removeDefinition('console.command.messenger_failed_messages_remove');
|
||||
}
|
||||
|
||||
if ($this->httpClientConfigEnabled = $this->isConfigEnabled($container, $config['http_client'])) {
|
||||
$this->registerHttpClientConfiguration($config['http_client'], $container, $loader, $config['profiler']);
|
||||
}
|
||||
|
||||
$propertyInfoEnabled = $this->isConfigEnabled($container, $config['property_info']);
|
||||
$this->registerValidationConfiguration($config['validation'], $container, $loader, $propertyInfoEnabled);
|
||||
$this->registerEsiConfiguration($config['esi'], $container, $loader);
|
||||
@ -341,10 +346,6 @@ class FrameworkExtension extends Extension
|
||||
$this->registerLockConfiguration($config['lock'], $container, $loader);
|
||||
}
|
||||
|
||||
if ($this->isConfigEnabled($container, $config['http_client'])) {
|
||||
$this->registerHttpClientConfiguration($config['http_client'], $container, $loader);
|
||||
}
|
||||
|
||||
if ($this->mailerConfigEnabled = $this->isConfigEnabled($container, $config['mailer'])) {
|
||||
$this->registerMailerConfiguration($config['mailer'], $container, $loader);
|
||||
}
|
||||
@ -562,6 +563,10 @@ class FrameworkExtension extends Extension
|
||||
$loader->load('mailer_debug.xml');
|
||||
}
|
||||
|
||||
if ($this->httpClientConfigEnabled) {
|
||||
$loader->load('http_client_debug.xml');
|
||||
}
|
||||
|
||||
$container->setParameter('profiler_listener.only_exceptions', $config['only_exceptions']);
|
||||
$container->setParameter('profiler_listener.only_master_requests', $config['only_master_requests']);
|
||||
|
||||
@ -1915,7 +1920,7 @@ class FrameworkExtension extends Extension
|
||||
}
|
||||
}
|
||||
|
||||
private function registerHttpClientConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader)
|
||||
private function registerHttpClientConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader, array $profilerConfig)
|
||||
{
|
||||
$loader->load('http_client.xml');
|
||||
|
||||
@ -1930,6 +1935,8 @@ class FrameworkExtension extends Extension
|
||||
$container->removeDefinition(HttpClient::class);
|
||||
}
|
||||
|
||||
$httpClientId = $this->isConfigEnabled($container, $profilerConfig) ? '.debug.http_client.inner' : 'http_client';
|
||||
|
||||
foreach ($config['scoped_clients'] as $name => $scopeConfig) {
|
||||
if ('http_client' === $name) {
|
||||
throw new InvalidArgumentException(sprintf('Invalid scope name: "%s" is reserved.', $name));
|
||||
@ -1941,10 +1948,14 @@ class FrameworkExtension extends Extension
|
||||
if (null === $scope) {
|
||||
$container->register($name, ScopingHttpClient::class)
|
||||
->setFactory([ScopingHttpClient::class, 'forBaseUri'])
|
||||
->setArguments([new Reference('http_client'), $scopeConfig['base_uri'], $scopeConfig]);
|
||||
->setArguments([new Reference($httpClientId), $scopeConfig['base_uri'], $scopeConfig])
|
||||
->addTag('http_client.client')
|
||||
;
|
||||
} else {
|
||||
$container->register($name, ScopingHttpClient::class)
|
||||
->setArguments([new Reference('http_client'), [$scope => $scopeConfig], $scope]);
|
||||
->setArguments([new Reference($httpClientId), [$scope => $scopeConfig], $scope])
|
||||
->addTag('http_client.client')
|
||||
;
|
||||
}
|
||||
|
||||
$container->registerAliasForArgument($name, HttpClientInterface::class);
|
||||
|
@ -36,6 +36,7 @@ use Symfony\Component\ErrorHandler\ErrorHandler;
|
||||
use Symfony\Component\ErrorRenderer\DependencyInjection\ErrorRendererPass;
|
||||
use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass;
|
||||
use Symfony\Component\Form\DependencyInjection\FormPass;
|
||||
use Symfony\Component\HttpClient\DependencyInjection\HttpClientPass;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Bundle\Bundle;
|
||||
use Symfony\Component\HttpKernel\DependencyInjection\ControllerArgumentValueResolverPass;
|
||||
@ -129,6 +130,7 @@ class FrameworkBundle extends Bundle
|
||||
$container->addCompilerPass(new TestServiceContainerRealRefPass(), PassConfig::TYPE_AFTER_REMOVING);
|
||||
$this->addCompilerPassIfExists($container, AddMimeTypeGuesserPass::class);
|
||||
$this->addCompilerPassIfExists($container, MessengerPass::class);
|
||||
$this->addCompilerPassIfExists($container, HttpClientPass::class);
|
||||
$this->addCompilerPassIfExists($container, AddAutoMappingConfigurationPass::class);
|
||||
$container->addCompilerPass(new RegisterReverseContainerPass(true));
|
||||
$container->addCompilerPass(new RegisterReverseContainerPass(false), PassConfig::TYPE_AFTER_REMOVING);
|
||||
|
@ -7,6 +7,7 @@
|
||||
<services>
|
||||
<service id="http_client" class="Symfony\Contracts\HttpClient\HttpClientInterface">
|
||||
<tag name="monolog.logger" channel="http_client" />
|
||||
<tag name="http_client.client" />
|
||||
<factory class="Symfony\Component\HttpClient\HttpClient" method="create" />
|
||||
<argument type="collection" /> <!-- default options -->
|
||||
<argument /> <!-- max host connections -->
|
||||
|
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" ?>
|
||||
|
||||
<container xmlns="http://symfony.com/schema/dic/services"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
|
||||
|
||||
<services>
|
||||
<service id="data_collector.http_client" class="Symfony\Component\HttpClient\DataCollector\HttpClientDataCollector">
|
||||
<tag name="data_collector" template="@WebProfiler/Collector/http_client.html.twig" id="http_client" priority="250" />
|
||||
</service>
|
||||
</services>
|
||||
</container>
|
@ -40,7 +40,7 @@
|
||||
"symfony/polyfill-intl-icu": "~1.0",
|
||||
"symfony/form": "^4.3.4|^5.0",
|
||||
"symfony/expression-language": "^3.4|^4.0|^5.0",
|
||||
"symfony/http-client": "^4.3|^5.0",
|
||||
"symfony/http-client": "^4.4|^5.0",
|
||||
"symfony/lock": "^4.4|^5.0",
|
||||
"symfony/mailer": "^4.4|^5.0",
|
||||
"symfony/messenger": "^4.3|^5.0",
|
||||
@ -71,6 +71,7 @@
|
||||
"symfony/console": "<4.3",
|
||||
"symfony/dotenv": "<4.2",
|
||||
"symfony/dom-crawler": "<4.3",
|
||||
"symfony/http-client": "<4.4",
|
||||
"symfony/form": "<4.3",
|
||||
"symfony/lock": "<4.4",
|
||||
"symfony/mailer": "<4.4",
|
||||
|
@ -5,6 +5,7 @@ CHANGELOG
|
||||
-----
|
||||
|
||||
* added support for the Mailer component
|
||||
* added support for the HttpClient component
|
||||
* added button to clear the ajax request tab
|
||||
* deprecated the `ExceptionController::templateExists()` method
|
||||
* deprecated the `TemplateManager::templateExists()` method
|
||||
|
@ -0,0 +1,98 @@
|
||||
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
|
||||
|
||||
{% block toolbar %}
|
||||
{% if collector.requestCount %}
|
||||
{% set icon %}
|
||||
{{ include('@WebProfiler/Icon/http-client.svg') }}
|
||||
{% set status_color = '' %}
|
||||
<span class="sf-toolbar-value">{{ collector.requestCount }}</span>
|
||||
{% endset %}
|
||||
|
||||
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url, status: status_color }) }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block menu %}
|
||||
<span class="label {{ collector.requestCount == 0 ? 'disabled' }}">
|
||||
<span class="icon">{{ include('@WebProfiler/Icon/http-client.svg') }}</span>
|
||||
<strong>HTTP Client</strong>
|
||||
{% if collector.requestCount %}
|
||||
<span class="count">
|
||||
{{ collector.requestCount }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endblock %}
|
||||
|
||||
{% block panel %}
|
||||
<h2>HTTP Client</h2>
|
||||
{% if collector.requestCount == 0 %}
|
||||
<div class="empty">
|
||||
<p>No HTTP requests were made.</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="metrics">
|
||||
<div class="metric">
|
||||
<span class="value">{{ collector.requestCount }}</span>
|
||||
<span class="label">Total requests</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="value">{{ collector.errorCount }}</span>
|
||||
<span class="label">HTTP errors</span>
|
||||
</div>
|
||||
</div>
|
||||
<h2>Clients</h2>
|
||||
<div class="sf-tabs">
|
||||
{% for name, client in collector.clients %}
|
||||
<div class="tab {{ client.traces|length == 0 ? 'disabled' }}">
|
||||
<h3 class="tab-title">{{ name }} <span class="badge">{{ client.traces|length }}</span></h3>
|
||||
<div class="tab-content">
|
||||
{% if client.traces|length == 0 %}
|
||||
<div class="empty">
|
||||
<p>No requests were made with the "{{ name }}" service.</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<h4>Requests</h4>
|
||||
{% for trace in client.traces %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<span class="label">{{ trace.method }}</span>
|
||||
</th>
|
||||
<th class="full-width">
|
||||
{{ trace.url }}
|
||||
{% if trace.options is not empty %}
|
||||
{{ profiler_dump(trace.options, maxDepth=1) }}
|
||||
{% endif %}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>
|
||||
{% if trace.http_code >= 500 %}
|
||||
{% set responseStatus = 'error' %}
|
||||
{% elseif trace.http_code >= 400 %}
|
||||
{% set responseStatus = 'warning' %}
|
||||
{% else %}
|
||||
{% set responseStatus = 'success' %}
|
||||
{% endif %}
|
||||
<span class="label status-{{ responseStatus }}">
|
||||
{{ trace.http_code }}
|
||||
</span>
|
||||
</th>
|
||||
<td>
|
||||
{{ profiler_dump(trace.info, maxDepth=1) }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#AAA" d="M20.4 12c-1 0-1.8.6-2.2 1.4l-2.6-.9c.1-.3.1-.5.1-.8 0-1.2-.6-2.2-1.5-2.9l1.5-2.6c.3.1.6.2 1 .2 1.4 0 2.5-1.1 2.5-2.5s-1.1-2.5-2.5-2.5-2.5 1.1-2.5 2.5c0 .8.4 1.5.9 1.9l-1.5 2.6c-.5-.3-1-.4-1.6-.4-.9 0-1.7.3-2.3.9L7.4 6.6c.3-.4.5-.9.5-1.5 0-1.4-1.1-2.5-2.5-2.5S2.7 3.7 2.7 5.1s1.1 2.5 2.5 2.5c.6 0 1.1-.2 1.5-.5L9 9.4c-.5.6-.8 1.4-.8 2.3 0 .7.2 1.4.6 2l-3.9 3.8c-.4-.3-.9-.5-1.5-.5C2 17 .9 18.1.9 19.5S2.2 22 3.6 22s2.5-1.1 2.5-2.5c0-.5-.2-1-.5-1.5l3.8-3.7c.7.7 1.6 1.1 2.6 1.1h.2l.4 2.4c-1 .3-1.7 1.3-1.7 2.4 0 1.4 1.1 2.5 2.5 2.5s2.5-1.1 2.5-2.5-1.1-2.5-2.5-2.5l-.4-2.5c1-.3 1.9-1 2.3-2l2.6.9v.4c0 1.4 1.1 2.5 2.5 2.5s2.5-1.1 2.5-2.5c.1-1.4-1.1-2.5-2.5-2.5z"/></svg>
|
After Width: | Height: | Size: 771 B |
@ -6,10 +6,11 @@ CHANGELOG
|
||||
|
||||
* added `StreamWrapper`
|
||||
* added `HttplugClient`
|
||||
* added `max_duration` option
|
||||
* added support for NTLM authentication
|
||||
* added `$response->toStream()` to cast responses to regular PHP streams
|
||||
* made `Psr18Client` implement relevant PSR-17 factories and have streaming responses
|
||||
* added `max_duration` option
|
||||
* added `TraceableHttpClient`, `HttpClientDataCollector` and `HttpClientPass` to integrate with the web profiler
|
||||
|
||||
4.3.0
|
||||
-----
|
||||
|
@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient\DataCollector;
|
||||
|
||||
use Symfony\Component\HttpClient\TraceableHttpClient;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
|
||||
|
||||
/**
|
||||
* @author Jérémy Romey <jeremy@free-agent.fr>
|
||||
*/
|
||||
final class HttpClientDataCollector extends DataCollector
|
||||
{
|
||||
/**
|
||||
* @var TraceableHttpClient[]
|
||||
*/
|
||||
private $clients = [];
|
||||
|
||||
public function registerClient(string $name, TraceableHttpClient $client)
|
||||
{
|
||||
$this->clients[$name] = $client;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function collect(Request $request, Response $response, \Exception $exception = null)
|
||||
{
|
||||
$this->initData();
|
||||
|
||||
foreach ($this->clients as $name => $client) {
|
||||
[$errorCount, $traces] = $this->collectOnClient($client);
|
||||
|
||||
$this->data['clients'][$name] = [
|
||||
'traces' => $traces,
|
||||
'error_count' => $errorCount,
|
||||
];
|
||||
|
||||
$this->data['request_count'] += \count($traces);
|
||||
$this->data['error_count'] += $errorCount;
|
||||
}
|
||||
}
|
||||
|
||||
public function getClients(): array
|
||||
{
|
||||
return $this->data['clients'] ?? [];
|
||||
}
|
||||
|
||||
public function getRequestCount(): int
|
||||
{
|
||||
return $this->data['request_count'] ?? 0;
|
||||
}
|
||||
|
||||
public function getErrorCount(): int
|
||||
{
|
||||
return $this->data['error_count'] ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function reset()
|
||||
{
|
||||
$this->initData();
|
||||
foreach ($this->clients as $client) {
|
||||
$client->reset();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return 'http_client';
|
||||
}
|
||||
|
||||
private function initData()
|
||||
{
|
||||
$this->data = [
|
||||
'clients' => [],
|
||||
'request_count' => 0,
|
||||
'error_count' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
private function collectOnClient(TraceableHttpClient $client): array
|
||||
{
|
||||
$traces = $client->getTracedRequests();
|
||||
$errorCount = 0;
|
||||
$baseInfo = [
|
||||
'response_headers' => 1,
|
||||
'redirect_count' => 1,
|
||||
'redirect_url' => 1,
|
||||
'user_data' => 1,
|
||||
'error' => 1,
|
||||
'url' => 1,
|
||||
];
|
||||
|
||||
foreach ($traces as $i => $trace) {
|
||||
if (400 <= ($trace['info']['http_code'] ?? 0)) {
|
||||
++$errorCount;
|
||||
}
|
||||
|
||||
$info = $trace['info'];
|
||||
$traces[$i]['http_code'] = $info['http_code'];
|
||||
|
||||
unset($info['filetime'], $info['http_code'], $info['ssl_verify_result'], $info['content_type']);
|
||||
|
||||
if ($trace['method'] === $info['http_method']) {
|
||||
unset($info['http_method']);
|
||||
}
|
||||
|
||||
if ($trace['url'] === $info['url']) {
|
||||
unset($info['url']);
|
||||
}
|
||||
|
||||
foreach ($info as $k => $v) {
|
||||
if (!$v || (is_numeric($v) && 0 > $v)) {
|
||||
unset($info[$k]);
|
||||
}
|
||||
}
|
||||
|
||||
$debugInfo = array_diff_key($info, $baseInfo);
|
||||
$info = array_diff_key($info, $debugInfo) + ['debug_info' => $debugInfo];
|
||||
$traces[$i]['info'] = $this->cloneVar($info);
|
||||
$traces[$i]['options'] = $this->cloneVar($trace['options']);
|
||||
}
|
||||
|
||||
return [$errorCount, $traces];
|
||||
}
|
||||
}
|
45
src/Symfony/Component/HttpClient/DependencyInjection/HttpClientPass.php
Executable file
45
src/Symfony/Component/HttpClient/DependencyInjection/HttpClientPass.php
Executable file
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient\DependencyInjection;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
use Symfony\Component\HttpClient\TraceableHttpClient;
|
||||
|
||||
final class HttpClientPass implements CompilerPassInterface
|
||||
{
|
||||
private $clientTag;
|
||||
|
||||
public function __construct(string $clientTag = 'http_client.client')
|
||||
{
|
||||
$this->clientTag = $clientTag;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function process(ContainerBuilder $container)
|
||||
{
|
||||
if (!$container->hasDefinition('data_collector.http_client')) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($container->findTaggedServiceIds($this->clientTag) as $id => $tags) {
|
||||
$container->register('.debug.'.$id, TraceableHttpClient::class)
|
||||
->setArguments([new Reference('.debug.'.$id.'.inner')])
|
||||
->setDecoratedService($id);
|
||||
$container->getDefinition('data_collector.http_client')
|
||||
->addMethodCall('registerClient', [$id, new Reference('.debug.'.$id)]);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,178 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\HttpClient\DataCollector\HttpClientDataCollector;
|
||||
use Symfony\Component\HttpClient\NativeHttpClient;
|
||||
use Symfony\Component\HttpClient\TraceableHttpClient;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Contracts\HttpClient\Test\TestHttpServer;
|
||||
|
||||
class HttpClientDataCollectorTest extends TestCase
|
||||
{
|
||||
public function testItCollectsRequestCount()
|
||||
{
|
||||
TestHttpServer::start();
|
||||
$httpClient1 = $this->httpClientThatHasTracedRequests([
|
||||
[
|
||||
'method' => 'GET',
|
||||
'url' => 'http://localhost:8057/',
|
||||
],
|
||||
[
|
||||
'method' => 'GET',
|
||||
'url' => 'http://localhost:8057/301',
|
||||
],
|
||||
]);
|
||||
$httpClient2 = $this->httpClientThatHasTracedRequests([
|
||||
[
|
||||
'method' => 'GET',
|
||||
'url' => 'http://localhost:8057/404',
|
||||
],
|
||||
]);
|
||||
$httpClient3 = $this->httpClientThatHasTracedRequests([]);
|
||||
$sut = new HttpClientDataCollector();
|
||||
$sut->registerClient('http_client1', $httpClient1);
|
||||
$sut->registerClient('http_client2', $httpClient2);
|
||||
$sut->registerClient('http_client3', $httpClient3);
|
||||
$this->assertEquals(0, $sut->getRequestCount());
|
||||
$sut->collect(new Request(), new Response());
|
||||
$this->assertEquals(3, $sut->getRequestCount());
|
||||
}
|
||||
|
||||
public function testItCollectsErrorCount()
|
||||
{
|
||||
TestHttpServer::start();
|
||||
$httpClient1 = $this->httpClientThatHasTracedRequests([
|
||||
[
|
||||
'method' => 'GET',
|
||||
'url' => 'http://localhost:8057/',
|
||||
],
|
||||
[
|
||||
'method' => 'GET',
|
||||
'url' => 'http://localhost:8057/301',
|
||||
],
|
||||
]);
|
||||
$httpClient2 = $this->httpClientThatHasTracedRequests([
|
||||
[
|
||||
'method' => 'GET',
|
||||
'url' => '/404',
|
||||
'options' => ['base_uri' => 'http://localhost:8057/'],
|
||||
],
|
||||
]);
|
||||
$httpClient3 = $this->httpClientThatHasTracedRequests([]);
|
||||
$sut = new HttpClientDataCollector();
|
||||
$sut->registerClient('http_client1', $httpClient1);
|
||||
$sut->registerClient('http_client2', $httpClient2);
|
||||
$sut->registerClient('http_client3', $httpClient3);
|
||||
$this->assertEquals(0, $sut->getErrorCount());
|
||||
$sut->collect(new Request(), new Response());
|
||||
$this->assertEquals(1, $sut->getErrorCount());
|
||||
}
|
||||
|
||||
public function testItCollectsErrorCountByClient()
|
||||
{
|
||||
TestHttpServer::start();
|
||||
$httpClient1 = $this->httpClientThatHasTracedRequests([
|
||||
[
|
||||
'method' => 'GET',
|
||||
'url' => 'http://localhost:8057/',
|
||||
],
|
||||
[
|
||||
'method' => 'GET',
|
||||
'url' => 'http://localhost:8057/301',
|
||||
],
|
||||
]);
|
||||
$httpClient2 = $this->httpClientThatHasTracedRequests([
|
||||
[
|
||||
'method' => 'GET',
|
||||
'url' => '/404',
|
||||
'options' => ['base_uri' => 'http://localhost:8057/'],
|
||||
],
|
||||
]);
|
||||
$httpClient3 = $this->httpClientThatHasTracedRequests([]);
|
||||
$sut = new HttpClientDataCollector();
|
||||
$sut->registerClient('http_client1', $httpClient1);
|
||||
$sut->registerClient('http_client2', $httpClient2);
|
||||
$sut->registerClient('http_client3', $httpClient3);
|
||||
$this->assertEquals([], $sut->getClients());
|
||||
$sut->collect(new Request(), new Response());
|
||||
$collectedData = $sut->getClients();
|
||||
$this->assertEquals(0, $collectedData['http_client1']['error_count']);
|
||||
$this->assertEquals(1, $collectedData['http_client2']['error_count']);
|
||||
$this->assertEquals(0, $collectedData['http_client3']['error_count']);
|
||||
}
|
||||
|
||||
public function testItCollectsTracesByClient()
|
||||
{
|
||||
TestHttpServer::start();
|
||||
$httpClient1 = $this->httpClientThatHasTracedRequests([
|
||||
[
|
||||
'method' => 'GET',
|
||||
'url' => 'http://localhost:8057/',
|
||||
],
|
||||
[
|
||||
'method' => 'GET',
|
||||
'url' => 'http://localhost:8057/301',
|
||||
],
|
||||
]);
|
||||
$httpClient2 = $this->httpClientThatHasTracedRequests([
|
||||
[
|
||||
'method' => 'GET',
|
||||
'url' => '/404',
|
||||
'options' => ['base_uri' => 'http://localhost:8057/'],
|
||||
],
|
||||
]);
|
||||
$httpClient3 = $this->httpClientThatHasTracedRequests([]);
|
||||
$sut = new HttpClientDataCollector();
|
||||
$sut->registerClient('http_client1', $httpClient1);
|
||||
$sut->registerClient('http_client2', $httpClient2);
|
||||
$sut->registerClient('http_client3', $httpClient3);
|
||||
$this->assertEquals([], $sut->getClients());
|
||||
$sut->collect(new Request(), new Response());
|
||||
$collectedData = $sut->getClients();
|
||||
$this->assertCount(2, $collectedData['http_client1']['traces']);
|
||||
$this->assertCount(1, $collectedData['http_client2']['traces']);
|
||||
$this->assertCount(0, $collectedData['http_client3']['traces']);
|
||||
}
|
||||
|
||||
public function testItIsEmptyAfterReset()
|
||||
{
|
||||
TestHttpServer::start();
|
||||
$httpClient1 = $this->httpClientThatHasTracedRequests([
|
||||
[
|
||||
'method' => 'GET',
|
||||
'url' => 'http://localhost:8057/',
|
||||
],
|
||||
]);
|
||||
$sut = new HttpClientDataCollector();
|
||||
$sut->registerClient('http_client1', $httpClient1);
|
||||
$sut->collect(new Request(), new Response());
|
||||
$collectedData = $sut->getClients();
|
||||
$this->assertCount(1, $collectedData['http_client1']['traces']);
|
||||
$sut->reset();
|
||||
$this->assertEquals([], $sut->getClients());
|
||||
$this->assertEquals(0, $sut->getErrorCount());
|
||||
$this->assertEquals(0, $sut->getRequestCount());
|
||||
}
|
||||
|
||||
private function httpClientThatHasTracedRequests($tracedRequests)
|
||||
{
|
||||
$httpClient = new TraceableHttpClient(new NativeHttpClient());
|
||||
|
||||
foreach ($tracedRequests as $request) {
|
||||
$response = $httpClient->request($request['method'], $request['url'], $request['options'] ?? []);
|
||||
$response->getContent(false); // To avoid exception in ResponseTrait::doDestruct
|
||||
}
|
||||
|
||||
return $httpClient;
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient\Tests\DependencyInjection;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
use Symfony\Component\HttpClient\DataCollector\HttpClientDataCollector;
|
||||
use Symfony\Component\HttpClient\DependencyInjection\HttpClientPass;
|
||||
use Symfony\Component\HttpClient\TraceableHttpClient;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
class HttpClientPassTest extends TestCase
|
||||
{
|
||||
public function testItRequiresDataCollector()
|
||||
{
|
||||
$container = $this->buildContainerBuilder('http_client');
|
||||
$sut = new HttpClientPass();
|
||||
$sut->process($container);
|
||||
|
||||
$this->assertFalse($container->hasDefinition('.debug.http_client'));
|
||||
}
|
||||
|
||||
public function testItDecoratesHttpClientWithTraceableHttpClient()
|
||||
{
|
||||
$container = $this->buildContainerBuilder('foo');
|
||||
$container->register('data_collector.http_client', HttpClientDataCollector::class);
|
||||
$sut = new HttpClientPass();
|
||||
$sut->process($container);
|
||||
$this->assertTrue($container->hasDefinition('.debug.foo'));
|
||||
$this->assertSame(TraceableHttpClient::class, $container->getDefinition('.debug.foo')->getClass());
|
||||
$this->assertSame(['foo', null, 0], $container->getDefinition('.debug.foo')->getDecoratedService());
|
||||
}
|
||||
|
||||
public function testItRegistersDebugHttpClientToCollector()
|
||||
{
|
||||
$container = $this->buildContainerBuilder('foo_client');
|
||||
$container->register('data_collector.http_client', HttpClientDataCollector::class);
|
||||
$sut = new HttpClientPass();
|
||||
$sut->process($container);
|
||||
$this->assertEquals(
|
||||
[['registerClient', ['foo_client', new Reference('.debug.foo_client')]]],
|
||||
$container->getDefinition('data_collector.http_client')->getMethodCalls()
|
||||
);
|
||||
}
|
||||
|
||||
private function buildContainerBuilder(string $clientId = 'http_client'): ContainerBuilder
|
||||
{
|
||||
$container = new ContainerBuilder();
|
||||
$container->setParameter('kernel.debug', true);
|
||||
|
||||
$container->register($clientId, HttpClientInterface::class)->addTag('http_client.client')->setArgument(0, []);
|
||||
|
||||
return $container;
|
||||
}
|
||||
}
|
82
src/Symfony/Component/HttpClient/Tests/TraceableHttpClientTest.php
Executable file
82
src/Symfony/Component/HttpClient/Tests/TraceableHttpClientTest.php
Executable file
@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient\Tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\HttpClient\MockHttpClient;
|
||||
use Symfony\Component\HttpClient\Response\MockResponse;
|
||||
use Symfony\Component\HttpClient\TraceableHttpClient;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
class TraceableHttpClientTest extends TestCase
|
||||
{
|
||||
public function testItTracesRequest()
|
||||
{
|
||||
$httpClient = $this->getMockBuilder(HttpClientInterface::class)->getMock();
|
||||
$httpClient
|
||||
->expects($this->once())
|
||||
->method('request')
|
||||
->with(
|
||||
'GET',
|
||||
'/foo/bar',
|
||||
$this->callback(function ($subject) {
|
||||
$onprogress = $subject['on_progress'];
|
||||
unset($subject['on_progress']);
|
||||
$this->assertEquals(['options1' => 'foo'], $subject);
|
||||
|
||||
return true;
|
||||
})
|
||||
)
|
||||
->willReturn(MockResponse::fromRequest('GET', '/foo/bar', ['options1' => 'foo'], new MockResponse()))
|
||||
;
|
||||
$sut = new TraceableHttpClient($httpClient);
|
||||
$sut->request('GET', '/foo/bar', ['options1' => 'foo']);
|
||||
$this->assertCount(1, $tracedRequests = $sut->getTracedRequests());
|
||||
$actualTracedRequest = $tracedRequests[0];
|
||||
$this->assertEquals([
|
||||
'method' => 'GET',
|
||||
'url' => '/foo/bar',
|
||||
'options' => ['options1' => 'foo'],
|
||||
'info' => [],
|
||||
], $actualTracedRequest);
|
||||
}
|
||||
|
||||
public function testItCollectsInfoOnRealRequest()
|
||||
{
|
||||
$sut = new TraceableHttpClient(new MockHttpClient());
|
||||
$sut->request('GET', 'http://localhost:8057');
|
||||
$this->assertCount(1, $tracedRequests = $sut->getTracedRequests());
|
||||
$actualTracedRequest = $tracedRequests[0];
|
||||
$this->assertSame('GET', $actualTracedRequest['info']['http_method']);
|
||||
$this->assertSame('http://localhost:8057/', $actualTracedRequest['info']['url']);
|
||||
}
|
||||
|
||||
public function testItExecutesOnProgressOption()
|
||||
{
|
||||
$sut = new TraceableHttpClient(new MockHttpClient());
|
||||
$foo = 0;
|
||||
$sut->request('GET', 'http://localhost:8057', ['on_progress' => function (int $dlNow, int $dlSize, array $info) use (&$foo) {
|
||||
++$foo;
|
||||
}]);
|
||||
$this->assertCount(1, $tracedRequests = $sut->getTracedRequests());
|
||||
$actualTracedRequest = $tracedRequests[0];
|
||||
$this->assertGreaterThan(0, $foo);
|
||||
}
|
||||
|
||||
public function testItResetsTraces()
|
||||
{
|
||||
$sut = new TraceableHttpClient(new MockHttpClient());
|
||||
$sut->request('GET', 'https://example.com/foo/bar');
|
||||
$sut->reset();
|
||||
$this->assertCount(0, $sut->getTracedRequests());
|
||||
}
|
||||
}
|
73
src/Symfony/Component/HttpClient/TraceableHttpClient.php
Normal file
73
src/Symfony/Component/HttpClient/TraceableHttpClient.php
Normal file
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpClient;
|
||||
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
|
||||
|
||||
/**
|
||||
* @author Jérémy Romey <jeremy@free-agent.fr>
|
||||
*/
|
||||
final class TraceableHttpClient implements HttpClientInterface
|
||||
{
|
||||
private $client;
|
||||
private $tracedRequests = [];
|
||||
|
||||
public function __construct(HttpClientInterface $client)
|
||||
{
|
||||
$this->client = $client;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function request(string $method, string $url, array $options = []): ResponseInterface
|
||||
{
|
||||
$traceInfo = [];
|
||||
$this->tracedRequests[] = [
|
||||
'method' => $method,
|
||||
'url' => $url,
|
||||
'options' => $options,
|
||||
'info' => &$traceInfo,
|
||||
];
|
||||
$onProgress = $options['on_progress'] ?? null;
|
||||
|
||||
$options['on_progress'] = function (int $dlNow, int $dlSize, array $info) use (&$traceInfo, $onProgress) {
|
||||
$traceInfo = $info;
|
||||
|
||||
if (null !== $onProgress) {
|
||||
$onProgress($dlNow, $dlSize, $info);
|
||||
}
|
||||
};
|
||||
|
||||
return $this->client->request($method, $url, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function stream($responses, float $timeout = null): ResponseStreamInterface
|
||||
{
|
||||
return $this->client->stream($responses, $timeout);
|
||||
}
|
||||
|
||||
public function getTracedRequests(): array
|
||||
{
|
||||
return $this->tracedRequests;
|
||||
}
|
||||
|
||||
public function reset()
|
||||
{
|
||||
$this->tracedRequests = [];
|
||||
}
|
||||
}
|
@ -29,8 +29,11 @@
|
||||
"nyholm/psr7": "^1.0",
|
||||
"php-http/httplug": "^1.0|^2.0",
|
||||
"psr/http-client": "^1.0",
|
||||
"symfony/dependency-injection": "^4.3|^5.0",
|
||||
"symfony/http-kernel": "^4.3|^5.0",
|
||||
"symfony/process": "^4.2|^5.0"
|
||||
"symfony/process": "^4.2|^5.0",
|
||||
"symfony/service-contracts": "^1.0",
|
||||
"symfony/var-dumper": "^4.3|^5.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": { "Symfony\\Component\\HttpClient\\": "" },
|
||||
|
Reference in New Issue
Block a user