Skip to content

Commit

Permalink
Add request telemetry (#434) (#435)
Browse files Browse the repository at this point in the history
* Add recording of telemetry to ApiClient

* Allow reading of RecordTelemetry from .json config

* Formatting

* Add test to confirm configuring RecordTelemetry

* Readonly CodeQL fixes

* Readme spelling mistakes

* Change constant variables to const

Co-authored-by: nick-saxelby-cko <[email protected]>
  • Loading branch information
armando-rodriguez-cko and nick-saxelby-cko authored Oct 23, 2024
1 parent 7458826 commit 99cee71
Show file tree
Hide file tree
Showing 13 changed files with 231 additions and 17 deletions.
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,29 @@ The execution of integration tests require the following environment variables s
* For Default account systems (OAuth): `CHECKOUT_DEFAULT_OAUTH_CLIENT_ID` & `CHECKOUT_DEFAULT_OAUTH_CLIENT_SECRET`
* For Previous account systems (ABC): `CHECKOUT_PREVIOUS_PUBLIC_KEY` & `CHECKOUT_PREVIOUS_SECRET_KEY`

## Telemetry
Request telemetry is enabled by default in the .NET SDK. Request latency is included in the telemetry data. Recording the request latency allows Checkout.com to continuously monitor and improve the merchant experience.

Request telemetry can be disabled by opting out during CheckoutSdk builder step:
```
ICheckoutApi api = CheckoutSdk.Builder().StaticKeys()
.SecretKey("secret_key")
.RecordTelemetry(false)
.Environment(Environment.Sandbox)
.Build();
```

Or when using `CheckoutSDK.Extensions.Microsoft`:
```
{
"Checkout": {
...
"RecordTelemetry": false,
...
}
}
```

## Code of Conduct

Please refer to [Code of Conduct](CODE_OF_CONDUCT.md)
Expand Down
3 changes: 2 additions & 1 deletion src/CheckoutSdk.Extensions/CheckoutOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public class CheckoutOptions
public PlatformType? PlatformType { get; set; }

public IHttpClientFactory HttpClientFactory { get; set; }


public bool RecordTelemetry { get; set; } = true;
}
}
1 change: 1 addition & 0 deletions src/CheckoutSdk.Extensions/CheckoutServiceCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ private static void SetCommonAttributes<TB, TC>(
{
builder.HttpClientFactory(httpClientFactory);
}
builder.RecordTelemetry(options.RecordTelemetry);
}
}
}
10 changes: 9 additions & 1 deletion src/CheckoutSdk/AbstractCheckoutSdkBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ namespace Checkout
public abstract class AbstractCheckoutSdkBuilder<T>
{
protected Environment Env = Checkout.Environment.Sandbox;

private bool _recordTelemetry = true;
private EnvironmentSubdomain _envSubdomain;
protected IHttpClientFactory ClientFactory = new DefaultHttpClientFactory();

Expand All @@ -24,6 +26,12 @@ public AbstractCheckoutSdkBuilder<T> EnvironmentSubdomain(string subdomain)
return this;
}

public AbstractCheckoutSdkBuilder<T> RecordTelemetry(bool recordTelemetry)
{
_recordTelemetry = recordTelemetry;
return this;
}

#if (NETSTANDARD2_0_OR_GREATER || NETCOREAPP3_1_OR_GREATER)
public AbstractCheckoutSdkBuilder<T> LogProvider(ILoggerFactory loggerFactory)
{
Expand All @@ -40,7 +48,7 @@ public AbstractCheckoutSdkBuilder<T> HttpClientFactory(IHttpClientFactory httpCl

protected CheckoutConfiguration GetCheckoutConfiguration()
{
return new CheckoutConfiguration(GetSdkCredentials(), Env, _envSubdomain, ClientFactory);
return new CheckoutConfiguration(GetSdkCredentials(), Env, _envSubdomain, ClientFactory, _recordTelemetry);
}

protected abstract SdkCredentials GetSdkCredentials();
Expand Down
42 changes: 39 additions & 3 deletions src/CheckoutSdk/ApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
using System.Web;
#endif
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.Http;
Expand All @@ -19,16 +21,21 @@ public class ApiClient : IApiClient
private readonly ILogger _log = LogProvider.GetLogger(typeof(ApiClient));
#endif

private const string sdkTelemetryHeader = "cko-sdk-telemetry";
private const int maxCountInTelemetryQueue = 10;
private readonly HttpClient _httpClient;
private readonly Uri _baseUri;
private readonly ISerializer _serializer = new JsonSerializer();
private readonly ConcurrentQueue<RequestMetrics> requestMetricsQueue = new ConcurrentQueue<RequestMetrics>();
private readonly bool _enableTelemetry;

public ApiClient(IHttpClientFactory httpClientFactory, Uri baseUri)
public ApiClient(IHttpClientFactory httpClientFactory, Uri baseUri, bool enableTelemetry)
{
CheckoutUtils.ValidateParams("httpClientFactory", httpClientFactory, "baseUri", baseUri);
var httpClient = httpClientFactory.CreateClient();
_baseUri = baseUri;
_httpClient = httpClient;
_enableTelemetry = enableTelemetry;
}

public async Task<TResult> Get<TResult>(
Expand Down Expand Up @@ -170,7 +177,7 @@ public async Task<TResult> Query<TResult>(
$"{WebUtility.UrlEncode(kvp.Key)}={WebUtility.UrlEncode(kvp.Value)}"));
#endif

path = $"{path}?{queryString}";
path = $"{path}?{queryString}";
}
}

