From e9b572b867a3d525ef0d4d6a5047f8804a73b42f Mon Sep 17 00:00:00 2001 From: Albert Chen Date: Fri, 14 Nov 2025 18:59:03 +0800 Subject: [PATCH] feat: implement HttpClientWatcher for better consistency --- src/telescope/config/telescope.php | 26 +-- .../src/Aspects/GuzzleHttpClientAspect.php | 202 +--------------- .../src/Watchers/HttpClientWatcher.php | 219 ++++++++++++++++++ 3 files changed, 231 insertions(+), 216 deletions(-) create mode 100644 src/telescope/src/Watchers/HttpClientWatcher.php diff --git a/src/telescope/config/telescope.php b/src/telescope/config/telescope.php index 384f12091..18902050c 100644 --- a/src/telescope/config/telescope.php +++ b/src/telescope/config/telescope.php @@ -2,7 +2,6 @@ declare(strict_types=1); -use Hypervel\Telescope\Aspects; use Hypervel\Telescope\Http\Middleware\Authorize; use Hypervel\Telescope\Watchers; @@ -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 => [ @@ -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), - ], - ], ]; diff --git a/src/telescope/src/Aspects/GuzzleHttpClientAspect.php b/src/telescope/src/Aspects/GuzzleHttpClientAspect.php index ac773a2d8..44c100a06 100644 --- a/src/telescope/src/Aspects/GuzzleHttpClientAspect.php +++ b/src/telescope/src/Aspects/GuzzleHttpClientAspect.php @@ -5,17 +5,9 @@ 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 { @@ -23,196 +15,14 @@ class GuzzleHttpClientAspect extends AbstractAspect 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); } } diff --git a/src/telescope/src/Watchers/HttpClientWatcher.php b/src/telescope/src/Watchers/HttpClientWatcher.php new file mode 100644 index 000000000..2fefee0da --- /dev/null +++ b/src/telescope/src/Watchers/HttpClientWatcher.php @@ -0,0 +1,219 @@ +options['enabled'] ?? false) + || ! Telescope::$started + || ! 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) { + // 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; + } +}