Skip to content

Commit 4d9beba

Browse files
authored
feat(agent): add AgentClient (#2062)
1 parent 010f212 commit 4d9beba

5 files changed

Lines changed: 510 additions & 3 deletions

File tree

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sentry\Agent\Transport;
6+
7+
use Sentry\HttpClient\HttpClientInterface;
8+
use Sentry\HttpClient\Request;
9+
use Sentry\HttpClient\Response;
10+
use Sentry\Options;
11+
12+
class AgentClient implements HttpClientInterface
13+
{
14+
/**
15+
* @var string
16+
*/
17+
private $host;
18+
19+
/**
20+
* @var int
21+
*/
22+
private $port;
23+
24+
/**
25+
* @var resource|null
26+
*/
27+
private $socket;
28+
29+
public function __construct(string $host = '127.0.0.1', int $port = 5148)
30+
{
31+
$this->host = $host;
32+
$this->port = $port;
33+
}
34+
35+
public function __destruct()
36+
{
37+
$this->disconnect();
38+
}
39+
40+
/**
41+
* @phpstan-assert-if-true resource $this->socket
42+
*/
43+
private function connect(): bool
44+
{
45+
if ($this->socket !== null) {
46+
return true;
47+
}
48+
49+
// We set the timeout to 10ms to avoid blocking the request for too long if the agent is not running
50+
// @TODO: 10ms should be low enough? Do we want to go lower and/or make this configurable? Only applies to initial connection.
51+
$socket = fsockopen($this->host, $this->port, $errorNo, $errorMsg, 0.01);
52+
53+
// @TODO: Error handling? See $errorNo and $errorMsg
54+
if ($socket === false) {
55+
return false;
56+
}
57+
58+
// @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
59+
$this->socket = $socket;
60+
61+
return true;
62+
}
63+
64+
private function disconnect(): void
65+
{
66+
if ($this->socket === null) {
67+
return;
68+
}
69+
70+
fclose($this->socket);
71+
72+
$this->socket = null;
73+
}
74+
75+
private function send(string $message): void
76+
{
77+
if (!$this->connect()) {
78+
return;
79+
}
80+
81+
// @TODO: Make sure we don't send more than 2^32 - 1 bytes
82+
$contentLength = pack('N', \strlen($message) + 4);
83+
84+
// @TODO: Error handling?
85+
fwrite($this->socket, $contentLength . $message);
86+
}
87+
88+
public function sendRequest(Request $request, Options $options): Response
89+
{
90+
$body = $request->getStringBody();
91+
92+
if (empty($body)) {
93+
return new Response(400, [], 'Request body is empty');
94+
}
95+
96+
$this->send($body);
97+
98+
// Since we are sending async there is no feedback so we always return an empty response
99+
return new Response(202, [], '');
100+
}
101+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sentry\Tests\HttpClient;
6+
7+
use PHPUnit\Framework\TestCase;
8+
use Sentry\Agent\Transport\AgentClient;
9+
use Sentry\Event;
10+
use Sentry\HttpClient\Request;
11+
use Sentry\Options;
12+
use Sentry\Serializer\PayloadSerializer;
13+
14+
final class AgentClientTest extends TestCase
15+
{
16+
use TestAgent;
17+
18+
protected function tearDown(): void
19+
{
20+
if ($this->agentProcess !== null) {
21+
$this->stopTestAgent();
22+
}
23+
}
24+
25+
public function testClientHandsOffEnvelopeToLocalAgent(): void
26+
{
27+
$this->startTestAgent();
28+
29+
$envelope = $this->createEnvelope('http://public@example.com/1', 'Hello from agent client test!');
30+
31+
$request = new Request();
32+
$request->setStringBody($envelope);
33+
34+
$client = new AgentClient('127.0.0.1', $this->agentPort);
35+
$response = $client->sendRequest($request, new Options());
36+
37+
$this->waitForEnvelopeCount(1);
38+
$agentOutput = $this->stopTestAgent();
39+
40+
$this->assertSame(202, $response->getStatusCode());
41+
$this->assertSame('', $response->getError());
42+
$this->assertCount(1, $agentOutput['messages']);
43+
$this->assertStringContainsString('Hello from agent client test!', $agentOutput['messages'][0]);
44+
$this->assertStringContainsString('"type":"event"', $agentOutput['messages'][0]);
45+
}
46+
47+
public function testClientReturnsAcceptedWhenLocalAgentIsUnavailable(): void
48+
{
49+
$envelope = $this->createEnvelope('http://public@example.com/1', 'Hello from unavailable agent test!');
50+
51+
$request = new Request();
52+
$request->setStringBody($envelope);
53+
54+
$client = new AgentClient('127.0.0.1', 65001);
55+
56+
set_error_handler(static function (): bool {
57+
return true;
58+
});
59+
60+
try {
61+
$response = $client->sendRequest($request, new Options());
62+
} finally {
63+
restore_error_handler();
64+
}
65+
66+
$this->assertSame(202, $response->getStatusCode());
67+
$this->assertSame('', $response->getError());
68+
}
69+
70+
public function testClientReturnsErrorWhenBodyIsEmpty(): void
71+
{
72+
$client = new AgentClient();
73+
$response = $client->sendRequest(new Request(), new Options());
74+
75+
$this->assertSame(400, $response->getStatusCode());
76+
$this->assertTrue($response->hasError());
77+
$this->assertSame('Request body is empty', $response->getError());
78+
}
79+
80+
private function createEnvelope(string $dsn, string $message): string
81+
{
82+
$options = new Options(['dsn' => $dsn]);
83+
84+
$event = Event::createEvent();
85+
$event->setMessage($message);
86+
87+
$serializer = new PayloadSerializer($options);
88+
89+
return $serializer->serialize($event);
90+
}
91+
}

0 commit comments

Comments
 (0)