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
158 changes: 158 additions & 0 deletions src/telescope/src/Aspect/GuzzleHttpClientAspect.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
<?php

declare(strict_types=1);
/**
* This file is part of friendsofhyperf/components.
*
* @link https://github.com/friendsofhyperf/components
* @document https://github.com/friendsofhyperf/components/blob/main/README.md
* @contact huangdijia@gmail.com
*/

namespace FriendsOfHyperf\Telescope\Aspect;

use FriendsOfHyperf\Telescope\IncomingEntry;
use FriendsOfHyperf\Telescope\Telescope;
use FriendsOfHyperf\Telescope\TelescopeConfig;
use FriendsOfHyperf\Telescope\TelescopeContext;
use GuzzleHttp\Client;
use GuzzleHttp\TransferStats;
use Hyperf\Collection\Arr;
use Hyperf\Di\Aop\AbstractAspect;
use Hyperf\Di\Aop\ProceedingJoinPoint;
use Hyperf\Stringable\Str;
use Psr\Http\Message\ResponseInterface;
use Throwable;

/**
* @property array $config
*/
class GuzzleHttpClientAspect extends AbstractAspect
{
public array $classes = [
Client::class . '::transfer',
];

public function __construct(protected TelescopeConfig $telescopeConfig)
{
}

public function process(ProceedingJoinPoint $proceedingJoinPoint)
{
// If the guzzle aspect is disabled or the batch id is not set, we will not record the request.
if (
! $this->telescopeConfig->isEnable('guzzle')
|| ! TelescopeContext::getBatchId()
) {
return $proceedingJoinPoint->process();
}

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

// If the no_telescope_aspect option is set to true, we will not record the request.
if (
($options['no_telescope_aspect'] ?? null) === true
|| ($guzzleConfig['no_telescope_aspect'] ?? null) === true
) {
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 {
$request = $stats->getRequest();
$response = $stats->getResponse();
$data = [
'status' => $response->getStatusCode(),
'reason' => $response->getReasonPhrase(),
'headers' => $response->getHeaders(),
'body' => $this->getResponsePayload($response),
];

Telescope::recordClientRequest(IncomingEntry::make([
'method' => $request->getMethod(),
'uri' => $request->getUri()->__toString(),
'headers' => $request->getHeaders(),
'response_status' => $data['status'],
'response_headers' => $data['headers'],
'response_data' => $data,
'duration' => $stats->getTransferTime() * 1000,
]));
} catch (Throwable $e) {
// We will catch the exception to prevent the request from being interrupted.
}

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

return $proceedingJoinPoint->process();
}

public function getResponsePayload(ResponseInterface $response)
{
$stream = $response->getBody();
try {
if ($stream->isSeekable()) {
$stream->rewind();
}

$content = $stream->getContents();

if (is_string($content)) {
if (! $this->contentWithinLimits($content)) {
return 'Purged By Hyperf Telescope';
}
if (
is_array(json_decode($content, true))
&& json_last_error() === JSON_ERROR_NONE
) {
return $this->contentWithinLimits($content) /* @phpstan-ignore-line */
? $this->hideParameters(json_decode($content, true), Telescope::$hiddenResponseParameters)
: 'Purged By Hyperf Telescope';
}
if (Str::startsWith(strtolower($response->getHeaderLine('content-type') ?: ''), 'text/plain')) {
return $this->contentWithinLimits($content) ? $content : 'Purged By Hyperf Telescope'; /* @phpstan-ignore-line */
}
if (Str::contains($response->getHeaderLine('content-type'), 'application/grpc') !== false) {
return TelescopeContext::getGrpcResponsePayload() ?: 'Purged By Hyperf Telescope';
}
}

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

return 'HTML Response';
}

protected function contentWithinLimits(string $content): bool
{
$limit = 64;
return mb_strlen($content) / 1000 <= $limit;
}

/**
* 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;
}
}
136 changes: 2 additions & 134 deletions src/telescope/src/Aspect/HttpClientAspect.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,141 +11,9 @@

namespace FriendsOfHyperf\Telescope\Aspect;

use FriendsOfHyperf\Telescope\IncomingEntry;
use FriendsOfHyperf\Telescope\Telescope;
use FriendsOfHyperf\Telescope\TelescopeConfig;
use FriendsOfHyperf\Telescope\TelescopeContext;
use GuzzleHttp\Client;
use Hyperf\Collection\Arr;
use Hyperf\Di\Aop\AbstractAspect;
use Hyperf\Di\Aop\ProceedingJoinPoint;
use Hyperf\Stringable\Str;
use Psr\Http\Message\ResponseInterface;
use Throwable;

/**
* @property array $config
* @deprecated since v3.1, use `GuzzleHttpClientAspect` instead, will be removed in v3.2.
*/
class HttpClientAspect extends AbstractAspect
class HttpClientAspect extends GuzzleHttpClientAspect
{
public array $classes = [
Client::class . '::request',
Client::class . '::requestAsync',
];

public function __construct(protected TelescopeConfig $telescopeConfig)
{
}

public function process(ProceedingJoinPoint $proceedingJoinPoint)
{
if (
! $this->telescopeConfig->isEnable('guzzle')
|| ! TelescopeContext::getBatchId()
) {
return $proceedingJoinPoint->process();
}

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

if (($options['no_telescope_aspect'] ?? null) === true || ($guzzleConfig['no_telescope_aspect'] ?? null) === true) {
return $proceedingJoinPoint->process();
}

// Disable the aspect for the requestAsync method.
if ($proceedingJoinPoint->methodName == 'request') {
$proceedingJoinPoint->arguments['keys']['options']['no_telescope_aspect'] = true;
}

$arguments = $proceedingJoinPoint->arguments;
$method = $arguments['keys']['method'] ?? 'GET';
$uri = $arguments['keys']['uri'] ?? '';
$headers = $options['headers'] ?? [];
$result = $proceedingJoinPoint->process();
$response = [];

if ($result instanceof ResponseInterface) {
$response['status'] = $result->getStatusCode();
$response['reason'] = $result->getReasonPhrase();
$response['headers'] = $result->getHeaders();
$response['body'] = $this->getResponsePayload($result);
$result->getBody()->rewind();
}

Telescope::recordClientRequest(IncomingEntry::make([
'method' => $method,
'uri' => $uri,
'headers' => $headers,
'response_status' => $response['status'] ?? 0,
'response_headers' => $response['headers'] ?? '',
'response' => $response,
'duration' => floor((microtime(true) - $startTime) * 1000),
]));

return $result;
}

public function getResponsePayload(ResponseInterface $response)
{
$stream = $response->getBody();
try {
if ($stream->isSeekable()) {
$stream->rewind();
}

$content = $stream->getContents();
} catch (Throwable $e) {
return 'Purged By Hyperf Telescope: ' . $e->getMessage();
}

if (is_string($content)) {
if (! $this->contentWithinLimits($content)) {
return 'Purged By Hyperf Telescope';
}
if (
is_array(json_decode($content, true))
&& json_last_error() === JSON_ERROR_NONE
) {
return $this->contentWithinLimits($content) /* @phpstan-ignore-line */
? $this->hideParameters(json_decode($content, true), Telescope::$hiddenResponseParameters)
: 'Purged By Hyperf Telescope';
}
if (Str::startsWith(strtolower($response->getHeaderLine('content-type') ?: ''), 'text/plain')) {
return $this->contentWithinLimits($content) ? $content : 'Purged By Hyperf Telescope'; /* @phpstan-ignore-line */
}
if (Str::contains($response->getHeaderLine('content-type'), 'application/grpc') !== false) {
return TelescopeContext::getGrpcResponsePayload() ?: 'Purged By Hyperf Telescope';
}
}

if (empty($content)) {
return 'Empty Response';
}

return 'HTML Response';
}

protected function contentWithinLimits(string $content): bool
{
$limit = 64;
return mb_strlen($content) / 1000 <= $limit;
}

/**
* 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;
}
}
8 changes: 4 additions & 4 deletions src/telescope/src/ConfigProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,16 @@ public function __invoke(): array

return [
'aspects' => [
Aspect\CoroutineAspect::class,
Aspect\CacheAspect::class,
Aspect\CoroutineAspect::class,
Aspect\EventAspect::class,
Aspect\GrpcClientAspect::class,
Aspect\HttpClientAspect::class,
Aspect\GrpcCoreMiddlewareAspect::class,
Aspect\GuzzleHttpClientAspect::class,
Aspect\LogAspect::class,
Aspect\RedisAspect::class,
Aspect\RpcAspect::class,
Aspect\RequestDispatcherAspect::class,
Aspect\GrpcCoreMiddlewareAspect::class,
Aspect\RpcAspect::class,
],
'commands' => [
Command\ClearCommand::class,
Expand Down