Skip to content

Commit 82aabf2

Browse files
authored
fix: share single Telemetry instance per SDK client (#290)
* fix: share single Telemetry instance per SDK client (#209) Instead of creating new Telemetry, Metrics, and OpenTelemetry instruments for every HTTP request, share a single Telemetry instance per SDK client. This avoids wasted allocation, fragmented OTel instrument instances, and repeated GlobalOpenTelemetry meter lookups. Also fixes thread-safety bugs that become relevant once the instance is shared across concurrent async requests: - Telemetry.metrics(): volatile field + double-checked locking - Metrics counters/histograms: HashMap → ConcurrentHashMap with computeIfAbsent All existing public constructors are preserved for backward compatibility. Closes #209 * fix: improve TelemetryTest concurrent access test robustness Assert awaitTermination returns true for clear timeout failures, and wrap test body in try/finally with shutdownNow to prevent thread leaks.
1 parent 6db1626 commit 82aabf2

11 files changed

Lines changed: 218 additions & 81 deletions

File tree

src/main/java/dev/openfga/sdk/api/OpenFgaApi.java

Lines changed: 44 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,17 @@ public OpenFgaApi(Configuration configuration) throws FgaInvalidParameterExcepti
7777
}
7878

7979
public OpenFgaApi(Configuration configuration, ApiClient apiClient) throws FgaInvalidParameterException {
80+
this(configuration, apiClient, new Telemetry(configuration));
81+
}
82+
83+
public OpenFgaApi(Configuration configuration, ApiClient apiClient, Telemetry telemetry)
84+
throws FgaInvalidParameterException {
85+
if (telemetry == null) {
86+
throw new IllegalArgumentException("Telemetry cannot be null");
87+
}
8088
this.apiClient = apiClient;
8189
this.configuration = configuration;
82-
this.telemetry = new Telemetry(this.configuration);
90+
this.telemetry = telemetry;
8391

8492
if (configuration.getCredentials().getCredentialsMethod() == CredentialsMethod.CLIENT_CREDENTIALS) {
8593
this.oAuth2Client = new OAuth2Client(configuration, apiClient);
@@ -146,7 +154,8 @@ private CompletableFuture<ApiResponse<BatchCheckResponse>> batchCheck(
146154

147155
try {
148156
HttpRequest request = buildHttpRequest("POST", path, body, configuration);
149-
return new HttpRequestAttempt<>(request, "batchCheck", BatchCheckResponse.class, apiClient, configuration)
157+
return new HttpRequestAttempt<>(
158+
request, "batchCheck", BatchCheckResponse.class, apiClient, configuration, telemetry)
150159
.addTelemetryAttributes(telemetryAttributes)
151160
.attemptHttpRequest();
152161
} catch (ApiException e) {
@@ -202,7 +211,7 @@ private CompletableFuture<ApiResponse<CheckResponse>> check(
202211

203212
try {
204213
HttpRequest request = buildHttpRequest("POST", path, body, configuration);
205-
return new HttpRequestAttempt<>(request, "check", CheckResponse.class, apiClient, configuration)
214+
return new HttpRequestAttempt<>(request, "check", CheckResponse.class, apiClient, configuration, telemetry)
206215
.addTelemetryAttributes(telemetryAttributes)
207216
.attemptHttpRequest();
208217
} catch (ApiException e) {
@@ -252,7 +261,8 @@ private CompletableFuture<ApiResponse<CreateStoreResponse>> createStore(
252261

253262
try {
254263
HttpRequest request = buildHttpRequest("POST", path, body, configuration);
255-
return new HttpRequestAttempt<>(request, "createStore", CreateStoreResponse.class, apiClient, configuration)
264+
return new HttpRequestAttempt<>(
265+
request, "createStore", CreateStoreResponse.class, apiClient, configuration, telemetry)
256266
.addTelemetryAttributes(telemetryAttributes)
257267
.attemptHttpRequest();
258268
} catch (ApiException e) {
@@ -301,7 +311,7 @@ private CompletableFuture<ApiResponse<Void>> deleteStore(String storeId, Configu
301311

302312
try {
303313
HttpRequest request = buildHttpRequest("DELETE", path, configuration);
304-
return new HttpRequestAttempt<>(request, "deleteStore", Void.class, apiClient, configuration)
314+
return new HttpRequestAttempt<>(request, "deleteStore", Void.class, apiClient, configuration, telemetry)
305315
.addTelemetryAttributes(telemetryAttributes)
306316
.attemptHttpRequest();
307317
} catch (ApiException e) {
@@ -357,7 +367,8 @@ private CompletableFuture<ApiResponse<ExpandResponse>> expand(
357367

358368
try {
359369
HttpRequest request = buildHttpRequest("POST", path, body, configuration);
360-
return new HttpRequestAttempt<>(request, "expand", ExpandResponse.class, apiClient, configuration)
370+
return new HttpRequestAttempt<>(
371+
request, "expand", ExpandResponse.class, apiClient, configuration, telemetry)
361372
.addTelemetryAttributes(telemetryAttributes)
362373
.attemptHttpRequest();
363374
} catch (ApiException e) {
@@ -407,7 +418,8 @@ private CompletableFuture<ApiResponse<GetStoreResponse>> getStore(String storeId
407418

408419
try {
409420
HttpRequest request = buildHttpRequest("GET", path, configuration);
410-
return new HttpRequestAttempt<>(request, "getStore", GetStoreResponse.class, apiClient, configuration)
421+
return new HttpRequestAttempt<>(
422+
request, "getStore", GetStoreResponse.class, apiClient, configuration, telemetry)
411423
.addTelemetryAttributes(telemetryAttributes)
412424
.attemptHttpRequest();
413425
} catch (ApiException e) {
@@ -463,7 +475,8 @@ private CompletableFuture<ApiResponse<ListObjectsResponse>> listObjects(
463475

464476
try {
465477
HttpRequest request = buildHttpRequest("POST", path, body, configuration);
466-
return new HttpRequestAttempt<>(request, "listObjects", ListObjectsResponse.class, apiClient, configuration)
478+
return new HttpRequestAttempt<>(
479+
request, "listObjects", ListObjectsResponse.class, apiClient, configuration, telemetry)
467480
.addTelemetryAttributes(telemetryAttributes)
468481
.attemptHttpRequest();
469482
} catch (ApiException e) {
@@ -516,7 +529,8 @@ private CompletableFuture<ApiResponse<ListStoresResponse>> listStores(
516529

517530
try {
518531
HttpRequest request = buildHttpRequest("GET", path, configuration);
519-
return new HttpRequestAttempt<>(request, "listStores", ListStoresResponse.class, apiClient, configuration)
532+
return new HttpRequestAttempt<>(
533+
request, "listStores", ListStoresResponse.class, apiClient, configuration, telemetry)
520534
.addTelemetryAttributes(telemetryAttributes)
521535
.attemptHttpRequest();
522536
} catch (ApiException e) {
@@ -572,7 +586,8 @@ private CompletableFuture<ApiResponse<ListUsersResponse>> listUsers(
572586

573587
try {
574588
HttpRequest request = buildHttpRequest("POST", path, body, configuration);
575-
return new HttpRequestAttempt<>(request, "listUsers", ListUsersResponse.class, apiClient, configuration)
589+
return new HttpRequestAttempt<>(
590+
request, "listUsers", ListUsersResponse.class, apiClient, configuration, telemetry)
576591
.addTelemetryAttributes(telemetryAttributes)
577592
.attemptHttpRequest();
578593
} catch (ApiException e) {
@@ -628,7 +643,7 @@ private CompletableFuture<ApiResponse<ReadResponse>> read(
628643

629644
try {
630645
HttpRequest request = buildHttpRequest("POST", path, body, configuration);
631-
return new HttpRequestAttempt<>(request, "read", ReadResponse.class, apiClient, configuration)
646+
return new HttpRequestAttempt<>(request, "read", ReadResponse.class, apiClient, configuration, telemetry)
632647
.addTelemetryAttributes(telemetryAttributes)
633648
.attemptHttpRequest();
634649
} catch (ApiException e) {
@@ -687,7 +702,12 @@ private CompletableFuture<ApiResponse<ReadAssertionsResponse>> readAssertions(
687702
try {
688703
HttpRequest request = buildHttpRequest("GET", path, configuration);
689704
return new HttpRequestAttempt<>(
690-
request, "readAssertions", ReadAssertionsResponse.class, apiClient, configuration)
705+
request,
706+
"readAssertions",
707+
ReadAssertionsResponse.class,
708+
apiClient,
709+
configuration,
710+
telemetry)
691711
.addTelemetryAttributes(telemetryAttributes)
692712
.attemptHttpRequest();
693713
} catch (ApiException e) {
@@ -749,7 +769,8 @@ private CompletableFuture<ApiResponse<ReadAuthorizationModelResponse>> readAutho
749769
"readAuthorizationModel",
750770
ReadAuthorizationModelResponse.class,
751771
apiClient,
752-
configuration)
772+
configuration,
773+
telemetry)
753774
.addTelemetryAttributes(telemetryAttributes)
754775
.attemptHttpRequest();
755776
} catch (ApiException e) {
@@ -813,7 +834,8 @@ private CompletableFuture<ApiResponse<ReadAuthorizationModelsResponse>> readAuth
813834
"readAuthorizationModels",
814835
ReadAuthorizationModelsResponse.class,
815836
apiClient,
816-
configuration)
837+
configuration,
838+
telemetry)
817839
.addTelemetryAttributes(telemetryAttributes)
818840
.attemptHttpRequest();
819841
} catch (ApiException e) {
@@ -899,7 +921,8 @@ private CompletableFuture<ApiResponse<ReadChangesResponse>> readChanges(
899921

900922
try {
901923
HttpRequest request = buildHttpRequest("GET", path, configuration);
902-
return new HttpRequestAttempt<>(request, "readChanges", ReadChangesResponse.class, apiClient, configuration)
924+
return new HttpRequestAttempt<>(
925+
request, "readChanges", ReadChangesResponse.class, apiClient, configuration, telemetry)
903926
.addTelemetryAttributes(telemetryAttributes)
904927
.attemptHttpRequest();
905928
} catch (ApiException e) {
@@ -961,7 +984,8 @@ private CompletableFuture<ApiResponse<StreamResultOfStreamedListObjectsResponse>
961984
"streamedListObjects",
962985
StreamResultOfStreamedListObjectsResponse.class,
963986
apiClient,
964-
configuration)
987+
configuration,
988+
telemetry)
965989
.addTelemetryAttributes(telemetryAttributes)
966990
.attemptHttpRequest();
967991
} catch (ApiException e) {
@@ -1016,7 +1040,7 @@ private CompletableFuture<ApiResponse<Object>> write(String storeId, WriteReques
10161040

10171041
try {
10181042
HttpRequest request = buildHttpRequest("POST", path, body, configuration);
1019-
return new HttpRequestAttempt<>(request, "write", Object.class, apiClient, configuration)
1043+
return new HttpRequestAttempt<>(request, "write", Object.class, apiClient, configuration, telemetry)
10201044
.addTelemetryAttributes(telemetryAttributes)
10211045
.attemptHttpRequest();
10221046
} catch (ApiException e) {
@@ -1083,7 +1107,7 @@ private CompletableFuture<ApiResponse<Void>> writeAssertions(
10831107

10841108
try {
10851109
HttpRequest request = buildHttpRequest("PUT", path, body, configuration);
1086-
return new HttpRequestAttempt<>(request, "writeAssertions", Void.class, apiClient, configuration)
1110+
return new HttpRequestAttempt<>(request, "writeAssertions", Void.class, apiClient, configuration, telemetry)
10871111
.addTelemetryAttributes(telemetryAttributes)
10881112
.attemptHttpRequest();
10891113
} catch (ApiException e) {
@@ -1145,7 +1169,8 @@ private CompletableFuture<ApiResponse<WriteAuthorizationModelResponse>> writeAut
11451169
"writeAuthorizationModel",
11461170
WriteAuthorizationModelResponse.class,
11471171
apiClient,
1148-
configuration)
1172+
configuration,
1173+
telemetry)
11491174
.addTelemetryAttributes(telemetryAttributes)
11501175
.attemptHttpRequest();
11511176
} catch (ApiException e) {

src/main/java/dev/openfga/sdk/api/auth/OAuth2Client.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@ private CompletableFuture<CredentialsFlowResponse> exchangeToken()
7878
ApiClient.formRequestBuilder("POST", "", this.authRequest.buildFormRequestBody(), config);
7979
HttpRequest request = requestBuilder.build();
8080

81-
return new HttpRequestAttempt<>(request, "exchangeToken", CredentialsFlowResponse.class, apiClient, config)
81+
return new HttpRequestAttempt<>(
82+
request, "exchangeToken", CredentialsFlowResponse.class, apiClient, config, telemetry)
8283
.attemptHttpRequest()
8384
.thenApply(ApiResponse::getData);
8485
}

src/main/java/dev/openfga/sdk/api/client/ApiExecutor.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import dev.openfga.sdk.api.configuration.Configuration;
44
import dev.openfga.sdk.errors.ApiException;
55
import dev.openfga.sdk.errors.FgaInvalidParameterException;
6+
import dev.openfga.sdk.telemetry.Telemetry;
67
import java.io.IOException;
78
import java.net.http.HttpRequest;
89
import java.util.concurrent.CompletableFuture;
@@ -28,6 +29,7 @@
2829
public class ApiExecutor {
2930
private final ApiClient apiClient;
3031
private final Configuration configuration;
32+
private final Telemetry telemetry;
3133

3234
/**
3335
* Constructs an ApiExecutor instance. Typically called via {@link OpenFgaClient#apiExecutor()}.
@@ -36,14 +38,29 @@ public class ApiExecutor {
3638
* @param configuration Client configuration
3739
*/
3840
public ApiExecutor(ApiClient apiClient, Configuration configuration) {
41+
this(apiClient, configuration, new Telemetry(configuration));
42+
}
43+
44+
/**
45+
* Constructs an ApiExecutor instance. Typically called via {@link OpenFgaClient#apiExecutor()}.
46+
*
47+
* @param apiClient API client for HTTP operations
48+
* @param configuration Client configuration
49+
* @param telemetry Telemetry instance for collecting metrics
50+
*/
51+
public ApiExecutor(ApiClient apiClient, Configuration configuration, Telemetry telemetry) {
3952
if (apiClient == null) {
4053
throw new IllegalArgumentException("ApiClient cannot be null");
4154
}
4255
if (configuration == null) {
4356
throw new IllegalArgumentException("Configuration cannot be null");
4457
}
58+
if (telemetry == null) {
59+
throw new IllegalArgumentException("Telemetry cannot be null");
60+
}
4561
this.apiClient = apiClient;
4662
this.configuration = configuration;
63+
this.telemetry = telemetry;
4764
}
4865

4966
/**
@@ -84,7 +101,7 @@ public <T> CompletableFuture<ApiResponse<T>> send(ApiExecutorRequestBuilder requ
84101
HttpRequest httpRequest = requestBuilder.buildHttpRequest(configuration, apiClient);
85102
String methodName = "apiExecutor:" + requestBuilder.getMethod() + ":" + requestBuilder.getPath();
86103

87-
return new HttpRequestAttempt<>(httpRequest, methodName, responseType, apiClient, configuration)
104+
return new HttpRequestAttempt<>(httpRequest, methodName, responseType, apiClient, configuration, telemetry)
88105
.attemptHttpRequest();
89106

90107
} catch (IOException e) {

src/main/java/dev/openfga/sdk/api/client/HttpRequestAttempt.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,27 @@ public class HttpRequestAttempt<T> {
3737
public HttpRequestAttempt(
3838
HttpRequest request, String name, Class<T> clazz, ApiClient apiClient, Configuration configuration)
3939
throws FgaInvalidParameterException {
40+
this(request, name, clazz, apiClient, configuration, new Telemetry(configuration));
41+
}
42+
43+
public HttpRequestAttempt(
44+
HttpRequest request,
45+
String name,
46+
Class<T> clazz,
47+
ApiClient apiClient,
48+
Configuration configuration,
49+
Telemetry telemetry)
50+
throws FgaInvalidParameterException {
4051
assertParamExists(configuration.getMaxRetries(), "maxRetries", "Configuration");
52+
if (telemetry == null) {
53+
throw new IllegalArgumentException("Telemetry cannot be null");
54+
}
4155
this.apiClient = apiClient;
4256
this.configuration = configuration;
4357
this.name = name;
4458
this.request = request;
4559
this.clazz = clazz;
46-
this.telemetry = new Telemetry(configuration);
60+
this.telemetry = telemetry;
4761
this.telemetryAttributes = new HashMap<>();
4862
}
4963

src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import dev.openfga.sdk.api.model.StreamResult;
1212
import dev.openfga.sdk.constants.FgaConstants;
1313
import dev.openfga.sdk.errors.*;
14+
import dev.openfga.sdk.telemetry.Telemetry;
1415
import java.util.ArrayList;
1516
import java.util.HashMap;
1617
import java.util.List;
@@ -24,6 +25,7 @@
2425

2526
public class OpenFgaClient {
2627
private final ApiClient apiClient;
28+
private Telemetry telemetry;
2729
private ClientConfiguration configuration;
2830
private OpenFgaApi api;
2931

@@ -34,7 +36,8 @@ public OpenFgaClient(ClientConfiguration configuration) throws FgaInvalidParamet
3436
public OpenFgaClient(ClientConfiguration configuration, ApiClient apiClient) throws FgaInvalidParameterException {
3537
this.apiClient = apiClient;
3638
this.configuration = configuration;
37-
this.api = new OpenFgaApi(configuration, apiClient);
39+
this.telemetry = new Telemetry(configuration);
40+
this.api = new OpenFgaApi(configuration, apiClient, telemetry);
3841
}
3942

4043
/* ***********
@@ -65,7 +68,7 @@ public OpenFgaApi getApi() {
6568
* @return ApiExecutor instance
6669
*/
6770
public ApiExecutor apiExecutor() {
68-
return new ApiExecutor(this.apiClient, this.configuration);
71+
return new ApiExecutor(this.apiClient, this.configuration, this.telemetry);
6972
}
7073

7174
/**
@@ -112,7 +115,8 @@ public void setAuthorizationModelId(String authorizationModelId) {
112115

113116
public void setConfiguration(ClientConfiguration configuration) throws FgaInvalidParameterException {
114117
this.configuration = configuration;
115-
this.api = new OpenFgaApi(configuration, apiClient);
118+
this.telemetry = new Telemetry(configuration);
119+
this.api = new OpenFgaApi(configuration, apiClient, telemetry);
116120
}
117121

118122
/* ********

0 commit comments

Comments
 (0)