From 729e81edaa98156be0aa5543966346a412a0aa56 Mon Sep 17 00:00:00 2001 From: Yenfry Herrera Feliz Date: Fri, 20 Sep 2024 08:59:24 -0700 Subject: [PATCH] enhancement: user agent 2.1 This change provides: - A builder class for appending metrics - Default initialization of a metrics builder within command instantiation - Wraps user agent logic into a single middleware class - Adds middlewares into the different features from where metrics can be gather, to acomplish this purpose. --- src/AwsClient.php | 18 + src/ClientResolver.php | 84 +---- src/Command.php | 3 + src/EndpointV2/EndpointV2Middleware.php | 29 +- src/MetricsBuilder.php | 166 ++++++++++ src/ResultPaginator.php | 4 + src/S3/ApplyChecksumMiddleware.php | 35 ++ src/S3/Crypto/S3EncryptionClient.php | 6 +- src/S3/Crypto/S3EncryptionClientV2.php | 6 +- src/S3/Transfer.php | 5 + src/UserAgentMiddleware.php | 326 +++++++++++++++++++ src/Waiter.php | 4 + tests/AwsClientTest.php | 38 +++ tests/ClientResolverTest.php | 260 --------------- tests/CommandTest.php | 47 ++- tests/MetricsBuilderTest.php | 122 +++++++ tests/ResultPaginatorTest.php | 24 ++ tests/S3/Crypto/S3EncryptionClientTest.php | 16 +- tests/S3/Crypto/S3EncryptionClientV2Test.php | 15 +- tests/UserAgentMiddlewareTest.php | 277 ++++++++++++++++ tests/WaiterTest.php | 114 +++++-- 21 files changed, 1206 insertions(+), 393 deletions(-) create mode 100644 src/MetricsBuilder.php create mode 100644 src/UserAgentMiddleware.php create mode 100644 tests/MetricsBuilderTest.php create mode 100644 tests/UserAgentMiddlewareTest.php diff --git a/src/AwsClient.php b/src/AwsClient.php index 67ea82f4b8..534ffb1949 100644 --- a/src/AwsClient.php +++ b/src/AwsClient.php @@ -280,6 +280,11 @@ public function __construct(array $args) if (isset($args['with_resolved'])) { $args['with_resolved']($config); } + MetricsBuilder::appendMetricsCaptureMiddleware( + $this->getHandlerList(), + MetricsBuilder::RESOURCE_MODEL + ); + $this->addUserAgentMiddleware($config); } public function getHandlerList() @@ -490,6 +495,11 @@ private function addSignatureMiddleware(array $args) $region = $signingRegionSet ?? $commandSigningRegionSet ?? $region; + + MetricsBuilder::appendMetricsCaptureMiddleware( + $this->getHandlerList(), + MetricsBuilder::SIGV4A_SIGNING + ); } return SignatureProvider::resolve($provider, $signatureVersion, $name, $region); @@ -595,6 +605,14 @@ private function addEndpointV2Middleware() ); } + private function addUserAgentMiddleware($args) + { + $this->getHandlerList()->prependSign( + UserAgentMiddleware::wrap($args), + 'user-agent' + ); + } + /** * Retrieves client context param definition from service model, * creates mapping of client context param names with client-provided diff --git a/src/ClientResolver.php b/src/ClientResolver.php index 02f5270f14..ee5a1113cc 100644 --- a/src/ClientResolver.php +++ b/src/ClientResolver.php @@ -980,66 +980,8 @@ public static function _default_app_id(array $args) public static function _apply_user_agent($inputUserAgent, array &$args, HandlerList $list) { - // Add SDK version - $userAgent = ['aws-sdk-php/' . Sdk::VERSION]; - - // User Agent Metadata - $userAgent[] = 'ua/2.0'; - - // If on HHVM add the HHVM version - if (defined('HHVM_VERSION')) { - $userAgent []= 'HHVM/' . HHVM_VERSION; - } - - // Add OS version - $disabledFunctions = explode(',', ini_get('disable_functions')); - if (function_exists('php_uname') - && !in_array('php_uname', $disabledFunctions, true) - ) { - $osName = "OS/" . php_uname('s') . '#' . php_uname('r'); - if (!empty($osName)) { - $userAgent []= $osName; - } - } - - // Add the language version - $userAgent []= 'lang/php#' . phpversion(); - - // Add exec environment if present - if ($executionEnvironment = getenv('AWS_EXECUTION_ENV')) { - $userAgent []= $executionEnvironment; - } - // Add endpoint discovery if set - if (isset($args['endpoint_discovery'])) { - if (($args['endpoint_discovery'] instanceof \Aws\EndpointDiscovery\Configuration - && $args['endpoint_discovery']->isEnabled()) - ) { - $userAgent []= 'cfg/endpoint-discovery'; - } elseif (is_array($args['endpoint_discovery']) - && isset($args['endpoint_discovery']['enabled']) - && $args['endpoint_discovery']['enabled'] - ) { - $userAgent []= 'cfg/endpoint-discovery'; - } - } - - // Add retry mode if set - if (isset($args['retries'])) { - if ($args['retries'] instanceof \Aws\Retry\Configuration) { - $userAgent []= 'cfg/retry-mode#' . $args["retries"]->getMode(); - } elseif (is_array($args['retries']) - && isset($args["retries"]["mode"]) - ) { - $userAgent []= 'cfg/retry-mode#' . $args["retries"]["mode"]; - } - } - - // AppID Metadata - if (!empty($args['app_id'])) { - $userAgent[] = 'app/' . $args['app_id']; - } - + $userAgent = []; // Add the input to the end if ($inputUserAgent){ if (!is_array($inputUserAgent)) { @@ -1050,30 +992,6 @@ public static function _apply_user_agent($inputUserAgent, array &$args, HandlerL } $args['ua_append'] = $userAgent; - - $list->appendBuild(static function (callable $handler) use ($userAgent) { - return function ( - CommandInterface $command, - RequestInterface $request - ) use ($handler, $userAgent) { - return $handler( - $command, - $request->withHeader( - 'X-Amz-User-Agent', - implode(' ', array_merge( - $userAgent, - $request->getHeader('X-Amz-User-Agent') - )) - )->withHeader( - 'User-Agent', - implode(' ', array_merge( - $userAgent, - $request->getHeader('User-Agent') - )) - ) - ); - }; - }); } public static function _apply_endpoint($value, array &$args, HandlerList $list) diff --git a/src/Command.php b/src/Command.php index b15af4df4e..c06735d90a 100644 --- a/src/Command.php +++ b/src/Command.php @@ -38,6 +38,9 @@ public function __construct($name, array $args = [], HandlerList $list = null) if (!isset($this->data['@context'])) { $this->data['@context'] = []; } + $this->data['@context'][ + MetricsBuilder::COMMAND_METRICS_BUILDER + ] = new MetricsBuilder(); } public function __clone() diff --git a/src/EndpointV2/EndpointV2Middleware.php b/src/EndpointV2/EndpointV2Middleware.php index b1628bcb47..e6349e01a8 100644 --- a/src/EndpointV2/EndpointV2Middleware.php +++ b/src/EndpointV2/EndpointV2Middleware.php @@ -5,6 +5,7 @@ use Aws\Api\Service; use Aws\Auth\Exception\UnresolvedAuthSchemeException; use Aws\CommandInterface; +use Aws\MetricsBuilder; use Closure; use GuzzleHttp\Promise\Promise; use function JmesPath\search; @@ -98,8 +99,15 @@ public function __invoke(CommandInterface $command) $operation = $this->api->getOperation($command->getName()); $commandArgs = $command->toArray(); $providerArgs = $this->resolveArgs($commandArgs, $operation); + $this->hookAccountIdMetric( + $providerArgs[self::ACCOUNT_ID_PARAM] ?? null, + $command + ); $endpoint = $this->endpointProvider->resolveEndpoint($providerArgs); - + $this->hookAccountIdEndpointMetric( + $endpoint, + $command + ); if (!empty($authSchemes = $endpoint->getProperty('authSchemes'))) { $this->applyAuthScheme( $authSchemes, @@ -394,4 +402,23 @@ private function resolveAccountId(): ?string return $identity->getAccountId(); } + + private function hookAccountIdMetric($accountId, &$command) + { + if (!empty($accountId)) { + MetricsBuilder::fromCommand($command)->append( + MetricsBuilder::RESOLVED_ACCOUNT_ID + ); + } + } + + private function hookAccountIdEndpointMetric($endpoint, $command) + { + $regex = "/^(https?:\/\/\d{12}\.[^\s\/$.?#].\S*)$/"; + if (preg_match($regex, $endpoint->getUrl())) { + MetricsBuilder::fromCommand($command)->append( + MetricsBuilder::ACCOUNT_ID_ENDPOINT + ); + } + } } diff --git a/src/MetricsBuilder.php b/src/MetricsBuilder.php new file mode 100644 index 0000000000..013e1084cb --- /dev/null +++ b/src/MetricsBuilder.php @@ -0,0 +1,166 @@ +metrics = []; + // The first metrics does not include the separator + // therefore it is reduced by default. + $this->metricsSize = -(strlen(self::$METRIC_SEPARATOR)); + } + + /** + * Build the metrics string value. + * + * @return string + */ + public function build(): string + { + if (empty($this->metrics)) { + return ""; + } + + return $this->encode(); + } + + /** + * Encodes the metrics by separating each metric + * with a comma. Example: for the metrics[A,B,C] then + * the output would be "A,B,C". + * + * @return string + */ + private function encode(): string + { + return implode(self::$METRIC_SEPARATOR, array_keys($this->metrics)); + } + + /** + * Appends a metric into the internal metrics holder. + * It checks if the metric can be appended before doing so. + * If the metric can be appended then, it is added into the + * metrics holder and the current metrics size is increased + * by summing the length of the metric being appended plus the length + * of the separator used for encoding. + * Example: $currentSize = $currentSize + len($newMetric) + len($separator) + * + * @param string $metric + * + * @return void + */ + public function append(string $metric): void + { + if (!$this->canMetricBeAppended($metric)) { + return; + } + + $this->metrics[$metric] = true; + $this->metricsSize += strlen($metric) + strlen(self::$METRIC_SEPARATOR); + } + + /** + * Validates if a metric can be appended by verifying if the current + * metrics size plus the new metric plus the length of the separator + * exceeds the metrics size limit. It also checks if the metric already + * exists, if so then it returns false. + * Example: metric can be appended just if: + * $currentSize + len($newMetric) + len($metricSeparator) <= MAX_SIZE + * and: + * $newMetric not in $existentMetrics + * + * @param string $newMetric + * + * @return bool + */ + private function canMetricBeAppended(string $newMetric): bool + { + if ($this->metricsSize + + (strlen($newMetric) + strlen(self::$METRIC_SEPARATOR)) + > self::$MAX_METRICS_SIZE) { + @trigger_error( + "The metric `{$newMetric}` " + . "can not be added due to size constraints", + E_USER_WARNING + ); + + return false; + } + + if (isset($this->metrics[$newMetric])) { + @trigger_error( + 'The metric ' . $newMetric. ' is already appended!', + E_USER_WARNING + ); + + return false; + } + + return true; + } + + /** + * Returns the metrics builder from the property @context of a command. + * + * @param CommandInterface $command + * + * @return MetricsBuilder + */ + public static function fromCommand(CommandInterface $command): MetricsBuilder + { + return $command['@context'][MetricsBuilder::COMMAND_METRICS_BUILDER]; + } + + public static function appendMetricsCaptureMiddleware( + HandlerList $handlerList, + $metric + ): void + { + $handlerList->appendBuild( + Middleware::tap( + function (CommandInterface $command) use ($metric) { + self::fromCommand($command)->append( + $metric + ); + } + ), + 'metrics-capture-'.$metric + ); + } +} diff --git a/src/ResultPaginator.php b/src/ResultPaginator.php index 2b0c7c9af0..4bf624ddf2 100644 --- a/src/ResultPaginator.php +++ b/src/ResultPaginator.php @@ -45,6 +45,10 @@ public function __construct( $this->operation = $operation; $this->args = $args; $this->config = $config; + MetricsBuilder::appendMetricsCaptureMiddleware( + $this->client->getHandlerList(), + MetricsBuilder::PAGINATOR + ); } /** diff --git a/src/S3/ApplyChecksumMiddleware.php b/src/S3/ApplyChecksumMiddleware.php index a0ff65d6dc..e3294daff5 100644 --- a/src/S3/ApplyChecksumMiddleware.php +++ b/src/S3/ApplyChecksumMiddleware.php @@ -3,6 +3,7 @@ use Aws\Api\Service; use Aws\CommandInterface; +use Aws\MetricsBuilder; use GuzzleHttp\Psr7; use InvalidArgumentException; use Psr\Http\Message\RequestInterface; @@ -82,6 +83,9 @@ public function __invoke( . implode(", ", $supportedAlgorithms) . "." ); } + + $this->hookChecksumAlgorithmMetric($requestedAlgorithm, $command); + return $next($command, $request); } @@ -94,6 +98,7 @@ public function __invoke( //S3Express doesn't support MD5; default to crc32 instead if ($this->isS3Express($command)) { $request = $this->addAlgorithmHeader('crc32', $request, $body); + $this->hookChecksumAlgorithmMetric('crc32', $command); } elseif (!$request->hasHeader('Content-MD5')) { // Set the content MD5 header for operations that require it. $request = $request->withHeader( @@ -144,4 +149,34 @@ private function isS3Express(CommandInterface $command): bool return isset($command['@context']['signing_service']) && $command['@context']['signing_service'] === 's3express'; } + + private function hookChecksumAlgorithmMetric($algorithm, &$command) + { + if (empty($algorithm)) { + return; + } + + if ($algorithm === 'crc32') { + MetricsBuilder::fromCommand($command)->append( + MetricsBuilder::FLEXIBLE_CHECKSUMS_REQ_CRC32 + ); + } elseif ($algorithm === 'crc32c') { + MetricsBuilder::fromCommand($command)->append( + MetricsBuilder::FLEXIBLE_CHECKSUMS_REQ_CRC32C + ); + } elseif ($algorithm === 'crc64') { + MetricsBuilder::fromCommand($command)->append( + MetricsBuilder::FLEXIBLE_CHECKSUMS_REQ_CRC64 + ); + } elseif ($algorithm === 'sha1') { + MetricsBuilder::fromCommand($command)->append( + MetricsBuilder::FLEXIBLE_CHECKSUMS_REQ_SHA1 + ); + } elseif ($algorithm === 'sha256') { + MetricsBuilder::fromCommand($command)->append( + MetricsBuilder::FLEXIBLE_CHECKSUMS_REQ_SHA256 + ); + } + + } } diff --git a/src/S3/Crypto/S3EncryptionClient.php b/src/S3/Crypto/S3EncryptionClient.php index 30b51007bc..96cb152a34 100644 --- a/src/S3/Crypto/S3EncryptionClient.php +++ b/src/S3/Crypto/S3EncryptionClient.php @@ -3,6 +3,7 @@ use Aws\Crypto\DecryptionTrait; use Aws\HashingStream; +use Aws\MetricsBuilder; use Aws\PhpHash; use Aws\Crypto\AbstractCryptoClient; use Aws\Crypto\EncryptionTrait; @@ -53,9 +54,12 @@ public function __construct( S3Client $client, $instructionFileSuffix = null ) { - $this->appendUserAgent($client, 'feat/s3-encrypt/' . self::CRYPTO_VERSION); $this->client = $client; $this->instructionFileSuffix = $instructionFileSuffix; + MetricsBuilder::appendMetricsCaptureMiddleware( + $this->client->getHandlerList(), + MetricsBuilder::S3_CRYPTO_V1N + ); } private static function getDefaultStrategy() diff --git a/src/S3/Crypto/S3EncryptionClientV2.php b/src/S3/Crypto/S3EncryptionClientV2.php index 5690c76dd9..fe917800f7 100644 --- a/src/S3/Crypto/S3EncryptionClientV2.php +++ b/src/S3/Crypto/S3EncryptionClientV2.php @@ -4,6 +4,7 @@ use Aws\Crypto\DecryptionTraitV2; use Aws\Exception\CryptoException; use Aws\HashingStream; +use Aws\MetricsBuilder; use Aws\PhpHash; use Aws\Crypto\AbstractCryptoClientV2; use Aws\Crypto\EncryptionTraitV2; @@ -105,10 +106,13 @@ public function __construct( S3Client $client, $instructionFileSuffix = null ) { - $this->appendUserAgent($client, 'feat/s3-encrypt/' . self::CRYPTO_VERSION); $this->client = $client; $this->instructionFileSuffix = $instructionFileSuffix; $this->legacyWarningCount = 0; + MetricsBuilder::appendMetricsCaptureMiddleware( + $this->client->getHandlerList(), + MetricsBuilder::S3_CRYPTO_V2 + ); } private static function getDefaultStrategy() diff --git a/src/S3/Transfer.php b/src/S3/Transfer.php index 600f441008..1beb7473e9 100644 --- a/src/S3/Transfer.php +++ b/src/S3/Transfer.php @@ -4,6 +4,7 @@ use Aws; use Aws\CommandInterface; use Aws\Exception\AwsException; +use Aws\MetricsBuilder; use GuzzleHttp\Promise; use GuzzleHttp\Promise\PromiseInterface; use GuzzleHttp\Promise\PromisorInterface; @@ -139,6 +140,10 @@ public function __construct( // Handle "add_content_md5" option. $this->addContentMD5 = isset($options['add_content_md5']) && $options['add_content_md5'] === true; + MetricsBuilder::appendMetricsCaptureMiddleware( + $this->client->getHandlerList(), + MetricsBuilder::S3_TRANSFER + ); } /** diff --git a/src/UserAgentMiddleware.php b/src/UserAgentMiddleware.php new file mode 100644 index 0000000000..6c7b2cc464 --- /dev/null +++ b/src/UserAgentMiddleware.php @@ -0,0 +1,326 @@ +nextHandler = $nextHandler; + $this->args = $args; + } + + /** + * When invoked, its injects the user agent header into the + * request headers. + * @param CommandInterface $command + * @param RequestInterface $request + * + * @return mixed + */ + public function __invoke(CommandInterface $command, RequestInterface $request) + { + $handler = $this->nextHandler; + $this->metricsBuilder = MetricsBuilder::fromCommand($command); + $request = $this->requestWithUserAgentHeader($request); + + return $handler($command, $request); + } + + /** + * Builds the user agent header value, and injects it into the request + * headers. Then, it returns the mutated request. + * + * @param RequestInterface $request + * + * @return RequestInterface + */ + private function requestWithUserAgentHeader(RequestInterface $request): RequestInterface + { + $uaAppend = $this->args['ua_append'] ?? []; + $userAgentValue = array_merge( + $this->buildUserAgentValue(), + $uaAppend + ); + // It includes the user agent values just for the User-Agent header. + // The reason is that the SEP does not mention appending the + // metrics into the X-Amz-User-Agent header. + return $request->withHeader( + 'X-Amz-User-Agent', + implode(' ', array_merge( + $uaAppend, + $request->getHeader('X-Amz-User-Agent') + )) + )->withHeader( + 'User-Agent', + implode(' ', array_merge( + $userAgentValue, + $request->getHeader('User-Agent') + )) + ); + } + + /** + * Builds the different user agent values. + * + * @return array + */ + private function buildUserAgentValue(): array + { + static $fnList = [ + 'sdkVersion', + 'userAgentVersion', + 'hhvmVersion', + 'osName', + 'langVersion', + 'execEnv', + 'endpointDiscovery', + 'appId', + 'metrics' + ]; + $userAgentValue = []; + foreach ($fnList as $fn) { + $val = $this->{$fn}(); + if (!empty($val)) { + $userAgentValue[] = $val; + } + } + + return $userAgentValue; + } + + /** + * Returns the user agent value for SDK version. + * + * @return string + */ + private function sdkVersion(): string + { + return 'aws-sdk-php/' . Sdk::VERSION; + } + + /** + * Returns the user agent value for the agent version. + * + * @return string + */ + private function userAgentVersion(): string + { + return 'ua/' . self::AGENT_VERSION; + } + + /** + * Returns the user agent value for the hhvm version, but just + * when it is defined. + * + * @return string + */ + private function hhvmVersion(): string + { + if (defined('HHVM_VERSION')) { + return 'HHVM/' . HHVM_VERSION; + } + + return ""; + } + + /** + * Returns the user agent value for the os version. + * + * @return string + */ + private function osName(): string + { + $disabledFunctions = explode(',', ini_get('disable_functions')); + if (function_exists('php_uname') + && !in_array('php_uname', $disabledFunctions, true) + ) { + $osName = "OS/" . php_uname('s') . '#' . php_uname('r'); + if (!empty($osName)) { + return $osName; + } + } + + return ""; + } + + /** + * Returns the user agent value for the php language used. + * + * @return string + */ + private function langVersion(): string + { + return 'lang/php#' . phpversion(); + } + + /** + * Returns the user agent value for the execution env. + * + * @return string + */ + private function execEnv(): string + { + if ($executionEnvironment = getenv('AWS_EXECUTION_ENV')) { + return $executionEnvironment; + } + + return ""; + } + + private function endpointDiscovery(): string + { + $args = $this->args; + if (isset($args['endpoint_discovery'])) { + if (($args['endpoint_discovery'] instanceof Configuration + && $args['endpoint_discovery']->isEnabled()) + ) { + return 'cfg/endpoint-discovery'; + } elseif (is_array($args['endpoint_discovery']) + && isset($args['endpoint_discovery']['enabled']) + && $args['endpoint_discovery']['enabled'] + ) { + return 'cfg/endpoint-discovery'; + } + } + + return ""; + } + + /** + * Returns the user agent value for app id, but just when an + * app id was provided as a client argument. + * + * @return string + */ + private function appId(): string + { + if (empty($this->args['app_id'])) { + return ""; + } + + return 'app/' . $this->args['app_id']; + } + + /** + * Returns the user agent value for metrics. + * + * @return string + */ + private function metrics(): string + { + static $metricsFn = [ + 'endpointMetric', + 'accountIdModeMetric', + 'retryConfigMetric' + ]; + foreach ($metricsFn as $fn) { + $this->{$fn}(); + } + + $metricsEncoded = $this->metricsBuilder->build(); + if (empty($metricsEncoded)) { + return ""; + } + + return "m/" . $metricsEncoded; + } + + /** + * Appends the endpoint metric into the metrics builder, + * just if a custom endpoint was provided at client construction. + */ + private function endpointMetric(): void + { + if (!empty($this->args['endpoint'])) { + $this->metricsBuilder->append(MetricsBuilder::ENDPOINT_OVERRIDE); + } + } + + /** + * Appends the account id endpoint mode metric into the metrics builder, + * based on the account id endpoint mode provide as client argument. + */ + private function accountIdModeMetric(): void + { + $accountIdMode = $this->args['account_id_endpoint_mode'] ?? null; + if ($accountIdMode === null) { + return; + } + + if ($accountIdMode === 'preferred') { + $this->metricsBuilder->append(MetricsBuilder::ACCOUNT_ID_MODE_PREFERRED); + } elseif ($accountIdMode === 'disabled') { + $this->metricsBuilder->append(MetricsBuilder::ACCOUNT_ID_MODE_DISABLED); + } elseif ($accountIdMode === 'required') { + $this->metricsBuilder->append(MetricsBuilder::ACCOUNT_ID_MODE_REQUIRED); + } + } + + /** + * Appends the retry mode metric into the metrics builder, + * based on the resolved retry config mode. + */ + private function retryConfigMetric(): void + { + $retries = $this->args['retries'] ?? null; + if ($retries === null) { + return; + } + + $retryMode = ''; + if ($retries instanceof \Aws\Retry\Configuration) { + $retryMode = $retries->getMode(); + } elseif (is_array($retries) + && isset($retries["mode"]) + ) { + $retryMode = $retries["mode"]; + } + + if ($retryMode === 'legacy') { + $this->metricsBuilder->append( + MetricsBuilder::RETRY_MODE_LEGACY + ); + } elseif ($retryMode === 'standard') { + $this->metricsBuilder->append( + MetricsBuilder::RETRY_MODE_STANDARD + ); + } elseif ($retryMode === 'adaptive') { + $this->metricsBuilder->append( + MetricsBuilder::RETRY_MODE_ADAPTIVE + ); + } + } +} diff --git a/src/Waiter.php b/src/Waiter.php index 16b86fb2fb..3310e8e3b7 100644 --- a/src/Waiter.php +++ b/src/Waiter.php @@ -85,6 +85,10 @@ public function __construct( 'The provided "before" callback is not callable.' ); } + MetricsBuilder::appendMetricsCaptureMiddleware( + $this->client->getHandlerList(), + MetricsBuilder::WAITER + ); } /** diff --git a/tests/AwsClientTest.php b/tests/AwsClientTest.php index 702843d3b1..28f50bc1d3 100644 --- a/tests/AwsClientTest.php +++ b/tests/AwsClientTest.php @@ -10,6 +10,7 @@ use Aws\Endpoint\UseFipsEndpoint\Configuration as FipsConfiguration; use Aws\Endpoint\UseDualStackEndpoint\Configuration as DualStackConfiguration; use Aws\EndpointV2\EndpointProviderV2; +use Aws\MetricsBuilder; use Aws\Middleware; use Aws\ResultPaginator; use Aws\S3\Exception\S3Exception; @@ -24,6 +25,7 @@ use Aws\WrappedHttpHandler; use Exception; use GuzzleHttp\Promise\RejectedPromise; +use GuzzleHttp\Psr7\Response; use Psr\Http\Message\RequestInterface; use Yoast\PHPUnitPolyfills\TestCases\TestCase; @@ -982,4 +984,40 @@ public function testClientParameterOverridesDefaultAccountIdEndpointModeBuiltIns self::assertEquals($expectedAccountIdEndpointMode, $builtIns['AWS::Auth::AccountIdEndpointMode']); } + + public function testAppendsC2jMetricsCaptureMiddleware() + { + $client = new S3Client([ + 'region' => 'us-east-2', + 'http_handler' => function (RequestInterface $request) { + + return new Response(); + } + ]); + $client->getHandlerList()->appendSign( + Middleware::tap(function (CommandInterface $command) { + $metricsBuilder = MetricsBuilder::fromCommand($command); + $metricsEncoded = $metricsBuilder->build(); + $expectedEncodedMetrics = MetricsBuilder::RESOURCE_MODEL; + + $this->assertEquals($expectedEncodedMetrics, $metricsEncoded); + }) + ); + $client->listBuckets(); + } + + public function testAppendsUserAgentMiddleware() + { + $client = new S3Client([ + 'region' => 'us-east-2', + 'http_handler' => function (RequestInterface $request) { + $userAgentValue = $request->getHeaderLine('User-Agent'); + + $this->assertNotEmpty($userAgentValue); + + return new Response(); + } + ]); + $client->listBuckets(); + } } diff --git a/tests/ClientResolverTest.php b/tests/ClientResolverTest.php index 638fabfc4f..422b4655c2 100644 --- a/tests/ClientResolverTest.php +++ b/tests/ClientResolverTest.php @@ -830,268 +830,8 @@ public function testAppliesUserAgent() $this->assertArrayHasKey('ua_append', $conf); $this->assertIsArray($conf['ua_append']); $this->assertContains('PHPUnit/Unit', $conf['ua_append']); - $this->assertContains('aws-sdk-php/' . Sdk::VERSION, $conf['ua_append']); - } - - public function testUserAgentAlwaysStartsWithSdkAgentString() - { - $command = $this->getMockBuilder(CommandInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $request = $this->getMockBuilder(RequestInterface::class) - ->disableOriginalConstructor() - ->getMock(); - - $request->expects($this->exactly(2)) - ->method('getHeader') - ->withConsecutive( - ['X-Amz-User-Agent'], - ['User-Agent'] - ) - ->willReturnOnConsecutiveCalls( - ["MockBuilder"], - ['MockBuilder'] - ); - - $request->expects($this->exactly(2)) - ->method('withHeader') - ->withConsecutive( - [ - 'X-Amz-User-Agent', - new \PHPUnit\Framework\Constraint\RegularExpression( - '/aws-sdk-php\/' . Sdk::VERSION . '.* MockBuilder/' - ) - ], - [ - 'User-Agent', - new \PHPUnit\Framework\Constraint\RegularExpression( - '/aws-sdk-php\/' . Sdk::VERSION . '.* MockBuilder/' - ) - ] - ) - ->willReturnOnConsecutiveCalls( - $request, - $request - ); - - $args = []; - $list = new HandlerList(function () { - }); - ClientResolver::_apply_user_agent([], $args, $list); - call_user_func($list->resolve(), $command, $request); - } - - public function testUserAgentAddsEndpointDiscoveryConfiguration() - { - $command = $this->getMockBuilder(CommandInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $request = $this->getMockBuilder(RequestInterface::class) - ->disableOriginalConstructor() - ->getMock(); - - $request->expects($this->exactly(2)) - ->method('getHeader') - ->withConsecutive( - ['X-Amz-User-Agent'], - ['User-Agent'] - ) - ->willReturnOnConsecutiveCalls( - ["MockBuilder"], - ['MockBuilder'] - ); - - $request->expects($this->exactly(2)) - ->method('withHeader') - ->withConsecutive( - [ - 'X-Amz-User-Agent', - new \PHPUnit\Framework\Constraint\RegularExpression( - '/aws-sdk-php\/' . Sdk::VERSION . '.* cfg\/endpoint-discovery/' - ) - ], - [ - 'User-Agent', - new \PHPUnit\Framework\Constraint\RegularExpression( - '/aws-sdk-php\/' . Sdk::VERSION . '.* cfg\/endpoint-discovery/' - ) - ] - ) - ->willReturnOnConsecutiveCalls( - $request, - $request - ); - - $args = [ - 'endpoint_discovery' => new \Aws\EndpointDiscovery\Configuration ( - true, - 1000 - ), - ]; - $list = new HandlerList(function () { - }); - ClientResolver::_apply_user_agent([], $args, $list); - call_user_func($list->resolve(), $command, $request); - } - - - public function testUserAgentAddsEndpointDiscoveryArray() - { - $command = $this->getMockBuilder(CommandInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $request = $this->getMockBuilder(RequestInterface::class) - ->disableOriginalConstructor() - ->getMock(); - - $request->expects($this->exactly(2)) - ->method('getHeader') - ->withConsecutive( - ['X-Amz-User-Agent'], - ['User-Agent'] - ) - ->willReturnOnConsecutiveCalls( - ["MockBuilder"], - ['MockBuilder'] - ); - - $request->expects($this->exactly(2)) - ->method('withHeader') - ->withConsecutive( - [ - 'X-Amz-User-Agent', - new \PHPUnit\Framework\Constraint\RegularExpression( - '/aws-sdk-php\/' . Sdk::VERSION . '.* cfg\/endpoint-discovery/' - ) - ], - [ - 'User-Agent', - new \PHPUnit\Framework\Constraint\RegularExpression( - '/aws-sdk-php\/' . Sdk::VERSION . '.* cfg\/endpoint-discovery/' - ) - ] - ) - ->willReturnOnConsecutiveCalls( - $request, - $request - ); - - $args = [ - 'endpoint_discovery' => [ - 'enabled' => true, - 'cache_limit' => 1000 - ], - ]; - $list = new HandlerList(function () { - }); - ClientResolver::_apply_user_agent([], $args, $list); - call_user_func($list->resolve(), $command, $request); - } - - public function testUserAgentAddsRetryModeConfiguration() - { - $command = $this->getMockBuilder(CommandInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $request = $this->getMockBuilder(RequestInterface::class) - ->disableOriginalConstructor() - ->getMock(); - - $request->expects($this->exactly(2)) - ->method('getHeader') - ->withConsecutive( - ['X-Amz-User-Agent'], - ['User-Agent'] - ) - ->willReturnOnConsecutiveCalls( - ["MockBuilder"], - ['MockBuilder'] - ); - - $request->expects($this->exactly(2)) - ->method('withHeader') - ->withConsecutive( - [ - 'X-Amz-User-Agent', - new \PHPUnit\Framework\Constraint\RegularExpression( - '/aws-sdk-php\/' . Sdk::VERSION . '.* cfg\/retry-mode#adaptive/' - ) - ], - [ - 'User-Agent', - new \PHPUnit\Framework\Constraint\RegularExpression( - '/aws-sdk-php\/' . Sdk::VERSION . '.* cfg\/retry-mode#adaptive/' - ) - ] - ) - ->willReturnOnConsecutiveCalls( - $request, - $request - ); - - $args = [ - 'retries' => new \Aws\Retry\Configuration('adaptive', 10) - ]; - $list = new HandlerList(function () { - }); - ClientResolver::_apply_user_agent([], $args, $list); - call_user_func($list->resolve(), $command, $request); - } - - - public function testUserAgentAddsRetryWithArray() - { - $command = $this->getMockBuilder(CommandInterface::class) - ->disableOriginalConstructor() - ->getMock(); - $request = $this->getMockBuilder(RequestInterface::class) - ->disableOriginalConstructor() - ->getMock(); - - $request->expects($this->exactly(2)) - ->method('getHeader') - ->withConsecutive( - ['X-Amz-User-Agent'], - ['User-Agent'] - ) - ->willReturnOnConsecutiveCalls( - ["MockBuilder"], - ['MockBuilder'] - ); - - $request->expects($this->exactly(2)) - ->method('withHeader') - ->withConsecutive( - [ - 'X-Amz-User-Agent', - new \PHPUnit\Framework\Constraint\RegularExpression( - '/aws-sdk-php\/' . Sdk::VERSION . '.* cfg\/retry-mode#standard/' - ) - ], - [ - 'User-Agent', - new \PHPUnit\Framework\Constraint\RegularExpression( - '/aws-sdk-php\/' . Sdk::VERSION . '.* cfg\/retry-mode#standard/' - ) - ] - ) - ->willReturnOnConsecutiveCalls( - $request, - $request - ); - - $args = [ - 'retries' => [ - 'mode' => 'standard', - ], - ]; - $list = new HandlerList(function () { - }); - ClientResolver::_apply_user_agent([], $args, $list); - call_user_func($list->resolve(), $command, $request); } - /** * @dataProvider statValueProvider * @param bool|array $userValue diff --git a/tests/CommandTest.php b/tests/CommandTest.php index 08d21d7106..4fba40b01f 100644 --- a/tests/CommandTest.php +++ b/tests/CommandTest.php @@ -3,6 +3,7 @@ use Aws\Command; use Aws\HandlerList; +use Aws\MetricsBuilder; use Yoast\PHPUnitPolyfills\TestCases\TestCase; /** @@ -45,20 +46,32 @@ public function testHasGetMethod() public function testIsIterable() { - $c = new Command('foo', ['bar' => 'baz', 'qux' => 'boo']); - $data = iterator_to_array($c); - $this->assertEquals( - ['bar' => 'baz', 'qux' => 'boo', '@http' => [], '@context' => []], + $command = new Command('foo', ['bar' => 'baz', 'qux' => 'boo']); + $data = iterator_to_array($command); + $this->assertEquals([ + 'bar' => 'baz', + 'qux' => 'boo', + '@http' => [], + '@context' => [ + MetricsBuilder::COMMAND_METRICS_BUILDER => new MetricsBuilder() + ] + ], $data ); } public function testConvertToArray() { - $c = new Command('foo', ['bar' => 'baz', 'qux' => 'boo']); - $this->assertEquals( - ['bar' => 'baz', 'qux' => 'boo', '@http' => [], '@context' => []], - $c->toArray() + $command = new Command('foo', ['bar' => 'baz', 'qux' => 'boo']); + $this->assertEquals([ + 'bar' => 'baz', + 'qux' => 'boo', + '@http' => [], + '@context' => [ + MetricsBuilder::COMMAND_METRICS_BUILDER => new MetricsBuilder() + ] + ], + $command->toArray() ); } @@ -109,4 +122,22 @@ public function testSetAuthSchemesEmitsWarning() $c = new Command('foo', ['bar' => 'baz', 'qux' => 'boo']); $c->setAuthSchemes([]); } + + public function testInitializeMetricsBuilderObject() + { + $command = new Command('Foo', []); + $this->assertArrayHasKey( + '@context', + $command, + "`@context` property not found in command" + ); + $this->assertArrayHasKey( + MetricsBuilder::COMMAND_METRICS_BUILDER, + $command['@context'], + "`" . MetricsBuilder::COMMAND_METRICS_BUILDER + . "` property not found in command" + ); + $metricsBuilder = MetricsBuilder::fromCommand($command); + $this->assertInstanceOf(MetricsBuilder::class, $metricsBuilder); + } } diff --git a/tests/MetricsBuilderTest.php b/tests/MetricsBuilderTest.php new file mode 100644 index 0000000000..99c9e23cd4 --- /dev/null +++ b/tests/MetricsBuilderTest.php @@ -0,0 +1,122 @@ +append(chr($char)); + $expectedMetrics[] = chr($char); + } + + $this->assertEquals( + implode(',', $expectedMetrics), + $metricsBuilder->build() + ); + } + + public function testEncodeMetrics() + { + $metricsBuilder = new MetricsBuilder(); + $expectedMetrics = "A,B,C"; // encoding format + $metricsBuilder->append("A"); + $metricsBuilder->append("B"); + $metricsBuilder->append("C"); + + $this->assertEquals( + $expectedMetrics, + $metricsBuilder->build() + ); + } + + public function testConstraintsAppendToMetricsSize() + { + try { + set_error_handler( + static function ( $errno, $errstr ) { + // Mute warning + }, + E_ALL + ); + $metricsBuilder = new MetricsBuilder(); + $firstMetric = str_repeat("*", 1024); + $metricsBuilder->append($firstMetric); + $metricsBuilder->append("A"); + $metricsBuilder->append("B"); + + $this->assertEquals($firstMetric, $metricsBuilder->build()); + } finally { + restore_error_handler(); + } + } + + public function testEmitMetricsSizeConstraintWarning() + { + try { + // Prevent deprecation warning for expectWarning + set_error_handler( + static function ( $errno, $errstr ) { + throw new \Exception( $errstr, $errno ); + }, + E_ALL + ); + $this->expectException(Exception::class); + $this->expectExceptionMessage( + "The metric `A` can not be added due to size constraints" + ); + $metricsBuilder = new MetricsBuilder(); + $firstMetric = str_repeat("*", 1024); + $metricsBuilder->append($firstMetric); + $metricsBuilder->append("A"); + } finally { + restore_error_handler(); + } + } + + public function testGetMetricsBuilderFromCommand() + { + $command = new Command('TestCommand', [], new HandlerList()); + $metricsBuilder = MetricsBuilder::fromCommand($command); + $this->assertInstanceOf( MetricsBuilder::class, $metricsBuilder); + } + + public function testAppendMetricsCaptureMiddleware() + { + $handlerList = new HandlerList(function (){}); + $metric = "Foo"; + // It should be appended into the build step + MetricsBuilder::appendMetricsCaptureMiddleware( + $handlerList, + "$metric" + ); + // The sign step is ahead of the build step + // which means we should catch the metric appended + // previously. + $handlerList->appendSign(Middleware::tap( + function ( + CommandInterface $command + ) use ($metric) { + $metricsBuilder = MetricsBuilder::fromCommand($command); + + $this->assertEquals( + $metric, + $metricsBuilder->build() + ); + } + )); + $handlerFn = $handlerList->resolve(); + $command = new Command('Buzz', []); + $handlerFn($command); + } +} diff --git a/tests/ResultPaginatorTest.php b/tests/ResultPaginatorTest.php index c4dc0ad2dc..a07c21d385 100644 --- a/tests/ResultPaginatorTest.php +++ b/tests/ResultPaginatorTest.php @@ -5,8 +5,12 @@ use Aws\CloudWatchLogs\CloudWatchLogsClient; use Aws\CommandInterface; use Aws\DynamoDb\DynamoDbClient; +use Aws\MetricsBuilder; +use Aws\Middleware; use Aws\Result; +use Aws\S3\S3Client; use GuzzleHttp\Promise; +use GuzzleHttp\Psr7\Response; use Psr\Http\Message\RequestInterface; use Yoast\PHPUnitPolyfills\TestCases\TestCase; @@ -16,6 +20,7 @@ class ResultPaginatorTest extends TestCase { use UsesServiceTrait; + use MetricsBuilderTestTrait; private function getCustomClientProvider(array $config) { @@ -457,4 +462,23 @@ function () use (&$requestCount) { $this->assertInstanceOf(Result::class, $result); $this->assertEquals(3, $requestCount); } + + public function testAppendsMetricsCaptureMiddleware() + { + $client = new S3Client([ + 'region' => 'us-east-2', + 'http_handler' => function (RequestInterface $request) { + $this->assertTrue( + in_array( + MetricsBuilder::WAITER, + $this->getMetricsAsArray($request) + ) + ); + + return new Response(); + } + ]); + $paginator = $client->getPaginator('ListBuckets'); + $paginator->current(); + } } diff --git a/tests/S3/Crypto/S3EncryptionClientTest.php b/tests/S3/Crypto/S3EncryptionClientTest.php index 6eb53f30fa..38248ab435 100644 --- a/tests/S3/Crypto/S3EncryptionClientTest.php +++ b/tests/S3/Crypto/S3EncryptionClientTest.php @@ -2,15 +2,14 @@ namespace Aws\Test\S3\Crypto; use Aws\Crypto\KmsMaterialsProviderV2; +use Aws\MetricsBuilder; use Aws\S3\Crypto\S3EncryptionClient; use Aws\Result; use Aws\HashingStream; -use Aws\Crypto\MaterialsProvider; use Aws\Crypto\AesDecryptingStream; use Aws\Crypto\AesGcmDecryptingStream; use Aws\Crypto\KmsMaterialsProvider; use Aws\Crypto\MetadataEnvelope; -use Aws\S3\Crypto\S3EncryptionClientV2; use Aws\S3\S3Client; use Aws\S3\Crypto\HeadersMetadataStrategy; use Aws\S3\Crypto\InstructionFileMetadataStrategy; @@ -20,6 +19,7 @@ use GuzzleHttp\Promise; use GuzzleHttp\Promise\FulfilledPromise; use GuzzleHttp\Psr7\Response; +use Aws\Test\MetricsBuilderTestTrait; use Psr\Http\Message\RequestInterface; use Yoast\PHPUnitPolyfills\TestCases\TestCase; @@ -29,6 +29,7 @@ class S3EncryptionClientTest extends TestCase use UsesCryptoParamsTrait; use UsesMetadataEnvelopeTrait; use UsesServiceTrait; + use MetricsBuilderTestTrait; protected function getS3Client() { @@ -766,7 +767,7 @@ public function testTriggersWarningForGcmEncryptionWithAad() $this->assertTrue($this->mockQueueEmpty()); } - public function testAddsCryptoUserAgent() + public function testAppendsMetricsCaptureMiddleware() { $kms = $this->getKmsClient(); $provider = new KmsMaterialsProvider($kms); @@ -778,10 +779,13 @@ public function testAddsCryptoUserAgent() 'region' => 'us-west-2', 'version' => 'latest', 'http_handler' => function (RequestInterface $req) use ($provider) { - $this->assertStringContainsString( - 'feat/s3-encrypt/' . S3EncryptionClient::CRYPTO_VERSION, - $req->getHeaderLine('User-Agent') + $this->assertTrue( + in_array( + MetricsBuilder::S3_CRYPTO_V1N, + $this->getMetricsAsArray($req) + ) ); + return Promise\Create::promiseFor(new Response( 200, $this->getFieldsAsMetaHeaders( diff --git a/tests/S3/Crypto/S3EncryptionClientV2Test.php b/tests/S3/Crypto/S3EncryptionClientV2Test.php index 4930182eb0..f76919fac6 100644 --- a/tests/S3/Crypto/S3EncryptionClientV2Test.php +++ b/tests/S3/Crypto/S3EncryptionClientV2Test.php @@ -7,11 +7,13 @@ use Aws\Crypto\KmsMaterialsProviderV2; use Aws\Crypto\MetadataEnvelope; use Aws\HashingStream; +use Aws\MetricsBuilder; use Aws\Result; use Aws\S3\S3Client; use Aws\S3\Crypto\InstructionFileMetadataStrategy; use Aws\S3\Crypto\S3EncryptionClientV2; use Aws\Test\Crypto\UsesCryptoParamsTraitV2; +use Aws\Test\MetricsBuilderTestTrait; use Aws\Test\UsesServiceTrait; use Aws\Test\Crypto\UsesMetadataEnvelopeTrait; use GuzzleHttp\Promise; @@ -26,6 +28,7 @@ class S3EncryptionClientV2Test extends TestCase use UsesCryptoParamsTraitV2; use UsesMetadataEnvelopeTrait; use UsesServiceTrait; + use MetricsBuilderTestTrait; protected function getS3Client() { @@ -1032,22 +1035,24 @@ public function testThrowsForIncorrectSecurityProfile() ]); } - public function testAddsCryptoUserAgent() + public function testAppendsMetricsCaptureMiddleware() { $kms = $this->getKmsClient(); $provider = new KmsMaterialsProviderV2($kms, 'foo'); $this->addMockResults($kms, [ new Result(['Plaintext' => random_bytes(32)]) ]); - $s3 = new S3Client([ 'region' => 'us-west-2', 'version' => 'latest', 'http_handler' => function (RequestInterface $req) use ($provider) { - $this->assertStringContainsString( - 'feat/s3-encrypt/' . S3EncryptionClientV2::CRYPTO_VERSION, - $req->getHeaderLine('User-Agent') + $this->assertTrue( + in_array( + MetricsBuilder::S3_CRYPTO_V2, + $this->getMetricsAsArray($req) + ) ); + return Promise\Create::promiseFor(new Response( 200, $this->getFieldsAsMetaHeaders( diff --git a/tests/UserAgentMiddlewareTest.php b/tests/UserAgentMiddlewareTest.php new file mode 100644 index 0000000000..93b6a05c2e --- /dev/null +++ b/tests/UserAgentMiddlewareTest.php @@ -0,0 +1,277 @@ +deferFns) > 0) { + $fn = array_pop($this->deferFns); + $fn(); + } + } + + /** + * Tests the user agent header is appended into the request headers. + * + * @return void + */ + public function testAppendsUserAgentHeader() + { + $handler = UserAgentMiddleware::wrap([]); + $middleware = $handler(function ( + CommandInterface $command, + RequestInterface $request + ) { + $userAgent = $request->getHeaderLine('User-Agent'); + + $this->assertNotEmpty($userAgent); + }); + $request = new Request('post', 'foo', [], 'buzz'); + $middleware(new Command('buzz'), $request); + } + + /** + * Tests the user agent header value contains the expected + * component. + * + * @dataProvider userAgentCasesDataProvider + * @param array $args + * @param string $expected + * + * @return void + */ + public function testUserAgentContainsValue(array $args, string $expected) + { + $handler = UserAgentMiddleware::wrap($args); + $middleware = $handler(function ( + CommandInterface $command, + RequestInterface $request + ) use ($expected) { + if (empty($expected)) { + $this->markTestSkipped('Expected value is empty'); + } + $userAgent = $request->getHeaderLine('User-Agent'); + $userAgentValues = explode(' ', $userAgent); + $this->assertTrue(in_array($expected, $userAgentValues)); + }); + $request = new Request('post', 'foo', [], 'buzz'); + $middleware(new Command('buzz'), $request); + } + + /** + * It returns a generator that yields an argument and an expected value + * per iteration. + * Example: yield [$arguments, 'ExpectedValue'] + * + * @return Generator + */ + public function userAgentCasesDataProvider(): Generator + { + $userAgentCases = [ + 'sdkVersion' => [[], 'aws-sdk-php/' . Sdk::VERSION], + 'userAgentVersion' => [ + [], 'ua/' . UserAgentMiddleware::AGENT_VERSION + ], + 'hhvmVersion' => function (): array { + if (defined('HHVM_VERSION')) { + return [[], 'HHVM/' . HHVM_VERSION]; + } + + return [[], ""]; + }, + 'osName' => function (): array { + $disabledFunctions = explode( + ',', + ini_get('disable_functions') + ); + if (function_exists('php_uname') + && !in_array( + 'php_uname', + $disabledFunctions, + true + ) + ) { + $osName = "OS/" . php_uname('s') . '#' . php_uname('r'); + if (!empty($osName)) { + return [[], $osName]; + } + } + + return [[], ""]; + }, + 'langVersion' => [[], 'lang/php#' . phpversion()], + 'execEnv' => function (): array { + $expectedEnv = "LambdaFooEnvironment"; + $currentEnv = getenv('AWS_EXECUTION_ENV'); + putenv("AWS_EXECUTION_ENV={$expectedEnv}"); + + $this->deferFns[] = function () use ($currentEnv) { + if ($currentEnv !== false) { + putenv("AWS_EXECUTION_ENV={$currentEnv}"); + } else { + putenv('AWS_EXECUTION_ENV'); + } + }; + + return [[], $expectedEnv]; + }, + 'appId' => function (): array { + $expectedAppId = "FooAppId"; + $args = [ + 'app_id' => $expectedAppId + ]; + + return [$args, "app/{$expectedAppId}"]; + }, + 'metricsWithEndpoint' => function (): array { + $expectedEndpoint = "https://foo-endpoint.com"; + $args = [ + 'endpoint' => $expectedEndpoint + ]; + + return [$args, 'm/' . MetricsBuilder::ENDPOINT_OVERRIDE]; + }, + 'metricsWithAccountIdModePreferred' => function (): array { + $args = [ + 'account_id_endpoint_mode' => 'preferred' + ]; + + return [$args, 'm/' . MetricsBuilder::ACCOUNT_ID_MODE_PREFERRED]; + }, + 'metricsWithAccountIdModeRequired' => function (): array { + $args = [ + 'account_id_endpoint_mode' => 'required' + ]; + + return [$args, 'm/' . MetricsBuilder::ACCOUNT_ID_MODE_REQUIRED]; + }, + 'metricsWithAccountIdModeDisabled' => function (): array { + $args = [ + 'account_id_endpoint_mode' => 'disabled' + ]; + + return [$args, 'm/' . MetricsBuilder::ACCOUNT_ID_MODE_DISABLED]; + }, + 'metricsWithRetryConfigArrayStandardMode' => function (): array { + $args = [ + 'retries' => [ + 'mode' => 'standard' + ] + ]; + + return [$args, 'm/' . MetricsBuilder::RETRY_MODE_STANDARD]; + }, + 'metricsWithRetryConfigArrayAdaptiveMode' => function (): array { + $args = [ + 'retries' => [ + 'mode' => 'adaptive' + ] + ]; + + return [$args, 'm/' . MetricsBuilder::RETRY_MODE_ADAPTIVE]; + }, + 'metricsWithRetryConfigArrayLegacyMode' => function (): array { + $args = [ + 'retries' => [ + 'mode' => 'legacy' + ] + ]; + + return [$args, 'm/' . MetricsBuilder::RETRY_MODE_LEGACY]; + }, + 'metricsWithRetryConfigStandardMode' => function (): array { + $args = [ + 'retries' => new \Aws\Retry\Configuration( + 'standard', + 10 + ) + ]; + + return [$args, 'm/' . MetricsBuilder::RETRY_MODE_STANDARD]; + }, + 'metricsWithRetryConfigAdaptiveMode' => function (): array { + $args = [ + 'retries' => new \Aws\Retry\Configuration( + 'adaptive', + 10 + ) + ]; + + return [$args, 'm/' . MetricsBuilder::RETRY_MODE_ADAPTIVE]; + }, + 'metricsWithRetryConfigLegacyMode' => function (): array { + $args = [ + 'retries' => new \Aws\Retry\Configuration( + 'legacy', + 10 + ) + ]; + + return [$args, 'm/' . MetricsBuilder::RETRY_MODE_LEGACY]; + }, + 'cfgWithEndpointDiscoveryConfigArray' => function (): array { + $args = [ + 'endpoint_discovery' => [ + 'enabled' => true, + 'cache_limit' => 1000 + ] + ]; + + return [$args, 'cfg/endpoint-discovery']; + }, + 'cfgWithEndpointDiscoveryConfig' => function (): array { + $args = [ + 'endpoint_discovery' => new \Aws\EndpointDiscovery\Configuration ( + true, + 1000 + ), + ]; + + return [$args, 'cfg/endpoint-discovery']; + } + ]; + + foreach ($userAgentCases as $key => $case) { + if (is_callable($case)) { + yield $key => $case(); + } else { + yield $key => $case; + } + } + } + + /** + * Tests the user agent header values starts with the SDK/version string. + * Example: aws-sdk-php/3.x.x + * + * @return void + */ + public function testUserAgentValueStartsWithSdkVersionString() + { + $handler = UserAgentMiddleware::wrap([]); + $middleware = $handler(function ( + CommandInterface $command, + RequestInterface $request + ) { + $userAgent = $request->getHeaderLine('User-Agent'); + $pattern = "aws-sdk-php/" . Sdk::VERSION; + + $this->assertTrue( + str_starts_with($userAgent, $pattern) + ); + }); + $request = new Request('post', 'foo', [], 'buzz'); + $middleware(new Command('buzz'), $request); + } +} diff --git a/tests/WaiterTest.php b/tests/WaiterTest.php index 509ba921d1..e3aa991fa0 100644 --- a/tests/WaiterTest.php +++ b/tests/WaiterTest.php @@ -2,9 +2,11 @@ namespace Aws\Test; use Aws\Api\ApiProvider; +use Aws\AwsClientInterface; use Aws\CommandInterface; use Aws\DynamoDb\DynamoDbClient; use Aws\Exception\AwsException; +use Aws\MetricsBuilder; use Aws\Result; use Aws\S3\S3Client; use Aws\Waiter; @@ -25,6 +27,7 @@ class WaiterTest extends TestCase { use UsesServiceTrait; + use MetricsBuilderTestTrait; public function testErrorOnBadConfig() { @@ -425,23 +428,18 @@ public function testWaiterMatcherExpectNoError(): void 'Bucket' => 'fuzz', 'Key' => 'bazz' ]; - $waiterConfig = [ - 'delay' => 5, - 'operation' => 'headObject', - 'maxAttempts' => 20, - 'acceptors' => [ - [ - 'expected' => false, - 'matcher' => 'error', - 'state' => 'success' - ] + $acceptors = [ + [ + 'expected' => false, + 'matcher' => 'error', + 'state' => 'success' ] ]; - $waiter = new Waiter( - $client, - 'foo', + $waiter = $this->getTestWaiter( + $acceptors, + 'headObject', $commandArgs, - $waiterConfig + $client ); $waiter->promise() ->then(function (CommandInterface $_) { @@ -478,27 +476,87 @@ public function testWaiterMatcherExpectsAnyError(): void 'Bucket' => 'fuzz', 'Key' => 'bazz' ]; - $waiterConfig = [ - 'delay' => 5, - 'operation' => 'headObject', - 'maxAttempts' => 20, - 'acceptors' => [ - [ - 'expected' => true, - 'matcher' => 'error', - 'state' => 'success' - ] + $acceptors = [ + [ + 'expected' => true, + 'matcher' => 'error', + 'state' => 'success' ] ]; - $waiter = new Waiter( - $client, - 'foo', + $waiter = $this->getTestWaiter( + $acceptors, + 'headObject', $commandArgs, - $waiterConfig + $client ); $waiter->promise() ->then(function (CommandInterface $_) { $this->assertTrue(true); // Waiter succeeded })->wait(); } + + public function testAppendsMetricsCaptureMiddleware() + { + $client = new S3Client([ + 'region' => 'us-east-2', + 'http_handler' => function (RequestInterface $request) { + $this->assertTrue( + in_array( + MetricsBuilder::WAITER, + $this->getMetricsAsArray($request) + ) + ); + + return new Response(); + } + ]); + $commandArgs = [ + 'Bucket' => 'foo' + ]; + $acceptors = [ + [ + 'expected' => 200, + 'matcher' => 'status', + 'state' => 'success' + ] + ]; + $waiter = $this->getTestWaiter( + $acceptors, + 'headBucket', + $commandArgs, + $client + ); + $waiter->promise()->wait(); + } + + /** + * Creates a test waiter. + * + * @param string $operation + * @param array $commandArgs + * @param AwsClientInterface $client + * + * @return Waiter + */ + private function getTestWaiter( + array $acceptors, + string $operation, + array $commandArgs, + AwsClientInterface $client + ): Waiter + { + $waiterConfig = [ + 'delay' => 5, + 'operation' => $operation, + 'maxAttempts' => 20, + 'acceptors' => $acceptors + ]; + + return new Waiter( + $client, + 'waiter-' . $operation, + $commandArgs, + $waiterConfig + ); + } }