Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 19 additions & 12 deletions src/Instrumentation/Laravel/src/Watchers/ClientRequestWatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
class ClientRequestWatcher extends Watcher
{
/**
* @var array<string, SpanInterface>
* @var array<string, list<SpanInterface>>
*/
protected array $spans = [];

Expand Down Expand Up @@ -67,31 +67,25 @@ public function recordRequest(RequestSending $request): void
ServerAttributes::SERVER_PORT => $parsedUrl['port'] ?? '',
])
->startSpan();
$this->spans[$this->createRequestComparisonHash($request->request)] = $span;
$this->spans[$this->createRequestComparisonHash($request->request)][] = $span;
}

/** @psalm-suppress PossiblyUnusedMethod */
public function recordConnectionFailed(ConnectionFailed $request): void
{
$requestHash = $this->createRequestComparisonHash($request->request);

$span = $this->spans[$requestHash] ?? null;
$span = $this->shiftSpan($this->createRequestComparisonHash($request->request));
if (null === $span) {
return;
}

$span->setStatus(StatusCode::STATUS_ERROR, 'Connection failed');
$span->end();

unset($this->spans[$requestHash]);
}

/** @psalm-suppress PossiblyUnusedMethod */
public function recordResponse(ResponseReceived $request): void
{
$requestHash = $this->createRequestComparisonHash($request->request);

$span = $this->spans[$requestHash] ?? null;
$span = $this->shiftSpan($this->createRequestComparisonHash($request->request));
if (null === $span) {
return;
}
Expand All @@ -103,15 +97,28 @@ public function recordResponse(ResponseReceived $request): void

$this->maybeRecordError($span, $request->response);
$span->end();

unset($this->spans[$requestHash]);
}

private function createRequestComparisonHash(Request $request): string
{
return sha1($request->method() . '|' . $request->url() . '|' . $request->body());
}

private function shiftSpan(string $requestHash): ?SpanInterface
{
if (empty($this->spans[$requestHash])) {
return null;
}

$span = array_shift($this->spans[$requestHash]);

if (empty($this->spans[$requestHash])) {
unset($this->spans[$requestHash]);
}

return $span;
}

private function maybeRecordError(SpanInterface $span, Response $response): void
{
if ($response->successful()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,18 @@

use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Promise\RejectedPromise;
use GuzzleHttp\Psr7\Request as Psr7Request;
use GuzzleHttp\Psr7\Response as Psr7Response;
use Illuminate\Http\Client\Events\ConnectionFailed;
use Illuminate\Http\Client\Events\ResponseReceived;
use Illuminate\Http\Client\Pool;
use Illuminate\Http\Client\Request;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Http;
use OpenTelemetry\API\Trace\StatusCode;
use OpenTelemetry\SDK\Trace\StatusData;
use OpenTelemetry\SemConv\Attributes\HttpAttributes;
use OpenTelemetry\SemConv\Attributes\UrlAttributes;
use OpenTelemetry\Tests\Contrib\Instrumentation\Laravel\Integration\TestCase;

Expand Down Expand Up @@ -60,4 +68,51 @@ public function test_it_records_connection_failures(): void
self::assertEquals('http://fail', $span->getAttributes()->get(UrlAttributes::URL_FULL));
self::assertEquals(StatusData::create(StatusCode::STATUS_ERROR, 'Connection failed'), $span->getStatus());
}

public function test_it_matches_spans_to_responses_for_concurrent_identical_requests(): void
{
Http::fake([
'same.opentelemetry.io' => Http::sequence()
->push(status: 200)
->push(status: 500),
]);

$responses = Http::pool(fn (Pool $pool) => [
$pool->get('same.opentelemetry.io'),
$pool->get('same.opentelemetry.io'),
]);

self::assertEquals(200, $responses[0]->status());
self::assertEquals(500, $responses[1]->status());

self::assertCount(2, $this->storage);

$firstSpan = $this->storage[0];
$secondSpan = $this->storage[1];

self::assertEquals(200, $firstSpan->getAttributes()->get(HttpAttributes::HTTP_RESPONSE_STATUS_CODE));
self::assertEquals(StatusCode::STATUS_UNSET, $firstSpan->getStatus()->getCode());

self::assertEquals(500, $secondSpan->getAttributes()->get(HttpAttributes::HTTP_RESPONSE_STATUS_CODE));
self::assertEquals(StatusCode::STATUS_ERROR, $secondSpan->getStatus()->getCode());
}

public function test_it_ignores_response_with_no_matching_request(): void
{
$request = new Request(new Psr7Request('GET', 'http://untracked.opentelemetry.io'));
$response = new Response(new Psr7Response(200));

Event::dispatch(new ResponseReceived($request, $response));

self::assertCount(0, $this->storage);
}

public function test_it_ignores_connection_failure_with_no_matching_request(): void
{
$request = new Request(new Psr7Request('GET', 'http://untracked.opentelemetry.io'));

Event::dispatch(new ConnectionFailed($request));

self::assertCount(0, $this->storage);
}
}
Loading