Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
101 changes: 101 additions & 0 deletions src/Agent/Transport/AgentClient.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?php

declare(strict_types=1);

namespace Sentry\Agent\Transport;

use Sentry\HttpClient\HttpClientInterface;
use Sentry\HttpClient\Request;
use Sentry\HttpClient\Response;
use Sentry\Options;

class AgentClient implements HttpClientInterface
{
/**
* @var string
*/
private $host;

/**
* @var int
*/
private $port;

/**
* @var resource|null
*/
private $socket;

public function __construct(string $host = '127.0.0.1', int $port = 5148)
{
$this->host = $host;
$this->port = $port;
}

public function __destruct()
{
$this->disconnect();
}

/**
* @phpstan-assert-if-true resource $this->socket
*/
private function connect(): bool
{
if ($this->socket !== null) {
return true;
}

// We set the timeout to 10ms to avoid blocking the request for too long if the agent is not running
// @TODO: 10ms should be low enough? Do we want to go lower and/or make this configurable? Only applies to initial connection.
$socket = fsockopen($this->host, $this->port, $errorNo, $errorMsg, 0.01);

// @TODO: Error handling? See $errorNo and $errorMsg
if ($socket === false) {
return false;
}

// @TODO: Set a timeout for the socket to prevent blocking (?) if the socket connection stops working after the connection (e.g. the agent is stopped) if needed
$this->socket = $socket;

return true;
}

private function disconnect(): void
{
if ($this->socket === null) {
return;
}

fclose($this->socket);

$this->socket = null;
}

private function send(string $message): void
{
if (!$this->connect()) {
return;
}

// @TODO: Make sure we don't send more than 2^32 - 1 bytes
$contentLength = pack('N', \strlen($message) + 4);

// @TODO: Error handling?
fwrite($this->socket, $contentLength . $message);
}

public function sendRequest(Request $request, Options $options): Response
{
$body = $request->getStringBody();

if (empty($body)) {
return new Response(400, [], 'Request body is empty');
}

$this->send($body);

// Since we are sending async there is no feedback so we always return an empty response
return new Response(202, [], '');
}
}
91 changes: 91 additions & 0 deletions tests/HttpClient/AgentClientTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?php

declare(strict_types=1);

namespace Sentry\Tests\HttpClient;

use PHPUnit\Framework\TestCase;
use Sentry\Agent\Transport\AgentClient;
use Sentry\Event;
use Sentry\HttpClient\Request;
use Sentry\Options;
use Sentry\Serializer\PayloadSerializer;

final class AgentClientTest extends TestCase
{
use TestAgent;

protected function tearDown(): void
{
if ($this->agentProcess !== null) {
$this->stopTestAgent();
}
}

public function testClientHandsOffEnvelopeToLocalAgent(): void
{
$this->startTestAgent();

$envelope = $this->createEnvelope('http://public@example.com/1', 'Hello from agent client test!');

$request = new Request();
$request->setStringBody($envelope);

$client = new AgentClient('127.0.0.1', $this->agentPort);
$response = $client->sendRequest($request, new Options());

$this->waitForEnvelopeCount(1);
$agentOutput = $this->stopTestAgent();

$this->assertSame(202, $response->getStatusCode());
$this->assertSame('', $response->getError());
$this->assertCount(1, $agentOutput['messages']);
$this->assertStringContainsString('Hello from agent client test!', $agentOutput['messages'][0]);
$this->assertStringContainsString('"type":"event"', $agentOutput['messages'][0]);
}

public function testClientReturnsAcceptedWhenLocalAgentIsUnavailable(): void
{
$envelope = $this->createEnvelope('http://public@example.com/1', 'Hello from unavailable agent test!');

$request = new Request();
$request->setStringBody($envelope);

$client = new AgentClient('127.0.0.1', 65001);

set_error_handler(static function (): bool {
return true;
});

try {
$response = $client->sendRequest($request, new Options());
} finally {
restore_error_handler();
}

$this->assertSame(202, $response->getStatusCode());
$this->assertSame('', $response->getError());
}

public function testClientReturnsErrorWhenBodyIsEmpty(): void
{
$client = new AgentClient();
$response = $client->sendRequest(new Request(), new Options());

$this->assertSame(400, $response->getStatusCode());
$this->assertTrue($response->hasError());
$this->assertSame('Request body is empty', $response->getError());
}

private function createEnvelope(string $dsn, string $message): string
{
$options = new Options(['dsn' => $dsn]);

$event = Event::createEvent();
$event->setMessage($message);

$serializer = new PayloadSerializer($options);

return $serializer->serialize($event);
}
}
Loading
Loading