From 2871922bb19b20e25d7966975c1c68abc8d1ea5e Mon Sep 17 00:00:00 2001 From: Evan Sims Date: Mon, 22 Jul 2024 20:23:08 -0500 Subject: [PATCH] feat: improvements to OpenTelemetry metrics Co-Authored-By: Ewan Harris --- .github/dependabot.yaml | 8 + .openapi-generator/FILES | 20 ++ README.md | 5 + build.gradle | 12 +- example/example1/build.gradle | 4 +- .../java/dev/openfga/sdk/api/OpenFgaApi.java | 260 +++++++++++++++--- .../openfga/sdk/api/auth/OAuth2Client.java | 17 +- .../sdk/api/client/HttpRequestAttempt.java | 49 ++-- .../sdk/api/configuration/Configuration.java | 14 + .../configuration/ConfigurationOverride.java | 11 + .../configuration/TelemetryConfiguration.java | 19 ++ .../dev/openfga/sdk/telemetry/Attributes.java | 129 ++++++--- .../dev/openfga/sdk/telemetry/Counter.java | 40 +-- .../dev/openfga/sdk/telemetry/Counters.java | 3 +- .../dev/openfga/sdk/telemetry/Histogram.java | 24 +- .../dev/openfga/sdk/telemetry/Histograms.java | 12 +- .../dev/openfga/sdk/telemetry/Metric.java | 31 +++ .../dev/openfga/sdk/telemetry/Metrics.java | 21 +- .../TelemetryConfigurationTest.java | 31 +++ .../openfga/sdk/telemetry/AttributeTest.java | 21 ++ .../openfga/sdk/telemetry/AttributesTest.java | 103 +++++++ .../openfga/sdk/telemetry/CounterTest.java | 25 ++ .../openfga/sdk/telemetry/CountersTest.java | 23 ++ .../openfga/sdk/telemetry/HistogramTest.java | 45 +++ .../openfga/sdk/telemetry/HistogramsTest.java | 42 +++ .../dev/openfga/sdk/telemetry/MetricTest.java | 25 ++ .../openfga/sdk/telemetry/MetricsTest.java | 119 ++++++++ .../openfga/sdk/telemetry/TelemetryTest.java | 22 ++ 28 files changed, 961 insertions(+), 174 deletions(-) create mode 100644 src/main/java/dev/openfga/sdk/api/configuration/TelemetryConfiguration.java create mode 100644 src/main/java/dev/openfga/sdk/telemetry/Metric.java create mode 100644 src/test/java/dev/openfga/sdk/api/configuration/TelemetryConfigurationTest.java create mode 100644 src/test/java/dev/openfga/sdk/telemetry/AttributeTest.java create mode 100644 src/test/java/dev/openfga/sdk/telemetry/AttributesTest.java create mode 100644 src/test/java/dev/openfga/sdk/telemetry/CounterTest.java create mode 100644 src/test/java/dev/openfga/sdk/telemetry/CountersTest.java create mode 100644 src/test/java/dev/openfga/sdk/telemetry/HistogramTest.java create mode 100644 src/test/java/dev/openfga/sdk/telemetry/HistogramsTest.java create mode 100644 src/test/java/dev/openfga/sdk/telemetry/MetricTest.java create mode 100644 src/test/java/dev/openfga/sdk/telemetry/MetricsTest.java create mode 100644 src/test/java/dev/openfga/sdk/telemetry/TelemetryTest.java diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index 5a916f6..06ad99e 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -8,6 +8,14 @@ updates: dependencies: patterns: - "*" + - package-ecosystem: "gradle" + directory: "/example/example1" + schedule: + interval: "monthly" + groups: + dependencies: + patterns: + - "*" - package-ecosystem: "github-actions" directory: "/" schedule: diff --git a/.openapi-generator/FILES b/.openapi-generator/FILES index a59ca74..f26e90c 100644 --- a/.openapi-generator/FILES +++ b/.openapi-generator/FILES @@ -184,6 +184,7 @@ src/main/java/dev/openfga/sdk/api/configuration/Configuration.java src/main/java/dev/openfga/sdk/api/configuration/ConfigurationOverride.java src/main/java/dev/openfga/sdk/api/configuration/Credentials.java src/main/java/dev/openfga/sdk/api/configuration/CredentialsMethod.java +src/main/java/dev/openfga/sdk/api/configuration/TelemetryConfiguration.java src/main/java/dev/openfga/sdk/api/model/AbortedMessageResponse.java src/main/java/dev/openfga/sdk/api/model/AbstractOpenApiSchema.java src/main/java/dev/openfga/sdk/api/model/Any.java @@ -272,6 +273,15 @@ src/main/java/dev/openfga/sdk/errors/FgaApiValidationError.java src/main/java/dev/openfga/sdk/errors/FgaError.java src/main/java/dev/openfga/sdk/errors/FgaInvalidParameterException.java src/main/java/dev/openfga/sdk/errors/HttpStatusCode.java +src/main/java/dev/openfga/sdk/telemetry/Attribute.java +src/main/java/dev/openfga/sdk/telemetry/Attributes.java +src/main/java/dev/openfga/sdk/telemetry/Counter.java +src/main/java/dev/openfga/sdk/telemetry/Counters.java +src/main/java/dev/openfga/sdk/telemetry/Histogram.java +src/main/java/dev/openfga/sdk/telemetry/Histograms.java +src/main/java/dev/openfga/sdk/telemetry/Metric.java +src/main/java/dev/openfga/sdk/telemetry/Metrics.java +src/main/java/dev/openfga/sdk/telemetry/Telemetry.java src/main/java/dev/openfga/sdk/util/Pair.java src/main/java/dev/openfga/sdk/util/StringUtil.java src/main/java/dev/openfga/sdk/util/Validation.java @@ -290,4 +300,14 @@ src/test/java/dev/openfga/sdk/api/client/ApiClientTest.java src/test/java/dev/openfga/sdk/api/client/OpenFgaClientTest.java src/test/java/dev/openfga/sdk/api/configuration/ClientCredentialsTest.java src/test/java/dev/openfga/sdk/api/configuration/ConfigurationTest.java +src/test/java/dev/openfga/sdk/api/configuration/TelemetryConfigurationTest.java +src/test/java/dev/openfga/sdk/telemetry/AttributeTest.java +src/test/java/dev/openfga/sdk/telemetry/AttributesTest.java +src/test/java/dev/openfga/sdk/telemetry/CounterTest.java +src/test/java/dev/openfga/sdk/telemetry/CountersTest.java +src/test/java/dev/openfga/sdk/telemetry/HistogramTest.java +src/test/java/dev/openfga/sdk/telemetry/HistogramsTest.java +src/test/java/dev/openfga/sdk/telemetry/MetricTest.java +src/test/java/dev/openfga/sdk/telemetry/MetricsTest.java +src/test/java/dev/openfga/sdk/telemetry/TelemetryTest.java src/test/java/dev/openfga/sdk/util/StringUtilTest.java diff --git a/README.md b/README.md index 209ac16..b5a7dab 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ This is an autogenerated Java SDK for OpenFGA. It provides a wrapper around the - [Retries](#retries) - [API Endpoints](#api-endpoints) - [Models](#models) + - [OpenTelemetry](#opentelemetry) - [Contributing](#contributing) - [Issues](#issues) - [Pull Requests](#pull-requests) @@ -1060,6 +1061,10 @@ public class Example { +### OpenTelemetry + +This SDK supports producing metrics that can be consumed as part of an [OpenTelemetry](https://opentelemetry.io/) setup. For more information, please see [the documentation](https://github.com/openfga/java-sdk/blob/main/docs/opentelemetry.md) + ## Contributing ### Issues diff --git a/build.gradle b/build.gradle index c5e9d2a..3845b03 100644 --- a/build.gradle +++ b/build.gradle @@ -60,13 +60,13 @@ ext { } dependencies { - implementation "com.google.code.findbugs:jsr305:3.0.2" + implementation "com.google.code.findbugs:jsr305:3.0.+" implementation "com.fasterxml.jackson.core:jackson-core:$jackson_version" implementation "com.fasterxml.jackson.core:jackson-annotations:$jackson_version" 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 "org.openapitools:jackson-databind-nullable:0.2.+" + implementation platform("io.opentelemetry:opentelemetry-bom:1.40.+") implementation "io.opentelemetry:opentelemetry-api" } @@ -80,7 +80,7 @@ testing { implementation "org.junit.jupiter:junit-jupiter:$junit_version" implementation "org.mockito:mockito-core:5.+" runtimeOnly "org.junit.platform:junit-platform-launcher" - implementation "org.wiremock:wiremock:3.8.0" + implementation "org.wiremock:wiremock:3.8.+" // This test-only dependency is convenient but not widely used. // Review project activity before updating the version here. @@ -106,8 +106,8 @@ testing { dependencies { implementation "com.fasterxml.jackson.core:jackson-core:$jackson_version" implementation "com.fasterxml.jackson.core:jackson-databind:$jackson_version" - implementation "org.testcontainers:junit-jupiter:1.19.8" - implementation "org.testcontainers:openfga:1.19.8" + implementation "org.testcontainers:junit-jupiter:1.19.+" + implementation "org.testcontainers:openfga:1.19.+" implementation project() } diff --git a/example/example1/build.gradle b/example/example1/build.gradle index 9ea6879..3bf9dbd 100644 --- a/example/example1/build.gradle +++ b/example/example1/build.gradle @@ -19,11 +19,11 @@ repositories { } ext { - jacksonVersion = "2.16.0" + jacksonVersion = "2.17.1" } dependencies { - implementation("dev.openfga:openfga-sdk:0.4.+") + implementation("dev.openfga:openfga-sdk:0.5.+") // Serialization implementation("com.fasterxml.jackson.core:jackson-core:$jacksonVersion") diff --git a/src/main/java/dev/openfga/sdk/api/OpenFgaApi.java b/src/main/java/dev/openfga/sdk/api/OpenFgaApi.java index 069b4eb..7ff9746 100644 --- a/src/main/java/dev/openfga/sdk/api/OpenFgaApi.java +++ b/src/main/java/dev/openfga/sdk/api/OpenFgaApi.java @@ -15,14 +15,9 @@ import static dev.openfga.sdk.util.StringUtil.isNullOrWhitespace; import static dev.openfga.sdk.util.Validation.assertParamExists; -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.auth.*; +import dev.openfga.sdk.api.client.*; +import dev.openfga.sdk.api.configuration.*; import dev.openfga.sdk.api.model.CheckRequest; import dev.openfga.sdk.api.model.CheckResponse; import dev.openfga.sdk.api.model.CreateStoreRequest; @@ -45,8 +40,8 @@ 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.ApiException; -import dev.openfga.sdk.errors.FgaInvalidParameterException; +import dev.openfga.sdk.errors.*; +import dev.openfga.sdk.telemetry.Attribute; import dev.openfga.sdk.telemetry.Attributes; import dev.openfga.sdk.telemetry.Telemetry; import dev.openfga.sdk.util.Pair; @@ -54,6 +49,8 @@ import java.net.URI; import java.net.http.HttpRequest; import java.time.Duration; +import java.util.HashMap; +import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -129,11 +126,18 @@ private CompletableFuture> check( String path = "/stores/{store_id}/check".replace("{store_id}", ApiClient.urlEncode(storeId.toString())); + Map methodParameters = new HashMap<>(); + methodParameters.put("storeId", storeId); + methodParameters.put("body", body); + + Map telemetryAttributes = buildTelemetryAttributes(methodParameters); + + telemetryAttributes.put(Attributes.FGA_CLIENT_REQUEST_METHOD, "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) + .addTelemetryAttributes(telemetryAttributes) .attemptHttpRequest(); } catch (ApiException e) { return CompletableFuture.failedFuture(e); @@ -173,10 +177,17 @@ private CompletableFuture> createStore( String path = "/stores"; + Map methodParameters = new HashMap<>(); + methodParameters.put("body", body); + + Map telemetryAttributes = buildTelemetryAttributes(methodParameters); + + telemetryAttributes.put(Attributes.FGA_CLIENT_REQUEST_METHOD, "CreateStore"); + try { HttpRequest request = buildHttpRequest("POST", path, body, configuration); return new HttpRequestAttempt<>(request, "createStore", CreateStoreResponse.class, apiClient, configuration) - .addTelemetryAttribute(Attributes.REQUEST_METHOD, "createStore") + .addTelemetryAttributes(telemetryAttributes) .attemptHttpRequest(); } catch (ApiException e) { return CompletableFuture.failedFuture(e); @@ -215,11 +226,17 @@ private CompletableFuture> deleteStore(String storeId, Configu String path = "/stores/{store_id}".replace("{store_id}", ApiClient.urlEncode(storeId.toString())); + Map methodParameters = new HashMap<>(); + methodParameters.put("storeId", storeId); + + Map telemetryAttributes = buildTelemetryAttributes(methodParameters); + + telemetryAttributes.put(Attributes.FGA_CLIENT_REQUEST_METHOD, "DeleteStore"); + 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) + .addTelemetryAttributes(telemetryAttributes) .attemptHttpRequest(); } catch (ApiException e) { return CompletableFuture.failedFuture(e); @@ -264,11 +281,18 @@ private CompletableFuture> expand( String path = "/stores/{store_id}/expand".replace("{store_id}", ApiClient.urlEncode(storeId.toString())); + Map methodParameters = new HashMap<>(); + methodParameters.put("storeId", storeId); + methodParameters.put("body", body); + + Map telemetryAttributes = buildTelemetryAttributes(methodParameters); + + telemetryAttributes.put(Attributes.FGA_CLIENT_REQUEST_METHOD, "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) + .addTelemetryAttributes(telemetryAttributes) .attemptHttpRequest(); } catch (ApiException e) { return CompletableFuture.failedFuture(e); @@ -308,11 +332,17 @@ private CompletableFuture> getStore(String storeId String path = "/stores/{store_id}".replace("{store_id}", ApiClient.urlEncode(storeId.toString())); + Map methodParameters = new HashMap<>(); + methodParameters.put("storeId", storeId); + + Map telemetryAttributes = buildTelemetryAttributes(methodParameters); + + telemetryAttributes.put(Attributes.FGA_CLIENT_REQUEST_METHOD, "GetStore"); + 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) + .addTelemetryAttributes(telemetryAttributes) .attemptHttpRequest(); } catch (ApiException e) { return CompletableFuture.failedFuture(e); @@ -357,11 +387,18 @@ private CompletableFuture> listObjects( String path = "/stores/{store_id}/list-objects".replace("{store_id}", ApiClient.urlEncode(storeId.toString())); + Map methodParameters = new HashMap<>(); + methodParameters.put("storeId", storeId); + methodParameters.put("body", body); + + Map telemetryAttributes = buildTelemetryAttributes(methodParameters); + + telemetryAttributes.put(Attributes.FGA_CLIENT_REQUEST_METHOD, "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) + .addTelemetryAttributes(telemetryAttributes) .attemptHttpRequest(); } catch (ApiException e) { return CompletableFuture.failedFuture(e); @@ -403,10 +440,16 @@ private CompletableFuture> listStores( String path = "/stores"; path = pathWithParams(path, "page_size", pageSize, "continuation_token", continuationToken); + Map methodParameters = new HashMap<>(); + + Map telemetryAttributes = buildTelemetryAttributes(methodParameters); + + telemetryAttributes.put(Attributes.FGA_CLIENT_REQUEST_METHOD, "ListStores"); + try { HttpRequest request = buildHttpRequest("GET", path, configuration); return new HttpRequestAttempt<>(request, "listStores", ListStoresResponse.class, apiClient, configuration) - .addTelemetryAttribute(Attributes.REQUEST_METHOD, "listStores") + .addTelemetryAttributes(telemetryAttributes) .attemptHttpRequest(); } catch (ApiException e) { return CompletableFuture.failedFuture(e); @@ -451,11 +494,18 @@ private CompletableFuture> listUsers( String path = "/stores/{store_id}/list-users".replace("{store_id}", ApiClient.urlEncode(storeId.toString())); + Map methodParameters = new HashMap<>(); + methodParameters.put("storeId", storeId); + methodParameters.put("body", body); + + Map telemetryAttributes = buildTelemetryAttributes(methodParameters); + + telemetryAttributes.put(Attributes.FGA_CLIENT_REQUEST_METHOD, "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) + .addTelemetryAttributes(telemetryAttributes) .attemptHttpRequest(); } catch (ApiException e) { return CompletableFuture.failedFuture(e); @@ -500,11 +550,18 @@ private CompletableFuture> read( String path = "/stores/{store_id}/read".replace("{store_id}", ApiClient.urlEncode(storeId.toString())); + Map methodParameters = new HashMap<>(); + methodParameters.put("storeId", storeId); + methodParameters.put("body", body); + + Map telemetryAttributes = buildTelemetryAttributes(methodParameters); + + telemetryAttributes.put(Attributes.FGA_CLIENT_REQUEST_METHOD, "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) + .addTelemetryAttributes(telemetryAttributes) .attemptHttpRequest(); } catch (ApiException e) { return CompletableFuture.failedFuture(e); @@ -551,13 +608,19 @@ private CompletableFuture> readAssertions( .replace("{store_id}", ApiClient.urlEncode(storeId.toString())) .replace("{authorization_model_id}", ApiClient.urlEncode(authorizationModelId.toString())); + Map methodParameters = new HashMap<>(); + methodParameters.put("storeId", storeId); + methodParameters.put("authorizationModelId", authorizationModelId); + + Map telemetryAttributes = buildTelemetryAttributes(methodParameters); + + telemetryAttributes.put(Attributes.FGA_CLIENT_REQUEST_METHOD, "ReadAssertions"); + try { 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) + .addTelemetryAttributes(telemetryAttributes) .attemptHttpRequest(); } catch (ApiException e) { return CompletableFuture.failedFuture(e); @@ -603,6 +666,14 @@ private CompletableFuture> readAutho .replace("{store_id}", ApiClient.urlEncode(storeId.toString())) .replace("{id}", ApiClient.urlEncode(id.toString())); + Map methodParameters = new HashMap<>(); + methodParameters.put("storeId", storeId); + methodParameters.put("id", id); + + Map telemetryAttributes = buildTelemetryAttributes(methodParameters); + + telemetryAttributes.put(Attributes.FGA_CLIENT_REQUEST_METHOD, "ReadAuthorizationModel"); + try { HttpRequest request = buildHttpRequest("GET", path, configuration); return new HttpRequestAttempt<>( @@ -611,9 +682,7 @@ private CompletableFuture> readAutho ReadAuthorizationModelResponse.class, apiClient, configuration) - .addTelemetryAttribute(Attributes.REQUEST_METHOD, "readAuthorizationModel") - .addTelemetryAttribute(Attributes.REQUEST_STORE_ID, storeId) - .addTelemetryAttribute(Attributes.REQUEST_MODEL_ID, id) + .addTelemetryAttributes(telemetryAttributes) .attemptHttpRequest(); } catch (ApiException e) { return CompletableFuture.failedFuture(e); @@ -662,6 +731,13 @@ private CompletableFuture> readAuth .replace("{store_id}", ApiClient.urlEncode(storeId.toString())); path = pathWithParams(path, "page_size", pageSize, "continuation_token", continuationToken); + Map methodParameters = new HashMap<>(); + methodParameters.put("storeId", storeId); + + Map telemetryAttributes = buildTelemetryAttributes(methodParameters); + + telemetryAttributes.put(Attributes.FGA_CLIENT_REQUEST_METHOD, "ReadAuthorizationModels"); + try { HttpRequest request = buildHttpRequest("GET", path, configuration); return new HttpRequestAttempt<>( @@ -670,8 +746,7 @@ private CompletableFuture> readAuth ReadAuthorizationModelsResponse.class, apiClient, configuration) - .addTelemetryAttribute(Attributes.REQUEST_METHOD, "readAuthorizationModels") - .addTelemetryAttribute(Attributes.REQUEST_STORE_ID, storeId) + .addTelemetryAttributes(telemetryAttributes) .attemptHttpRequest(); } catch (ApiException e) { return CompletableFuture.failedFuture(e); @@ -725,11 +800,17 @@ private CompletableFuture> readChanges( String path = "/stores/{store_id}/changes".replace("{store_id}", ApiClient.urlEncode(storeId.toString())); path = pathWithParams(path, "type", type, "page_size", pageSize, "continuation_token", continuationToken); + Map methodParameters = new HashMap<>(); + methodParameters.put("storeId", storeId); + + Map telemetryAttributes = buildTelemetryAttributes(methodParameters); + + telemetryAttributes.put(Attributes.FGA_CLIENT_REQUEST_METHOD, "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) + .addTelemetryAttributes(telemetryAttributes) .attemptHttpRequest(); } catch (ApiException e) { return CompletableFuture.failedFuture(e); @@ -773,11 +854,18 @@ private CompletableFuture> write(String storeId, WriteReques String path = "/stores/{store_id}/write".replace("{store_id}", ApiClient.urlEncode(storeId.toString())); + Map methodParameters = new HashMap<>(); + methodParameters.put("storeId", storeId); + methodParameters.put("body", body); + + Map telemetryAttributes = buildTelemetryAttributes(methodParameters); + + telemetryAttributes.put(Attributes.FGA_CLIENT_REQUEST_METHOD, "Write"); + 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) + .addTelemetryAttributes(telemetryAttributes) .attemptHttpRequest(); } catch (ApiException e) { return CompletableFuture.failedFuture(e); @@ -832,12 +920,19 @@ private CompletableFuture> writeAssertions( .replace("{store_id}", ApiClient.urlEncode(storeId.toString())) .replace("{authorization_model_id}", ApiClient.urlEncode(authorizationModelId.toString())); + Map methodParameters = new HashMap<>(); + methodParameters.put("storeId", storeId); + methodParameters.put("authorizationModelId", authorizationModelId); + methodParameters.put("body", body); + + Map telemetryAttributes = buildTelemetryAttributes(methodParameters); + + telemetryAttributes.put(Attributes.FGA_CLIENT_REQUEST_METHOD, "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) + .addTelemetryAttributes(telemetryAttributes) .attemptHttpRequest(); } catch (ApiException e) { return CompletableFuture.failedFuture(e); @@ -883,6 +978,14 @@ private CompletableFuture> writeAut String path = "/stores/{store_id}/authorization-models" .replace("{store_id}", ApiClient.urlEncode(storeId.toString())); + Map methodParameters = new HashMap<>(); + methodParameters.put("storeId", storeId); + methodParameters.put("body", body); + + Map telemetryAttributes = buildTelemetryAttributes(methodParameters); + + telemetryAttributes.put(Attributes.FGA_CLIENT_REQUEST_METHOD, "WriteAuthorizationModel"); + try { HttpRequest request = buildHttpRequest("POST", path, body, configuration); return new HttpRequestAttempt<>( @@ -891,14 +994,85 @@ private CompletableFuture> writeAut WriteAuthorizationModelResponse.class, apiClient, configuration) - .addTelemetryAttribute(Attributes.REQUEST_METHOD, "writeAuthorizationModel") - .addTelemetryAttribute(Attributes.REQUEST_STORE_ID, storeId) + .addTelemetryAttributes(telemetryAttributes) .attemptHttpRequest(); } catch (ApiException e) { return CompletableFuture.failedFuture(e); } } + private Map buildTelemetryAttributes(Map attributes) { + Map telemetryAttributes = new HashMap<>(); + + Object storeId = attributes.get("storeId"); + Object authorizationModelId = attributes.get("authorizationModelId"); + Object body = attributes.get("body"); + + if (storeId != null) { + telemetryAttributes.put(Attributes.FGA_CLIENT_REQUEST_STORE_ID, storeId.toString()); + } + + if (authorizationModelId != null) { + telemetryAttributes.put(Attributes.FGA_CLIENT_REQUEST_MODEL_ID, authorizationModelId.toString()); + } + + if (body != null) { + if (body instanceof CheckRequest) { + CheckRequest checkRequest = (CheckRequest) body; + + if (checkRequest.getTupleKey() != null + && !isNullOrWhitespace(checkRequest.getTupleKey().getUser())) { + telemetryAttributes.put( + Attributes.FGA_CLIENT_USER, + checkRequest.getTupleKey().getUser()); + } + + if (!isNullOrWhitespace(checkRequest.getAuthorizationModelId())) { + telemetryAttributes.put( + Attributes.FGA_CLIENT_REQUEST_MODEL_ID, checkRequest.getAuthorizationModelId()); + } + } + + if (body instanceof ExpandRequest) { + ExpandRequest expandRequest = (ExpandRequest) body; + + if (!isNullOrWhitespace(expandRequest.getAuthorizationModelId())) { + telemetryAttributes.put( + Attributes.FGA_CLIENT_REQUEST_MODEL_ID, expandRequest.getAuthorizationModelId()); + } + } + + if (body instanceof ListObjectsRequest) { + ListObjectsRequest listObjectsRequest = (ListObjectsRequest) body; + + if (!isNullOrWhitespace(listObjectsRequest.getAuthorizationModelId())) { + telemetryAttributes.put( + Attributes.FGA_CLIENT_REQUEST_MODEL_ID, listObjectsRequest.getAuthorizationModelId()); + } + } + + if (body instanceof ListUsersRequest) { + ListUsersRequest listUsersRequest = (ListUsersRequest) body; + + if (!isNullOrWhitespace(listUsersRequest.getAuthorizationModelId())) { + telemetryAttributes.put( + Attributes.FGA_CLIENT_REQUEST_MODEL_ID, listUsersRequest.getAuthorizationModelId()); + } + } + + if (body instanceof WriteRequest) { + WriteRequest writeRequest = (WriteRequest) body; + + if (!isNullOrWhitespace(writeRequest.getAuthorizationModelId())) { + telemetryAttributes.put( + Attributes.FGA_CLIENT_REQUEST_MODEL_ID, writeRequest.getAuthorizationModelId()); + } + } + } + + return telemetryAttributes; + } + private HttpRequest buildHttpRequest(String method, String path, Configuration configuration) throws ApiException, FgaInvalidParameterException { return buildHttpRequestWithPublisher(method, path, HttpRequest.BodyPublishers.noBody(), configuration); 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 ad13da0..f8b918a 100644 --- a/src/main/java/dev/openfga/sdk/api/auth/OAuth2Client.java +++ b/src/main/java/dev/openfga/sdk/api/auth/OAuth2Client.java @@ -12,10 +12,10 @@ package dev.openfga.sdk.api.auth; -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 static dev.openfga.sdk.util.StringUtil.isNullOrWhitespace; + +import dev.openfga.sdk.api.client.*; +import dev.openfga.sdk.api.configuration.*; import dev.openfga.sdk.errors.ApiException; import dev.openfga.sdk.errors.FgaInvalidParameterException; import dev.openfga.sdk.telemetry.Attribute; @@ -71,9 +71,12 @@ public CompletableFuture getAccessToken() throws FgaInvalidParameterExce Map attributesMap = new HashMap<>(); try { - attributesMap.put( - dev.openfga.sdk.telemetry.Attributes.REQUEST_CLIENT_ID, - config.getCredentials().getClientCredentials().getClientId()); + if (!isNullOrWhitespace( + config.getCredentials().getClientCredentials().getClientId())) { + attributesMap.put( + dev.openfga.sdk.telemetry.Attributes.FGA_CLIENT_REQUEST_CLIENT_ID, + config.getCredentials().getClientCredentials().getClientId()); + } } catch (Exception 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 f4a1c58..076b703 100644 --- a/src/main/java/dev/openfga/sdk/api/client/HttpRequestAttempt.java +++ b/src/main/java/dev/openfga/sdk/api/client/HttpRequestAttempt.java @@ -4,10 +4,7 @@ import static dev.openfga.sdk.util.Validation.assertParamExists; import dev.openfga.sdk.api.configuration.Configuration; -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.errors.*; import dev.openfga.sdk.telemetry.Attribute; import dev.openfga.sdk.telemetry.Attributes; import dev.openfga.sdk.telemetry.Telemetry; @@ -22,9 +19,7 @@ import java.util.HashMap; import java.util.Map; import java.util.Optional; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Flow; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.*; public class HttpRequestAttempt { private final ApiClient apiClient; @@ -79,13 +74,19 @@ public CompletableFuture> attemptHttpRequest() throws ApiExceptio requestBodyPublisher.subscribe(new BodyLogger(System.err, "request"))); } - addTelemetryAttribute(Attributes.HTTP_HOST, configuration.getApiUrl()); - addTelemetryAttribute(Attributes.HTTP_METHOD, request.method()); + addTelemetryAttribute(Attributes.HTTP_HOST, request.uri().getHost()); + addTelemetryAttribute(Attributes.URL_SCHEME, request.uri().getScheme()); + addTelemetryAttribute(Attributes.URL_FULL, request.uri().toString()); + addTelemetryAttribute(Attributes.HTTP_REQUEST_METHOD, request.method()); + addTelemetryAttribute(Attributes.USER_AGENT, configuration.getUserAgent()); try { - addTelemetryAttribute( - Attributes.REQUEST_CLIENT_ID, - configuration.getCredentials().getClientCredentials().getClientId()); + if (!isNullOrWhitespace( + configuration.getCredentials().getClientCredentials().getClientId())) { + addTelemetryAttribute( + Attributes.FGA_CLIENT_REQUEST_CLIENT_ID, + configuration.getCredentials().getClientCredentials().getClientId()); + } } catch (Exception e) { } @@ -119,20 +120,26 @@ private CompletableFuture> attemptHttpRequest( } addTelemetryAttributes(Attributes.fromHttpResponse(response, this.configuration.getCredentials())); - addTelemetryAttribute(Attributes.REQUEST_RETRIES, String.valueOf(retryNumber)); + addTelemetryAttribute(Attributes.HTTP_REQUEST_RESEND_COUNT, String.valueOf(retryNumber)); if (response.headers().firstValue("fga-query-duration-ms").isPresent()) { - double queryDuration = Double.parseDouble(response.headers() + String queryDuration = response.headers() .firstValue("fga-query-duration-ms") - .get()); - telemetry.metrics().queryDuration(queryDuration, this.getTelemetryAttributes()); + .orElse(null); + + if (!isNullOrWhitespace(queryDuration)) { + addTelemetryAttribute(Attributes.HTTP_SERVER_REQUEST_DURATION, queryDuration); + + double queryDurationDouble = Double.parseDouble(queryDuration); + telemetry.metrics().queryDuration(queryDurationDouble, this.getTelemetryAttributes()); + } } - telemetry - .metrics() - .requestDuration( - (double) (System.currentTimeMillis() - this.requestStarted), - this.getTelemetryAttributes()); + Double requestDuration = (double) (System.currentTimeMillis() - requestStarted); + + telemetry.metrics().requestDuration(requestDuration, this.getTelemetryAttributes()); + + addTelemetryAttribute(Attributes.HTTP_CLIENT_REQUEST_DURATION, String.valueOf(requestDuration)); return deserializeResponse(response) .thenApply(modeledResponse -> new ApiResponse<>( diff --git a/src/main/java/dev/openfga/sdk/api/configuration/Configuration.java b/src/main/java/dev/openfga/sdk/api/configuration/Configuration.java index ce536a1..84cc97c 100644 --- a/src/main/java/dev/openfga/sdk/api/configuration/Configuration.java +++ b/src/main/java/dev/openfga/sdk/api/configuration/Configuration.java @@ -45,6 +45,7 @@ public class Configuration implements BaseConfiguration { private int maxRetries; private Duration minimumRetryDelay; private Map defaultHeaders; + private TelemetryConfiguration telemetryConfiguration; public Configuration() { this.apiUrl = DEFAULT_API_URL; @@ -124,6 +125,10 @@ public Configuration override(ConfigurationOverride configurationOverride) { } result.defaultHeaders(headers); + TelemetryConfiguration overrideTelemetryConfiguration = configurationOverride.getTelemetryConfiguration(); + result.telemetryConfiguration = + overrideTelemetryConfiguration != null ? overrideTelemetryConfiguration : telemetryConfiguration; + return result; } @@ -290,4 +295,13 @@ public Map getDefaultHeaders() { } return this.defaultHeaders; } + + public TelemetryConfiguration getTelemetryConfiguration() { + return telemetryConfiguration; + } + + public Configuration telemetryConfiguration(TelemetryConfiguration telemetryConfiguration) { + this.telemetryConfiguration = telemetryConfiguration; + return this; + } } diff --git a/src/main/java/dev/openfga/sdk/api/configuration/ConfigurationOverride.java b/src/main/java/dev/openfga/sdk/api/configuration/ConfigurationOverride.java index 0f06a52..ca73254 100644 --- a/src/main/java/dev/openfga/sdk/api/configuration/ConfigurationOverride.java +++ b/src/main/java/dev/openfga/sdk/api/configuration/ConfigurationOverride.java @@ -33,6 +33,7 @@ public class ConfigurationOverride implements BaseConfiguration { private Integer maxRetries; private Duration minimumRetryDelay; private Map additionalHeaders; + private TelemetryConfiguration telemetryConfiguration; public ConfigurationOverride() { this.apiUrl = null; @@ -41,6 +42,7 @@ public ConfigurationOverride() { this.readTimeout = null; this.connectTimeout = null; this.additionalHeaders = null; + this.telemetryConfiguration = null; } /** @@ -206,4 +208,13 @@ public ConfigurationOverride addHeaders(AdditionalHeadersSupplier supplier) { public Map getAdditionalHeaders() { return this.additionalHeaders; } + + public ConfigurationOverride telemetryConfiguration(TelemetryConfiguration telemetryConfiguration) { + this.telemetryConfiguration = telemetryConfiguration; + return this; + } + + public TelemetryConfiguration getTelemetryConfiguration() { + return telemetryConfiguration; + } } diff --git a/src/main/java/dev/openfga/sdk/api/configuration/TelemetryConfiguration.java b/src/main/java/dev/openfga/sdk/api/configuration/TelemetryConfiguration.java new file mode 100644 index 0000000..8fc78a8 --- /dev/null +++ b/src/main/java/dev/openfga/sdk/api/configuration/TelemetryConfiguration.java @@ -0,0 +1,19 @@ +package dev.openfga.sdk.api.configuration; + +import dev.openfga.sdk.telemetry.Attribute; +import dev.openfga.sdk.telemetry.Metric; +import java.util.Map; +import java.util.Optional; + +public class TelemetryConfiguration { + private Map>> metrics; + + public TelemetryConfiguration metrics(Map>> metrics) { + this.metrics = metrics; + return this; + } + + public Map>> metrics() { + return metrics; + } +} diff --git a/src/main/java/dev/openfga/sdk/telemetry/Attributes.java b/src/main/java/dev/openfga/sdk/telemetry/Attributes.java index 65c9e11..3dbc21f 100644 --- a/src/main/java/dev/openfga/sdk/telemetry/Attributes.java +++ b/src/main/java/dev/openfga/sdk/telemetry/Attributes.java @@ -1,6 +1,9 @@ package dev.openfga.sdk.telemetry; +import static dev.openfga.sdk.util.StringUtil.isNullOrWhitespace; + import dev.openfga.sdk.api.client.ApiResponse; +import dev.openfga.sdk.api.configuration.Configuration; import dev.openfga.sdk.api.configuration.Credentials; import dev.openfga.sdk.api.configuration.CredentialsMethod; import io.opentelemetry.api.common.AttributeKey; @@ -8,60 +11,87 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; /** * This class represents a collection of attributes used for telemetry purposes. */ public class Attributes { + /** - * Attribute representing the model ID of a request. + * The client ID used in the request, if applicable. */ - public static final Attribute REQUEST_MODEL_ID = new Attribute("fga-client.request.model_id"); + public static final Attribute FGA_CLIENT_REQUEST_CLIENT_ID = new Attribute("fga-client.request.client_id"); /** - * Attribute representing the method of a request. + * The FGA method/action of the request. */ - public static final Attribute REQUEST_METHOD = new Attribute("fga-client.request.method"); + public static final Attribute FGA_CLIENT_REQUEST_METHOD = new Attribute("fga-client.request.method"); /** - * Attribute representing the store ID of a request. + * The authorization model ID used in the request, if applicable. */ - public static final Attribute REQUEST_STORE_ID = new Attribute("fga-client.request.store_id"); + public static final Attribute FGA_CLIENT_REQUEST_MODEL_ID = new Attribute("fga-client.request.model_id"); /** - * Attribute representing the client ID of a request. + * The store ID used in the request, if applicable. */ - public static final Attribute REQUEST_CLIENT_ID = new Attribute("fga-client.request.client_id"); + public static final Attribute FGA_CLIENT_REQUEST_STORE_ID = new Attribute("fga-client.request.store_id"); /** - * Attribute representing the number of retries for a request. + * The authorization model ID used by the server when evaluating the request, if applicable. */ - public static final Attribute REQUEST_RETRIES = new Attribute("fga-client.request.retries"); + public static final Attribute FGA_CLIENT_RESPONSE_MODEL_ID = new Attribute("fga-client.response.model_id"); /** - * Attribute representing the model ID of a response. + * The user associated with the action of the request, if applicable. */ - public static final Attribute RESPONSE_MODEL_ID = new Attribute("fga-client.response.model_id"); + public static final Attribute FGA_CLIENT_USER = new Attribute("fga-client.user"); /** - * Attribute representing the user of a client. + * The total time (in milliseconds) it took for the request to complete, including the time it took to send the request and receive the response. */ - public static final Attribute CLIENT_USER = new Attribute("fga-client.user"); + public static final Attribute HTTP_CLIENT_REQUEST_DURATION = new Attribute("http.client.request.duration"); /** - * Attribute representing the host of an HTTP request. + * The HTTP host used in the request. */ public static final Attribute HTTP_HOST = new Attribute("http.host"); /** - * Attribute representing the method of an HTTP request. + * The HTTP method used in the request. + */ + public static final Attribute HTTP_REQUEST_METHOD = new Attribute("http.request.method"); + + /** + * The number of times the request was retried. + */ + public static final Attribute HTTP_REQUEST_RESEND_COUNT = new Attribute("http.request.resend_count"); + + /** + * The HTTP status code returned by the server for the request. + */ + public static final Attribute HTTP_RESPONSE_STATUS_CODE = new Attribute("http.response.status_code"); + + /** + * The total time it took (in milliseconds) for the FGA server to process and evaluate the request. + */ + public static final Attribute HTTP_SERVER_REQUEST_DURATION = new Attribute("http.server.request.duration"); + + /** + * The scheme used in the request. */ - public static final Attribute HTTP_METHOD = new Attribute("http.method"); + public static final Attribute URL_SCHEME = new Attribute("url.scheme"); /** - * Attribute representing the status code of an HTTP response. + * The complete URL used in the request. */ - public static final Attribute HTTP_STATUS_CODE = new Attribute("http.status_code"); + public static final Attribute URL_FULL = new Attribute("url.full"); + + /** + * The user agent used in the request. + */ + public static final Attribute USER_AGENT = new Attribute("user_agent.original"); /** * Prepares the attributes for OpenTelemetry publishing by converting them into the expected format. @@ -70,12 +100,41 @@ public class Attributes { * * @return the prepared attributes */ - public static io.opentelemetry.api.common.Attributes prepare(Map attributes) { + public static io.opentelemetry.api.common.Attributes prepare( + Map attributes, Metric metric, Configuration configuration) { + if (attributes == null + || attributes.isEmpty() + || configuration == null + || configuration.getTelemetryConfiguration() == null + || configuration.getTelemetryConfiguration().metrics() == null + || configuration.getTelemetryConfiguration().metrics().isEmpty() + || !configuration.getTelemetryConfiguration().metrics().containsKey(metric) + || configuration + .getTelemetryConfiguration() + .metrics() + .get(metric) + .isEmpty()) { + return io.opentelemetry.api.common.Attributes.empty(); + } + + Map> configAllowedAttributes = + configuration.getTelemetryConfiguration().metrics().get(metric); + io.opentelemetry.api.common.AttributesBuilder builder = io.opentelemetry.api.common.Attributes.builder(); - attributes.forEach((key, value) -> { - builder.put(AttributeKey.stringKey(key.getName()), value); - }); + for (Map.Entry> configAllowedAttr : configAllowedAttributes.entrySet()) { + Attribute attr = configAllowedAttr.getKey(); + + if (!attributes.containsKey(attr)) { + continue; + } + + String attrVal = attributes.getOrDefault(attr, ""); + + if (!isNullOrWhitespace(attrVal)) { + builder.put(AttributeKey.stringKey(attr.getName()), attrVal); + } + } return builder.build(); } @@ -92,19 +151,23 @@ public static Map fromHttpResponse(HttpResponse response, Map attributes = new HashMap<>(); if (response != null) { - attributes.put(HTTP_STATUS_CODE, String.valueOf(response.statusCode())); + attributes.put(HTTP_RESPONSE_STATUS_CODE, String.valueOf(response.statusCode())); String responseModelId = response.headers() .firstValue("openfga-authorization-model-id") .orElse(null); - if (responseModelId != null) { - attributes.put(RESPONSE_MODEL_ID, responseModelId); + if (!isNullOrWhitespace(responseModelId)) { + attributes.put(FGA_CLIENT_RESPONSE_MODEL_ID, responseModelId); } } if (credentials != null && credentials.getCredentialsMethod() == CredentialsMethod.CLIENT_CREDENTIALS) { - attributes.put(REQUEST_CLIENT_ID, credentials.getClientCredentials().getClientId()); + if (!isNullOrWhitespace(credentials.getClientCredentials().getClientId())) { + attributes.put( + FGA_CLIENT_REQUEST_CLIENT_ID, + credentials.getClientCredentials().getClientId()); + } } return attributes; @@ -122,19 +185,23 @@ public static Map fromApiResponse(ApiResponse response, Cr Map attributes = new HashMap<>(); if (response != null) { - attributes.put(HTTP_STATUS_CODE, String.valueOf(response.getStatusCode())); + attributes.put(HTTP_RESPONSE_STATUS_CODE, String.valueOf(response.getStatusCode())); List responseModelIdList = response.getHeaders().getOrDefault("openfga-authorization-model-id", null); String responseModelId = responseModelIdList != null ? responseModelIdList.get(0) : null; - if (responseModelId != null) { - attributes.put(RESPONSE_MODEL_ID, responseModelId); + if (!isNullOrWhitespace(responseModelId)) { + attributes.put(FGA_CLIENT_RESPONSE_MODEL_ID, responseModelId); } } if (credentials != null && credentials.getCredentialsMethod() == CredentialsMethod.CLIENT_CREDENTIALS) { - attributes.put(REQUEST_CLIENT_ID, credentials.getClientCredentials().getClientId()); + if (!isNullOrWhitespace(credentials.getClientCredentials().getClientId())) { + attributes.put( + FGA_CLIENT_REQUEST_CLIENT_ID, + credentials.getClientCredentials().getClientId()); + } } return attributes; diff --git a/src/main/java/dev/openfga/sdk/telemetry/Counter.java b/src/main/java/dev/openfga/sdk/telemetry/Counter.java index 3a88c34..ef7a966 100644 --- a/src/main/java/dev/openfga/sdk/telemetry/Counter.java +++ b/src/main/java/dev/openfga/sdk/telemetry/Counter.java @@ -3,48 +3,14 @@ /** * Represents a counter used for telemetry purposes. */ -public class Counter { - private final String name; - private final String unit; - private final String description; - +public class Counter extends Metric { /** * Constructs a new Counter with the specified name, unit, and description. * * @param name the name of the counter - * @param unit the unit of measurement for the counter * @param description the description of the counter */ - public Counter(String name, String unit, String description) { - this.name = name; - this.unit = unit; - this.description = description; - } - - /** - * Returns the name of the counter. - * - * @return the name of the counter - */ - public String getName() { - return name; - } - - /** - * Returns the unit of measurement for the counter. - * - * @return the unit of measurement for the counter - */ - public String getUnit() { - return unit; - } - - /** - * Returns the description of the counter. - * - * @return the description of the counter - */ - public String getDescription() { - return description; + public Counter(String name, String description) { + super(name, description); } } diff --git a/src/main/java/dev/openfga/sdk/telemetry/Counters.java b/src/main/java/dev/openfga/sdk/telemetry/Counters.java index 1d52807..81b02e6 100644 --- a/src/main/java/dev/openfga/sdk/telemetry/Counters.java +++ b/src/main/java/dev/openfga/sdk/telemetry/Counters.java @@ -8,5 +8,6 @@ public class Counters { * The CREDENTIALS_REQUEST counter represents the number of times an access token is requested. */ public static final Counter CREDENTIALS_REQUEST = new Counter( - "fga-client.credentials.request", "milliseconds", "The number of times an access token is requested."); + "fga-client.credentials.request", + "The total number of times new access tokens have been requested using ClientCredentials."); } diff --git a/src/main/java/dev/openfga/sdk/telemetry/Histogram.java b/src/main/java/dev/openfga/sdk/telemetry/Histogram.java index d8f6b7a..e6963c9 100644 --- a/src/main/java/dev/openfga/sdk/telemetry/Histogram.java +++ b/src/main/java/dev/openfga/sdk/telemetry/Histogram.java @@ -3,10 +3,8 @@ /** * Represents a histogram for telemetry data. */ -public class Histogram { - private final String name; +public class Histogram extends Metric { private final String unit; - private final String description; /** * Constructs a Histogram object with the specified name, unit, and description. @@ -16,16 +14,19 @@ public class Histogram { * @param description the description of the histogram */ public Histogram(String name, String unit, String description) { - this.name = name; + super(name, description); this.unit = unit; - this.description = description; } /** - * Returns the name of the histogram. + * Constructs a Histogram object with the specified name and description. The unit of measurement is set to "milliseconds" by default. + * + * @param name the name of the histogram + * @param description the description of the histogram */ - public String getName() { - return name; + public Histogram(String name, String description) { + super(name, description); + this.unit = "milliseconds"; } /** @@ -34,11 +35,4 @@ public String getName() { public String getUnit() { return unit; } - - /** - * Returns the description of the histogram. - */ - public String getDescription() { - return description; - } } diff --git a/src/main/java/dev/openfga/sdk/telemetry/Histograms.java b/src/main/java/dev/openfga/sdk/telemetry/Histograms.java index 82a7217..ebc84d6 100644 --- a/src/main/java/dev/openfga/sdk/telemetry/Histograms.java +++ b/src/main/java/dev/openfga/sdk/telemetry/Histograms.java @@ -5,14 +5,16 @@ */ public class Histograms { /** - * A histogram for measuring the duration of a request. + * A histogram for measuring the total time (in milliseconds) it took for the request to complete, including the time it took to send the request and receive the response. */ public static final Histogram REQUEST_DURATION = new Histogram( - "fga-client.request.duration", "milliseconds", "How long it took for a request to be fulfilled."); + "fga-client.request.duration", + "The total time (in milliseconds) it took for the request to complete, including the time it took to send the request and receive the response."); /** - * A histogram for measuring the duration of a query request. + * A histogram for measuring the total time it took (in milliseconds) for the FGA server to process and evaluate the request. */ - public static final Histogram QUERY_DURATION = - new Histogram("fga-client.query.duration", "milliseconds", "How long it took to perform a query request."); + public static final Histogram QUERY_DURATION = new Histogram( + "fga-client.query.duration", + "The total time it took (in milliseconds) for the FGA server to process and evaluate the request."); } diff --git a/src/main/java/dev/openfga/sdk/telemetry/Metric.java b/src/main/java/dev/openfga/sdk/telemetry/Metric.java new file mode 100644 index 0000000..5a0cd1b --- /dev/null +++ b/src/main/java/dev/openfga/sdk/telemetry/Metric.java @@ -0,0 +1,31 @@ +package dev.openfga.sdk.telemetry; + +public class Metric { + protected final String name; + protected final String description; + + /** + * Constructs a new metric with the specified name and description. + * + * @param name the name of the counter + * @param description the description of the counter + */ + public Metric(String name, String description) { + this.name = name; + this.description = description; + } + + /** + * Returns the name of the metric. + */ + public String getName() { + return name; + } + + /** + * Returns the description of the metric. + */ + public String getDescription() { + return description; + } +} diff --git a/src/main/java/dev/openfga/sdk/telemetry/Metrics.java b/src/main/java/dev/openfga/sdk/telemetry/Metrics.java index 3b3d564..4705715 100644 --- a/src/main/java/dev/openfga/sdk/telemetry/Metrics.java +++ b/src/main/java/dev/openfga/sdk/telemetry/Metrics.java @@ -1,5 +1,6 @@ package dev.openfga.sdk.telemetry; +import dev.openfga.sdk.api.configuration.Configuration; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.metrics.DoubleHistogram; import io.opentelemetry.api.metrics.LongCounter; @@ -15,11 +16,20 @@ public class Metrics { private final Meter meter; private final Map counters; private final Map histograms; + private final Configuration configuration; public Metrics() { this.meter = OpenTelemetry.noop().getMeterProvider().get("openfga-sdk/0.5.0"); this.counters = new HashMap<>(); this.histograms = new HashMap<>(); + this.configuration = new Configuration(); + } + + public Metrics(Configuration configuration) { + this.meter = OpenTelemetry.noop().getMeterProvider().get("openfga-sdk/0.5.0"); + this.counters = new HashMap<>(); + this.histograms = new HashMap<>(); + this.configuration = configuration; } /** @@ -46,14 +56,13 @@ public LongCounter getCounter(Counter counter, Long value, Map attribu } /** - * Returns a DoubleHistogram metric instance, for publishing the duration of requests. + * Returns a DoubleHistogram histogram for measuring the total roundtrip time it took to process a request, including the time it took to send the request and receive the response. * * @param value The value to be recorded in the histogram. * @param attributes A map of attributes associated with the metric. @@ -108,7 +117,7 @@ public DoubleHistogram requestDuration(Double value, Map attr } /** - * Returns a DoubleHistogram metric instance, for publishing the duration of queries. + * Returns a DoubleHistogram for measuring how long the FGA server took to process and evaluate a request. * * @param value The value to be recorded in the histogram. * @param attributes A map of attributes associated with the metric. diff --git a/src/test/java/dev/openfga/sdk/api/configuration/TelemetryConfigurationTest.java b/src/test/java/dev/openfga/sdk/api/configuration/TelemetryConfigurationTest.java new file mode 100644 index 0000000..97b2eca --- /dev/null +++ b/src/test/java/dev/openfga/sdk/api/configuration/TelemetryConfigurationTest.java @@ -0,0 +1,31 @@ +package dev.openfga.sdk.api.configuration; + +import static org.junit.jupiter.api.Assertions.*; + +import dev.openfga.sdk.telemetry.Attribute; +import dev.openfga.sdk.telemetry.Metric; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import org.junit.jupiter.api.Test; + +class TelemetryConfigurationTest { + + @Test + void testSetAndGetMetrics() { + // Arrange + TelemetryConfiguration telemetryConfiguration = new TelemetryConfiguration(); + Map>> metrics = new HashMap<>(); + Metric metric = new Metric("testMetric", "A metric for testing"); + Map> attributes = new HashMap<>(); + Attribute attribute = new Attribute("testAttribute"); + attributes.put(attribute, Optional.of("testValue")); + metrics.put(metric, attributes); + + // Act + telemetryConfiguration.metrics(metrics); + + // Assert + assertEquals(metrics, telemetryConfiguration.metrics(), "The metrics map should match the one set."); + } +} diff --git a/src/test/java/dev/openfga/sdk/telemetry/AttributeTest.java b/src/test/java/dev/openfga/sdk/telemetry/AttributeTest.java new file mode 100644 index 0000000..775ee7d --- /dev/null +++ b/src/test/java/dev/openfga/sdk/telemetry/AttributeTest.java @@ -0,0 +1,21 @@ +package dev.openfga.sdk.telemetry; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class AttributeTest { + + @Test + void testGetName() { + // Arrange + String attributeName = "testAttribute"; + Attribute attribute = new Attribute(attributeName); + + // Act + String result = attribute.getName(); + + // Assert + assertEquals(attributeName, result, "The name should match the one provided in the constructor."); + } +} diff --git a/src/test/java/dev/openfga/sdk/telemetry/AttributesTest.java b/src/test/java/dev/openfga/sdk/telemetry/AttributesTest.java new file mode 100644 index 0000000..c86c199 --- /dev/null +++ b/src/test/java/dev/openfga/sdk/telemetry/AttributesTest.java @@ -0,0 +1,103 @@ +package dev.openfga.sdk.telemetry; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import dev.openfga.sdk.api.client.ApiResponse; +import dev.openfga.sdk.api.configuration.ClientCredentials; +import dev.openfga.sdk.api.configuration.Configuration; +import dev.openfga.sdk.api.configuration.Credentials; +import dev.openfga.sdk.api.configuration.CredentialsMethod; +import dev.openfga.sdk.api.configuration.TelemetryConfiguration; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.AttributesBuilder; +import java.net.http.HttpHeaders; +import java.net.http.HttpResponse; +import java.util.*; +import org.junit.jupiter.api.Test; + +class AttributesTest { + + @Test + void testPrepare() { + // Arrange + Map attributes = new HashMap<>(); + attributes.put(Attributes.FGA_CLIENT_REQUEST_CLIENT_ID, "client-id-value"); + Metric metric = mock(Metric.class); + + TelemetryConfiguration telemetryConfiguration = mock(TelemetryConfiguration.class); + Map>> metricsMap = new HashMap<>(); + Map> attributeMap = new HashMap<>(); + attributeMap.put(Attributes.FGA_CLIENT_REQUEST_CLIENT_ID, Optional.of("config-value")); + metricsMap.put(metric, attributeMap); + when(telemetryConfiguration.metrics()).thenReturn(metricsMap); + + Configuration configuration = mock(Configuration.class); + when(configuration.getTelemetryConfiguration()).thenReturn(telemetryConfiguration); + + // Act + io.opentelemetry.api.common.Attributes result = Attributes.prepare(attributes, metric, configuration); + + // Assert + AttributesBuilder builder = io.opentelemetry.api.common.Attributes.builder(); + builder.put(AttributeKey.stringKey(Attributes.FGA_CLIENT_REQUEST_CLIENT_ID.getName()), "client-id-value"); + io.opentelemetry.api.common.Attributes expected = builder.build(); + + assertEquals(expected, result); + } + + @Test + void testFromHttpResponse() { + // Arrange + HttpResponse response = mock(HttpResponse.class); + HttpHeaders headers = mock(HttpHeaders.class); + when(response.headers()).thenReturn(headers); + when(headers.firstValue("openfga-authorization-model-id")).thenReturn(Optional.of("model-id-value")); + when(response.statusCode()).thenReturn(200); + + Credentials credentials = mock(Credentials.class); + ClientCredentials clientCredentials = mock(ClientCredentials.class); + when(credentials.getCredentialsMethod()).thenReturn(CredentialsMethod.CLIENT_CREDENTIALS); + when(credentials.getClientCredentials()).thenReturn(clientCredentials); + when(clientCredentials.getClientId()).thenReturn("client-id-value"); + + // Act + Map result = Attributes.fromHttpResponse(response, credentials); + + // Assert + Map expected = new HashMap<>(); + expected.put(Attributes.HTTP_RESPONSE_STATUS_CODE, "200"); + expected.put(Attributes.FGA_CLIENT_RESPONSE_MODEL_ID, "model-id-value"); + expected.put(Attributes.FGA_CLIENT_REQUEST_CLIENT_ID, "client-id-value"); + + assertEquals(expected, result); + } + + @Test + void testFromApiResponse() { + // Arrange + ApiResponse response = mock(ApiResponse.class); + Map> headers = new HashMap<>(); + headers.put("openfga-authorization-model-id", Collections.singletonList("model-id-value")); + when(response.getHeaders()).thenReturn(headers); + when(response.getStatusCode()).thenReturn(200); + + Credentials credentials = mock(Credentials.class); + ClientCredentials clientCredentials = mock(ClientCredentials.class); + when(credentials.getCredentialsMethod()).thenReturn(CredentialsMethod.CLIENT_CREDENTIALS); + when(credentials.getClientCredentials()).thenReturn(clientCredentials); + when(clientCredentials.getClientId()).thenReturn("client-id-value"); + + // Act + Map result = Attributes.fromApiResponse(response, credentials); + + // Assert + Map expected = new HashMap<>(); + expected.put(Attributes.HTTP_RESPONSE_STATUS_CODE, "200"); + expected.put(Attributes.FGA_CLIENT_RESPONSE_MODEL_ID, "model-id-value"); + expected.put(Attributes.FGA_CLIENT_REQUEST_CLIENT_ID, "client-id-value"); + + assertEquals(expected, result); + } +} diff --git a/src/test/java/dev/openfga/sdk/telemetry/CounterTest.java b/src/test/java/dev/openfga/sdk/telemetry/CounterTest.java new file mode 100644 index 0000000..4c849bd --- /dev/null +++ b/src/test/java/dev/openfga/sdk/telemetry/CounterTest.java @@ -0,0 +1,25 @@ +package dev.openfga.sdk.telemetry; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class CounterTest { + + @Test + void testCounterConstructor() { + // Arrange + String name = "testCounter"; + String description = "A counter for testing"; + + // Act + Counter counter = new Counter(name, description); + + // Assert + assertEquals(name, counter.getName(), "The name should match the one provided in the constructor."); + assertEquals( + description, + counter.getDescription(), + "The description should match the one provided in the constructor."); + } +} diff --git a/src/test/java/dev/openfga/sdk/telemetry/CountersTest.java b/src/test/java/dev/openfga/sdk/telemetry/CountersTest.java new file mode 100644 index 0000000..ab99add --- /dev/null +++ b/src/test/java/dev/openfga/sdk/telemetry/CountersTest.java @@ -0,0 +1,23 @@ +package dev.openfga.sdk.telemetry; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class CountersTest { + + @Test + void testCredentialsRequestCounter() { + // Arrange + String expectedName = "fga-client.credentials.request"; + String expectedDescription = + "The total number of times new access tokens have been requested using ClientCredentials."; + + // Act + Counter counter = Counters.CREDENTIALS_REQUEST; + + // Assert + assertEquals(expectedName, counter.getName(), "The name should match the expected value."); + assertEquals(expectedDescription, counter.getDescription(), "The description should match the expected value."); + } +} diff --git a/src/test/java/dev/openfga/sdk/telemetry/HistogramTest.java b/src/test/java/dev/openfga/sdk/telemetry/HistogramTest.java new file mode 100644 index 0000000..f344601 --- /dev/null +++ b/src/test/java/dev/openfga/sdk/telemetry/HistogramTest.java @@ -0,0 +1,45 @@ +package dev.openfga.sdk.telemetry; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class HistogramTest { + + @Test + void testHistogramConstructorWithUnit() { + // Arrange + String name = "testHistogram"; + String unit = "seconds"; + String description = "A histogram for testing"; + + // Act + Histogram histogram = new Histogram(name, unit, description); + + // Assert + assertEquals(name, histogram.getName(), "The name should match the one provided in the constructor."); + assertEquals(unit, histogram.getUnit(), "The unit should match the one provided in the constructor."); + assertEquals( + description, + histogram.getDescription(), + "The description should match the one provided in the constructor."); + } + + @Test + void testHistogramConstructorWithoutUnit() { + // Arrange + String name = "testHistogram"; + String description = "A histogram for testing"; + + // Act + Histogram histogram = new Histogram(name, description); + + // Assert + assertEquals(name, histogram.getName(), "The name should match the one provided in the constructor."); + assertEquals("milliseconds", histogram.getUnit(), "The default unit should be 'milliseconds'."); + assertEquals( + description, + histogram.getDescription(), + "The description should match the one provided in the constructor."); + } +} diff --git a/src/test/java/dev/openfga/sdk/telemetry/HistogramsTest.java b/src/test/java/dev/openfga/sdk/telemetry/HistogramsTest.java new file mode 100644 index 0000000..c1a3bc4 --- /dev/null +++ b/src/test/java/dev/openfga/sdk/telemetry/HistogramsTest.java @@ -0,0 +1,42 @@ +package dev.openfga.sdk.telemetry; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class HistogramsTest { + + @Test + void testRequestDurationHistogram() { + // Arrange + String expectedName = "fga-client.request.duration"; + String expectedDescription = + "The total time (in milliseconds) it took for the request to complete, including the time it took to send the request and receive the response."; + + // Act + Histogram histogram = Histograms.REQUEST_DURATION; + + // Assert + assertEquals(expectedName, histogram.getName(), "The name should match the expected value."); + assertEquals("milliseconds", histogram.getUnit(), "The default unit should be 'milliseconds'."); + assertEquals( + expectedDescription, histogram.getDescription(), "The description should match the expected value."); + } + + @Test + void testQueryDurationHistogram() { + // Arrange + String expectedName = "fga-client.query.duration"; + String expectedDescription = + "The total time it took (in milliseconds) for the FGA server to process and evaluate the request."; + + // Act + Histogram histogram = Histograms.QUERY_DURATION; + + // Assert + assertEquals(expectedName, histogram.getName(), "The name should match the expected value."); + assertEquals("milliseconds", histogram.getUnit(), "The default unit should be 'milliseconds'."); + assertEquals( + expectedDescription, histogram.getDescription(), "The description should match the expected value."); + } +} diff --git a/src/test/java/dev/openfga/sdk/telemetry/MetricTest.java b/src/test/java/dev/openfga/sdk/telemetry/MetricTest.java new file mode 100644 index 0000000..018571b --- /dev/null +++ b/src/test/java/dev/openfga/sdk/telemetry/MetricTest.java @@ -0,0 +1,25 @@ +package dev.openfga.sdk.telemetry; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class MetricTest { + + @Test + void testMetricConstructor() { + // Arrange + String name = "testMetric"; + String description = "A metric for testing"; + + // Act + Metric metric = new Metric(name, description); + + // Assert + assertEquals(name, metric.getName(), "The name should match the one provided in the constructor."); + assertEquals( + description, + metric.getDescription(), + "The description should match the one provided in the constructor."); + } +} diff --git a/src/test/java/dev/openfga/sdk/telemetry/MetricsTest.java b/src/test/java/dev/openfga/sdk/telemetry/MetricsTest.java new file mode 100644 index 0000000..f335c31 --- /dev/null +++ b/src/test/java/dev/openfga/sdk/telemetry/MetricsTest.java @@ -0,0 +1,119 @@ +package dev.openfga.sdk.telemetry; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; + +import dev.openfga.sdk.api.configuration.Configuration; +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 org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class MetricsTest { + + private Metrics metrics; + private Configuration configuration; + + @BeforeEach + void setUp() { + configuration = mock(Configuration.class); + metrics = new Metrics(configuration); + } + + @Test + void testConstructorWithConfiguration() { + // Act + Metrics metricsWithConfig = new Metrics(configuration); + + // Assert + assertNotNull(metricsWithConfig.getMeter(), "The Meter object should not be null."); + } + + @Test + void testConstructorWithoutConfiguration() { + // Act + Metrics metricsWithoutConfig = new Metrics(); + + // Assert + assertNotNull(metricsWithoutConfig.getMeter(), "The Meter object should not be null."); + } + + @Test + void testGetMeter() { + // Act + Meter meter = metrics.getMeter(); + + // Assert + assertNotNull(meter, "The Meter object should not be null."); + } + + @Test + void testGetCounter() { + // Arrange + Counter counter = Counters.CREDENTIALS_REQUEST; + Long value = 10L; + Map attributes = new HashMap<>(); + + // Act + LongCounter longCounter = metrics.getCounter(counter, value, attributes); + + // Assert + assertNotNull(longCounter, "The LongCounter object should not be null."); + } + + @Test + void testGetHistogram() { + // Arrange + Histogram histogram = Histograms.REQUEST_DURATION; + Double value = 100.0; + Map attributes = new HashMap<>(); + + // Act + DoubleHistogram doubleHistogram = metrics.getHistogram(histogram, value, attributes); + + // Assert + assertNotNull(doubleHistogram, "The DoubleHistogram object should not be null."); + } + + @Test + void testCredentialsRequest() { + // Arrange + Long value = 5L; + Map attributes = new HashMap<>(); + + // Act + LongCounter longCounter = metrics.credentialsRequest(value, attributes); + + // Assert + assertNotNull(longCounter, "The LongCounter object should not be null."); + } + + @Test + void testRequestDuration() { + // Arrange + Double value = 200.0; + Map attributes = new HashMap<>(); + + // Act + DoubleHistogram doubleHistogram = metrics.requestDuration(value, attributes); + + // Assert + assertNotNull(doubleHistogram, "The DoubleHistogram object should not be null."); + } + + @Test + void testQueryDuration() { + // Arrange + Double value = 150.0; + Map attributes = new HashMap<>(); + + // Act + DoubleHistogram doubleHistogram = metrics.queryDuration(value, attributes); + + // Assert + assertNotNull(doubleHistogram, "The DoubleHistogram object should not be null."); + } +} diff --git a/src/test/java/dev/openfga/sdk/telemetry/TelemetryTest.java b/src/test/java/dev/openfga/sdk/telemetry/TelemetryTest.java new file mode 100644 index 0000000..601cefb --- /dev/null +++ b/src/test/java/dev/openfga/sdk/telemetry/TelemetryTest.java @@ -0,0 +1,22 @@ +package dev.openfga.sdk.telemetry; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class TelemetryTest { + + @Test + void testMetricsInitialization() { + // Arrange + Telemetry telemetry = new Telemetry(); + + // Act + Metrics firstCall = telemetry.metrics(); + Metrics secondCall = telemetry.metrics(); + + // Assert + assertNotNull(firstCall, "The Metrics object should not be null after initialization."); + assertSame(firstCall, secondCall, "The same Metrics object should be returned on subsequent calls."); + } +}