Skip to content

Commit 7e0c601

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

File tree

2 files changed

+110
-74
lines changed

2 files changed

+110
-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: 47 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,21 @@
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;
@@ -53,24 +61,40 @@ protected function setUp(): void
5361

5462
public function testRequest(): void
5563
{
64+
$options = new Options([
65+
'dsn' => 'http://public:[email protected]/sentry/1',
66+
]);
67+
$client = $this->createMock(ClientInterface::class);
68+
$client
69+
->expects($this->once())
70+
->method('getOptions')
71+
->willReturn($options);
72+
5673
$transaction = new Transaction(new TransactionContext());
5774
$transaction->initSpanRecorder();
5875

5976
$this->hub->expects($this->once())
6077
->method('getSpan')
6178
->willReturn($transaction);
79+
$this->hub->expects($this->once())
80+
->method('getClient')
81+
->willReturn($client);
6282

6383
$mockResponse = new MockResponse();
6484
$decoratedHttpClient = new MockHttpClient($mockResponse);
6585
$httpClient = new TraceableHttpClient($decoratedHttpClient, $this->hub);
6686
$response = $httpClient->request('GET', 'https://username:[email protected]/test-page?foo=bar#baz');
6787

88+
$this->assertNotNull($transaction->getSpanRecorder());
89+
90+
$spans = $transaction->getSpanRecorder()->getSpans();
91+
6892
$this->assertInstanceOf(AbstractTraceableResponse::class, $response);
6993
$this->assertSame(200, $response->getStatusCode());
7094
$this->assertSame('GET', $response->getInfo('http_method'));
7195
$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']);
96+
$this->assertSame(['sentry-trace: ' . $spans[1]->toTraceparent()], $mockResponse->getRequestOptions()['normalized_headers']['sentry-trace']);
97+
$this->assertSame(['baggage: ' . $transaction->toBaggage()], $mockResponse->getRequestOptions()['normalized_headers']['baggage']);
7498
$this->assertNotNull($transaction->getSpanRecorder());
7599

76100
$spans = $transaction->getSpanRecorder()->getSpans();
@@ -91,15 +115,14 @@ public function testRequest(): void
91115
$this->assertSame($expectedData, $spans[1]->getData());
92116
}
93117

94-
public function testRequestDoesNotContainBaggageHeader(): void
118+
public function testRequestDoesNotContainTracingHeaders(): void
95119
{
96120
$options = new Options([
97121
'dsn' => 'http://public:[email protected]/sentry/1',
98-
'trace_propagation_targets' => ['non-matching-host.invalid'],
122+
'trace_propagation_targets' => null,
99123
]);
100124
$client = $this->createMock(ClientInterface::class);
101-
$client
102-
->expects($this->once())
125+
$client->expects($this->once())
103126
->method('getOptions')
104127
->willReturn($options);
105128

@@ -122,7 +145,7 @@ public function testRequestDoesNotContainBaggageHeader(): void
122145
$this->assertSame(200, $response->getStatusCode());
123146
$this->assertSame('PUT', $response->getInfo('http_method'));
124147
$this->assertSame('https://www.example.com/test-page', $response->getInfo('url'));
125-
$this->assertSame(['sentry-trace: ' . $transaction->toTraceparent()], $mockResponse->getRequestOptions()['normalized_headers']['sentry-trace']);
148+
$this->assertArrayNotHasKey('sentry-trace', $mockResponse->getRequestOptions()['normalized_headers']);
126149
$this->assertArrayNotHasKey('baggage', $mockResponse->getRequestOptions()['normalized_headers']);
127150
$this->assertNotNull($transaction->getSpanRecorder());
128151

@@ -139,52 +162,36 @@ public function testRequestDoesNotContainBaggageHeader(): void
139162
$this->assertSame($expectedTags, $spans[1]->getTags());
140163
}
141164

142-
public function testRequestDoesContainBaggageHeader(): void
165+
public function testRequestDoesContainsTracingHeadersWithoutTransaction(): void
143166
{
144-
$options = new Options([
167+
$client = new Client(new Options([
145168
'dsn' => 'http://public:[email protected]/sentry/1',
169+
'release' => '1.0.0',
170+
'environment' => 'test',
146171
'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);
172+
]), new NullTransport());
153173

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

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

164184
$mockResponse = new MockResponse();
165185
$decoratedHttpClient = new MockHttpClient($mockResponse);
166-
$httpClient = new TraceableHttpClient($decoratedHttpClient, $this->hub);
186+
$httpClient = new TraceableHttpClient($decoratedHttpClient, $hub);
167187
$response = $httpClient->request('POST', 'https://www.example.com/test-page');
168188

169189
$this->assertInstanceOf(AbstractTraceableResponse::class, $response);
170190
$this->assertSame(200, $response->getStatusCode());
171191
$this->assertSame('POST', $response->getInfo('http_method'));
172192
$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());
193+
$this->assertSame(['sentry-trace: 566e3688a61d4bc888951642d6f14a19-566e3688a61d4bc8'], $mockResponse->getRequestOptions()['normalized_headers']['sentry-trace']);
194+
$this->assertSame(['baggage: sentry-trace_id=566e3688a61d4bc888951642d6f14a19,sentry-public_key=public,sentry-release=1.0.0,sentry-environment=test'], $mockResponse->getRequestOptions()['normalized_headers']['baggage']);
188195
}
189196

190197
public function testStream(): void
@@ -217,6 +224,7 @@ public function testStream(): void
217224

218225
$this->assertSame('foobar', implode('', $chunks));
219226
$this->assertCount(2, $spans);
227+
220228
$this->assertNotNull($spans[1]->getEndTimestamp());
221229
$this->assertSame('http.client', $spans[1]->getOp());
222230
$this->assertSame('GET https://www.example.com/test-page', $spans[1]->getDescription());

0 commit comments

Comments
 (0)