Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions src/main/java/com/stripe/net/ClientTelemetryPayload.java

This file was deleted.

22 changes: 7 additions & 15 deletions src/main/java/com/stripe/net/LiveStripeResponseGetter.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,15 @@
import java.net.PasswordAuthentication;
import java.net.URL;
import java.net.URLStreamHandler;
import java.time.Duration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.concurrent.ConcurrentLinkedQueue;
import lombok.Cleanup;

public class LiveStripeResponseGetter implements StripeResponseGetter {
private static final String DNS_CACHE_TTL_PROPERTY_NAME = "networkaddress.cache.ttl";
private static final int MAX_REQUEST_METRICS_BUFFER_SIZE = 100;

/*
* Set this property to override your environment's default
Expand All @@ -50,6 +49,8 @@ public class LiveStripeResponseGetter implements StripeResponseGetter {
private static final String CUSTOM_URL_STREAM_HANDLER_PROPERTY_NAME =
"com.stripe.net.customURLStreamHandler";

private static final RequestTelemetry requestTelemetry = new RequestTelemetry();

@Override
public <T> T request(
ApiResource.RequestMethod method,
Expand Down Expand Up @@ -138,11 +139,7 @@ static Map<String, String> getHeaders(RequestOptions options) {
headers.put("Stripe-Account", options.getStripeAccount());
}

RequestMetrics lastRequestMetrics = prevRequestMetrics.poll();
if (Stripe.enableTelemetry && lastRequestMetrics != null) {
headers.put(
"X-Stripe-Client-Telemetry", ApiResource.GSON.toJson(lastRequestMetrics.payload()));
}
requestTelemetry.MaybeAddTelemetryHeader(headers);

return headers;
}
Expand Down Expand Up @@ -246,9 +243,6 @@ private static HttpURLConnection createDeleteConnection(
return conn;
}

private static ConcurrentLinkedQueue<RequestMetrics> prevRequestMetrics =
new ConcurrentLinkedQueue<RequestMetrics>();

private static String getResponseBody(InputStream responseStream) throws IOException {
try (final Scanner scanner = new Scanner(responseStream, ApiResource.CHARSET)) {
// \A is the beginning of the stream boundary
Expand Down Expand Up @@ -384,11 +378,11 @@ private static <T> T staticRequest(
ApiResource.RequestType type,
RequestOptions options)
throws StripeException {
long requestStartMs = System.currentTimeMillis();
long requestStartNanos = System.nanoTime();

StripeResponse response = rawRequest(method, url, params, type, options);

long requestDurationMs = System.currentTimeMillis() - requestStartMs;
Duration requestDuration = Duration.ofNanos(System.nanoTime() - requestStartNanos);

int responseCode = response.code();
String responseBody = response.body();
Expand All @@ -410,9 +404,7 @@ private static <T> T staticRequest(
obj.setLastResponse(response);
}

if (Stripe.enableTelemetry && prevRequestMetrics.size() < MAX_REQUEST_METRICS_BUFFER_SIZE) {
prevRequestMetrics.add(new RequestMetrics(requestId, requestDurationMs));
}
requestTelemetry.MaybeEnqueueMetrics(response, requestDuration);

return resource;
}
Expand Down
23 changes: 0 additions & 23 deletions src/main/java/com/stripe/net/RequestMetrics.java

This file was deleted.

82 changes: 82 additions & 0 deletions src/main/java/com/stripe/net/RequestTelemetry.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package com.stripe.net;

import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;
import com.stripe.Stripe;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.ConcurrentLinkedQueue;
import lombok.Data;

/** Helper class used by {@link LiveStripeResponseGetter} to manage request telemetry. */
class RequestTelemetry {
private static final int MAX_REQUEST_METRICS_QUEUE_SIZE = 100;

private static final Gson gson = new Gson();

private static ConcurrentLinkedQueue<RequestMetrics> prevRequestMetrics =
new ConcurrentLinkedQueue<RequestMetrics>();

/**
* If telemetry is enabled and there is at least one metrics item in the queue, then add a {@code
* X-Stripe-Client-Telemetry} header with the item; otherwise, do nothing.
*
* @param headers The request headers.
*/
public void MaybeAddTelemetryHeader(Map<String, String> headers) {
if (headers.containsKey("X-Stripe-Telemetry")) {
return;
}

RequestMetrics requestMetrics = prevRequestMetrics.poll();
if (requestMetrics == null) {
return;
}

if (!Stripe.enableTelemetry) {
return;
}

ClientTelemetryPayload payload = new ClientTelemetryPayload(requestMetrics);
headers.put("X-Stripe-Client-Telemetry", gson.toJson(payload));
}

/**
* If telemetry is enabled and the queue is not full, then enqueue a new metrics item; otherwise,
* do nothing.
*
* @param response The Stripe response.
* @param duration The request duration.
*/
public void MaybeEnqueueMetrics(StripeResponse response, Duration duration) {
if (!Stripe.enableTelemetry) {
return;
}

if (response.requestId() == null) {
return;
}

if (prevRequestMetrics.size() >= MAX_REQUEST_METRICS_QUEUE_SIZE) {
return;
}

RequestMetrics metrics = new RequestMetrics(response.requestId(), duration.toMillis());
prevRequestMetrics.add(metrics);
}

@Data
private static class ClientTelemetryPayload {
@SerializedName("last_request_metrics")
private final RequestMetrics lastRequestMetrics;
}

@Data
private static class RequestMetrics {
@SerializedName("request_id")
private final String requestId;

@SerializedName("request_duration_ms")
private final long requestDurationMs;
}
}
53 changes: 38 additions & 15 deletions src/test/java/com/stripe/functional/TelemetryTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.stripe.BaseStripeTest;
import com.stripe.Stripe;
import com.stripe.exception.StripeException;
import com.stripe.model.Balance;
import com.stripe.net.ApiResource;
import com.stripe.net.ClientTelemetryPayload;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
Expand All @@ -24,6 +24,8 @@
public class TelemetryTest extends BaseStripeTest {
@Test
public void testTelemetryEnabled() throws StripeException, IOException, InterruptedException {
final JsonParser jsonParser = new JsonParser();

@Cleanup MockWebServer server = new MockWebServer();
server.enqueue(
new MockResponse()
Expand Down Expand Up @@ -52,18 +54,30 @@ public void testTelemetryEnabled() throws StripeException, IOException, Interrup
Balance.retrieve();
RecordedRequest request2 = server.takeRequest();
String telemetry1 = request2.getHeader("X-Stripe-Client-Telemetry");
ClientTelemetryPayload payload1 =
ApiResource.GSON.fromJson(telemetry1, ClientTelemetryPayload.class);
assertEquals(payload1.lastRequestMetrics.requestId, "req_1");
assertTrue(payload1.lastRequestMetrics.requestDurationMs > 30);
JsonObject requestMetrics1 =
jsonParser
.parse(telemetry1)
.getAsJsonObject()
.get("last_request_metrics")
.getAsJsonObject();
String requestId1 = requestMetrics1.get("request_id").getAsString();
Long requestDurationMs1 = requestMetrics1.get("request_duration_ms").getAsLong();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is a little bit more awkward because we can no longer deserialize the JSON string into a ClientTelemetryPayload instance so we're using a raw JsonObject instead.

assertEquals("req_1", requestId1);
assertTrue(requestDurationMs1 > 30);

Balance.retrieve();
RecordedRequest request3 = server.takeRequest();
String telemetry2 = request3.getHeader("X-Stripe-Client-Telemetry");
ClientTelemetryPayload payload2 =
ApiResource.GSON.fromJson(telemetry2, ClientTelemetryPayload.class);
assertEquals(payload2.lastRequestMetrics.requestId, "req_2");
assertTrue(payload2.lastRequestMetrics.requestDurationMs > 120);
JsonObject requestMetrics2 =
jsonParser
.parse(telemetry2)
.getAsJsonObject()
.get("last_request_metrics")
.getAsJsonObject();
String requestId2 = requestMetrics2.get("request_id").getAsString();
Long requestDurationMs2 = requestMetrics2.get("request_duration_ms").getAsLong();
assertEquals("req_2", requestId2);
assertTrue(requestDurationMs2 > 30);

server.shutdown();
}
Expand Down Expand Up @@ -92,14 +106,16 @@ public void testTelemetryDisabled() throws StripeException, IOException, Interru

@Test
public void testTelemetryWorksWithConcurrentRequests() throws IOException, InterruptedException {
final JsonParser jsonParser = new JsonParser();

@Cleanup MockWebServer server = new MockWebServer();

for (int i = 0; i < 20; i++) {
server.enqueue(
new MockResponse()
.setBody("{}")
.addHeader("Request-Id", "req_" + i)
.setBodyDelay(50, TimeUnit.MILLISECONDS));
.setBodyDelay(100, TimeUnit.MILLISECONDS));
}
server.start();

Expand Down Expand Up @@ -144,15 +160,22 @@ public void run() {

for (int i = 0; i < 10; i++) {
RecordedRequest request = server.takeRequest();
assertNull(request.getHeader("X-Stripe-Client-Telemetry"));
assertNull(
request.getHeader("X-Stripe-Client-Telemetry"),
String.format("Expected telemetry header to be absent for request #%d", i));
}

for (int i = 0; i < 10; i++) {
RecordedRequest request = server.takeRequest();
String telemetry = request.getHeader("X-Stripe-Client-Telemetry");
ClientTelemetryPayload payload =
ApiResource.GSON.fromJson(telemetry, ClientTelemetryPayload.class);
seenRequestIds.add(payload.lastRequestMetrics.requestId);
JsonObject requestMetrics =
jsonParser
.parse(telemetry)
.getAsJsonObject()
.get("last_request_metrics")
.getAsJsonObject();
String requestId = requestMetrics.get("request_id").getAsString();
seenRequestIds.add(requestId);
}

// check that each telemetry payload corresponds to a unique request id
Expand Down