Skip to content
Merged
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
26 changes: 6 additions & 20 deletions src/telescope/config/telescope.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

declare(strict_types=1);

use Hypervel\Telescope\Aspects;
use Hypervel\Telescope\Http\Middleware\Authorize;
use Hypervel\Telescope\Watchers;

Expand Down Expand Up @@ -179,6 +178,12 @@
'ignore_paths' => [],
],

Watchers\HttpClientWatcher::class => [
'enabled' => env('TELESCOPE_HTTP_CLIENT_WATCHER', true),
'request_size_limit' => env('TELESCOPE_HTTP_CLIENT_REQUEST_SIZE_LIMIT', 64),
'response_size_limit' => env('TELESCOPE_HTTP_CLIENT_RESPONSE_SIZE_LIMIT', 64),
],

Watchers\JobWatcher::class => env('TELESCOPE_JOB_WATCHER', true),

Watchers\LogWatcher::class => [
Expand Down Expand Up @@ -214,23 +219,4 @@
Watchers\ScheduleWatcher::class => env('TELESCOPE_SCHEDULE_WATCHER', true),
Watchers\ViewWatcher::class => env('TELESCOPE_VIEW_WATCHER', true),
],

/*
|--------------------------------------------------------------------------
| Telescope Aspects
|--------------------------------------------------------------------------
|
| The following array lists the "aspects" that will be registered with
| Telescope. Aspects are used to provide additional context to the data
| that Telescope collects. Feel free to customize this list as needed.
|
*/

'aspects' => [
Aspects\GuzzleHttpClientAspect::class => [
'enabled' => env('TELESCOPE_GUZZLE_HTTP_CLIENT_ASPECT', true),
'request_size_limit' => env('TELESCOPE_GUZZLE_HTTP_CLIENT_REQUEST_SIZE_LIMIT', 64),
'response_size_limit' => env('TELESCOPE_GUZZLE_HTTP_CLIENT_RESPONSE_SIZE_LIMIT', 64),
],
],
];
202 changes: 6 additions & 196 deletions src/telescope/src/Aspects/GuzzleHttpClientAspect.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,214 +5,24 @@
namespace Hypervel\Telescope\Aspects;

use GuzzleHttp\Client;
use GuzzleHttp\TransferStats;
use Hyperf\Collection\Arr;
use Hyperf\Contract\ConfigInterface;
use Hyperf\Di\Aop\AbstractAspect;
use Hyperf\Di\Aop\ProceedingJoinPoint;
use Hyperf\Stringable\Str;
use Hypervel\Telescope\IncomingEntry;
use Hypervel\Telescope\Telescope;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Throwable;
use Hypervel\Telescope\Watchers\HttpClientWatcher;

