Skip to content

Commit

Permalink
Merge pull request #94 from openfga/feat/support-opentelemetry
Browse files Browse the repository at this point in the history
feat: support OpenTelemetry metrics
  • Loading branch information
evansims authored Jul 19, 2024
2 parents fcb256c + 4159d51 commit b1e03e5
Show file tree
Hide file tree
Showing 13 changed files with 575 additions and 11 deletions.
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ dependencies {
implementation "com.fasterxml.jackson.core:jackson-databind:$jackson_version"
implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jackson_version"
implementation "org.openapitools:jackson-databind-nullable:0.2.6"
implementation platform("io.opentelemetry:opentelemetry-bom:1.40.0")
implementation "io.opentelemetry:opentelemetry-api"
}

testing {
Expand Down
51 changes: 47 additions & 4 deletions src/main/java/dev/openfga/sdk/api/OpenFgaApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,14 @@
import static dev.openfga.sdk.util.StringUtil.isNullOrWhitespace;
import static dev.openfga.sdk.util.Validation.assertParamExists;

import dev.openfga.sdk.api.auth.*;
import dev.openfga.sdk.api.client.*;
import dev.openfga.sdk.api.configuration.*;
import dev.openfga.sdk.api.auth.OAuth2Client;
import dev.openfga.sdk.api.client.ApiClient;
import dev.openfga.sdk.api.client.ApiResponse;
import dev.openfga.sdk.api.client.HttpRequestAttempt;
import dev.openfga.sdk.api.client.OpenFgaClient;
import dev.openfga.sdk.api.configuration.Configuration;
import dev.openfga.sdk.api.configuration.ConfigurationOverride;
import dev.openfga.sdk.api.configuration.CredentialsMethod;
import dev.openfga.sdk.api.model.CheckRequest;
import dev.openfga.sdk.api.model.CheckResponse;
import dev.openfga.sdk.api.model.CreateStoreRequest;
Expand All @@ -40,7 +45,10 @@
import dev.openfga.sdk.api.model.WriteAuthorizationModelRequest;
import dev.openfga.sdk.api.model.WriteAuthorizationModelResponse;
import dev.openfga.sdk.api.model.WriteRequest;
import dev.openfga.sdk.errors.*;
import dev.openfga.sdk.errors.ApiException;
import dev.openfga.sdk.errors.FgaInvalidParameterException;
import dev.openfga.sdk.telemetry.Attributes;
import dev.openfga.sdk.telemetry.Telemetry;
import dev.openfga.sdk.util.Pair;
import java.io.IOException;
import java.net.URI;
Expand All @@ -60,6 +68,7 @@ public class OpenFgaApi {

private final ApiClient apiClient;
private final OAuth2Client oAuth2Client;
private final Telemetry telemetry;

public OpenFgaApi(Configuration configuration) throws FgaInvalidParameterException {
this(configuration, new ApiClient());
Expand All @@ -68,6 +77,7 @@ public OpenFgaApi(Configuration configuration) throws FgaInvalidParameterExcepti
public OpenFgaApi(Configuration configuration, ApiClient apiClient) throws FgaInvalidParameterException {
this.apiClient = apiClient;
this.configuration = configuration;
this.telemetry = new Telemetry();

if (configuration.getCredentials().getCredentialsMethod() == CredentialsMethod.CLIENT_CREDENTIALS) {
this.oAuth2Client = new OAuth2Client(configuration, apiClient);
Expand Down Expand Up @@ -122,6 +132,8 @@ private CompletableFuture<ApiResponse<CheckResponse>> check(
try {
HttpRequest request = buildHttpRequest("POST", path, body, configuration);
return new HttpRequestAttempt<>(request, "check", CheckResponse.class, apiClient, configuration)
.addTelemetryAttribute(Attributes.REQUEST_METHOD, "check")
.addTelemetryAttribute(Attributes.REQUEST_STORE_ID, storeId)
.attemptHttpRequest();
} catch (ApiException e) {
return CompletableFuture.failedFuture(e);
Expand Down Expand Up @@ -164,6 +176,7 @@ private CompletableFuture<ApiResponse<CreateStoreResponse>> createStore(
try {
HttpRequest request = buildHttpRequest("POST", path, body, configuration);
return new HttpRequestAttempt<>(request, "createStore", CreateStoreResponse.class, apiClient, configuration)
.addTelemetryAttribute(Attributes.REQUEST_METHOD, "createStore")
.attemptHttpRequest();
} catch (ApiException e) {
return CompletableFuture.failedFuture(e);
Expand Down Expand Up @@ -205,6 +218,8 @@ private CompletableFuture<ApiResponse<Void>> deleteStore(String storeId, Configu
try {
HttpRequest request = buildHttpRequest("DELETE", path, configuration);
return new HttpRequestAttempt<>(request, "deleteStore", Void.class, apiClient, configuration)
.addTelemetryAttribute(Attributes.REQUEST_METHOD, "deleteStore")
.addTelemetryAttribute(Attributes.REQUEST_STORE_ID, storeId)
.attemptHttpRequest();
} catch (ApiException e) {
return CompletableFuture.failedFuture(e);
Expand Down Expand Up @@ -252,6 +267,8 @@ private CompletableFuture<ApiResponse<ExpandResponse>> expand(
try {
HttpRequest request = buildHttpRequest("POST", path, body, configuration);
return new HttpRequestAttempt<>(request, "expand", ExpandResponse.class, apiClient, configuration)
.addTelemetryAttribute(Attributes.REQUEST_METHOD, "expand")
.addTelemetryAttribute(Attributes.REQUEST_STORE_ID, storeId)
.attemptHttpRequest();
} catch (ApiException e) {
return CompletableFuture.failedFuture(e);
Expand Down Expand Up @@ -294,6 +311,8 @@ private CompletableFuture<ApiResponse<GetStoreResponse>> getStore(String storeId
try {
HttpRequest request = buildHttpRequest("GET", path, configuration);
return new HttpRequestAttempt<>(request, "getStore", GetStoreResponse.class, apiClient, configuration)
.addTelemetryAttribute(Attributes.REQUEST_METHOD, "getStore")
.addTelemetryAttribute(Attributes.REQUEST_STORE_ID, storeId)
.attemptHttpRequest();
} catch (ApiException e) {
return CompletableFuture.failedFuture(e);
Expand Down Expand Up @@ -341,6 +360,8 @@ private CompletableFuture<ApiResponse<ListObjectsResponse>> listObjects(
try {
HttpRequest request = buildHttpRequest("POST", path, body, configuration);
return new HttpRequestAttempt<>(request, "listObjects", ListObjectsResponse.class, apiClient, configuration)
.addTelemetryAttribute(Attributes.REQUEST_METHOD, "listObjects")
.addTelemetryAttribute(Attributes.REQUEST_STORE_ID, storeId)
.attemptHttpRequest();
} catch (ApiException e) {
return CompletableFuture.failedFuture(e);
Expand Down Expand Up @@ -385,6 +406,7 @@ private CompletableFuture<ApiResponse<ListStoresResponse>> listStores(
try {
HttpRequest request = buildHttpRequest("GET", path, configuration);
return new HttpRequestAttempt<>(request, "listStores", ListStoresResponse.class, apiClient, configuration)
.addTelemetryAttribute(Attributes.REQUEST_METHOD, "listStores")
.attemptHttpRequest();
} catch (ApiException e) {
return CompletableFuture.failedFuture(e);
Expand Down Expand Up @@ -432,6 +454,8 @@ private CompletableFuture<ApiResponse<ListUsersResponse>> listUsers(
try {
HttpRequest request = buildHttpRequest("POST", path, body, configuration);
return new HttpRequestAttempt<>(request, "listUsers", ListUsersResponse.class, apiClient, configuration)
.addTelemetryAttribute(Attributes.REQUEST_METHOD, "listUsers")
.addTelemetryAttribute(Attributes.REQUEST_STORE_ID, storeId)
.attemptHttpRequest();
} catch (ApiException e) {
return CompletableFuture.failedFuture(e);
Expand Down Expand Up @@ -479,6 +503,8 @@ private CompletableFuture<ApiResponse<ReadResponse>> read(
try {
HttpRequest request = buildHttpRequest("POST", path, body, configuration);
return new HttpRequestAttempt<>(request, "read", ReadResponse.class, apiClient, configuration)
.addTelemetryAttribute(Attributes.REQUEST_METHOD, "read")
.addTelemetryAttribute(Attributes.REQUEST_STORE_ID, storeId)
.attemptHttpRequest();
} catch (ApiException e) {
return CompletableFuture.failedFuture(e);
Expand Down Expand Up @@ -529,6 +555,9 @@ private CompletableFuture<ApiResponse<ReadAssertionsResponse>> readAssertions(
HttpRequest request = buildHttpRequest("GET", path, configuration);
return new HttpRequestAttempt<>(
request, "readAssertions", ReadAssertionsResponse.class, apiClient, configuration)
.addTelemetryAttribute(Attributes.REQUEST_METHOD, "readAssertions")
.addTelemetryAttribute(Attributes.REQUEST_STORE_ID, storeId)
.addTelemetryAttribute(Attributes.REQUEST_MODEL_ID, authorizationModelId)
.attemptHttpRequest();
} catch (ApiException e) {
return CompletableFuture.failedFuture(e);
Expand Down Expand Up @@ -582,6 +611,9 @@ private CompletableFuture<ApiResponse<ReadAuthorizationModelResponse>> readAutho
ReadAuthorizationModelResponse.class,
apiClient,
configuration)
.addTelemetryAttribute(Attributes.REQUEST_METHOD, "readAuthorizationModel")
.addTelemetryAttribute(Attributes.REQUEST_STORE_ID, storeId)
.addTelemetryAttribute(Attributes.REQUEST_MODEL_ID, id)
.attemptHttpRequest();
} catch (ApiException e) {
return CompletableFuture.failedFuture(e);
Expand Down Expand Up @@ -638,6 +670,8 @@ private CompletableFuture<ApiResponse<ReadAuthorizationModelsResponse>> readAuth
ReadAuthorizationModelsResponse.class,
apiClient,
configuration)
.addTelemetryAttribute(Attributes.REQUEST_METHOD, "readAuthorizationModels")
.addTelemetryAttribute(Attributes.REQUEST_STORE_ID, storeId)
.attemptHttpRequest();
} catch (ApiException e) {
return CompletableFuture.failedFuture(e);
Expand Down Expand Up @@ -694,6 +728,8 @@ private CompletableFuture<ApiResponse<ReadChangesResponse>> readChanges(
try {
HttpRequest request = buildHttpRequest("GET", path, configuration);
return new HttpRequestAttempt<>(request, "readChanges", ReadChangesResponse.class, apiClient, configuration)
.addTelemetryAttribute(Attributes.REQUEST_METHOD, "readChanges")
.addTelemetryAttribute(Attributes.REQUEST_STORE_ID, storeId)
.attemptHttpRequest();
} catch (ApiException e) {
return CompletableFuture.failedFuture(e);
Expand Down Expand Up @@ -740,6 +776,8 @@ private CompletableFuture<ApiResponse<Object>> write(String storeId, WriteReques
try {
HttpRequest request = buildHttpRequest("POST", path, body, configuration);
return new HttpRequestAttempt<>(request, "write", Object.class, apiClient, configuration)
.addTelemetryAttribute(Attributes.REQUEST_METHOD, "write")
.addTelemetryAttribute(Attributes.REQUEST_STORE_ID, storeId)
.attemptHttpRequest();
} catch (ApiException e) {
return CompletableFuture.failedFuture(e);
Expand Down Expand Up @@ -797,6 +835,9 @@ private CompletableFuture<ApiResponse<Void>> writeAssertions(
try {
HttpRequest request = buildHttpRequest("PUT", path, body, configuration);
return new HttpRequestAttempt<>(request, "writeAssertions", Void.class, apiClient, configuration)
.addTelemetryAttribute(Attributes.REQUEST_METHOD, "writeAssertions")
.addTelemetryAttribute(Attributes.REQUEST_STORE_ID, storeId)
.addTelemetryAttribute(Attributes.REQUEST_MODEL_ID, authorizationModelId)
.attemptHttpRequest();
} catch (ApiException e) {
return CompletableFuture.failedFuture(e);
Expand Down Expand Up @@ -850,6 +891,8 @@ private CompletableFuture<ApiResponse<WriteAuthorizationModelResponse>> writeAut
WriteAuthorizationModelResponse.class,
apiClient,
configuration)
.addTelemetryAttribute(Attributes.REQUEST_METHOD, "writeAuthorizationModel")
.addTelemetryAttribute(Attributes.REQUEST_STORE_ID, storeId)
.attemptHttpRequest();
} catch (ApiException e) {
return CompletableFuture.failedFuture(e);
Expand Down
24 changes: 22 additions & 2 deletions src/main/java/dev/openfga/sdk/api/auth/OAuth2Client.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,19 @@

package dev.openfga.sdk.api.auth;

import dev.openfga.sdk.api.client.*;
import dev.openfga.sdk.api.configuration.*;
import dev.openfga.sdk.api.client.ApiClient;
import dev.openfga.sdk.api.client.ApiResponse;
import dev.openfga.sdk.api.client.HttpRequestAttempt;
import dev.openfga.sdk.api.configuration.Configuration;
import dev.openfga.sdk.errors.ApiException;
import dev.openfga.sdk.errors.FgaInvalidParameterException;
import dev.openfga.sdk.telemetry.Attribute;
import dev.openfga.sdk.telemetry.Telemetry;
import java.net.URI;
import java.net.http.HttpRequest;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;

public class OAuth2Client {
Expand All @@ -28,6 +34,7 @@ public class OAuth2Client {
private final AccessToken token = new AccessToken();
private final CredentialsFlowRequest authRequest;
private final Configuration config;
private final Telemetry telemetry;

/**
* Initializes a new instance of the {@link OAuth2Client} class
Expand All @@ -46,6 +53,7 @@ public OAuth2Client(Configuration configuration, ApiClient apiClient) throws Fga
.apiUrl(buildApiTokenIssuer(clientCredentials.getApiTokenIssuer()))
.maxRetries(configuration.getMaxRetries())
.minimumRetryDelay(configuration.getMinimumRetryDelay());
this.telemetry = new Telemetry();
}

/**
Expand All @@ -59,6 +67,18 @@ public CompletableFuture<String> getAccessToken() throws FgaInvalidParameterExce
return exchangeToken().thenCompose(response -> {
token.setToken(response.getAccessToken());
token.setExpiresAt(Instant.now().plusSeconds(response.getExpiresInSeconds()));

Map<Attribute, String> attributesMap = new HashMap<>();

try {
attributesMap.put(
dev.openfga.sdk.telemetry.Attributes.REQUEST_CLIENT_ID,
config.getCredentials().getClientCredentials().getClientId());
} catch (Exception e) {
}

telemetry.metrics().credentialsRequest(1L, attributesMap);

return CompletableFuture.completedFuture(token.getToken());
});
}
Expand Down
77 changes: 73 additions & 4 deletions src/main/java/dev/openfga/sdk/api/client/HttpRequestAttempt.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@
import static dev.openfga.sdk.util.Validation.assertParamExists;

import dev.openfga.sdk.api.configuration.Configuration;
import dev.openfga.sdk.errors.*;
import dev.openfga.sdk.errors.ApiException;
import dev.openfga.sdk.errors.FgaError;
import dev.openfga.sdk.errors.FgaInvalidParameterException;
import dev.openfga.sdk.errors.HttpStatusCode;
import dev.openfga.sdk.telemetry.Attribute;
import dev.openfga.sdk.telemetry.Attributes;
import dev.openfga.sdk.telemetry.Telemetry;
import java.io.IOException;
import java.io.PrintStream;
import java.net.http.HttpClient;
Expand All @@ -13,15 +19,22 @@
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Flow;
import java.util.concurrent.TimeUnit;

public class HttpRequestAttempt<T> {
private final ApiClient apiClient;
private final Configuration configuration;
private final Class<T> clazz;
private final String name;
private final HttpRequest request;
private final Telemetry telemetry = new Telemetry();
private Long requestStarted;
private Map<Attribute, String> telemetryAttributes;

// Intended for only testing the OpenFGA SDK itself.
private final boolean enableDebugLogging = "enable".equals(System.getProperty("HttpRequestAttempt.debug-logging"));
Expand All @@ -35,16 +48,52 @@ public HttpRequestAttempt(
this.name = name;
this.request = request;
this.clazz = clazz;
this.telemetryAttributes = new HashMap<>();
}

public Map<Attribute, String> getTelemetryAttributes() {
return telemetryAttributes;
}

public HttpRequestAttempt<T> setTelemetryAttributes(Map<Attribute, String> attributes) {
this.telemetryAttributes = attributes;
return this;
}

public HttpRequestAttempt<T> addTelemetryAttribute(Attribute attribute, String value) {
this.telemetryAttributes.put(attribute, value);
return this;
}

public HttpRequestAttempt<T> addTelemetryAttributes(Map<Attribute, String> attributes) {
this.telemetryAttributes.putAll(attributes);
return this;
}

public CompletableFuture<ApiResponse<T>> attemptHttpRequest() throws ApiException {
this.requestStarted = System.currentTimeMillis();

if (enableDebugLogging) {
request.bodyPublisher()
.ifPresent(requestBodyPublisher ->
requestBodyPublisher.subscribe(new BodyLogger(System.err, "request")));
}
int retryNumber = 0;
return attemptHttpRequest(apiClient.getHttpClient(), retryNumber, null);

addTelemetryAttribute(Attributes.HTTP_HOST, configuration.getApiUrl());
addTelemetryAttribute(Attributes.HTTP_METHOD, request.method());

try {
addTelemetryAttribute(
Attributes.REQUEST_CLIENT_ID,
configuration.getCredentials().getClientCredentials().getClientId());
} catch (Exception e) {
}

return attemptHttpRequest(createClient(), 0, null);
}

private HttpClient createClient() {
return apiClient.getHttpClient();
}

private CompletableFuture<ApiResponse<T>> attemptHttpRequest(
Expand All @@ -57,15 +106,34 @@ private CompletableFuture<ApiResponse<T>> attemptHttpRequest(

if (fgaError.isPresent()) {
FgaError error = fgaError.get();

if (HttpStatusCode.isRetryable(error.getStatusCode())
&& retryNumber < configuration.getMaxRetries()) {

HttpClient delayingClient = getDelayedHttpClient();

return attemptHttpRequest(delayingClient, retryNumber + 1, error);
}

return CompletableFuture.failedFuture(error);
}

addTelemetryAttributes(Attributes.fromHttpResponse(response, this.configuration.getCredentials()));
addTelemetryAttribute(Attributes.REQUEST_RETRIES, String.valueOf(retryNumber));

if (response.headers().firstValue("fga-query-duration-ms").isPresent()) {
double queryDuration = Double.parseDouble(response.headers()
.firstValue("fga-query-duration-ms")
.get());
telemetry.metrics().queryDuration(queryDuration, this.getTelemetryAttributes());
}

telemetry
.metrics()
.requestDuration(
(double) (System.currentTimeMillis() - this.requestStarted),
this.getTelemetryAttributes());

return deserializeResponse(response)
.thenApply(modeledResponse -> new ApiResponse<>(
response.statusCode(), response.headers().map(), response.body(), modeledResponse));
Expand All @@ -88,6 +156,7 @@ private CompletableFuture<T> deserializeResponse(HttpResponse<String> response)

private HttpClient getDelayedHttpClient() {
Duration retryDelay = configuration.getMinimumRetryDelay();

return apiClient
.getHttpClientBuilder()
.executor(CompletableFuture.delayedExecutor(retryDelay.toNanos(), TimeUnit.NANOSECONDS))
Expand Down
Loading

0 comments on commit b1e03e5

Please sign in to comment.