diff --git a/src/main/java/dev/openfga/sdk/api/OpenFgaApi.java b/src/main/java/dev/openfga/sdk/api/OpenFgaApi.java index 0ea565a2..f9b780c2 100644 --- a/src/main/java/dev/openfga/sdk/api/OpenFgaApi.java +++ b/src/main/java/dev/openfga/sdk/api/OpenFgaApi.java @@ -77,9 +77,17 @@ public OpenFgaApi(Configuration configuration) throws FgaInvalidParameterExcepti } public OpenFgaApi(Configuration configuration, ApiClient apiClient) throws FgaInvalidParameterException { + this(configuration, apiClient, new Telemetry(configuration)); + } + + public OpenFgaApi(Configuration configuration, ApiClient apiClient, Telemetry telemetry) + throws FgaInvalidParameterException { + if (telemetry == null) { + throw new IllegalArgumentException("Telemetry cannot be null"); + } this.apiClient = apiClient; this.configuration = configuration; - this.telemetry = new Telemetry(this.configuration); + this.telemetry = telemetry; if (configuration.getCredentials().getCredentialsMethod() == CredentialsMethod.CLIENT_CREDENTIALS) { this.oAuth2Client = new OAuth2Client(configuration, apiClient); @@ -146,7 +154,8 @@ private CompletableFuture> batchCheck( try { HttpRequest request = buildHttpRequest("POST", path, body, configuration); - return new HttpRequestAttempt<>(request, "batchCheck", BatchCheckResponse.class, apiClient, configuration) + return new HttpRequestAttempt<>( + request, "batchCheck", BatchCheckResponse.class, apiClient, configuration, telemetry) .addTelemetryAttributes(telemetryAttributes) .attemptHttpRequest(); } catch (ApiException e) { @@ -202,7 +211,7 @@ private CompletableFuture> check( try { HttpRequest request = buildHttpRequest("POST", path, body, configuration); - return new HttpRequestAttempt<>(request, "check", CheckResponse.class, apiClient, configuration) + return new HttpRequestAttempt<>(request, "check", CheckResponse.class, apiClient, configuration, telemetry) .addTelemetryAttributes(telemetryAttributes) .attemptHttpRequest(); } catch (ApiException e) { @@ -252,7 +261,8 @@ private CompletableFuture> createStore( try { HttpRequest request = buildHttpRequest("POST", path, body, configuration); - return new HttpRequestAttempt<>(request, "createStore", CreateStoreResponse.class, apiClient, configuration) + return new HttpRequestAttempt<>( + request, "createStore", CreateStoreResponse.class, apiClient, configuration, telemetry) .addTelemetryAttributes(telemetryAttributes) .attemptHttpRequest(); } catch (ApiException e) { @@ -301,7 +311,7 @@ private CompletableFuture> deleteStore(String storeId, Configu try { HttpRequest request = buildHttpRequest("DELETE", path, configuration); - return new HttpRequestAttempt<>(request, "deleteStore", Void.class, apiClient, configuration) + return new HttpRequestAttempt<>(request, "deleteStore", Void.class, apiClient, configuration, telemetry) .addTelemetryAttributes(telemetryAttributes) .attemptHttpRequest(); } catch (ApiException e) { @@ -357,7 +367,8 @@ private CompletableFuture> expand( try { HttpRequest request = buildHttpRequest("POST", path, body, configuration); - return new HttpRequestAttempt<>(request, "expand", ExpandResponse.class, apiClient, configuration) + return new HttpRequestAttempt<>( + request, "expand", ExpandResponse.class, apiClient, configuration, telemetry) .addTelemetryAttributes(telemetryAttributes) .attemptHttpRequest(); } catch (ApiException e) { @@ -407,7 +418,8 @@ private CompletableFuture> getStore(String storeId try { HttpRequest request = buildHttpRequest("GET", path, configuration); - return new HttpRequestAttempt<>(request, "getStore", GetStoreResponse.class, apiClient, configuration) + return new HttpRequestAttempt<>( + request, "getStore", GetStoreResponse.class, apiClient, configuration, telemetry) .addTelemetryAttributes(telemetryAttributes) .attemptHttpRequest(); } catch (ApiException e) { @@ -463,7 +475,8 @@ private CompletableFuture> listObjects( try { HttpRequest request = buildHttpRequest("POST", path, body, configuration); - return new HttpRequestAttempt<>(request, "listObjects", ListObjectsResponse.class, apiClient, configuration) + return new HttpRequestAttempt<>( + request, "listObjects", ListObjectsResponse.class, apiClient, configuration, telemetry) .addTelemetryAttributes(telemetryAttributes) .attemptHttpRequest(); } catch (ApiException e) { @@ -516,7 +529,8 @@ private CompletableFuture> listStores( try { HttpRequest request = buildHttpRequest("GET", path, configuration); - return new HttpRequestAttempt<>(request, "listStores", ListStoresResponse.class, apiClient, configuration) + return new HttpRequestAttempt<>( + request, "listStores", ListStoresResponse.class, apiClient, configuration, telemetry) .addTelemetryAttributes(telemetryAttributes) .attemptHttpRequest(); } catch (ApiException e) { @@ -572,7 +586,8 @@ private CompletableFuture> listUsers( try { HttpRequest request = buildHttpRequest("POST", path, body, configuration); - return new HttpRequestAttempt<>(request, "listUsers", ListUsersResponse.class, apiClient, configuration) + return new HttpRequestAttempt<>( + request, "listUsers", ListUsersResponse.class, apiClient, configuration, telemetry) .addTelemetryAttributes(telemetryAttributes) .attemptHttpRequest(); } catch (ApiException e) { @@ -628,7 +643,7 @@ private CompletableFuture> read( try { HttpRequest request = buildHttpRequest("POST", path, body, configuration); - return new HttpRequestAttempt<>(request, "read", ReadResponse.class, apiClient, configuration) + return new HttpRequestAttempt<>(request, "read", ReadResponse.class, apiClient, configuration, telemetry) .addTelemetryAttributes(telemetryAttributes) .attemptHttpRequest(); } catch (ApiException e) { @@ -687,7 +702,12 @@ private CompletableFuture> readAssertions( try { HttpRequest request = buildHttpRequest("GET", path, configuration); return new HttpRequestAttempt<>( - request, "readAssertions", ReadAssertionsResponse.class, apiClient, configuration) + request, + "readAssertions", + ReadAssertionsResponse.class, + apiClient, + configuration, + telemetry) .addTelemetryAttributes(telemetryAttributes) .attemptHttpRequest(); } catch (ApiException e) { @@ -749,7 +769,8 @@ private CompletableFuture> readAutho "readAuthorizationModel", ReadAuthorizationModelResponse.class, apiClient, - configuration) + configuration, + telemetry) .addTelemetryAttributes(telemetryAttributes) .attemptHttpRequest(); } catch (ApiException e) { @@ -813,7 +834,8 @@ private CompletableFuture> readAuth "readAuthorizationModels", ReadAuthorizationModelsResponse.class, apiClient, - configuration) + configuration, + telemetry) .addTelemetryAttributes(telemetryAttributes) .attemptHttpRequest(); } catch (ApiException e) { @@ -899,7 +921,8 @@ private CompletableFuture> readChanges( try { HttpRequest request = buildHttpRequest("GET", path, configuration); - return new HttpRequestAttempt<>(request, "readChanges", ReadChangesResponse.class, apiClient, configuration) + return new HttpRequestAttempt<>( + request, "readChanges", ReadChangesResponse.class, apiClient, configuration, telemetry) .addTelemetryAttributes(telemetryAttributes) .attemptHttpRequest(); } catch (ApiException e) { @@ -961,7 +984,8 @@ private CompletableFuture "streamedListObjects", StreamResultOfStreamedListObjectsResponse.class, apiClient, - configuration) + configuration, + telemetry) .addTelemetryAttributes(telemetryAttributes) .attemptHttpRequest(); } catch (ApiException e) { @@ -1016,7 +1040,7 @@ private CompletableFuture> write(String storeId, WriteReques try { HttpRequest request = buildHttpRequest("POST", path, body, configuration); - return new HttpRequestAttempt<>(request, "write", Object.class, apiClient, configuration) + return new HttpRequestAttempt<>(request, "write", Object.class, apiClient, configuration, telemetry) .addTelemetryAttributes(telemetryAttributes) .attemptHttpRequest(); } catch (ApiException e) { @@ -1083,7 +1107,7 @@ private CompletableFuture> writeAssertions( try { HttpRequest request = buildHttpRequest("PUT", path, body, configuration); - return new HttpRequestAttempt<>(request, "writeAssertions", Void.class, apiClient, configuration) + return new HttpRequestAttempt<>(request, "writeAssertions", Void.class, apiClient, configuration, telemetry) .addTelemetryAttributes(telemetryAttributes) .attemptHttpRequest(); } catch (ApiException e) { @@ -1145,7 +1169,8 @@ private CompletableFuture> writeAut "writeAuthorizationModel", WriteAuthorizationModelResponse.class, apiClient, - configuration) + configuration, + telemetry) .addTelemetryAttributes(telemetryAttributes) .attemptHttpRequest(); } catch (ApiException e) { diff --git a/src/main/java/dev/openfga/sdk/api/auth/OAuth2Client.java b/src/main/java/dev/openfga/sdk/api/auth/OAuth2Client.java index 73916e91..21f17c5a 100644 --- a/src/main/java/dev/openfga/sdk/api/auth/OAuth2Client.java +++ b/src/main/java/dev/openfga/sdk/api/auth/OAuth2Client.java @@ -78,7 +78,8 @@ private CompletableFuture exchangeToken() ApiClient.formRequestBuilder("POST", "", this.authRequest.buildFormRequestBody(), config); HttpRequest request = requestBuilder.build(); - return new HttpRequestAttempt<>(request, "exchangeToken", CredentialsFlowResponse.class, apiClient, config) + return new HttpRequestAttempt<>( + request, "exchangeToken", CredentialsFlowResponse.class, apiClient, config, telemetry) .attemptHttpRequest() .thenApply(ApiResponse::getData); } diff --git a/src/main/java/dev/openfga/sdk/api/client/ApiExecutor.java b/src/main/java/dev/openfga/sdk/api/client/ApiExecutor.java index a6271e34..3f6eac44 100644 --- a/src/main/java/dev/openfga/sdk/api/client/ApiExecutor.java +++ b/src/main/java/dev/openfga/sdk/api/client/ApiExecutor.java @@ -3,6 +3,7 @@ import dev.openfga.sdk.api.configuration.Configuration; import dev.openfga.sdk.errors.ApiException; import dev.openfga.sdk.errors.FgaInvalidParameterException; +import dev.openfga.sdk.telemetry.Telemetry; import java.io.IOException; import java.net.http.HttpRequest; import java.util.Map; @@ -29,6 +30,7 @@ public class ApiExecutor { private final ApiClient apiClient; private final Configuration configuration; + private final Telemetry telemetry; /** * Constructs an ApiExecutor instance. Typically called via {@link OpenFgaClient#apiExecutor()}. @@ -37,14 +39,29 @@ public class ApiExecutor { * @param configuration Client configuration */ public ApiExecutor(ApiClient apiClient, Configuration configuration) { + this(apiClient, configuration, new Telemetry(configuration)); + } + + /** + * Constructs an ApiExecutor instance. Typically called via {@link OpenFgaClient#apiExecutor()}. + * + * @param apiClient API client for HTTP operations + * @param configuration Client configuration + * @param telemetry Telemetry instance for collecting metrics + */ + public ApiExecutor(ApiClient apiClient, Configuration configuration, Telemetry telemetry) { if (apiClient == null) { throw new IllegalArgumentException("ApiClient cannot be null"); } if (configuration == null) { throw new IllegalArgumentException("Configuration cannot be null"); } + if (telemetry == null) { + throw new IllegalArgumentException("Telemetry cannot be null"); + } this.apiClient = apiClient; this.configuration = configuration; + this.telemetry = telemetry; } /** @@ -87,7 +104,7 @@ public CompletableFuture> send(ApiExecutorRequestBuilder requ String methodName = "apiExecutor:" + requestBuilder.getMethod() + ":" + requestBuilder.getPath(); - return new HttpRequestAttempt<>(httpRequest, methodName, responseType, apiClient, configuration) + return new HttpRequestAttempt<>(httpRequest, methodName, responseType, apiClient, configuration, telemetry) .attemptHttpRequest(); } catch (IOException e) { diff --git a/src/main/java/dev/openfga/sdk/api/client/HttpRequestAttempt.java b/src/main/java/dev/openfga/sdk/api/client/HttpRequestAttempt.java index 1a2d657f..88f33f52 100644 --- a/src/main/java/dev/openfga/sdk/api/client/HttpRequestAttempt.java +++ b/src/main/java/dev/openfga/sdk/api/client/HttpRequestAttempt.java @@ -37,13 +37,27 @@ public class HttpRequestAttempt { public HttpRequestAttempt( HttpRequest request, String name, Class clazz, ApiClient apiClient, Configuration configuration) throws FgaInvalidParameterException { + this(request, name, clazz, apiClient, configuration, new Telemetry(configuration)); + } + + public HttpRequestAttempt( + HttpRequest request, + String name, + Class clazz, + ApiClient apiClient, + Configuration configuration, + Telemetry telemetry) + throws FgaInvalidParameterException { assertParamExists(configuration.getMaxRetries(), "maxRetries", "Configuration"); + if (telemetry == null) { + throw new IllegalArgumentException("Telemetry cannot be null"); + } this.apiClient = apiClient; this.configuration = configuration; this.name = name; this.request = request; this.clazz = clazz; - this.telemetry = new Telemetry(configuration); + this.telemetry = telemetry; this.telemetryAttributes = new HashMap<>(); } diff --git a/src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java b/src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java index 98a29ae1..1afd3fce 100644 --- a/src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java +++ b/src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java @@ -9,6 +9,7 @@ import dev.openfga.sdk.api.model.*; import dev.openfga.sdk.constants.FgaConstants; import dev.openfga.sdk.errors.*; +import dev.openfga.sdk.telemetry.Telemetry; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -22,6 +23,7 @@ public class OpenFgaClient { private final ApiClient apiClient; + private Telemetry telemetry; private ClientConfiguration configuration; private OpenFgaApi api; @@ -32,7 +34,8 @@ public OpenFgaClient(ClientConfiguration configuration) throws FgaInvalidParamet public OpenFgaClient(ClientConfiguration configuration, ApiClient apiClient) throws FgaInvalidParameterException { this.apiClient = apiClient; this.configuration = configuration; - this.api = new OpenFgaApi(configuration, apiClient); + this.telemetry = new Telemetry(configuration); + this.api = new OpenFgaApi(configuration, apiClient, telemetry); } /* *********** @@ -63,7 +66,7 @@ public OpenFgaApi getApi() { * @return ApiExecutor instance */ public ApiExecutor apiExecutor() { - return new ApiExecutor(this.apiClient, this.configuration); + return new ApiExecutor(this.apiClient, this.configuration, this.telemetry); } public void setStoreId(String storeId) { @@ -76,7 +79,8 @@ public void setAuthorizationModelId(String authorizationModelId) { public void setConfiguration(ClientConfiguration configuration) throws FgaInvalidParameterException { this.configuration = configuration; - this.api = new OpenFgaApi(configuration, apiClient); + this.telemetry = new Telemetry(configuration); + this.api = new OpenFgaApi(configuration, apiClient, telemetry); } /* ******** diff --git a/src/main/java/dev/openfga/sdk/telemetry/Metrics.java b/src/main/java/dev/openfga/sdk/telemetry/Metrics.java index 41f4d9af..88483262 100644 --- a/src/main/java/dev/openfga/sdk/telemetry/Metrics.java +++ b/src/main/java/dev/openfga/sdk/telemetry/Metrics.java @@ -6,8 +6,8 @@ import io.opentelemetry.api.metrics.DoubleHistogram; import io.opentelemetry.api.metrics.LongCounter; import io.opentelemetry.api.metrics.Meter; -import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; /** * The Metrics class provides methods for creating and publishing metrics using OpenTelemetry. @@ -24,8 +24,8 @@ public Metrics() { public Metrics(Configuration configuration) { this.meter = GlobalOpenTelemetry.get().getMeterProvider().get("openfga-sdk"); - this.counters = new HashMap<>(); - this.histograms = new HashMap<>(); + this.counters = new ConcurrentHashMap<>(); + this.histograms = new ConcurrentHashMap<>(); this.configuration = configuration; if (this.configuration.getTelemetryConfiguration() == null) { this.configuration.telemetryConfiguration(new TelemetryConfiguration()); @@ -55,15 +55,9 @@ public LongCounter getCounter(Counter counter, Long value, Map meter.counterBuilder(name) + .setDescription(counter.getDescription()) + .build()); if (value != null) { counterInstance.add(value, Attributes.prepare(attributes, counter, configuration)); @@ -87,16 +81,11 @@ public DoubleHistogram getHistogram(Histogram histogram, Double value, Map meter.histogramBuilder(name) + .setDescription(histogram.getDescription()) + .setUnit(histogram.getUnit()) + .build()); if (value != null) { histogramInstance.record(value, Attributes.prepare(attributes, histogram, configuration)); diff --git a/src/main/java/dev/openfga/sdk/telemetry/Telemetry.java b/src/main/java/dev/openfga/sdk/telemetry/Telemetry.java index 1fdf4a0b..aa2c9f3d 100644 --- a/src/main/java/dev/openfga/sdk/telemetry/Telemetry.java +++ b/src/main/java/dev/openfga/sdk/telemetry/Telemetry.java @@ -6,8 +6,8 @@ * The Telemetry class provides access to telemetry-related functionality. */ public class Telemetry { - private Configuration configuration = null; - private Metrics metrics = null; + private final Configuration configuration; + private volatile Metrics metrics; public Telemetry(Configuration configuration) { this.configuration = configuration; @@ -16,12 +16,19 @@ public Telemetry(Configuration configuration) { /** * Returns a Metrics singleton for collecting telemetry data. * If the Metrics singleton has not previously been initialized, it will be created. + * This method is thread-safe via double-checked locking. */ public Metrics metrics() { - if (metrics == null) { - metrics = new Metrics(configuration); + Metrics result = metrics; + if (result == null) { + synchronized (this) { + result = metrics; + if (result == null) { + result = new Metrics(configuration); + metrics = result; + } + } } - - return metrics; + return result; } } diff --git a/src/test/java/dev/openfga/sdk/api/OpenFgaApiTest.java b/src/test/java/dev/openfga/sdk/api/OpenFgaApiTest.java index b222fca2..6e8a7f55 100644 --- a/src/test/java/dev/openfga/sdk/api/OpenFgaApiTest.java +++ b/src/test/java/dev/openfga/sdk/api/OpenFgaApiTest.java @@ -2138,4 +2138,11 @@ DEFAULT_STORE_ID, DEFAULT_AUTH_MODEL_ID, new WriteAssertionsRequest()) assertEquals( "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}", exception.getResponseData()); } + + @Test + public void shouldRejectNullTelemetry() { + assertThrows( + IllegalArgumentException.class, + () -> new OpenFgaApi(new Configuration().apiUrl("https://localhost"), new ApiClient(), null)); + } } diff --git a/src/test/java/dev/openfga/sdk/api/client/ApiExecutorTest.java b/src/test/java/dev/openfga/sdk/api/client/ApiExecutorTest.java index 6db3cf50..af64a9ed 100644 --- a/src/test/java/dev/openfga/sdk/api/client/ApiExecutorTest.java +++ b/src/test/java/dev/openfga/sdk/api/client/ApiExecutorTest.java @@ -381,4 +381,18 @@ public void rawApi_throwsExceptionForNullResponseType() throws Exception { ApiExecutorRequestBuilder.builder(HttpMethod.GET, "/test").build(); assertThrows(IllegalArgumentException.class, () -> client.apiExecutor().send(request, null)); } + + @Test + public void twoParamConstructor_shouldCreateWithOwnTelemetry() throws Exception { + // Verifies the backward-compatible 2-param constructor works + ClientConfiguration config = new ClientConfiguration().apiUrl(fgaApiUrl).storeId(DEFAULT_STORE_ID); + ApiExecutor executor = new ApiExecutor(new ApiClient(), config); + assertNotNull(executor); + } + + @Test + public void threeParamConstructor_shouldRejectNullTelemetry() { + ClientConfiguration config = new ClientConfiguration().apiUrl(fgaApiUrl).storeId(DEFAULT_STORE_ID); + assertThrows(IllegalArgumentException.class, () -> new ApiExecutor(new ApiClient(), config, null)); + } } diff --git a/src/test/java/dev/openfga/sdk/api/client/HttpRequestAttemptRetryTest.java b/src/test/java/dev/openfga/sdk/api/client/HttpRequestAttemptRetryTest.java index 33020bb0..5d05b915 100644 --- a/src/test/java/dev/openfga/sdk/api/client/HttpRequestAttemptRetryTest.java +++ b/src/test/java/dev/openfga/sdk/api/client/HttpRequestAttemptRetryTest.java @@ -13,6 +13,7 @@ import dev.openfga.sdk.errors.ApiException; import dev.openfga.sdk.errors.FgaApiInternalError; import dev.openfga.sdk.errors.FgaError; +import dev.openfga.sdk.telemetry.Telemetry; import java.net.http.HttpRequest; import java.time.Duration; import java.time.Instant; @@ -26,6 +27,7 @@ class HttpRequestAttemptRetryTest { private WireMockServer wireMockServer; private ClientConfiguration configuration; private ApiClient apiClient; + private Telemetry telemetry; @BeforeEach void setUp() { @@ -39,6 +41,7 @@ void setUp() { .minimumRetryDelay(Duration.ofMillis(10)); // Short delay for testing apiClient = new ApiClient(); + telemetry = new Telemetry(configuration); } @AfterEach @@ -73,7 +76,7 @@ void shouldRetryWith429AndRetryAfterHeader() throws Exception { .build(); HttpRequestAttempt attempt = - new HttpRequestAttempt<>(request, "test", Void.class, apiClient, configuration); + new HttpRequestAttempt<>(request, "test", Void.class, apiClient, configuration, telemetry); // When ApiResponse response = attempt.attemptHttpRequest().get(); @@ -110,7 +113,7 @@ void shouldRetryWith500AndRetryAfterHeaderForGetRequest() throws Exception { .build(); HttpRequestAttempt attempt = - new HttpRequestAttempt<>(request, "test", Void.class, apiClient, configuration); + new HttpRequestAttempt<>(request, "test", Void.class, apiClient, configuration, telemetry); // When ApiResponse response = attempt.attemptHttpRequest().get(); @@ -134,7 +137,7 @@ void shouldRetryWith500WithoutRetryAfterHeaderForPostRequest() throws Exception .build(); HttpRequestAttempt attempt = - new HttpRequestAttempt<>(request, "test", Void.class, apiClient, configuration); + new HttpRequestAttempt<>(request, "test", Void.class, apiClient, configuration, telemetry); // When & Then ExecutionException exception = assertThrows( @@ -174,7 +177,7 @@ void shouldRetryWith500WithRetryAfterHeaderForPostRequest() throws Exception { .build(); HttpRequestAttempt attempt = - new HttpRequestAttempt<>(request, "test", Void.class, apiClient, configuration); + new HttpRequestAttempt<>(request, "test", Void.class, apiClient, configuration, telemetry); // When ApiResponse response = attempt.attemptHttpRequest().get(); @@ -198,7 +201,7 @@ void shouldNotRetryWith501() throws Exception { .build(); HttpRequestAttempt attempt = - new HttpRequestAttempt<>(request, "test", Void.class, apiClient, configuration); + new HttpRequestAttempt<>(request, "test", Void.class, apiClient, configuration, telemetry); // When & Then ExecutionException exception = assertThrows( @@ -224,7 +227,7 @@ void shouldRespectMaxRetries() throws Exception { .build(); HttpRequestAttempt attempt = - new HttpRequestAttempt<>(request, "test", Void.class, apiClient, configuration); + new HttpRequestAttempt<>(request, "test", Void.class, apiClient, configuration, telemetry); // When & Then ExecutionException exception = assertThrows( @@ -260,7 +263,7 @@ void shouldUseExponentialBackoffWhenNoRetryAfterHeader() throws Exception { .build(); HttpRequestAttempt attempt = - new HttpRequestAttempt<>(request, "test", Void.class, apiClient, configuration); + new HttpRequestAttempt<>(request, "test", Void.class, apiClient, configuration, telemetry); // When ApiResponse response = attempt.attemptHttpRequest().get(); @@ -301,7 +304,7 @@ void shouldHandleInvalidRetryAfterHeader() throws Exception { .build(); HttpRequestAttempt attempt = - new HttpRequestAttempt<>(request, "test", Void.class, apiClient, configuration); + new HttpRequestAttempt<>(request, "test", Void.class, apiClient, configuration, telemetry); // When ApiResponse response = attempt.attemptHttpRequest().get(); @@ -331,8 +334,8 @@ void shouldRetryNetworkErrorsWithExponentialBackoff() throws Exception { .timeout(Duration.ofMillis(50)) // Short timeout to force connection error .build(); - HttpRequestAttempt attempt = - new HttpRequestAttempt<>(request, "test", Void.class, apiClient, networkConfig); + HttpRequestAttempt attempt = new HttpRequestAttempt<>( + request, "test", Void.class, apiClient, networkConfig, new Telemetry(networkConfig)); Instant startTime = Instant.now(); @@ -371,8 +374,8 @@ void shouldHonorNetworkErrorRetryDelayTiming() throws Exception { .timeout(Duration.ofMillis(50)) // Short timeout to force connection error quickly .build(); - HttpRequestAttempt attempt = - new HttpRequestAttempt<>(request, "test", Void.class, apiClient, networkConfig); + HttpRequestAttempt attempt = new HttpRequestAttempt<>( + request, "test", Void.class, apiClient, networkConfig, new Telemetry(networkConfig)); Instant startTime = Instant.now(); @@ -408,7 +411,8 @@ void shouldUseExponentialBackoffForNetworkErrorsWithPreciseTiming() throws Excep .timeout(Duration.ofMillis(500)) // Reasonable timeout .build(); - HttpRequestAttempt attempt = new HttpRequestAttempt<>(request, "test", Void.class, apiClient, dnsConfig); + HttpRequestAttempt attempt = + new HttpRequestAttempt<>(request, "test", Void.class, apiClient, dnsConfig, new Telemetry(dnsConfig)); Instant startTime = Instant.now(); @@ -446,7 +450,8 @@ void shouldRetryOnUnknownHost() throws Exception { .timeout(Duration.ofMillis(1000)) .build(); - HttpRequestAttempt attempt = new HttpRequestAttempt<>(request, "test", Void.class, apiClient, dnsConfig); + HttpRequestAttempt attempt = + new HttpRequestAttempt<>(request, "test", Void.class, apiClient, dnsConfig, new Telemetry(dnsConfig)); // When & Then ExecutionException exception = assertThrows( @@ -506,8 +511,8 @@ void shouldRespectGlobalMinimumRetryDelayWithExponentialBackoff() throws Excepti .maxRetries(2) .minimumRetryDelay(Duration.ofMillis(100)); // Should act as floor for exponential backoff - HttpRequestAttempt attempt = - new HttpRequestAttempt<>(request, "test", Void.class, apiClient, globalConfig); + HttpRequestAttempt attempt = new HttpRequestAttempt<>( + request, "test", Void.class, apiClient, globalConfig, new Telemetry(globalConfig)); Instant startTime = Instant.now(); @@ -556,8 +561,8 @@ void shouldUseRetryAfterHeaderEvenWhenSmallerThanGlobalMinimumDelay() throws Exc .maxRetries(2) .minimumRetryDelay(Duration.ofMillis(150)); // Should NOT override Retry-After - HttpRequestAttempt attempt = - new HttpRequestAttempt<>(request, "test", Void.class, apiClient, globalConfig); + HttpRequestAttempt attempt = new HttpRequestAttempt<>( + request, "test", Void.class, apiClient, globalConfig, new Telemetry(globalConfig)); Instant startTime = Instant.now(); @@ -604,8 +609,8 @@ void shouldUseRetryAfterWhenLargerThanGlobalMinimumDelay() throws Exception { .maxRetries(2) .minimumRetryDelay(Duration.ofMillis(50)); // Should NOT override Retry-After: 100ms - HttpRequestAttempt attempt = - new HttpRequestAttempt<>(request, "test", Void.class, apiClient, globalConfig); + HttpRequestAttempt attempt = new HttpRequestAttempt<>( + request, "test", Void.class, apiClient, globalConfig, new Telemetry(globalConfig)); Instant startTime = Instant.now(); @@ -643,8 +648,8 @@ void shouldRespectPerRequestMinimumRetryDelayOverride() throws Exception { configuration.override(new dev.openfga.sdk.api.configuration.ConfigurationOverride() .minimumRetryDelay(Duration.ofMillis(100))); - HttpRequestAttempt attempt = - new HttpRequestAttempt<>(request, "test", Void.class, apiClient, overriddenConfig); + HttpRequestAttempt attempt = new HttpRequestAttempt<>( + request, "test", Void.class, apiClient, overriddenConfig, new Telemetry(overriddenConfig)); Instant startTime = Instant.now(); @@ -687,8 +692,8 @@ void shouldRespectPerRequestMaxRetriesOverride() throws Exception { .POST(HttpRequest.BodyPublishers.ofString("{}")) .build(); - HttpRequestAttempt attempt = - new HttpRequestAttempt<>(request, "check", Void.class, apiClient, effectiveConfig); + HttpRequestAttempt attempt = new HttpRequestAttempt<>( + request, "check", Void.class, apiClient, effectiveConfig, new Telemetry(effectiveConfig)); attempt.attemptHttpRequest().get(); }); @@ -726,8 +731,8 @@ void shouldUseRetryAfterHeaderEvenWhenSmallerThanMinimumDelay() throws Exception configuration.override(new dev.openfga.sdk.api.configuration.ConfigurationOverride() .minimumRetryDelay(Duration.ofMillis(150))); - HttpRequestAttempt attempt = - new HttpRequestAttempt<>(request, "test", Void.class, apiClient, overriddenConfig); + HttpRequestAttempt attempt = new HttpRequestAttempt<>( + request, "test", Void.class, apiClient, overriddenConfig, new Telemetry(overriddenConfig)); Instant startTime = Instant.now(); @@ -776,8 +781,8 @@ void shouldNotOverrideRetryAfterWhenItIsLargerThanMinimumDelayPerRequest() throw // Verify the override took effect assertEquals(Duration.ofMillis(500), overriddenConfig.getMinimumRetryDelay()); - HttpRequestAttempt attempt = - new HttpRequestAttempt<>(request, "test", Void.class, apiClient, overriddenConfig); + HttpRequestAttempt attempt = new HttpRequestAttempt<>( + request, "test", Void.class, apiClient, overriddenConfig, new Telemetry(overriddenConfig)); Instant startTime = Instant.now(); @@ -796,4 +801,16 @@ void shouldNotOverrideRetryAfterWhenItIsLargerThanMinimumDelayPerRequest() throw // Verify initial request + 1 retry = 2 total requests wireMockServer.verify(2, getRequestedFor(urlEqualTo("/test"))); } + + @Test + void shouldRejectNullTelemetry() { + HttpRequest request = HttpRequest.newBuilder() + .uri(java.net.URI.create("http://localhost:" + wireMockServer.port() + "/test")) + .GET() + .build(); + + assertThrows( + IllegalArgumentException.class, + () -> new HttpRequestAttempt<>(request, "test", Void.class, apiClient, configuration, null)); + } } diff --git a/src/test/java/dev/openfga/sdk/telemetry/TelemetryTest.java b/src/test/java/dev/openfga/sdk/telemetry/TelemetryTest.java index e42efe64..7a2119f7 100644 --- a/src/test/java/dev/openfga/sdk/telemetry/TelemetryTest.java +++ b/src/test/java/dev/openfga/sdk/telemetry/TelemetryTest.java @@ -3,6 +3,11 @@ import static org.assertj.core.api.Assertions.assertThat; import dev.openfga.sdk.api.configuration.Configuration; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import org.junit.jupiter.api.Test; class TelemetryTest { @@ -18,4 +23,41 @@ void shouldBeASingletonMetricsInitialization() { // then assertThat(firstCall).isNotNull().isSameAs(secondCall); } + + @Test + void shouldReturnSameMetricsInstanceUnderConcurrentAccess() throws InterruptedException { + // given + Telemetry telemetry = new Telemetry(new Configuration()); + int threadCount = 10; + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + CountDownLatch startLatch = new CountDownLatch(1); + List results = new CopyOnWriteArrayList<>(); + + try { + // when + for (int i = 0; i < threadCount; i++) { + executor.submit(() -> { + try { + startLatch.await(); + results.add(telemetry.metrics()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + } + startLatch.countDown(); + executor.shutdown(); + boolean terminated = executor.awaitTermination(5, java.util.concurrent.TimeUnit.SECONDS); + assertThat(terminated).isTrue(); + + // then + assertThat(results).hasSize(threadCount); + Metrics expected = results.get(0); + assertThat(results).allSatisfy(m -> assertThat(m).isSameAs(expected)); + } finally { + if (!executor.isTerminated()) { + executor.shutdownNow(); + } + } + } }