Expand Down Expand Up @@ -246,8 +253,37 @@ private async Task<HttpResponseMessage> Invoke(
{
httpRequest.Headers.Add("Cko-Idempotency-Key", idempotencyKey);
}

if (_enableTelemetry)
{
var currentRequestId = Guid.NewGuid().ToString();
if (requestMetricsQueue.TryDequeue(out var lastRequestMetric))
{
lastRequestMetric.RequestId = currentRequestId;
httpRequest.Headers.TryAddWithoutValidation(sdkTelemetryHeader, _serializer.Serialize(lastRequestMetric));
}

return await _httpClient.SendAsync(httpRequest, cancellationToken);
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
var result = await _httpClient.SendAsync(httpRequest, cancellationToken);
stopwatch.Stop();

if (requestMetricsQueue.Count < maxCountInTelemetryQueue)
{
lastRequestMetric.PrevRequestDuration = stopwatch.ElapsedMilliseconds;
requestMetricsQueue.Enqueue(new RequestMetrics()
{
PrevRequestDuration = stopwatch.ElapsedMilliseconds,
PrevRequestId = currentRequestId
});
}

return result;
}
else
{
return await _httpClient.SendAsync(httpRequest, cancellationToken);
}
}

private async Task ValidateResponseAsync(HttpResponseMessage httpResponse)
Expand Down
12 changes: 8 additions & 4 deletions src/CheckoutSdk/CheckoutApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,25 +79,29 @@ private static ApiClient BaseApiClient(CheckoutConfiguration configuration)
return new ApiClient(configuration.HttpClientFactory,
configuration.EnvironmentSubdomain != null
? configuration.EnvironmentSubdomain.ApiUri
: configuration.Environment.GetAttribute<EnvironmentAttribute>().ApiUri);
: configuration.Environment.GetAttribute<EnvironmentAttribute>().ApiUri,
configuration.RecordTelemetry);
}

private static ApiClient FilesApiClient(CheckoutConfiguration configuration)
{
return new ApiClient(configuration.HttpClientFactory,
configuration.Environment.GetAttribute<EnvironmentAttribute>().FilesApiUri);
configuration.Environment.GetAttribute<EnvironmentAttribute>().FilesApiUri,
configuration.RecordTelemetry);
}

private static ApiClient TransfersApiClient(CheckoutConfiguration configuration)
{
return new ApiClient(configuration.HttpClientFactory,
configuration.Environment.GetAttribute<EnvironmentAttribute>().TransfersApiUri);
configuration.Environment.GetAttribute<EnvironmentAttribute>().TransfersApiUri,
configuration.RecordTelemetry);
}

private static ApiClient BalancesApiClient(CheckoutConfiguration configuration)
{
return new ApiClient(configuration.HttpClientFactory,
configuration.Environment.GetAttribute<EnvironmentAttribute>().BalancesApiUri);
configuration.Environment.GetAttribute<EnvironmentAttribute>().BalancesApiUri,
configuration.RecordTelemetry);
}


Expand Down
6 changes: 5 additions & 1 deletion src/CheckoutSdk/CheckoutConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ public class CheckoutConfiguration

public IHttpClientFactory HttpClientFactory { get; }

public bool RecordTelemetry { get; }

