Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support OpenTelemetry metrics #94

Merged
merged 1 commit into from
Jul 19, 2024
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
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
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