class GuzzleHttpClientAspect extends AbstractAspect
{
public array $classes = [
Client::class . '::transfer',
];

protected array $options = [];

public function __construct(protected ConfigInterface $config)
{
$this->options = $config->get('telescope.aspects.' . static::class, []);
public function __construct(
protected HttpClientWatcher $watcher
) {
}

public function process(ProceedingJoinPoint $proceedingJoinPoint)
{
// If the guzzle aspect is disabled, we will not record the request.
if (! Telescope::$started
|| ! ($this->options['enabled'] ?? false)
|| ! Telescope::isRecording()
) {
return $proceedingJoinPoint->process();
}

$options = $proceedingJoinPoint->arguments['keys']['options'] ?? [];
$guzzleConfig = (fn () => $this->config ?? [])->call($proceedingJoinPoint->getInstance());

// If the telescope_enabled option is set to false, we will not record the request.
if (($options['telescope_enabled'] ?? null) === false
|| ($guzzleConfig['telescope_enabled'] ?? null) === false
) {
return $proceedingJoinPoint->process();
}

// Add or override the on_stats option to record the request duration.
$onStats = $options['on_stats'] ?? null;
$proceedingJoinPoint->arguments['keys']['options']['on_stats'] = function (TransferStats $stats) use ($onStats) {
try {
$content = $this->getRequest(
$request = $stats->getRequest(),
$stats
);

if ($response = $stats->getResponse()) {
$content = array_merge(
$content,
$this->getResponse($response)
);
}

Telescope::recordClientRequest(
IncomingEntry::make($content)
->tags([$request->getUri()->getHost()])
);
} catch (Throwable $e) {
// We will catch the exception to prevent the request from being interrupted.
}

if (is_callable($onStats)) {
$onStats($stats);
}
};

return $proceedingJoinPoint->process();
}

protected function getRequest(RequestInterface $request, TransferStats $stats): array
{
return [
'method' => $request->getMethod(),
'uri' => (string) $request->getUri(),
'headers' => $this->headers($request->getHeaders()),
'payload' => $this->getRequestPayload($request),
'duration' => floor($stats->getTransferTime() * 1000),
];
}

/**
* Extract the payload from the given request.
*/
protected function getRequestPayload(RequestInterface $request): array|string
{
$stream = $request->getBody();
try {
if ($stream->isSeekable()) {
$stream->rewind();
}

$sizeLimit = ($this->options['request_size_limit'] ?? 64) * 1024;
if ($stream->getSize() >= $sizeLimit) {
return $stream->read($sizeLimit) . ' (truncated...)';
}

$content = $stream->getContents();
if (is_array($decoded = json_decode($content, true))
&& json_last_error() === JSON_ERROR_NONE
) {
return $this->hideParameters($decoded, Telescope::$hiddenResponseParameters);
}

return $content;
} catch (Throwable $e) {
return 'Purged By Telescope: ' . $e->getMessage();
} finally {
if ($stream->isSeekable()) {
$stream->rewind();
}
}

return 'Unknown';
}

protected function getResponse(ResponseInterface $response): array
{
return [
'response_status' => $response->getStatusCode(),
'response_headers' => $response->getHeaders(),
'response' => $this->getResponsePayload($response),
];
}

protected function getResponsePayload(ResponseInterface $response): array|string
{
$stream = $response->getBody();
if ($stream->isSeekable()) {
$stream->rewind();
} else {
return 'Streamed Response';
}

try {
$sizeLimit = ($this->options['response_size_limit'] ?? 64) * 1024;
if ($stream->getSize() >= $sizeLimit) {
return $stream->read($sizeLimit) . ' (truncated...)';
}

$content = $stream->getContents();
if (is_array($decoded = json_decode($content, true))
&& json_last_error() === JSON_ERROR_NONE
) {
return $this->hideParameters($decoded, Telescope::$hiddenResponseParameters);
}
if (Str::startsWith(strtolower($response->getHeaderLine('content-type') ?: ''), 'text/plain')) {
return $content;
}

$statusCode = $response->getStatusCode();
if ($statusCode >= 300 && $statusCode < 400) {
return 'Redirected to ' . $response->getHeaderLine('Location');
}

if (empty($content)) {
return 'Empty Response';
}
} catch (Throwable $e) {
return 'Purged By Telescope: ' . $e->getMessage();
} finally {
if ($stream->isSeekable()) {
$stream->rewind();
}
}

return 'HTML Response';
}

/**
* Format the given headers.
*/
protected function headers(array $headers): array
{
$headerNames = array_map(function (string $headerName) {
return strtolower($headerName);
}, array_keys($headers));

$headerValues = array_map(function (array $header) {
return implode(', ', $header);
}, $headers);

$headers = array_combine($headerNames, $headerValues);

return $this->hideParameters(
$headers,
Telescope::$hiddenRequestHeaders
);
}

/**
* Hide the given parameters.
*/
protected function hideParameters(array $data, array $hidden): array
{
foreach ($hidden as $parameter) {
if (Arr::get($data, $parameter)) {
Arr::set($data, $parameter, '********');
}
}

return $data;
return $this->watcher
->recordRequest($proceedingJoinPoint);
}
}
Loading