public CheckoutConfiguration(
SdkCredentials sdkCredentials,
Environment environment,
Expand All @@ -28,7 +30,8 @@ public CheckoutConfiguration(
SdkCredentials sdkCredentials,
Environment environment,
EnvironmentSubdomain environmentSubdomain,
IHttpClientFactory httpClientFactory)
IHttpClientFactory httpClientFactory,
bool recordTelemetry)
{
CheckoutUtils.ValidateParams(
"sdkCredentials", sdkCredentials,
Expand All @@ -38,6 +41,7 @@ public CheckoutConfiguration(
Environment = environment;
EnvironmentSubdomain = environmentSubdomain;
HttpClientFactory = httpClientFactory;
RecordTelemetry = recordTelemetry;
}
}
}
3 changes: 2 additions & 1 deletion src/CheckoutSdk/Previous/AbstractCheckoutApmApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ protected AbstractCheckoutApmApi(CheckoutConfiguration configuration)
var apiClient = new ApiClient(configuration.HttpClientFactory,
configuration.EnvironmentSubdomain != null
? configuration.EnvironmentSubdomain.ApiUri
: configuration.Environment.GetAttribute<EnvironmentAttribute>().ApiUri);
: configuration.Environment.GetAttribute<EnvironmentAttribute>().ApiUri,
configuration.RecordTelemetry);
_idealClient = new IdealClient(apiClient, configuration);
_klarnaClient = new KlarnaClient(apiClient, configuration);
_sepaClient = new SepaClient(apiClient, configuration);
Expand Down
3 changes: 2 additions & 1 deletion src/CheckoutSdk/Previous/CheckoutApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ public CheckoutApi(CheckoutConfiguration configuration) : base(configuration)
var apiClient = new ApiClient(configuration.HttpClientFactory,
configuration.EnvironmentSubdomain != null
? configuration.EnvironmentSubdomain.ApiUri
: configuration.Environment.GetAttribute<EnvironmentAttribute>().ApiUri);
: configuration.Environment.GetAttribute<EnvironmentAttribute>().ApiUri,
configuration.RecordTelemetry);
_tokensClient = new TokensClient(apiClient, configuration);
_customersClient = new CustomersClient(apiClient, configuration);
_sourcesClient = new SourcesClient(apiClient, configuration);
Expand Down
9 changes: 9 additions & 0 deletions src/CheckoutSdk/RequestMetrics.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Checkout
{
public struct RequestMetrics
{
public string PrevRequestId { get; set; }
public string RequestId { get; set; }
public long PrevRequestDuration { get; set; }
}
}
4 changes: 2 additions & 2 deletions test/CheckoutSdkTest/CheckoutApiTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public void ShouldInstantiateAndRetrieveClientsPrevious()
httpClientFactoryMock.Setup(mock => mock.CreateClient())
.Returns(new HttpClient());
var checkoutConfiguration = new CheckoutConfiguration(sdkCredentialsMock.Object, Environment.Sandbox, null,
httpClientFactoryMock.Object);
httpClientFactoryMock.Object, false);

//Act
Previous.ICheckoutApi checkoutApi = new Previous.CheckoutApi(checkoutConfiguration);
Expand Down Expand Up @@ -48,7 +48,7 @@ public void ShouldInstantiateAndRetrieveClientsDefault()
httpClientFactoryMock.Setup(mock => mock.CreateClient())
.Returns(new HttpClient());
var checkoutConfiguration = new CheckoutConfiguration(sdkCredentialsMock.Object, Environment.Sandbox, null,
httpClientFactoryMock.Object);
httpClientFactoryMock.Object, false);

//Act
ICheckoutApi checkoutApi = new CheckoutApi(checkoutConfiguration);
Expand Down
6 changes: 3 additions & 3 deletions test/CheckoutSdkTest/CheckoutConfigurationTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ private void ShouldCreateConfiguration()
var credentials = new StaticKeysSdkCredentials(ValidDefaultSk, ValidDefaultPk);
var httpClientFactoryMock = new Mock<IHttpClientFactory>();
var configuration =
new CheckoutConfiguration(credentials, Environment.Production, null, httpClientFactoryMock.Object);
new CheckoutConfiguration(credentials, Environment.Production, null, httpClientFactoryMock.Object, false);
configuration.Environment.ShouldBe(Environment.Production);
configuration.SdkCredentials.ShouldBeAssignableTo(typeof(StaticKeysSdkCredentials));
}
Expand All @@ -30,7 +30,7 @@ public void ShouldCreateConfigurationWithSubdomain(string subdomain, string expe
var httpClientFactoryMock = new Mock<IHttpClientFactory>();
var environmentSubdomain = new EnvironmentSubdomain(Environment.Sandbox, subdomain);
var configuration = new CheckoutConfiguration(credentials, Environment.Sandbox, environmentSubdomain,
httpClientFactoryMock.Object);
httpClientFactoryMock.Object, false);

configuration.Environment.ShouldBe(Environment.Sandbox);
configuration.EnvironmentSubdomain.ApiUri.ToString().ShouldBe(expectedUri);
Expand All @@ -50,7 +50,7 @@ public void ShouldCreateConfigurationWithBadSubdomain(string subdomain, string e
var httpClientFactoryMock = new Mock<IHttpClientFactory>();
var environmentSubdomain = new EnvironmentSubdomain(Environment.Sandbox, subdomain);
var configuration = new CheckoutConfiguration(credentials, Environment.Sandbox, environmentSubdomain,
httpClientFactoryMock.Object);
httpClientFactoryMock.Object, false);

configuration.Environment.ShouldBe(Environment.Sandbox);
configuration.EnvironmentSubdomain.ApiUri.ToString().ShouldBe(expectedUri);
Expand Down
Loading

0 comments on commit 99cee71

Please sign in to comment.