Skip to content

Commit

Permalink
Adding client telemetry headers
Browse files Browse the repository at this point in the history
  • Loading branch information
akropp-stripe committed Nov 15, 2018
1 parent 77eb5bb commit b6ac17d
Show file tree
Hide file tree
Showing 7 changed files with 218 additions and 0 deletions.
1 change: 1 addition & 0 deletions init.php
Expand Up @@ -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');
Expand Down
51 changes: 51 additions & 0 deletions lib/ApiRequestor.php
Expand Up @@ -24,6 +24,11 @@ class ApiRequestor
*/
private static $_httpClient;

/**
* @var RequestTelemetry
*/
private static $requestTelemetry;

/**
* ApiRequestor constructor.
*
Expand All @@ -39,6 +44,30 @@ public function __construct($apiKey = null, $apiBase = null)
$this->_apiBase = $apiBase;
}

/**
* Creates a telemetry json blob for use in 'X-Stripe-Client-Telemetry' headers
* @static
*
* @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 {
Stripe::getLogger()->error("Serializing telemetry payload failed!");
return "{}";
}
}

/**
* @static
*
Expand Down Expand Up @@ -332,6 +361,10 @@ private function _requestRaw($method, $url, $params, $headers)
$defaultHeaders['Stripe-Account'] = Stripe::$accountId;
}

if (Stripe::$enableTelemetry && self::$requestTelemetry != null) {
$defaultHeaders["X-Stripe-Client-Telemetry"] = self::_telemetryJson(self::$requestTelemetry);
}

$hasFile = false;
$hasCurlFile = class_exists('\CURLFile', false);
foreach ($params as $k => $v) {
Expand All @@ -356,13 +389,21 @@ private function _requestRaw($method, $url, $params, $headers)
$rawHeaders[] = $header . ': ' . $value;
}

$requestStartMs = Util\Util::currentTimeMillis();

list($rbody, $rcode, $rheaders) = $this->httpClient()->request(
$method,
$absUrl,
$rawHeaders,
$params,
$hasFile
);

self::$requestTelemetry = new RequestTelemetry(
$rheaders['request-id'],
Util\Util::currentTimeMillis() - $requestStartMs
);

return [$rbody, $rcode, $rheaders, $myApiKey];
}

Expand Down Expand Up @@ -442,6 +483,16 @@ public static function setHttpClient($client)
self::$_httpClient = $client;
}

/**
* @static
*
* Resets any stateful telemetry data
*/
public static function resetTelemetry()
{
self::$requestTelemetry = null;
}

/**
* @return HttpClient\ClientInterface
*/
Expand Down
21 changes: 21 additions & 0 deletions lib/RequestTelemetry.php
@@ -0,0 +1,21 @@
<?php

namespace Stripe;

/**
* Class RequestTelemetry
*
* Tracks client request telemetry
* @package Stripe
*/
class RequestTelemetry
{
public $requestId;
public $requestDuration;

public function __construct($requestId, $requestDuration)
{
$this->requestId = $requestId;
$this->requestDuration = $requestDuration;
}
}
23 changes: 23 additions & 0 deletions lib/Stripe.php
Expand Up @@ -46,6 +46,9 @@ class Stripe
// @var int Maximum number of request retries
public static $maxNetworkRetries = 0;

// @var boolean Whether client telemetry is enabled. Defaults to false.
public static $enableTelemetry = false;

// @var float Maximum delay between retries, in seconds
private static $maxNetworkRetryDelay = 2.0;

Expand Down Expand Up @@ -239,4 +242,24 @@ public static function getInitialNetworkRetryDelay()
{
return self::$initialNetworkRetryDelay;
}

/**
* @return bool Whether client telemetry is enabled
*/
public static function getEnableTelemetry()
{
return self::$enableTelemetry;
}

/**
* @param bool $enableTelemetry Enables client telemetry.
*
* Client telemetry enables timing and request metrics to be sent back to Stripe as an HTTP Header
* with the current request. This enables Stripe to do latency and metrics analysis without adding extra
* overhead (such as extra network calls) on the client.
*/
public static function setEnableTelemetry($enableTelemetry)
{
self::$enableTelemetry = $enableTelemetry;
}
}
10 changes: 10 additions & 0 deletions lib/Util/Util.php
Expand Up @@ -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);
}
}
111 changes: 111 additions & 0 deletions tests/Stripe/StripeTelemetryTest.php
@@ -0,0 +1,111 @@
<?php

namespace Stripe;

class StripeTelemetryTest extends TestCase
{
const TEST_RESOURCE_ID = 'acct_123';
const TEST_EXTERNALACCOUNT_ID = 'ba_123';
const TEST_PERSON_ID = 'person_123';

const FAKE_VALID_RESPONSE = '{
"data": [],
"has_more": false,
"object": "list",
"url": "/v1/accounts"
}';

protected function setUp()
{
parent::setUp();

// clear static telemetry data
ApiRequestor::resetTelemetry();
}

public function testNoTelemetrySentIfNotEnabled()
{
$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) {
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::setEnableTelemetry(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);
}
}
1 change: 1 addition & 0 deletions tests/TestCase.php
Expand Up @@ -52,6 +52,7 @@ protected function tearDown()
{
// Restore original values
Stripe::$apiBase = $this->origApiBase;
Stripe::setEnableTelemetry(false);
Stripe::setApiKey($this->origApiKey);
Stripe::setClientId($this->origClientId);
Stripe::setApiVersion($this->origApiVersion);
Expand Down

0 comments on commit b6ac17d

Please sign in to comment.