Skip to content

Commit a5816df

Browse files
Kyra FarrowjeremyFreeAgenttyx
committed
[HttpClient] Added TraceableHttpClient and WebProfiler panel
Co-authored-by: Jérémy Romey <[email protected]> Co-authored-by: Timothée Barray <[email protected]>
1 parent 97119ef commit a5816df

File tree

8 files changed

+591
-2
lines changed

8 files changed

+591
-2
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@ CHANGELOG
66

77
* added `StreamWrapper`
88
* added `HttplugClient`
9+
* added `max_duration` option
910
* added support for NTLM authentication
1011
* added `$response->toStream()` to cast responses to regular PHP streams
1112
* made `Psr18Client` implement relevant PSR-17 factories and have streaming responses
12-
* added `max_duration` option
13+
* added `TraceableHttpClient`, `HttpClientDataCollector` and `HttpClientPass` to integrate with the web profiler
1314

1415
4.3.0
1516
-----
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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 Symfony\Component\HttpClient\DataCollector;
13+
14+
use Symfony\Component\HttpClient\TraceableHttpClient;
15+
use Symfony\Component\HttpFoundation\Request;
16+
use Symfony\Component\HttpFoundation\Response;
17+
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
18+
19+
/**
20+
* @author Jérémy Romey <[email protected]>
21+
*/
22+
final class HttpClientDataCollector extends DataCollector
23+
{
24+
/**
25+
* @var TraceableHttpClient[]
26+
*/
27+
private $clients = [];
28+
29+
public function registerClient(string $name, TraceableHttpClient $client)
30+
{
31+
$this->clients[$name] = $client;
32+
}
33+
34+
/**
35+
* {@inheritdoc}
36+
*/
37+
public function collect(Request $request, Response $response, \Exception $exception = null)
38+
{
39+
$this->initData();
40+
41+
foreach ($this->clients as $name => $client) {
42+
[$errorCount, $traces] = $this->collectOnClient($client);
43+
44+
$this->data['clients'][$name] = [
45+
'traces' => $traces,
46+
'error_count' => $errorCount,
47+
];
48+
49+
$this->data['request_count'] += \count($traces);
50+
$this->data['error_count'] += $errorCount;
51+
}
52+
}
53+
54+
public function getClients(): array
55+
{
56+
return $this->data['clients'] ?? [];
57+
}
58+
59+
public function getRequestCount(): int
60+
{
61+
return $this->data['request_count'] ?? 0;
62+
}
63+
64+
public function getErrorCount(): int
65+
{
66+
return $this->data['error_count'] ?? 0;
67+
}
68+
69+
/**
70+
* {@inheritdoc}
71+
*/
72+
public function reset()
73+
{
74+
$this->initData();
75+
foreach ($this->clients as $client) {
76+
$client->reset();
77+
}
78+
}
79+
80+
/**
81+
* {@inheritdoc}
82+
*/
83+
public function getName(): string
84+
{
85+
return 'http_client';
86+
}
87+
88+
private function initData()
89+
{
90+
$this->data = [
91+
'clients' => [],
92+
'request_count' => 0,
93+
'error_count' => 0,
94+
];
95+
}
96+
97+
private function collectOnClient(TraceableHttpClient $client): array
98+
{
99+
$traces = $client->getTracedRequests();
100+
$errorCount = 0;
101+
$baseInfo = [
102+
'response_headers' => 1,
103+
'redirect_count' => 1,
104+
'redirect_url' => 1,
105+
'user_data' => 1,
106+
'error' => 1,
107+
'url' => 1,
108+
];
109+
110+
foreach ($traces as $i => $trace) {
111+
if (400 <= ($trace['info']['http_code'] ?? 0)) {
112+
++$errorCount;
113+
}
114+
115+
$info = $trace['info'];
116+
$traces[$i]['http_code'] = $info['http_code'];
117+
118+
unset($info['filetime'], $info['http_code'], $info['ssl_verify_result'], $info['content_type']);
119+
120+
if ($trace['method'] === $info['http_method']) {
121+
unset($info['http_method']);
122+
}
123+
124+
if ($trace['url'] === $info['url']) {
125+
unset($info['url']);
126+
}
127+
128+
foreach ($info as $k => $v) {
129+
if (!$v || (is_numeric($v) && 0 > $v)) {
130+
unset($info[$k]);
131+
}
132+
}
133+
134+
$debugInfo = array_diff_key($info, $baseInfo);
135+
$info = array_diff_key($info, $debugInfo) + ['debug_info' => $debugInfo];
136+
$traces[$i]['info'] = $this->cloneVar($info);
137+
$traces[$i]['options'] = $this->cloneVar($trace['options']);
138+
}
139+
140+
return [$errorCount, $traces];
141+
}
142+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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 Symfony\Component\HttpClient\DependencyInjection;
13+
14+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
15+
use Symfony\Component\DependencyInjection\ContainerBuilder;
16+
use Symfony\Component\DependencyInjection\Reference;
17+
use Symfony\Component\HttpClient\TraceableHttpClient;
18+
19+
final class HttpClientPass implements CompilerPassInterface
20+
{
21+
private $clientTag;
22+
23+
public function __construct(string $clientTag = 'http_client.client')
24+
{
25+
$this->clientTag = $clientTag;
26+
}
27+
28+
/**
29+
* {@inheritdoc}
30+
*/
31+
public function process(ContainerBuilder $container)
32+
{
33+
if (!$container->hasDefinition('data_collector.http_client')) {
34+
return;
35+
}
36+
37+
foreach ($container->findTaggedServiceIds($this->clientTag) as $id => $tags) {
38+
$container->register('.debug.'.$id, TraceableHttpClient::class)
39+
->setArguments([new Reference('.debug.'.$id.'.inner')])
40+
->setDecoratedService($id);
41+
$container->getDefinition('data_collector.http_client')
42+
->addMethodCall('registerClient', [$id, new Reference('.debug.'.$id)]);
43+
}
44+
}
45+
}
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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+
use PHPUnit\Framework\TestCase;
13+
use Symfony\Component\HttpClient\DataCollector\HttpClientDataCollector;
14+
use Symfony\Component\HttpClient\NativeHttpClient;
15+
use Symfony\Component\HttpClient\TraceableHttpClient;
16+
use Symfony\Component\HttpFoundation\Request;
17+
use Symfony\Component\HttpFoundation\Response;
18+
use Symfony\Contracts\HttpClient\Test\TestHttpServer;
19+
20+
class HttpClientDataCollectorTest extends TestCase
21+
{
22+
public function testItCollectsRequestCount()
23+
{
24+
TestHttpServer::start();
25+
$httpClient1 = $this->httpClientThatHasTracedRequests([
26+
[
27+
'method' => 'GET',
28+
'url' => 'http://localhost:8057/',
29+
],
30+
[
31+
'method' => 'GET',
32+
'url' => 'http://localhost:8057/301',
33+
],
34+
]);
35+
$httpClient2 = $this->httpClientThatHasTracedRequests([
36+
[
37+
'method' => 'GET',
38+
'url' => 'http://localhost:8057/404',
39+
],
40+
]);
41+
$httpClient3 = $this->httpClientThatHasTracedRequests([]);
42+
$sut = new HttpClientDataCollector();
43+
$sut->registerClient('http_client1', $httpClient1);
44+
$sut->registerClient('http_client2', $httpClient2);
45+
$sut->registerClient('http_client3', $httpClient3);
46+
$this->assertEquals(0, $sut->getRequestCount());
47+
$sut->collect(new Request(), new Response());
48+
$this->assertEquals(3, $sut->getRequestCount());
49+
}
50+
51+
public function testItCollectsErrorCount()
52+
{
53+
TestHttpServer::start();
54+
$httpClient1 = $this->httpClientThatHasTracedRequests([
55+
[
56+
'method' => 'GET',
57+
'url' => 'http://localhost:8057/',
58+
],
59+
[
60+
'method' => 'GET',
61+
'url' => 'http://localhost:8057/301',
62+
],
63+
]);
64+
$httpClient2 = $this->httpClientThatHasTracedRequests([
65+
[
66+
'method' => 'GET',
67+
'url' => '/404',
68+
'options' => ['base_uri' => 'http://localhost:8057/'],
69+
],
70+
]);
71+
$httpClient3 = $this->httpClientThatHasTracedRequests([]);
72+
$sut = new HttpClientDataCollector();
73+
$sut->registerClient('http_client1', $httpClient1);
74+
$sut->registerClient('http_client2', $httpClient2);
75+
$sut->registerClient('http_client3', $httpClient3);
76+
$this->assertEquals(0, $sut->getErrorCount());
77+
$sut->collect(new Request(), new Response());
78+
$this->assertEquals(1, $sut->getErrorCount());
79+
}
80+
81+
public function testItCollectsErrorCountByClient()
82+
{
83+
TestHttpServer::start();
84+
$httpClient1 = $this->httpClientThatHasTracedRequests([
85+
[
86+
'method' => 'GET',
87+
'url' => 'http://localhost:8057/',
88+
],
89+
[
90+
'method' => 'GET',
91+
'url' => 'http://localhost:8057/301',
92+
],
93+
]);
94+
$httpClient2 = $this->httpClientThatHasTracedRequests([
95+
[
96+
'method' => 'GET',
97+
'url' => '/404',
98+
'options' => ['base_uri' => 'http://localhost:8057/'],
99+
],
100+
]);
101+
$httpClient3 = $this->httpClientThatHasTracedRequests([]);
102+
$sut = new HttpClientDataCollector();
103+
$sut->registerClient('http_client1', $httpClient1);
104+
$sut->registerClient('http_client2', $httpClient2);
105+
$sut->registerClient('http_client3', $httpClient3);
106+
$this->assertEquals([], $sut->getClients());
107+
$sut->collect(new Request(), new Response());
108+
$collectedData = $sut->getClients();
109+
$this->assertEquals(0, $collectedData['http_client1']['error_count']);
110+
$this->assertEquals(1, $collectedData['http_client2']['error_count']);
111+
$this->assertEquals(0, $collectedData['http_client3']['error_count']);
112+
}
113+
114+
public function testItCollectsTracesByClient()
115+
{
116+
TestHttpServer::start();
117+
$httpClient1 = $this->httpClientThatHasTracedRequests([
118+
[
119+
'method' => 'GET',
120+
'url' => 'http://localhost:8057/',
121+
],
122+
[
123+
'method' => 'GET',
124+
'url' => 'http://localhost:8057/301',
125+
],
126+
]);
127+
$httpClient2 = $this->httpClientThatHasTracedRequests([
128+
[
129+
'method' => 'GET',
130+
'url' => '/404',
131+
'options' => ['base_uri' => 'http://localhost:8057/'],
132+
],
133+
]);
134+
$httpClient3 = $this->httpClientThatHasTracedRequests([]);
135+
$sut = new HttpClientDataCollector();
136+
$sut->registerClient('http_client1', $httpClient1);
137+
$sut->registerClient('http_client2', $httpClient2);
138+
$sut->registerClient('http_client3', $httpClient3);
139+
$this->assertEquals([], $sut->getClients());
140+
$sut->collect(new Request(), new Response());
141+
$collectedData = $sut->getClients();
142+
$this->assertCount(2, $collectedData['http_client1']['traces']);
143+
$this->assertCount(1, $collectedData['http_client2']['traces']);
144+
$this->assertCount(0, $collectedData['http_client3']['traces']);
145+
}
146+
147+
public function testItIsEmptyAfterReset()
148+
{
149+
TestHttpServer::start();
150+
$httpClient1 = $this->httpClientThatHasTracedRequests([
151+
[
152+
'method' => 'GET',
153+
'url' => 'http://localhost:8057/',
154+
],
155+
]);
156+
$sut = new HttpClientDataCollector();
157+
$sut->registerClient('http_client1', $httpClient1);
158+
$sut->collect(new Request(), new Response());
159+
$collectedData = $sut->getClients();
160+
$this->assertCount(1, $collectedData['http_client1']['traces']);
161+
$sut->reset();
162+
$this->assertEquals([], $sut->getClients());
163+
$this->assertEquals(0, $sut->getErrorCount());
164+
$this->assertEquals(0, $sut->getRequestCount());
165+
}
166+
167+
private function httpClientThatHasTracedRequests($tracedRequests)
168+
{
169+
$httpClient = new TraceableHttpClient(new NativeHttpClient());
170+
171+
foreach ($tracedRequests as $request) {
172+
$response = $httpClient->request($request['method'], $request['url'], $request['options'] ?? []);
173+
$response->getContent(false); // To avoid exception in ResponseTrait::doDestruct
174+
}
175+
176+
return $httpClient;
177+
}
178+
}

0 commit comments

Comments
 (0)