Skip to content

Commit

Permalink
Add telemetry (#454)
Browse files Browse the repository at this point in the history
  • Loading branch information
armando-rodriguez-cko authored Dec 12, 2024
1 parent 730c090 commit 5653179
Show file tree
Hide file tree
Showing 17 changed files with 527 additions and 47 deletions.
19 changes: 16 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,13 @@ dependencies {

## How to use the SDK

This SDK can be used with two different pair of API keys provided by Checkout. However, using different API keys imply using specific API features. </br>
This SDK can be used with two different pair of API keys provided by Checkout. However, using different API keys imply using specific API features. </br>
Please find in the table below the types of keys that can be used within this SDK.

| Account System | Public Key (example) | Secret Key (example) |
|----------------|-----------------------------------------|-----------------------------------------|
| Default | pk_pkhpdtvabcf7hdgpwnbhw7r2uic | sk_m73dzypy7cf3gf5d2xr4k7sxo4e |
| Previous | pk_g650ff27-7c42-4ce1-ae90-5691a188ee7b | sk_gk3517a8-3z01-45fq-b4bd-4282384b0a64 |
| Default | pk_abcdef123456ghijkl789mnopqr | sk_123456ghijklm7890abcdefxyz |
| Previous | pk_12345678-abcd-efgh-ijkl-mnopqrstuvwx | sk_abcdef12-3456-ghij-klmn-opqrstuvwxyz |

Note: sandbox keys have a `sbox_` or `test_` identifier, for Default and Previous accounts respectively.

Expand Down Expand Up @@ -207,6 +207,19 @@ 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 Java 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:
```java
final CheckoutApi checkoutApi = CheckoutSdk.builder()
.staticKeys()
.secretKey("secret_key")
.environment(Environment.PRODUCTION)
.recordTelemetry(false)
.build();
```

## Code of Conduct

Please refer to [Code of Conduct](CODE_OF_CONDUCT.md)
Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ dependencies {
testImplementation 'org.apache.logging.log4j:log4j-api:2.17.1'
testImplementation 'org.apache.logging.log4j:log4j-core:2.17.1'
testImplementation 'org.apache.logging.log4j:log4j-slf4j-impl:2.17.1'

testImplementation 'org.apache.httpcomponents:httpclient:4.5.13'
}

buildScan {
Expand Down
8 changes: 7 additions & 1 deletion src/main/java/com/checkout/AbstractCheckoutSdkBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public abstract class AbstractCheckoutSdkBuilder<T extends CheckoutApiClient> {
private EnvironmentSubdomain environmentSubdomain;
private Executor executor = ForkJoinPool.commonPool();
private TransportConfiguration transportConfiguration;
private Boolean recordTelemetry = true;

public AbstractCheckoutSdkBuilder<T> environment(final IEnvironment environment) {
this.environment = environment;
Expand Down Expand Up @@ -48,6 +49,11 @@ protected EnvironmentSubdomain getEnvironmentSubdomain() {
return environmentSubdomain;
}

public AbstractCheckoutSdkBuilder<T> recordTelemetry(final Boolean recordTelemetry) {
this.recordTelemetry = recordTelemetry;
return this;
}

protected abstract SdkCredentials getSdkCredentials();

protected CheckoutConfiguration getCheckoutConfiguration() {
Expand All @@ -62,7 +68,7 @@ protected CheckoutConfiguration getCheckoutConfiguration() {
}

private CheckoutConfiguration buildCheckoutConfiguration(final SdkCredentials sdkCredentials) {
return new DefaultCheckoutConfiguration(sdkCredentials, getEnvironment(), getEnvironmentSubdomain(), httpClientBuilder, executor, transportConfiguration);
return new DefaultCheckoutConfiguration(sdkCredentials, getEnvironment(), getEnvironmentSubdomain(), httpClientBuilder, executor, transportConfiguration, recordTelemetry);
}

public abstract T build();
Expand Down
98 changes: 71 additions & 27 deletions src/main/java/com/checkout/ApacheHttpClientTransport.java
Original file line number Diff line number Diff line change
@@ -1,23 +1,10 @@
package com.checkout;

import static com.checkout.ClientOperation.POST;
import static com.checkout.common.CheckoutUtils.ACCEPT_JSON;
import static com.checkout.common.CheckoutUtils.PROJECT_NAME;
import static com.checkout.common.CheckoutUtils.getVersionFromManifest;
import static org.apache.http.HttpHeaders.ACCEPT;
import static org.apache.http.HttpHeaders.AUTHORIZATION;
import static org.apache.http.HttpHeaders.USER_AGENT;

import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.stream.Collectors;

import com.checkout.accounts.AccountsFileRequest;
import com.checkout.common.AbstractFileRequest;
import com.checkout.common.CheckoutUtils;
import com.checkout.common.FileRequest;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
Expand All @@ -42,12 +29,25 @@
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;

import com.checkout.accounts.AccountsFileRequest;
import com.checkout.common.AbstractFileRequest;
import com.checkout.common.CheckoutUtils;
import com.checkout.common.FileRequest;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.stream.Collectors;

import lombok.extern.slf4j.Slf4j;
import static com.checkout.ClientOperation.POST;
import static com.checkout.common.CheckoutUtils.ACCEPT_JSON;
import static com.checkout.common.CheckoutUtils.PROJECT_NAME;
import static com.checkout.common.CheckoutUtils.getVersionFromManifest;
import static org.apache.http.HttpHeaders.ACCEPT;
import static org.apache.http.HttpHeaders.AUTHORIZATION;
import static org.apache.http.HttpHeaders.USER_AGENT;

@Slf4j
class ApacheHttpClientTransport implements Transport {
Expand All @@ -57,19 +57,30 @@ class ApacheHttpClientTransport implements Transport {
private static final String FILE = "file";
private static final String PURPOSE = "purpose";
private static final String PATH = "path";

private final URI baseUri;
private final CloseableHttpClient httpClient;
private final Executor executor;
private final TransportConfiguration transportConfiguration;
private final CheckoutConfiguration configuration;

ApacheHttpClientTransport(final URI baseUri, final HttpClientBuilder httpClientBuilder, final Executor executor, final TransportConfiguration transportConfiguration) {
private static final ThreadLocal<Map<String, Object>> telemetryData = ThreadLocal.withInitial(HashMap::new);

ApacheHttpClientTransport(
final URI baseUri,
final HttpClientBuilder httpClientBuilder,
final Executor executor,
final TransportConfiguration transportConfiguration,
final CheckoutConfiguration configuration
) {
CheckoutUtils.validateParams("baseUri", baseUri, "httpClientBuilder", httpClientBuilder, "executor", executor);
this.baseUri = baseUri;
this.httpClient = httpClientBuilder
.setRedirectStrategy(new CustomAwsRedirectStrategy())
.build();
this.executor = executor;
this.transportConfiguration = transportConfiguration;
this.configuration = configuration;
}

@Override
Expand Down Expand Up @@ -154,15 +165,29 @@ private Response performCall(final SdkAuthorization authorization,
request.setHeader(ACCEPT, getAcceptHeader(clientOperation));
request.setHeader(AUTHORIZATION, authorization.getAuthorizationHeader());

String currentRequestId = UUID.randomUUID().toString();

if (configuration.isTelemetryEnabled()) {
String telemetryHeader = generateTelemetryHeader(currentRequestId);
request.setHeader("cko-sdk-telemetry", telemetryHeader);
}

long startTime = System.currentTimeMillis();

log.info("Request: " + Arrays.toString(sanitiseHeaders(request.getAllHeaders())));
if (requestBody != null && request instanceof HttpEntityEnclosingRequest) {
((HttpEntityEnclosingRequestBase) request).setEntity(new StringEntity(requestBody, ContentType.APPLICATION_JSON));
}
try (final CloseableHttpResponse response = httpClient.execute(request)) {
long elapsed = System.currentTimeMillis() - startTime;
log.info("Response: " + response.getStatusLine().getStatusCode() + " " + Arrays.toString(response.getAllHeaders()));

updateTelemetryData(currentRequestId, elapsed);

final int statusCode = response.getStatusLine().getStatusCode();
final Map<String, String> headers = Arrays.stream(response.getAllHeaders())
.collect(Collectors.toMap(Header::getName, Header::getValue));

if (statusCode != HttpStatus.SC_NOT_FOUND && response.getEntity() != null && response.getEntity().getContent() != null) {
return Response.builder()
.statusCode(statusCode)
Expand All @@ -174,13 +199,33 @@ private Response performCall(final SdkAuthorization authorization,
} catch (final NoHttpResponseException e) {
log.error("Target server failed to respond with a valid HTTP response.");
return Response.builder().statusCode(HttpStatus.SC_GATEWAY_TIMEOUT).build();

} catch (final Exception e) {
log.error("Exception occurred during the execution of the client...", e);
}
return Response.builder().statusCode(transportConfiguration.getDefaultHttpStatusCode()).build();
}

private String generateTelemetryHeader(String currentRequestId) {
Map<String, Object> data = getTelemetryData();
String prevRequestId = (String) data.get("prevRequestId");
Long prevRequestDuration = (Long) data.get("prevRequestDuration");

return String.format("{\"requestId\":\"%s\",\"prevRequestId\":\"%s\",\"prevRequestDuration\":%d}",
currentRequestId,
prevRequestId != null ? prevRequestId : "N/A",
prevRequestDuration != null ? prevRequestDuration : 0);
}

private static void updateTelemetryData(String requestId, long duration) {
Map<String, Object> data = telemetryData.get();
data.put("prevRequestId", requestId);
data.put("prevRequestDuration", duration);
}

private static Map<String, Object> getTelemetryData() {
return telemetryData.get();
}

private Header[] sanitiseHeaders(final Header[] headers) {
return Arrays.stream(headers)
.filter(it -> !it.getName().equals(AUTHORIZATION))
Expand Down Expand Up @@ -210,5 +255,4 @@ private String getRequestUrl(final String path) {
throw new CheckoutException(e);
}
}

}
}
2 changes: 1 addition & 1 deletion src/main/java/com/checkout/ApiClientImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public class ApiClientImpl implements ApiClient {

public ApiClientImpl(final CheckoutConfiguration configuration, final UriStrategy uriStrategy) {
this.serializer = new GsonSerializer();
this.transport = new ApacheHttpClientTransport(uriStrategy.getUri(), configuration.getHttpClientBuilder(), configuration.getExecutor(), configuration.getTransportConfiguration());
this.transport = new ApacheHttpClientTransport(uriStrategy.getUri(), configuration.getHttpClientBuilder(), configuration.getExecutor(), configuration.getTransportConfiguration(), configuration);
}

@Override
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/com/checkout/CheckoutConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,6 @@ public interface CheckoutConfiguration {

TransportConfiguration getTransportConfiguration();

Boolean isTelemetryEnabled();

}
14 changes: 12 additions & 2 deletions src/main/java/com/checkout/DefaultCheckoutConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,34 +14,39 @@ class DefaultCheckoutConfiguration implements CheckoutConfiguration {
private final IEnvironment environment;
private final EnvironmentSubdomain environmentSubdomain;
private final TransportConfiguration transportConfiguration;
private final boolean recordTelemetry;

DefaultCheckoutConfiguration(final SdkCredentials sdkCredentials,
final IEnvironment environment,
final HttpClientBuilder httpClientBuilder,
final Executor executor,
final TransportConfiguration transportConfiguration) {
final TransportConfiguration transportConfiguration,
final boolean recordTelemetry) {
validateParams("sdkCredentials", sdkCredentials, "environment", environment, "httpClientBuilder", httpClientBuilder, "executor", executor, "transportConfiguration", transportConfiguration);
this.sdkCredentials = sdkCredentials;
this.httpClientBuilder = httpClientBuilder;
this.executor = executor;
this.environment = environment;
this.environmentSubdomain = null;
this.transportConfiguration = transportConfiguration;
this.recordTelemetry = recordTelemetry;
}

DefaultCheckoutConfiguration(final SdkCredentials sdkCredentials,
final IEnvironment environment,
final EnvironmentSubdomain environmentSubdomain,
final HttpClientBuilder httpClientBuilder,
final Executor executor,
final TransportConfiguration transportConfiguration) {
final TransportConfiguration transportConfiguration,
final Boolean recordTelemetry) {
validateParams("sdkCredentials", sdkCredentials, "environment", environment, "httpClientBuilder", httpClientBuilder, "executor", executor, "transportConfiguration", transportConfiguration);
this.sdkCredentials = sdkCredentials;
this.httpClientBuilder = httpClientBuilder;
this.executor = executor;
this.environment = environment;
this.environmentSubdomain = environmentSubdomain;
this.transportConfiguration = transportConfiguration;
this.recordTelemetry = recordTelemetry;
}

@Override
Expand Down Expand Up @@ -73,4 +78,9 @@ public EnvironmentSubdomain getEnvironmentSubdomain() {
public TransportConfiguration getTransportConfiguration() {
return transportConfiguration;
}

@Override
public Boolean isTelemetryEnabled() {
return this.recordTelemetry;
}
}
17 changes: 17 additions & 0 deletions src/main/java/com/checkout/RequestMetrics.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.checkout;

import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
public class RequestMetrics {
private String requestId;
private Long prevRequestDuration;
private String prevRequestId;

public RequestMetrics(Long prevRequestDuration, String prevRequestId) {
this.prevRequestDuration = prevRequestDuration;
this.prevRequestId = prevRequestId;
}
}
Loading

0 comments on commit 5653179

Please sign in to comment.