Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding client telemetry headers #549

Merged
merged 1 commit into from Nov 15, 2018
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
53 changes: 53 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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think there's any sane circumstances where we'd expect json_encode to fail is there? I'd probably just return the result directly — in the unlikely event that there is a problem, it'd be nice to find out what it is.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My concern was that IF something did happen, all we'd get in the header is false, since the return is either json or a boolean. If we had false then we'd probably have to special case any analysis tooling in stripe to skip that as we'd only be expecting json. I can definitely pass it through, but I didn't think it would be helpful.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay fair enough. Could you add a logger line in the failure case like you did below then? IMO it's best to never paper over errors completely.

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,23 @@ 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
);

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

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

Expand Down Expand Up @@ -442,6 +485,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)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing phpDoc
$requestId probably must be a string
$requestDuration must be int in milliseconds

{
$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);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's probably should be casted to int like:
return (int) 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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As with Ruby, I'd probably put these tests in the ApiRequestor suite because it's really that class that's doing all the heavy lifting.

I don't feel super strongly about it though. (And thanks for writing tests in the first place!)

}
}
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