diff --git a/init.php b/init.php index 85fc5c5489..10ea220d69 100644 --- a/init.php +++ b/init.php @@ -49,6 +49,7 @@ // Plumbing require(dirname(__FILE__) . '/lib/ApiResponse.php'); +require(dirname(__FILE__) . '/lib/RequestTelemetry.php'); require(dirname(__FILE__) . '/lib/StripeObject.php'); require(dirname(__FILE__) . '/lib/ApiRequestor.php'); require(dirname(__FILE__) . '/lib/ApiResource.php'); diff --git a/lib/ApiRequestor.php b/lib/ApiRequestor.php index 4a147d01ad..3579a7653f 100644 --- a/lib/ApiRequestor.php +++ b/lib/ApiRequestor.php @@ -24,6 +24,11 @@ class ApiRequestor */ private static $_httpClient; + /** + * @var RequestTelemetry + */ + private static $requestTelemetry; + /** * ApiRequestor constructor. * @@ -39,6 +44,28 @@ public function __construct($apiKey = null, $apiBase = null) $this->_apiBase = $apiBase; } + /** + * @static Creates a telemetry json blob for use in 'X-Stripe-Client-Telemetry' headers + * + * @param RequestTelemetry $requestTelemetry + * @return string + */ + private static function _telemetryJson($requestTelemetry) + { + $payload = array( + 'last_request_metrics' => array( + 'request_id' => $requestTelemetry->requestId, + 'request_duration_ms' => $requestTelemetry->requestDuration, + )); + + $result = json_encode($payload); + if ($result != false) { + return $result; + } else { + return "{}"; + } + } + /** * @static * @@ -332,6 +359,10 @@ private function _requestRaw($method, $url, $params, $headers) $defaultHeaders['Stripe-Account'] = Stripe::$accountId; } + if (Stripe::$enableClientTelemetry && self::$requestTelemetry != null) { + $defaultHeaders["X-Stripe-Client-Telemetry"] = self::_telemetryJson(self::$requestTelemetry); + } + $hasFile = false; $hasCurlFile = class_exists('\CURLFile', false); foreach ($params as $k => $v) { @@ -356,6 +387,8 @@ private function _requestRaw($method, $url, $params, $headers) $rawHeaders[] = $header . ': ' . $value; } + $requestStartMs = Util\Util::currentTimeMillis(); + list($rbody, $rcode, $rheaders) = $this->httpClient()->request( $method, $absUrl, @@ -363,6 +396,16 @@ private function _requestRaw($method, $url, $params, $headers) $params, $hasFile ); + + try { + self::$requestTelemetry = new RequestTelemetry( + $rheaders['request-id'], + Util\Util::currentTimeMillis() - $requestStartMs + ); + } catch (\Exception $e) { + Stripe::getLogger()->error("Unable to track client telemetry: " .$e->getMessage()); + } + return [$rbody, $rcode, $rheaders, $myApiKey]; } @@ -442,6 +485,14 @@ public static function setHttpClient($client) self::$_httpClient = $client; } + /** + * Resets any stateful telemetry data + */ + public static function resetTelemetry() + { + self::$requestTelemetry = null; + } + /** * @return HttpClient\ClientInterface */ diff --git a/lib/RequestTelemetry.php b/lib/RequestTelemetry.php new file mode 100644 index 0000000000..5ddbb05612 --- /dev/null +++ b/lib/RequestTelemetry.php @@ -0,0 +1,21 @@ +requestId = $requestId; + $this->requestDuration = $requestDuration; + } +} diff --git a/lib/Stripe.php b/lib/Stripe.php index 2c2c382469..3475550350 100644 --- a/lib/Stripe.php +++ b/lib/Stripe.php @@ -46,6 +46,9 @@ class Stripe // @var int Maximum number of request retries public static $maxNetworkRetries = 0; + // @var boolean Defaults to false. + public static $enableClientTelemetry = false; + // @var float Maximum delay between retries, in seconds private static $maxNetworkRetryDelay = 2.0; @@ -239,4 +242,20 @@ public static function getInitialNetworkRetryDelay() { return self::$initialNetworkRetryDelay; } + + /** + * @return bool Whether client telemetry is enabled + */ + public static function isEnableClientTelemetry() + { + return self::$enableClientTelemetry; + } + + /** + * @param bool $enableClientTelemetry Enables client telemetry + */ + public static function setEnableClientTelemetry($enableClientTelemetry) + { + self::$enableClientTelemetry = $enableClientTelemetry; + } } diff --git a/lib/Util/Util.php b/lib/Util/Util.php index 7f257b0514..b9804c20d7 100644 --- a/lib/Util/Util.php +++ b/lib/Util/Util.php @@ -333,4 +333,14 @@ public static function normalizeId($id) } return [$id, $params]; } + + /** + * Returns UNIX timestamp in milliseconds + * + * @return float current time in millis + */ + public static function currentTimeMillis() + { + return round(microtime(true) * 1000); + } } diff --git a/tests/Stripe/StripeTelemetryTest.php b/tests/Stripe/StripeTelemetryTest.php new file mode 100644 index 0000000000..f62d916edc --- /dev/null +++ b/tests/Stripe/StripeTelemetryTest.php @@ -0,0 +1,112 @@ +getMockBuilder("HttpClient\ClientInterface") + ->setMethods(array('request')) + ->getMock(); + + $stub->expects($this->any()) + ->method("request") + ->with( + $this->anything(), + $this->anything(), + $this->callback(function ($headers) use (&$requestheaders) { + foreach ($headers as $index => $header) { + // capture the requested headers and format back to into an assoc array + $components = explode(": ", $header, 2); + $requestheaders[$components[0]] = $components[1]; + } + + return true; + }), + $this->anything(), + $this->anything() + )->willReturn(array(self::FAKE_VALID_RESPONSE, 200, ["request-id" => "123"])); + + ApiRequestor::setHttpClient($stub); + + // make one request to capture its result + Charge::all(); + $this->assertArrayNotHasKey('X-Stripe-Client-Telemetry', $requestheaders); + + // make another request and verify telemetry isn't sent + Charge::all(); + $this->assertArrayNotHasKey('X-Stripe-Client-Telemetry', $requestheaders); + + ApiRequestor::setHttpClient(null); + } + + public function testTelemetrySetIfEnabled() + { + Stripe::setEnableClientTelemetry(true); + + $requestheaders = null; + + $stub = $this + ->getMockBuilder("HttpClient\ClientInterface") + ->setMethods(array('request')) + ->getMock(); + + $stub->expects($this->any()) + ->method("request") + ->with( + $this->anything(), + $this->anything(), + $this->callback(function ($headers) use (&$requestheaders) { + // capture the requested headers and format back to into an assoc array + foreach ($headers as $index => $header) { + $components = explode(": ", $header, 2); + $requestheaders[$components[0]] = $components[1]; + } + + return true; + }), + $this->anything(), + $this->anything() + )->willReturn(array(self::FAKE_VALID_RESPONSE, 200, ["request-id" => "123"])); + + ApiRequestor::setHttpClient($stub); + + // make one request to capture its result + Charge::all(); + $this->assertArrayNotHasKey('X-Stripe-Client-Telemetry', $requestheaders); + + // make another request to send the previous + Charge::all(); + $this->assertArrayHasKey('X-Stripe-Client-Telemetry', $requestheaders); + + $data = json_decode($requestheaders['X-Stripe-Client-Telemetry'], true); + $this->assertEquals('123', $data['last_request_metrics']['request_id']); + $this->assertNotNull($data['last_request_metrics']['request_duration_ms']); + + ApiRequestor::setHttpClient(null); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index 93be3d1281..b4e9653030 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -52,6 +52,7 @@ protected function tearDown() { // Restore original values Stripe::$apiBase = $this->origApiBase; + Stripe::setEnableClientTelemetry(false); Stripe::setApiKey($this->origApiKey); Stripe::setClientId($this->origClientId); Stripe::setApiVersion($this->origApiVersion);