Skip to content

Commit 7c3307f

Browse files
committed
Add HTTP client header propagation
1 parent ca61db1 commit 7c3307f

File tree

2 files changed

+108
-74
lines changed

2 files changed

+108
-74
lines changed

src/Tracing/HttpClient/AbstractTraceableHttpClient.php

Lines changed: 63 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use GuzzleHttp\Psr7\Uri;
88
use Psr\Log\LoggerAwareInterface;
99
use Psr\Log\LoggerInterface;
10+
use Sentry\ClientInterface;
1011
use Sentry\State\HubInterface;
1112
use Sentry\Tracing\SpanContext;
1213
use Symfony\Component\HttpClient\Response\ResponseStream;
@@ -15,6 +16,9 @@
1516
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
1617
use Symfony\Contracts\Service\ResetInterface;
1718

19+
use function Sentry\getBaggage;
20+
use function Sentry\getTraceparent;
21+
1822
/**
1923
* This is an implementation of the {@see HttpClientInterface} that decorates
2024
* an existing http client to support distributed tracing capabilities.
@@ -44,49 +48,52 @@ public function __construct(HttpClientInterface $client, HubInterface $hub)
4448
*/
4549
public function request(string $method, string $url, array $options = []): ResponseInterface
4650
{
47-
$span = null;
48-
$parent = $this->hub->getSpan();
49-
50-
if (null !== $parent) {
51-
$headers = $options['headers'] ?? [];
52-
$headers['sentry-trace'] = $parent->toTraceparent();
51+
$uri = new Uri($url);
52+
$headers = $options['headers'] ?? [];
5353

54-
$uri = new Uri($url);
55-
$partialUri = Uri::fromParts([
56-
'scheme' => $uri->getScheme(),
57-
'host' => $uri->getHost(),
58-
'port' => $uri->getPort(),
59-
'path' => $uri->getPath(),
60-
]);
61-
62-
// Check if the request destination is allow listed in the trace_propagation_targets option.
63-
$client = $this->hub->getClient();
64-
if (null !== $client) {
65-
$sdkOptions = $client->getOptions();
54+
$span = $this->hub->getSpan();
55+
$client = $this->hub->getClient();
6656

67-
if (\in_array($uri->getHost(), (array) $sdkOptions->getTracePropagationTargets())) {
68-
$headers['baggage'] = $parent->toBaggage();
69-
}
57+
if (null === $span) {
58+
if (self::shouldAttachTracingHeaders($client, $uri)) {
59+
$headers['baggage'] = getBaggage();
60+
$headers['sentry-trace'] = getTraceparent();
7061
}
7162

7263
$options['headers'] = $headers;
7364

74-
$context = new SpanContext();
75-
$context->setOp('http.client');
76-
$context->setDescription($method . ' ' . (string) $partialUri);
77-
$context->setTags([
78-
'http.method' => $method,
79-
'http.url' => (string) $partialUri,
80-
]);
81-
$context->setData([
82-
'http.query' => $uri->getQuery(),
83-
'http.fragment' => $uri->getFragment(),
84-
]);
85-
86-
$span = $parent->startChild($context);
65+
return new TraceableResponse($this->client, $this->client->request($method, $url, $options), $span);
8766
}
8867

89-
return new TraceableResponse($this->client, $this->client->request($method, $url, $options), $span);
68+
$partialUri = Uri::fromParts([
69+
'scheme' => $uri->getScheme(),
70+
'host' => $uri->getHost(),
71+
'port' => $uri->getPort(),
72+
'path' => $uri->getPath(),
73+
]);
74+
75+
$context = new SpanContext();
76+
$context->setOp('http.client');
77+
$context->setDescription($method . ' ' . (string) $partialUri);
78+
$context->setTags([
79+
'http.method' => $method,
80+
'http.url' => (string) $partialUri,
81+
]);
82+
$context->setData([
83+
'http.query' => $uri->getQuery(),
84+
'http.fragment' => $uri->getFragment(),
85+
]);
86+
87+
$childSpan = $span->startChild($context);
88+
89+
if (self::shouldAttachTracingHeaders($client, $uri)) {
90+
$headers['baggage'] = $childSpan->toBaggage();
91+
$headers['sentry-trace'] = $childSpan->toTraceparent();
92+
}
93+
94+
$options['headers'] = $headers;
95+
96+
return new TraceableResponse($this->client, $this->client->request($method, $url, $options), $childSpan);
9097
}
9198

9299
/**
@@ -119,4 +126,25 @@ public function setLogger(LoggerInterface $logger): void
119126
$this->client->setLogger($logger);
120127
}
121128
}
129+
130+
private static function shouldAttachTracingHeaders(?ClientInterface $client, Uri $uri): bool
131+
{
132+
if (null !== $client) {
133+
$sdkOptions = $client->getOptions();
134+
135+
// Check if the request destination is allow listed in the trace_propagation_targets option.
136+
if (
137+
null !== $sdkOptions->getTracePropagationTargets() &&
138+
// Due to BC, we treat an empty array (the default) as all hosts are allow listed
139+
(
140+
[] === $sdkOptions->getTracePropagationTargets() ||
141+
\in_array($uri->getHost(), $sdkOptions->getTracePropagationTargets())
142+
)
143+
) {
144+
return true;
145+
}
146+
}
147+
148+
return false;
149+
}
122150
}

tests/Tracing/HttpClient/TraceableHttpClientTest.php

Lines changed: 45 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,28 @@
88
use PHPUnit\Framework\TestCase;
99
use Psr\Log\LoggerAwareInterface;
1010
use Psr\Log\NullLogger;
11+
use Sentry\Client;
1112
use Sentry\ClientInterface;
1213
use Sentry\Options;
1314
use Sentry\SentryBundle\Tracing\HttpClient\AbstractTraceableResponse;
1415
use Sentry\SentryBundle\Tracing\HttpClient\TraceableHttpClient;
16+
use Sentry\SentrySdk;
17+
use Sentry\State\Hub;
1518
use Sentry\State\HubInterface;
19+
use Sentry\State\Scope;
20+
use Sentry\Tracing\PropagationContext;
21+
use Sentry\Tracing\SpanId;
22+
use Sentry\Tracing\TraceId;
1623
use Sentry\Tracing\Transaction;
1724
use Sentry\Tracing\TransactionContext;
25+
use Sentry\Transport\NullTransport;
1826
use Symfony\Component\HttpClient\MockHttpClient;
1927
use Symfony\Component\HttpClient\Response\MockResponse;
2028
use Symfony\Contracts\HttpClient\HttpClientInterface;
2129
use Symfony\Contracts\Service\ResetInterface;
2230

31+
use function Sentry\configureScope;
32+
2333
final class TraceableHttpClientTest extends TestCase
2434
{
2535
/**
@@ -53,12 +63,24 @@ protected function setUp(): void
5363

5464
public function testRequest(): void
5565
{
66+
$options = new Options([
67+
'dsn' => 'http://public:[email protected]/sentry/1',
68+
]);
69+
$client = $this->createMock(ClientInterface::class);
70+
$client
71+
->expects($this->once())
72+
->method('getOptions')
73+
->willReturn($options);
74+
5675
$transaction = new Transaction(new TransactionContext());
5776
$transaction->initSpanRecorder();
5877

5978
$this->hub->expects($this->once())
6079
->method('getSpan')
6180
->willReturn($transaction);
81+
$this->hub->expects($this->once())
82+
->method('getClient')
83+
->willReturn($client);
6284

6385
$mockResponse = new MockResponse();
6486
$decoratedHttpClient = new MockHttpClient($mockResponse);
@@ -69,8 +91,8 @@ public function testRequest(): void
6991
$this->assertSame(200, $response->getStatusCode());
7092
$this->assertSame('GET', $response->getInfo('http_method'));
7193
$this->assertSame('https://username:[email protected]/test-page?foo=bar#baz', $response->getInfo('url'));
72-
$this->assertSame(['sentry-trace: ' . $transaction->toTraceparent()], $mockResponse->getRequestOptions()['normalized_headers']['sentry-trace']);
73-
$this->assertArrayNotHasKey('baggage', $mockResponse->getRequestOptions()['normalized_headers']);
94+
$this->assertSame(['sentry-trace: ' . $transaction->getSpanRecorder()->getSpans()[1]->toTraceparent()], $mockResponse->getRequestOptions()['normalized_headers']['sentry-trace']);
95+
$this->assertSame(['baggage: ' . $transaction->toBaggage()], $mockResponse->getRequestOptions()['normalized_headers']['baggage']);
7496
$this->assertNotNull($transaction->getSpanRecorder());
7597

7698
$spans = $transaction->getSpanRecorder()->getSpans();
@@ -91,15 +113,14 @@ public function testRequest(): void
91113
$this->assertSame($expectedData, $spans[1]->getData());
92114
}
93115

94-
public function testRequestDoesNotContainBaggageHeader(): void
116+
public function testRequestDoesNotContainTracingHeaders(): void
95117
{
96118
$options = new Options([
97119
'dsn' => 'http://public:[email protected]/sentry/1',
98-
'trace_propagation_targets' => ['non-matching-host.invalid'],
120+
'trace_propagation_targets' => null,
99121
]);
100122
$client = $this->createMock(ClientInterface::class);
101-
$client
102-
->expects($this->once())
123+
$client->expects($this->once())
103124
->method('getOptions')
104125
->willReturn($options);
105126

@@ -122,7 +143,7 @@ public function testRequestDoesNotContainBaggageHeader(): void
122143
$this->assertSame(200, $response->getStatusCode());
123144
$this->assertSame('PUT', $response->getInfo('http_method'));
124145
$this->assertSame('https://www.example.com/test-page', $response->getInfo('url'));
125-
$this->assertSame(['sentry-trace: ' . $transaction->toTraceparent()], $mockResponse->getRequestOptions()['normalized_headers']['sentry-trace']);
146+
$this->assertArrayNotHasKey('sentry-trace', $mockResponse->getRequestOptions()['normalized_headers']);
126147
$this->assertArrayNotHasKey('baggage', $mockResponse->getRequestOptions()['normalized_headers']);
127148
$this->assertNotNull($transaction->getSpanRecorder());
128149

@@ -139,52 +160,36 @@ public function testRequestDoesNotContainBaggageHeader(): void
139160
$this->assertSame($expectedTags, $spans[1]->getTags());
140161
}
141162

142-
public function testRequestDoesContainBaggageHeader(): void
163+
public function testRequestDoesContainsTracingHeadersWithoutTransaction(): void
143164
{
144-
$options = new Options([
165+
$client = new Client(new Options([
145166
'dsn' => 'http://public:[email protected]/sentry/1',
167+
'release' => '1.0.0',
168+
'environment' => 'test',
146169
'trace_propagation_targets' => ['www.example.com'],
147-
]);
148-
$client = $this->createMock(ClientInterface::class);
149-
$client
150-
->expects($this->once())
151-
->method('getOptions')
152-
->willReturn($options);
170+
]), new NullTransport());
153171

154-
$transaction = new Transaction(new TransactionContext());
155-
$transaction->initSpanRecorder();
172+
$propagationContext = PropagationContext::fromDefaults();
173+
$propagationContext->setTraceId(new TraceId('566e3688a61d4bc888951642d6f14a19'));
174+
$propagationContext->setSpanId(new SpanId('566e3688a61d4bc8'));
156175

157-
$this->hub->expects($this->once())
158-
->method('getSpan')
159-
->willReturn($transaction);
160-
$this->hub->expects($this->once())
161-
->method('getClient')
162-
->willReturn($client);
176+
$scope = new Scope($propagationContext);
177+
178+
$hub = new Hub($client, $scope);
179+
180+
SentrySdk::setCurrentHub($hub);
163181

164182
$mockResponse = new MockResponse();
165183
$decoratedHttpClient = new MockHttpClient($mockResponse);
166-
$httpClient = new TraceableHttpClient($decoratedHttpClient, $this->hub);
184+
$httpClient = new TraceableHttpClient($decoratedHttpClient, $hub);
167185
$response = $httpClient->request('POST', 'https://www.example.com/test-page');
168186

169187
$this->assertInstanceOf(AbstractTraceableResponse::class, $response);
170188
$this->assertSame(200, $response->getStatusCode());
171189
$this->assertSame('POST', $response->getInfo('http_method'));
172190
$this->assertSame('https://www.example.com/test-page', $response->getInfo('url'));
173-
$this->assertSame(['sentry-trace: ' . $transaction->toTraceparent()], $mockResponse->getRequestOptions()['normalized_headers']['sentry-trace']);
174-
$this->assertSame(['baggage: ' . $transaction->toBaggage()], $mockResponse->getRequestOptions()['normalized_headers']['baggage']);
175-
$this->assertNotNull($transaction->getSpanRecorder());
176-
177-
$spans = $transaction->getSpanRecorder()->getSpans();
178-
$expectedTags = [
179-
'http.method' => 'POST',
180-
'http.url' => 'https://www.example.com/test-page',
181-
];
182-
183-
$this->assertCount(2, $spans);
184-
$this->assertNull($spans[1]->getEndTimestamp());
185-
$this->assertSame('http.client', $spans[1]->getOp());
186-
$this->assertSame('POST https://www.example.com/test-page', $spans[1]->getDescription());
187-
$this->assertSame($expectedTags, $spans[1]->getTags());
191+
$this->assertSame(['sentry-trace: 566e3688a61d4bc888951642d6f14a19-566e3688a61d4bc8'], $mockResponse->getRequestOptions()['normalized_headers']['sentry-trace']);
192+
$this->assertSame(['baggage: sentry-trace_id=566e3688a61d4bc888951642d6f14a19,sentry-public_key=public,sentry-release=1.0.0,sentry-environment=test'], $mockResponse->getRequestOptions()['normalized_headers']['baggage']);
188193
}
189194

190195
public function testStream(): void
@@ -217,6 +222,7 @@ public function testStream(): void
217222

218223
$this->assertSame('foobar', implode('', $chunks));
219224
$this->assertCount(2, $spans);
225+
220226
$this->assertNotNull($spans[1]->getEndTimestamp());
221227
$this->assertSame('http.client', $spans[1]->getOp());
222228
$this->assertSame('GET https://www.example.com/test-page', $spans[1]->getDescription());

0 commit comments

Comments
 (0)