From 5afb45430073d1e825f391b1e1fa48cdedef8678 Mon Sep 17 00:00:00 2001 From: Evan Sims Date: Wed, 18 Sep 2024 13:35:32 -0500 Subject: [PATCH] refactor: improve OpenTelemetry configuration --- docs/opentelemetry.md | 20 +- example/opentelemetry/main.py | 119 ++- openfga_sdk/__init__.py | 2 + openfga_sdk/api/open_fga_api.py | 580 ++---------- openfga_sdk/api_client.py | 52 +- openfga_sdk/configuration.py | 69 +- openfga_sdk/oauth2.py | 3 +- openfga_sdk/sync/api_client.py | 30 +- openfga_sdk/sync/oauth2.py | 3 +- openfga_sdk/sync/open_fga_api.py | 576 ++---------- openfga_sdk/telemetry/__init__.py | 4 +- openfga_sdk/telemetry/attributes.py | 219 +++-- openfga_sdk/telemetry/configuration.py | 1155 +++++++++++++++++++++--- openfga_sdk/telemetry/counters.py | 23 +- openfga_sdk/telemetry/histograms.py | 28 +- openfga_sdk/telemetry/metrics.py | 176 ++-- openfga_sdk/telemetry/telemetry.py | 9 +- openfga_sdk/telemetry/utilities.py | 7 + test/client/client_test.py | 2 - test/sync/client/client_test.py | 1 - test/telemetry/attributes_test.py | 81 +- test/telemetry/configuration_test.py | 305 +++++-- test/telemetry/counters_test.py | 10 +- test/telemetry/histograms_test.py | 16 +- test/telemetry/metrics_test.py | 44 +- test/telemetry/telemetry_test.py | 18 +- test/telemetry/utilities_test.py | 15 + 27 files changed, 2043 insertions(+), 1524 deletions(-) create mode 100644 openfga_sdk/telemetry/utilities.py create mode 100644 test/telemetry/utilities_test.py diff --git a/docs/opentelemetry.md b/docs/opentelemetry.md index 460b3ae..3467ee8 100644 --- a/docs/opentelemetry.md +++ b/docs/opentelemetry.md @@ -42,7 +42,7 @@ If you configure the OpenTelemetry SDK, these metrics will be exported and sent | `http.request.method` | string | Yes | HTTP method for the request | | `http.request.resend_count` | int | Yes | Number of retries attempted, if any | | `http.response.status_code` | int | Yes | Status code of the response (e.g., `200` for success) | -| `http.server.request.duration` | int | Yes | Time taken by the FGA server to process and evaluate the request, in milliseconds | +| `http.server.request.duration` | int | No | Time taken by the FGA server to process and evaluate the request, in milliseconds | | `url.scheme` | string | Yes | HTTP scheme of the request (`http`/`https`) | | `url.full` | string | Yes | Full URL of the request | | `user_agent.original` | string | Yes | User Agent used in the query | @@ -102,15 +102,15 @@ configuration = ClientConfiguration( store_id=os.getenv("FGA_STORE_ID"), authorization_model_id=os.getenv("FGA_AUTHORIZATION_MODEL_ID"), - # If you are comfortable with the default configuration outlined in the tables above, you can omit providing your own TelemetryConfiguration object. - telemetry=TelemetryConfiguration( - metrics=TelemetryMetricsConfiguration( - histogram_request_duration=TelemetryMetricConfiguration( - attr_fga_client_request_method=True, - attr_http_response_status_code=True, - ), - ), - ), + # If you are comfortable with the default configuration outlined in the tables above, you can omit providing your own TelemetryConfiguration object, as one will be created for you. + telemetry={ + "metrics": { + "fga-client.request.duration": { + "fga-client.request.method": True, + "http.response.status_code": True, + }, + }, + }, ) fga = OpenFgaClient(configuration) diff --git a/example/opentelemetry/main.py b/example/opentelemetry/main.py index b0610bf..8a3a5da 100644 --- a/example/opentelemetry/main.py +++ b/example/opentelemetry/main.py @@ -15,12 +15,6 @@ ) from opentelemetry.sdk.resources import SERVICE_NAME, Resource -from openfga_sdk.telemetry.configuration import ( - TelemetryConfiguration, - TelemetryMetricConfiguration, - TelemetryMetricsConfiguration, -) - # For usage convenience of this example, we will import the OpenFGA SDK from the parent directory. sdk_path = os.path.realpath(os.path.join(os.path.abspath(__file__), "..", "..", "..")) sys.path.insert(0, sdk_path) @@ -36,6 +30,7 @@ Credentials, ) from openfga_sdk.exceptions import FgaValidationException +from openfga_sdk.telemetry.configuration import TelemetryConfiguration class app: @@ -88,62 +83,62 @@ async def fga_client(self, env: dict[str, str] = {}) -> OpenFgaClient: if not self._telemetry: # Configure the telemetry metrics to be collected. - # Note: the following represents the default configuration values, so unless you want to change them, you can omit this step. - self._telemetry = TelemetryConfiguration( - metrics=TelemetryMetricsConfiguration( - counter_credentials_request=TelemetryMetricConfiguration( - attr_fga_client_request_client_id=True, - attr_fga_client_request_method=True, - attr_fga_client_request_model_id=True, - attr_fga_client_request_store_id=True, - attr_fga_client_response_model_id=True, - attr_fga_client_user=False, - attr_http_client_request_duration=False, - attr_http_host=True, - attr_http_request_method=True, - attr_http_request_resend_count=True, - attr_http_response_status_code=True, - attr_http_server_request_duration=False, - attr_http_url_scheme=True, - attr_http_url_full=True, - attr_user_agent_original=True, - ), - histogram_request_duration=TelemetryMetricConfiguration( - attr_fga_client_request_client_id=True, - attr_fga_client_request_method=True, - attr_fga_client_request_model_id=True, - attr_fga_client_request_store_id=True, - attr_fga_client_response_model_id=True, - attr_fga_client_user=False, - attr_http_client_request_duration=False, - attr_http_host=True, - attr_http_request_method=True, - attr_http_request_resend_count=True, - attr_http_response_status_code=True, - attr_http_server_request_duration=False, - attr_http_url_scheme=True, - attr_http_url_full=True, - attr_user_agent_original=True, - ), - histogram_query_duration=TelemetryMetricConfiguration( - attr_fga_client_request_client_id=True, - attr_fga_client_request_method=True, - attr_fga_client_request_model_id=True, - attr_fga_client_request_store_id=True, - attr_fga_client_response_model_id=True, - attr_fga_client_user=False, - attr_http_client_request_duration=False, - attr_http_host=True, - attr_http_request_method=True, - attr_http_request_resend_count=True, - attr_http_response_status_code=True, - attr_http_server_request_duration=False, - attr_http_url_scheme=True, - attr_http_url_full=True, - attr_user_agent_original=True, - ), - ), - ) + # Note: the following represents the default configuration values, so unless you want to customize what's reported, you can omit this. + self._telemetry = { + "metrics": { + "fga-client.credentials.request": { + "fga-client.request.client_id": True, + "fga-client.request.method": True, + "fga-client.request.model_id": True, + "fga-client.request.store_id": True, + "fga-client.response.model_id": True, + "fga-client.user": False, + "http.client.request.duration": False, + "http.host": True, + "http.request.method": True, + "http.request.resend_count": True, + "http.response.status_code": True, + "http.server.request.duration": False, + "url.scheme": True, + "url.full": True, + "user_agent.original": True, + }, + "fga-client.request.duration": { + "fga-client.request.client_id": True, + "fga-client.request.method": True, + "fga-client.request.model_id": True, + "fga-client.request.store_id": True, + "fga-client.response.model_id": True, + "fga-client.user": False, + "http.client.request.duration": False, + "http.host": True, + "http.request.method": True, + "http.request.resend_count": True, + "http.response.status_code": True, + "http.server.request.duration": False, + "url.scheme": True, + "url.full": True, + "user_agent.original": True, + }, + "fga-client.query.duration": { + "fga-client.request.client_id": True, + "fga-client.request.method": True, + "fga-client.request.model_id": True, + "fga-client.request.store_id": True, + "fga-client.response.model_id": True, + "fga-client.user": False, + "http.client.request.duration": False, + "http.host": True, + "http.request.method": True, + "http.request.resend_count": True, + "http.response.status_code": True, + "http.server.request.duration": False, + "url.scheme": True, + "url.full": True, + "user_agent.original": True, + }, + } + } self._configuration.telemetry = self._telemetry diff --git a/openfga_sdk/__init__.py b/openfga_sdk/__init__.py index b34198c..a3f93f5 100644 --- a/openfga_sdk/__init__.py +++ b/openfga_sdk/__init__.py @@ -124,6 +124,8 @@ from openfga_sdk.models.write_request_writes import WriteRequestWrites from openfga_sdk.telemetry.configuration import ( TelemetryConfiguration, + TelemetryConfigurations, + TelemetryConfigurationType, TelemetryMetricConfiguration, TelemetryMetricsConfiguration, ) diff --git a/openfga_sdk/api/open_fga_api.py b/openfga_sdk/api/open_fga_api.py index 3ece705..294487c 100644 --- a/openfga_sdk/api/open_fga_api.py +++ b/openfga_sdk/api/open_fga_api.py @@ -33,7 +33,9 @@ def __init__(self, api_client=None): if api_client.configuration is not None: credentials = api_client.configuration.credentials if credentials is not None and credentials.method == "client_credentials": - self._oauth2_client = OAuth2Client(credentials) + self._oauth2_client = OAuth2Client( + credentials, api_client.configuration + ) self._telemetry = Telemetry() @@ -189,38 +191,14 @@ async def check_with_http_info(self, body, **kwargs): 500: "InternalErrorMessageResponse", } - telemetry_attributes: dict[TelemetryAttribute, str] = { - TelemetryAttributes.fga_client_request_method: "check" + telemetry_attributes: dict[TelemetryAttribute, str | int] = { + TelemetryAttributes.fga_client_request_method: "check", + TelemetryAttributes.fga_client_request_store_id: self.api_client.get_store_id(), + TelemetryAttributes.fga_client_request_model_id: local_var_params.get( + "authorization_model_id", "" + ), } - try: - if store_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_store_id - ] = store_id - except: - pass - - try: - if body_params.tuple_key: - telemetry_attributes[TelemetryAttributes.fga_client_user] = ( - body_params.tuple_key.user - ) - except: - pass - - try: - if "authorization_model_id" in local_var_params: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = local_var_params["authorization_model_id"] - elif body_params.authorization_model_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = body_params.authorization_model_id - except: - pass - return await self.api_client.call_api( "/stores/{store_id}/check".replace("{store_id}", store_id), "POST", @@ -373,38 +351,14 @@ async def create_store_with_http_info(self, body, **kwargs): 500: "InternalErrorMessageResponse", } - telemetry_attributes: dict[TelemetryAttribute, str] = { - TelemetryAttributes.fga_client_request_method: "create_store" + telemetry_attributes: dict[TelemetryAttribute, str | int] = { + TelemetryAttributes.fga_client_request_method: "create_store", + TelemetryAttributes.fga_client_request_store_id: self.api_client.get_store_id(), + TelemetryAttributes.fga_client_request_model_id: local_var_params.get( + "authorization_model_id", "" + ), } - try: - if store_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_store_id - ] = store_id - except: - pass - - try: - if body_params.tuple_key: - telemetry_attributes[TelemetryAttributes.fga_client_user] = ( - body_params.tuple_key.user - ) - except: - pass - - try: - if "authorization_model_id" in local_var_params: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = local_var_params["authorization_model_id"] - elif body_params.authorization_model_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = body_params.authorization_model_id - except: - pass - return await self.api_client.call_api( "/stores", "POST", @@ -539,38 +493,14 @@ async def delete_store_with_http_info(self, **kwargs): response_types_map = {} - telemetry_attributes: dict[TelemetryAttribute, str] = { - TelemetryAttributes.fga_client_request_method: "delete_store" + telemetry_attributes: dict[TelemetryAttribute, str | int] = { + TelemetryAttributes.fga_client_request_method: "delete_store", + TelemetryAttributes.fga_client_request_store_id: self.api_client.get_store_id(), + TelemetryAttributes.fga_client_request_model_id: local_var_params.get( + "authorization_model_id", "" + ), } - try: - if store_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_store_id - ] = store_id - except: - pass - - try: - if body_params.tuple_key: - telemetry_attributes[TelemetryAttributes.fga_client_user] = ( - body_params.tuple_key.user - ) - except: - pass - - try: - if "authorization_model_id" in local_var_params: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = local_var_params["authorization_model_id"] - elif body_params.authorization_model_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = body_params.authorization_model_id - except: - pass - return await self.api_client.call_api( "/stores/{store_id}".replace("{store_id}", store_id), "DELETE", @@ -736,38 +666,14 @@ async def expand_with_http_info(self, body, **kwargs): 500: "InternalErrorMessageResponse", } - telemetry_attributes: dict[TelemetryAttribute, str] = { - TelemetryAttributes.fga_client_request_method: "expand" + telemetry_attributes: dict[TelemetryAttribute, str | int] = { + TelemetryAttributes.fga_client_request_method: "expand", + TelemetryAttributes.fga_client_request_store_id: self.api_client.get_store_id(), + TelemetryAttributes.fga_client_request_model_id: local_var_params.get( + "authorization_model_id", "" + ), } - try: - if store_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_store_id - ] = store_id - except: - pass - - try: - if body_params.tuple_key: - telemetry_attributes[TelemetryAttributes.fga_client_user] = ( - body_params.tuple_key.user - ) - except: - pass - - try: - if "authorization_model_id" in local_var_params: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = local_var_params["authorization_model_id"] - elif body_params.authorization_model_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = body_params.authorization_model_id - except: - pass - return await self.api_client.call_api( "/stores/{store_id}/expand".replace("{store_id}", store_id), "POST", @@ -910,38 +816,14 @@ async def get_store_with_http_info(self, **kwargs): 500: "InternalErrorMessageResponse", } - telemetry_attributes: dict[TelemetryAttribute, str] = { - TelemetryAttributes.fga_client_request_method: "get_store" + telemetry_attributes: dict[TelemetryAttribute, str | int] = { + TelemetryAttributes.fga_client_request_method: "get_store", + TelemetryAttributes.fga_client_request_store_id: self.api_client.get_store_id(), + TelemetryAttributes.fga_client_request_model_id: local_var_params.get( + "authorization_model_id", "" + ), } - try: - if store_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_store_id - ] = store_id - except: - pass - - try: - if body_params.tuple_key: - telemetry_attributes[TelemetryAttributes.fga_client_user] = ( - body_params.tuple_key.user - ) - except: - pass - - try: - if "authorization_model_id" in local_var_params: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = local_var_params["authorization_model_id"] - elif body_params.authorization_model_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = body_params.authorization_model_id - except: - pass - return await self.api_client.call_api( "/stores/{store_id}".replace("{store_id}", store_id), "GET", @@ -1108,38 +990,14 @@ async def list_objects_with_http_info(self, body, **kwargs): 500: "InternalErrorMessageResponse", } - telemetry_attributes: dict[TelemetryAttribute, str] = { - TelemetryAttributes.fga_client_request_method: "list_objects" + telemetry_attributes: dict[TelemetryAttribute, str | int] = { + TelemetryAttributes.fga_client_request_method: "list_objects", + TelemetryAttributes.fga_client_request_store_id: self.api_client.get_store_id(), + TelemetryAttributes.fga_client_request_model_id: local_var_params.get( + "authorization_model_id", "" + ), } - try: - if store_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_store_id - ] = store_id - except: - pass - - try: - if body_params.tuple_key: - telemetry_attributes[TelemetryAttributes.fga_client_user] = ( - body_params.tuple_key.user - ) - except: - pass - - try: - if "authorization_model_id" in local_var_params: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = local_var_params["authorization_model_id"] - elif body_params.authorization_model_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = body_params.authorization_model_id - except: - pass - return await self.api_client.call_api( "/stores/{store_id}/list-objects".replace("{store_id}", store_id), "POST", @@ -1290,38 +1148,14 @@ async def list_stores_with_http_info(self, **kwargs): 500: "InternalErrorMessageResponse", } - telemetry_attributes: dict[TelemetryAttribute, str] = { - TelemetryAttributes.fga_client_request_method: "list_stores" + telemetry_attributes: dict[TelemetryAttribute, str | int] = { + TelemetryAttributes.fga_client_request_method: "list_stores", + TelemetryAttributes.fga_client_request_store_id: self.api_client.get_store_id(), + TelemetryAttributes.fga_client_request_model_id: local_var_params.get( + "authorization_model_id", "" + ), } - try: - if store_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_store_id - ] = store_id - except: - pass - - try: - if body_params.tuple_key: - telemetry_attributes[TelemetryAttributes.fga_client_user] = ( - body_params.tuple_key.user - ) - except: - pass - - try: - if "authorization_model_id" in local_var_params: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = local_var_params["authorization_model_id"] - elif body_params.authorization_model_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = body_params.authorization_model_id - except: - pass - return await self.api_client.call_api( "/stores", "GET", @@ -1488,38 +1322,14 @@ async def list_users_with_http_info(self, body, **kwargs): 500: "InternalErrorMessageResponse", } - telemetry_attributes: dict[TelemetryAttribute, str] = { - TelemetryAttributes.fga_client_request_method: "list_users" + telemetry_attributes: dict[TelemetryAttribute, str | int] = { + TelemetryAttributes.fga_client_request_method: "list_users", + TelemetryAttributes.fga_client_request_store_id: self.api_client.get_store_id(), + TelemetryAttributes.fga_client_request_model_id: local_var_params.get( + "authorization_model_id", "" + ), } - try: - if store_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_store_id - ] = store_id - except: - pass - - try: - if body_params.tuple_key: - telemetry_attributes[TelemetryAttributes.fga_client_user] = ( - body_params.tuple_key.user - ) - except: - pass - - try: - if "authorization_model_id" in local_var_params: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = local_var_params["authorization_model_id"] - elif body_params.authorization_model_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = body_params.authorization_model_id - except: - pass - return await self.api_client.call_api( "/stores/{store_id}/list-users".replace("{store_id}", store_id), "POST", @@ -1685,38 +1495,14 @@ async def read_with_http_info(self, body, **kwargs): 500: "InternalErrorMessageResponse", } - telemetry_attributes: dict[TelemetryAttribute, str] = { - TelemetryAttributes.fga_client_request_method: "read" + telemetry_attributes: dict[TelemetryAttribute, str | int] = { + TelemetryAttributes.fga_client_request_method: "read", + TelemetryAttributes.fga_client_request_store_id: self.api_client.get_store_id(), + TelemetryAttributes.fga_client_request_model_id: local_var_params.get( + "authorization_model_id", "" + ), } - try: - if store_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_store_id - ] = store_id - except: - pass - - try: - if body_params.tuple_key: - telemetry_attributes[TelemetryAttributes.fga_client_user] = ( - body_params.tuple_key.user - ) - except: - pass - - try: - if "authorization_model_id" in local_var_params: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = local_var_params["authorization_model_id"] - elif body_params.authorization_model_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = body_params.authorization_model_id - except: - pass - return await self.api_client.call_api( "/stores/{store_id}/read".replace("{store_id}", store_id), "POST", @@ -1878,38 +1664,14 @@ async def read_assertions_with_http_info(self, authorization_model_id, **kwargs) 500: "InternalErrorMessageResponse", } - telemetry_attributes: dict[TelemetryAttribute, str] = { - TelemetryAttributes.fga_client_request_method: "read_assertions" + telemetry_attributes: dict[TelemetryAttribute, str | int] = { + TelemetryAttributes.fga_client_request_method: "read_assertions", + TelemetryAttributes.fga_client_request_store_id: self.api_client.get_store_id(), + TelemetryAttributes.fga_client_request_model_id: local_var_params.get( + "authorization_model_id", "" + ), } - try: - if store_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_store_id - ] = store_id - except: - pass - - try: - if body_params.tuple_key: - telemetry_attributes[TelemetryAttributes.fga_client_user] = ( - body_params.tuple_key.user - ) - except: - pass - - try: - if "authorization_model_id" in local_var_params: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = local_var_params["authorization_model_id"] - elif body_params.authorization_model_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = body_params.authorization_model_id - except: - pass - return await self.api_client.call_api( "/stores/{store_id}/assertions/{authorization_model_id}".replace( "{store_id}", store_id @@ -2069,38 +1831,14 @@ async def read_authorization_model_with_http_info(self, id, **kwargs): 500: "InternalErrorMessageResponse", } - telemetry_attributes: dict[TelemetryAttribute, str] = { - TelemetryAttributes.fga_client_request_method: "read_authorization_model" + telemetry_attributes: dict[TelemetryAttribute, str | int] = { + TelemetryAttributes.fga_client_request_method: "read_authorization_model", + TelemetryAttributes.fga_client_request_store_id: self.api_client.get_store_id(), + TelemetryAttributes.fga_client_request_model_id: local_var_params.get( + "authorization_model_id", "" + ), } - try: - if store_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_store_id - ] = store_id - except: - pass - - try: - if body_params.tuple_key: - telemetry_attributes[TelemetryAttributes.fga_client_user] = ( - body_params.tuple_key.user - ) - except: - pass - - try: - if "authorization_model_id" in local_var_params: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = local_var_params["authorization_model_id"] - elif body_params.authorization_model_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = body_params.authorization_model_id - except: - pass - return await self.api_client.call_api( "/stores/{store_id}/authorization-models/{id}".replace( "{store_id}", store_id @@ -2259,38 +1997,14 @@ async def read_authorization_models_with_http_info(self, **kwargs): 500: "InternalErrorMessageResponse", } - telemetry_attributes: dict[TelemetryAttribute, str] = { - TelemetryAttributes.fga_client_request_method: "read_authorization_models" + telemetry_attributes: dict[TelemetryAttribute, str | int] = { + TelemetryAttributes.fga_client_request_method: "read_authorization_models", + TelemetryAttributes.fga_client_request_store_id: self.api_client.get_store_id(), + TelemetryAttributes.fga_client_request_model_id: local_var_params.get( + "authorization_model_id", "" + ), } - try: - if store_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_store_id - ] = store_id - except: - pass - - try: - if body_params.tuple_key: - telemetry_attributes[TelemetryAttributes.fga_client_user] = ( - body_params.tuple_key.user - ) - except: - pass - - try: - if "authorization_model_id" in local_var_params: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = local_var_params["authorization_model_id"] - elif body_params.authorization_model_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = body_params.authorization_model_id - except: - pass - return await self.api_client.call_api( "/stores/{store_id}/authorization-models".replace("{store_id}", store_id), "GET", @@ -2453,38 +2167,14 @@ async def read_changes_with_http_info(self, **kwargs): 500: "InternalErrorMessageResponse", } - telemetry_attributes: dict[TelemetryAttribute, str] = { - TelemetryAttributes.fga_client_request_method: "read_changes" + telemetry_attributes: dict[TelemetryAttribute, str | int] = { + TelemetryAttributes.fga_client_request_method: "read_changes", + TelemetryAttributes.fga_client_request_store_id: self.api_client.get_store_id(), + TelemetryAttributes.fga_client_request_model_id: local_var_params.get( + "authorization_model_id", "" + ), } - try: - if store_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_store_id - ] = store_id - except: - pass - - try: - if body_params.tuple_key: - telemetry_attributes[TelemetryAttributes.fga_client_user] = ( - body_params.tuple_key.user - ) - except: - pass - - try: - if "authorization_model_id" in local_var_params: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = local_var_params["authorization_model_id"] - elif body_params.authorization_model_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = body_params.authorization_model_id - except: - pass - return await self.api_client.call_api( "/stores/{store_id}/changes".replace("{store_id}", store_id), "GET", @@ -2650,38 +2340,14 @@ async def write_with_http_info(self, body, **kwargs): 500: "InternalErrorMessageResponse", } - telemetry_attributes: dict[TelemetryAttribute, str] = { - TelemetryAttributes.fga_client_request_method: "write" + telemetry_attributes: dict[TelemetryAttribute, str | int] = { + TelemetryAttributes.fga_client_request_method: "write", + TelemetryAttributes.fga_client_request_store_id: self.api_client.get_store_id(), + TelemetryAttributes.fga_client_request_model_id: local_var_params.get( + "authorization_model_id", "" + ), } - try: - if store_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_store_id - ] = store_id - except: - pass - - try: - if body_params.tuple_key: - telemetry_attributes[TelemetryAttributes.fga_client_user] = ( - body_params.tuple_key.user - ) - except: - pass - - try: - if "authorization_model_id" in local_var_params: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = local_var_params["authorization_model_id"] - elif body_params.authorization_model_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = body_params.authorization_model_id - except: - pass - return await self.api_client.call_api( "/stores/{store_id}/write".replace("{store_id}", store_id), "POST", @@ -2861,38 +2527,14 @@ async def write_assertions_with_http_info( response_types_map = {} - telemetry_attributes: dict[TelemetryAttribute, str] = { - TelemetryAttributes.fga_client_request_method: "write_assertions" + telemetry_attributes: dict[TelemetryAttribute, str | int] = { + TelemetryAttributes.fga_client_request_method: "write_assertions", + TelemetryAttributes.fga_client_request_store_id: self.api_client.get_store_id(), + TelemetryAttributes.fga_client_request_model_id: local_var_params.get( + "authorization_model_id", "" + ), } - try: - if store_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_store_id - ] = store_id - except: - pass - - try: - if body_params.tuple_key: - telemetry_attributes[TelemetryAttributes.fga_client_user] = ( - body_params.tuple_key.user - ) - except: - pass - - try: - if "authorization_model_id" in local_var_params: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = local_var_params["authorization_model_id"] - elif body_params.authorization_model_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = body_params.authorization_model_id - except: - pass - return await self.api_client.call_api( "/stores/{store_id}/assertions/{authorization_model_id}".replace( "{store_id}", store_id @@ -3061,38 +2703,14 @@ async def write_authorization_model_with_http_info(self, body, **kwargs): 500: "InternalErrorMessageResponse", } - telemetry_attributes: dict[TelemetryAttribute, str] = { - TelemetryAttributes.fga_client_request_method: "write_authorization_model" + telemetry_attributes: dict[TelemetryAttribute, str | int] = { + TelemetryAttributes.fga_client_request_method: "write_authorization_model", + TelemetryAttributes.fga_client_request_store_id: self.api_client.get_store_id(), + TelemetryAttributes.fga_client_request_model_id: local_var_params.get( + "authorization_model_id", "" + ), } - try: - if store_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_store_id - ] = store_id - except: - pass - - try: - if body_params.tuple_key: - telemetry_attributes[TelemetryAttributes.fga_client_user] = ( - body_params.tuple_key.user - ) - except: - pass - - try: - if "authorization_model_id" in local_var_params: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = local_var_params["authorization_model_id"] - elif body_params.authorization_model_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = body_params.authorization_model_id - except: - pass - return await self.api_client.call_api( "/stores/{store_id}/authorization-models".replace("{store_id}", store_id), "POST", diff --git a/openfga_sdk/api_client.py b/openfga_sdk/api_client.py index 1328b83..337a6e0 100644 --- a/openfga_sdk/api_client.py +++ b/openfga_sdk/api_client.py @@ -166,7 +166,7 @@ async def __call_api( _request_auth=None, _retry_params=None, _oauth2_client=None, - _telemetry_attributes: dict[TelemetryAttribute | str, str] = None, + _telemetry_attributes: dict[TelemetryAttribute, str | int] = None, ): self.configuration.is_valid() @@ -255,7 +255,20 @@ async def __call_api( max_retry = _retry_params.max_retry if _retry_params.min_wait_in_ms is not None: max_retry = _retry_params.min_wait_in_ms + + _telemetry_attributes = TelemetryAttributes.fromRequest( + user_agent=self.user_agent, + fga_method=resource_path, + http_method=method, + url=url, + resend_count=0, + start=start, + credentials=self.configuration.credentials, + attributes=_telemetry_attributes, + ) + for retry in range(max_retry + 1): + _telemetry_attributes[TelemetryAttributes.http_request_resend_count] = retry try: # perform request and return response response_data = await self.request( @@ -289,35 +302,40 @@ async def __call_api( json.loads(e.body), response_type ) e.body = None + + _telemetry_attributes = TelemetryAttributes.fromResponse( + response=e, + credentials=self.configuration.credentials, + attributes=_telemetry_attributes, + ) + + self._telemetry.metrics.queryDuration( + attributes=_telemetry_attributes, + configuration=self.configuration.telemetry, + ) + + self._telemetry.metrics.requestDuration( + attributes=_telemetry_attributes, + configuration=self.configuration.telemetry, + ) raise e self.last_response = response_data return_data = response_data - _telemetry_attributes = TelemetryAttributes().fromRequest( - user_agent=self.user_agent, - fga_method=resource_path, - http_method=method, - url=url, - resend_count=retry, - start=start, - credentials=self.configuration.credentials, - attributes=_telemetry_attributes, - ) - - _telemetry_attributes = TelemetryAttributes().fromResponse( + _telemetry_attributes = TelemetryAttributes.fromResponse( response=response_data, credentials=self.configuration.credentials, attributes=_telemetry_attributes, ) - self._telemetry.metrics().queryDuration( + self._telemetry.metrics.queryDuration( attributes=_telemetry_attributes, configuration=self.configuration.telemetry, ) - self._telemetry.metrics().requestDuration( + self._telemetry.metrics.requestDuration( attributes=_telemetry_attributes, configuration=self.configuration.telemetry, ) @@ -467,7 +485,7 @@ async def call_api( _request_auth=None, _retry_params=None, _oauth2_client=None, - _telemetry_attributes: dict[TelemetryAttribute, str] = None, + _telemetry_attributes: dict[TelemetryAttribute, str | int] = None, ): """Makes the HTTP request (synchronous) and returns deserialized data. @@ -728,7 +746,7 @@ async def update_params_for_auth( ) if credentials.method == "client_credentials": if oauth2_client is None: - oauth2_client = oauth2.OAuth2Client(credentials) + oauth2_client = oauth2.OAuth2Client(credentials, self.configuration) oauth2_headers = await oauth2_client.get_authentication_header( self.rest_client ) diff --git a/openfga_sdk/configuration.py b/openfga_sdk/configuration.py index cf75e00..f666a2d 100644 --- a/openfga_sdk/configuration.py +++ b/openfga_sdk/configuration.py @@ -15,11 +15,20 @@ import logging import sys import urllib +from typing import Optional import urllib3 from openfga_sdk.exceptions import ApiValueError, FgaValidationException -from openfga_sdk.telemetry.configuration import TelemetryConfiguration +from openfga_sdk.telemetry.attributes import TelemetryAttribute +from openfga_sdk.telemetry.configuration import ( + TelemetryConfiguration, + TelemetryConfigurationType, + TelemetryMetricConfiguration, + TelemetryMetricsConfiguration, +) +from openfga_sdk.telemetry.counters import TelemetryCounter +from openfga_sdk.telemetry.histograms import TelemetryHistogram from openfga_sdk.validation import is_well_formed_ulid_string JSON_SCHEMA_VALIDATION_KEYWORDS = { @@ -184,7 +193,19 @@ def __init__( server_operation_variables=None, ssl_ca_cert=None, api_url=None, # TODO: restructure when removing api_scheme/api_host - telemetry: TelemetryConfiguration | None = None, + telemetry: Optional[ + dict[ + TelemetryConfigurationType | str, + TelemetryMetricsConfiguration + | dict[ + TelemetryHistogram | TelemetryCounter, + TelemetryMetricConfiguration + | dict[TelemetryAttribute, bool] + | None, + ] + | None, + ] + ] = None, ): """Constructor""" self._url = api_url @@ -295,10 +316,16 @@ def __init__( """Options to pass down to the underlying urllib3 socket """ + self._telemetry: TelemetryConfiguration | None = None if telemetry is None: - telemetry = TelemetryConfiguration() + self._telemetry = TelemetryConfiguration( + TelemetryConfiguration.getSdkDefaults() + ) + elif isinstance(telemetry, dict): + self._telemetry = TelemetryConfiguration(telemetry) + elif isinstance(telemetry, TelemetryConfiguration): + self._telemetry = telemetry - self.telemetry = telemetry """Telemetry configuration """ @@ -426,6 +453,40 @@ def logger_format(self, value): self.__logger_format = value self.logger_formatter = logging.Formatter(self.__logger_format) + @property + def telemetry(self) -> TelemetryConfiguration | None: + """Return the telemetry configuration""" + return self._telemetry + + @telemetry.setter + def telemetry( + self, + value: ( + dict[ + TelemetryConfigurationType | str, + TelemetryMetricsConfiguration + | dict[ + TelemetryHistogram | TelemetryCounter, + TelemetryMetricConfiguration + | dict[TelemetryAttribute, bool] + | None, + ] + | None, + ] + | None + ), + ) -> None: + """Set the telemetry configuration""" + if value is not None: + if isinstance(value, dict): + self._telemetry = TelemetryConfiguration(value) + return + elif isinstance(value, TelemetryConfiguration): + self._telemetry = value + return + + self._telemetry = None + def get_api_key_with_prefix(self, identifier, alias=None): """Gets API key (with prefix if set). diff --git a/openfga_sdk/oauth2.py b/openfga_sdk/oauth2.py index d7fee9d..7b8651c 100644 --- a/openfga_sdk/oauth2.py +++ b/openfga_sdk/oauth2.py @@ -126,11 +126,12 @@ async def _obtain_token(self, client): seconds=int(api_response.get("expires_in")) ) self._access_token = api_response.get("access_token") - self._telemetry.metrics().credentialsRequest( + self._telemetry.metrics.credentialsRequest( 1, { TelemetryAttributes.fga_client_request_client_id: configuration.client_id }, + self.configuration.telemetry, ) break diff --git a/openfga_sdk/sync/api_client.py b/openfga_sdk/sync/api_client.py index 88e91a0..80abcb4 100644 --- a/openfga_sdk/sync/api_client.py +++ b/openfga_sdk/sync/api_client.py @@ -164,7 +164,7 @@ def __call_api( _request_auth=None, _retry_params=None, _oauth2_client=None, - _telemetry_attributes: dict[TelemetryAttribute | str, str] = None, + _telemetry_attributes: dict[TelemetryAttribute, str | int] = None, ): self.configuration.is_valid() @@ -286,13 +286,29 @@ def __call_api( json.loads(e.body), response_type ) e.body = None + + _telemetry_attributes = TelemetryAttributes.fromResponse( + response=e, + credentials=self.configuration.credentials, + attributes=_telemetry_attributes, + ) + + self._telemetry.metrics.queryDuration( + attributes=_telemetry_attributes, + configuration=self.configuration.telemetry, + ) + + self._telemetry.metrics.requestDuration( + attributes=_telemetry_attributes, + configuration=self.configuration.telemetry, + ) raise e self.last_response = response_data return_data = response_data - _telemetry_attributes = TelemetryAttributes().fromRequest( + _telemetry_attributes = TelemetryAttributes.fromRequest( user_agent=self.user_agent, fga_method=resource_path, http_method=method, @@ -303,18 +319,18 @@ def __call_api( attributes=_telemetry_attributes, ) - _telemetry_attributes = TelemetryAttributes().fromResponse( + _telemetry_attributes = TelemetryAttributes.fromResponse( response=response_data, credentials=self.configuration.credentials, attributes=_telemetry_attributes, ) - self._telemetry.metrics().queryDuration( + self._telemetry.metrics.queryDuration( attributes=_telemetry_attributes, configuration=self.configuration.telemetry, ) - self._telemetry.metrics().requestDuration( + self._telemetry.metrics.requestDuration( attributes=_telemetry_attributes, configuration=self.configuration.telemetry, ) @@ -464,7 +480,7 @@ def call_api( _request_auth=None, _retry_params=None, _oauth2_client=None, - _telemetry_attributes: dict[TelemetryAttribute, str] = None, + _telemetry_attributes: dict[TelemetryAttribute, str | int] = None, ): """Makes the HTTP request (synchronous) and returns deserialized data. @@ -725,7 +741,7 @@ def update_params_for_auth( ) if credentials.method == "client_credentials": if oauth2_client is None: - oauth2_client = oauth2.OAuth2Client(credentials) + oauth2_client = oauth2.OAuth2Client(credentials, self.configuration) oauth2_headers = oauth2_client.get_authentication_header( self.rest_client ) diff --git a/openfga_sdk/sync/oauth2.py b/openfga_sdk/sync/oauth2.py index f1cf37c..c35d3a2 100644 --- a/openfga_sdk/sync/oauth2.py +++ b/openfga_sdk/sync/oauth2.py @@ -126,11 +126,12 @@ def _obtain_token(self, client): seconds=int(api_response.get("expires_in")) ) self._access_token = api_response.get("access_token") - self._telemetry.metrics().credentialsRequest( + self._telemetry.metrics.credentialsRequest( 1, { TelemetryAttributes.fga_client_request_client_id: configuration.client_id }, + self.configuration.telemetry, ) break diff --git a/openfga_sdk/sync/open_fga_api.py b/openfga_sdk/sync/open_fga_api.py index 70b8d69..34fea99 100644 --- a/openfga_sdk/sync/open_fga_api.py +++ b/openfga_sdk/sync/open_fga_api.py @@ -189,38 +189,14 @@ def check_with_http_info(self, body, **kwargs): 500: "InternalErrorMessageResponse", } - telemetry_attributes: dict[TelemetryAttribute, str] = { - TelemetryAttributes.fga_client_request_method: "check" + telemetry_attributes: dict[TelemetryAttribute, str | int] = { + TelemetryAttributes.fga_client_request_method: "check", + TelemetryAttributes.fga_client_request_store_id: self.api_client.get_store_id(), + TelemetryAttributes.fga_client_request_model_id: local_var_params.get( + "authorization_model_id", "" + ), } - try: - if store_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_store_id - ] = store_id - except: - pass - - try: - if body_params.tuple_key: - telemetry_attributes[TelemetryAttributes.fga_client_user] = ( - body_params.tuple_key.user - ) - except: - pass - - try: - if "authorization_model_id" in local_var_params: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = local_var_params["authorization_model_id"] - elif body_params.authorization_model_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = body_params.authorization_model_id - except: - pass - return self.api_client.call_api( "/stores/{store_id}/check".replace("{store_id}", store_id), "POST", @@ -373,38 +349,14 @@ def create_store_with_http_info(self, body, **kwargs): 500: "InternalErrorMessageResponse", } - telemetry_attributes: dict[TelemetryAttribute, str] = { - TelemetryAttributes.fga_client_request_method: "create_store" + telemetry_attributes: dict[TelemetryAttribute, str | int] = { + TelemetryAttributes.fga_client_request_method: "create_store", + TelemetryAttributes.fga_client_request_store_id: self.api_client.get_store_id(), + TelemetryAttributes.fga_client_request_model_id: local_var_params.get( + "authorization_model_id", "" + ), } - try: - if store_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_store_id - ] = store_id - except: - pass - - try: - if body_params.tuple_key: - telemetry_attributes[TelemetryAttributes.fga_client_user] = ( - body_params.tuple_key.user - ) - except: - pass - - try: - if "authorization_model_id" in local_var_params: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = local_var_params["authorization_model_id"] - elif body_params.authorization_model_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = body_params.authorization_model_id - except: - pass - return self.api_client.call_api( "/stores", "POST", @@ -539,38 +491,14 @@ def delete_store_with_http_info(self, **kwargs): response_types_map = {} - telemetry_attributes: dict[TelemetryAttribute, str] = { - TelemetryAttributes.fga_client_request_method: "delete_store" + telemetry_attributes: dict[TelemetryAttribute, str | int] = { + TelemetryAttributes.fga_client_request_method: "delete_store", + TelemetryAttributes.fga_client_request_store_id: self.api_client.get_store_id(), + TelemetryAttributes.fga_client_request_model_id: local_var_params.get( + "authorization_model_id", "" + ), } - try: - if store_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_store_id - ] = store_id - except: - pass - - try: - if body_params.tuple_key: - telemetry_attributes[TelemetryAttributes.fga_client_user] = ( - body_params.tuple_key.user - ) - except: - pass - - try: - if "authorization_model_id" in local_var_params: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = local_var_params["authorization_model_id"] - elif body_params.authorization_model_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = body_params.authorization_model_id - except: - pass - return self.api_client.call_api( "/stores/{store_id}".replace("{store_id}", store_id), "DELETE", @@ -736,38 +664,14 @@ def expand_with_http_info(self, body, **kwargs): 500: "InternalErrorMessageResponse", } - telemetry_attributes: dict[TelemetryAttribute, str] = { - TelemetryAttributes.fga_client_request_method: "expand" + telemetry_attributes: dict[TelemetryAttribute, str | int] = { + TelemetryAttributes.fga_client_request_method: "expand", + TelemetryAttributes.fga_client_request_store_id: self.api_client.get_store_id(), + TelemetryAttributes.fga_client_request_model_id: local_var_params.get( + "authorization_model_id", "" + ), } - try: - if store_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_store_id - ] = store_id - except: - pass - - try: - if body_params.tuple_key: - telemetry_attributes[TelemetryAttributes.fga_client_user] = ( - body_params.tuple_key.user - ) - except: - pass - - try: - if "authorization_model_id" in local_var_params: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = local_var_params["authorization_model_id"] - elif body_params.authorization_model_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = body_params.authorization_model_id - except: - pass - return self.api_client.call_api( "/stores/{store_id}/expand".replace("{store_id}", store_id), "POST", @@ -910,38 +814,14 @@ def get_store_with_http_info(self, **kwargs): 500: "InternalErrorMessageResponse", } - telemetry_attributes: dict[TelemetryAttribute, str] = { - TelemetryAttributes.fga_client_request_method: "get_store" + telemetry_attributes: dict[TelemetryAttribute, str | int] = { + TelemetryAttributes.fga_client_request_method: "get_store", + TelemetryAttributes.fga_client_request_store_id: self.api_client.get_store_id(), + TelemetryAttributes.fga_client_request_model_id: local_var_params.get( + "authorization_model_id", "" + ), } - try: - if store_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_store_id - ] = store_id - except: - pass - - try: - if body_params.tuple_key: - telemetry_attributes[TelemetryAttributes.fga_client_user] = ( - body_params.tuple_key.user - ) - except: - pass - - try: - if "authorization_model_id" in local_var_params: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = local_var_params["authorization_model_id"] - elif body_params.authorization_model_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = body_params.authorization_model_id - except: - pass - return self.api_client.call_api( "/stores/{store_id}".replace("{store_id}", store_id), "GET", @@ -1108,38 +988,14 @@ def list_objects_with_http_info(self, body, **kwargs): 500: "InternalErrorMessageResponse", } - telemetry_attributes: dict[TelemetryAttribute, str] = { - TelemetryAttributes.fga_client_request_method: "list_objects" + telemetry_attributes: dict[TelemetryAttribute, str | int] = { + TelemetryAttributes.fga_client_request_method: "list_objects", + TelemetryAttributes.fga_client_request_store_id: self.api_client.get_store_id(), + TelemetryAttributes.fga_client_request_model_id: local_var_params.get( + "authorization_model_id", "" + ), } - try: - if store_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_store_id - ] = store_id - except: - pass - - try: - if body_params.tuple_key: - telemetry_attributes[TelemetryAttributes.fga_client_user] = ( - body_params.tuple_key.user - ) - except: - pass - - try: - if "authorization_model_id" in local_var_params: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = local_var_params["authorization_model_id"] - elif body_params.authorization_model_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = body_params.authorization_model_id - except: - pass - return self.api_client.call_api( "/stores/{store_id}/list-objects".replace("{store_id}", store_id), "POST", @@ -1290,38 +1146,14 @@ def list_stores_with_http_info(self, **kwargs): 500: "InternalErrorMessageResponse", } - telemetry_attributes: dict[TelemetryAttribute, str] = { - TelemetryAttributes.fga_client_request_method: "list_stores" + telemetry_attributes: dict[TelemetryAttribute, str | int] = { + TelemetryAttributes.fga_client_request_method: "list_stores", + TelemetryAttributes.fga_client_request_store_id: self.api_client.get_store_id(), + TelemetryAttributes.fga_client_request_model_id: local_var_params.get( + "authorization_model_id", "" + ), } - try: - if store_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_store_id - ] = store_id - except: - pass - - try: - if body_params.tuple_key: - telemetry_attributes[TelemetryAttributes.fga_client_user] = ( - body_params.tuple_key.user - ) - except: - pass - - try: - if "authorization_model_id" in local_var_params: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = local_var_params["authorization_model_id"] - elif body_params.authorization_model_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = body_params.authorization_model_id - except: - pass - return self.api_client.call_api( "/stores", "GET", @@ -1488,38 +1320,14 @@ def list_users_with_http_info(self, body, **kwargs): 500: "InternalErrorMessageResponse", } - telemetry_attributes: dict[TelemetryAttribute, str] = { - TelemetryAttributes.fga_client_request_method: "list_users" + telemetry_attributes: dict[TelemetryAttribute, str | int] = { + TelemetryAttributes.fga_client_request_method: "list_users", + TelemetryAttributes.fga_client_request_store_id: self.api_client.get_store_id(), + TelemetryAttributes.fga_client_request_model_id: local_var_params.get( + "authorization_model_id", "" + ), } - try: - if store_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_store_id - ] = store_id - except: - pass - - try: - if body_params.tuple_key: - telemetry_attributes[TelemetryAttributes.fga_client_user] = ( - body_params.tuple_key.user - ) - except: - pass - - try: - if "authorization_model_id" in local_var_params: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = local_var_params["authorization_model_id"] - elif body_params.authorization_model_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = body_params.authorization_model_id - except: - pass - return self.api_client.call_api( "/stores/{store_id}/list-users".replace("{store_id}", store_id), "POST", @@ -1685,38 +1493,14 @@ def read_with_http_info(self, body, **kwargs): 500: "InternalErrorMessageResponse", } - telemetry_attributes: dict[TelemetryAttribute, str] = { - TelemetryAttributes.fga_client_request_method: "read" + telemetry_attributes: dict[TelemetryAttribute, str | int] = { + TelemetryAttributes.fga_client_request_method: "read", + TelemetryAttributes.fga_client_request_store_id: self.api_client.get_store_id(), + TelemetryAttributes.fga_client_request_model_id: local_var_params.get( + "authorization_model_id", "" + ), } - try: - if store_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_store_id - ] = store_id - except: - pass - - try: - if body_params.tuple_key: - telemetry_attributes[TelemetryAttributes.fga_client_user] = ( - body_params.tuple_key.user - ) - except: - pass - - try: - if "authorization_model_id" in local_var_params: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = local_var_params["authorization_model_id"] - elif body_params.authorization_model_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = body_params.authorization_model_id - except: - pass - return self.api_client.call_api( "/stores/{store_id}/read".replace("{store_id}", store_id), "POST", @@ -1876,38 +1660,14 @@ def read_assertions_with_http_info(self, authorization_model_id, **kwargs): 500: "InternalErrorMessageResponse", } - telemetry_attributes: dict[TelemetryAttribute, str] = { - TelemetryAttributes.fga_client_request_method: "read_assertions" + telemetry_attributes: dict[TelemetryAttribute, str | int] = { + TelemetryAttributes.fga_client_request_method: "read_assertions", + TelemetryAttributes.fga_client_request_store_id: self.api_client.get_store_id(), + TelemetryAttributes.fga_client_request_model_id: local_var_params.get( + "authorization_model_id", "" + ), } - try: - if store_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_store_id - ] = store_id - except: - pass - - try: - if body_params.tuple_key: - telemetry_attributes[TelemetryAttributes.fga_client_user] = ( - body_params.tuple_key.user - ) - except: - pass - - try: - if "authorization_model_id" in local_var_params: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = local_var_params["authorization_model_id"] - elif body_params.authorization_model_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = body_params.authorization_model_id - except: - pass - return self.api_client.call_api( "/stores/{store_id}/assertions/{authorization_model_id}".replace( "{store_id}", store_id @@ -2067,38 +1827,14 @@ def read_authorization_model_with_http_info(self, id, **kwargs): 500: "InternalErrorMessageResponse", } - telemetry_attributes: dict[TelemetryAttribute, str] = { - TelemetryAttributes.fga_client_request_method: "read_authorization_model" + telemetry_attributes: dict[TelemetryAttribute, str | int] = { + TelemetryAttributes.fga_client_request_method: "read_authorization_model", + TelemetryAttributes.fga_client_request_store_id: self.api_client.get_store_id(), + TelemetryAttributes.fga_client_request_model_id: local_var_params.get( + "authorization_model_id", "" + ), } - try: - if store_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_store_id - ] = store_id - except: - pass - - try: - if body_params.tuple_key: - telemetry_attributes[TelemetryAttributes.fga_client_user] = ( - body_params.tuple_key.user - ) - except: - pass - - try: - if "authorization_model_id" in local_var_params: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = local_var_params["authorization_model_id"] - elif body_params.authorization_model_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = body_params.authorization_model_id - except: - pass - return self.api_client.call_api( "/stores/{store_id}/authorization-models/{id}".replace( "{store_id}", store_id @@ -2257,38 +1993,14 @@ def read_authorization_models_with_http_info(self, **kwargs): 500: "InternalErrorMessageResponse", } - telemetry_attributes: dict[TelemetryAttribute, str] = { - TelemetryAttributes.fga_client_request_method: "read_authorization_models" + telemetry_attributes: dict[TelemetryAttribute, str | int] = { + TelemetryAttributes.fga_client_request_method: "read_authorization_models", + TelemetryAttributes.fga_client_request_store_id: self.api_client.get_store_id(), + TelemetryAttributes.fga_client_request_model_id: local_var_params.get( + "authorization_model_id", "" + ), } - try: - if store_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_store_id - ] = store_id - except: - pass - - try: - if body_params.tuple_key: - telemetry_attributes[TelemetryAttributes.fga_client_user] = ( - body_params.tuple_key.user - ) - except: - pass - - try: - if "authorization_model_id" in local_var_params: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = local_var_params["authorization_model_id"] - elif body_params.authorization_model_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = body_params.authorization_model_id - except: - pass - return self.api_client.call_api( "/stores/{store_id}/authorization-models".replace("{store_id}", store_id), "GET", @@ -2451,38 +2163,14 @@ def read_changes_with_http_info(self, **kwargs): 500: "InternalErrorMessageResponse", } - telemetry_attributes: dict[TelemetryAttribute, str] = { - TelemetryAttributes.fga_client_request_method: "read_changes" + telemetry_attributes: dict[TelemetryAttribute, str | int] = { + TelemetryAttributes.fga_client_request_method: "read_changes", + TelemetryAttributes.fga_client_request_store_id: self.api_client.get_store_id(), + TelemetryAttributes.fga_client_request_model_id: local_var_params.get( + "authorization_model_id", "" + ), } - try: - if store_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_store_id - ] = store_id - except: - pass - - try: - if body_params.tuple_key: - telemetry_attributes[TelemetryAttributes.fga_client_user] = ( - body_params.tuple_key.user - ) - except: - pass - - try: - if "authorization_model_id" in local_var_params: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = local_var_params["authorization_model_id"] - elif body_params.authorization_model_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = body_params.authorization_model_id - except: - pass - return self.api_client.call_api( "/stores/{store_id}/changes".replace("{store_id}", store_id), "GET", @@ -2648,38 +2336,14 @@ def write_with_http_info(self, body, **kwargs): 500: "InternalErrorMessageResponse", } - telemetry_attributes: dict[TelemetryAttribute, str] = { - TelemetryAttributes.fga_client_request_method: "write" + telemetry_attributes: dict[TelemetryAttribute, str | int] = { + TelemetryAttributes.fga_client_request_method: "write", + TelemetryAttributes.fga_client_request_store_id: self.api_client.get_store_id(), + TelemetryAttributes.fga_client_request_model_id: local_var_params.get( + "authorization_model_id", "" + ), } - try: - if store_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_store_id - ] = store_id - except: - pass - - try: - if body_params.tuple_key: - telemetry_attributes[TelemetryAttributes.fga_client_user] = ( - body_params.tuple_key.user - ) - except: - pass - - try: - if "authorization_model_id" in local_var_params: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = local_var_params["authorization_model_id"] - elif body_params.authorization_model_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = body_params.authorization_model_id - except: - pass - return self.api_client.call_api( "/stores/{store_id}/write".replace("{store_id}", store_id), "POST", @@ -2857,38 +2521,14 @@ def write_assertions_with_http_info(self, authorization_model_id, body, **kwargs response_types_map = {} - telemetry_attributes: dict[TelemetryAttribute, str] = { - TelemetryAttributes.fga_client_request_method: "write_assertions" + telemetry_attributes: dict[TelemetryAttribute, str | int] = { + TelemetryAttributes.fga_client_request_method: "write_assertions", + TelemetryAttributes.fga_client_request_store_id: self.api_client.get_store_id(), + TelemetryAttributes.fga_client_request_model_id: local_var_params.get( + "authorization_model_id", "" + ), } - try: - if store_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_store_id - ] = store_id - except: - pass - - try: - if body_params.tuple_key: - telemetry_attributes[TelemetryAttributes.fga_client_user] = ( - body_params.tuple_key.user - ) - except: - pass - - try: - if "authorization_model_id" in local_var_params: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = local_var_params["authorization_model_id"] - elif body_params.authorization_model_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = body_params.authorization_model_id - except: - pass - return self.api_client.call_api( "/stores/{store_id}/assertions/{authorization_model_id}".replace( "{store_id}", store_id @@ -3057,38 +2697,14 @@ def write_authorization_model_with_http_info(self, body, **kwargs): 500: "InternalErrorMessageResponse", } - telemetry_attributes: dict[TelemetryAttribute, str] = { - TelemetryAttributes.fga_client_request_method: "write_authorization_model" + telemetry_attributes: dict[TelemetryAttribute, str | int] = { + TelemetryAttributes.fga_client_request_method: "write_authorization_model", + TelemetryAttributes.fga_client_request_store_id: self.api_client.get_store_id(), + TelemetryAttributes.fga_client_request_model_id: local_var_params.get( + "authorization_model_id", "" + ), } - try: - if store_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_store_id - ] = store_id - except: - pass - - try: - if body_params.tuple_key: - telemetry_attributes[TelemetryAttributes.fga_client_user] = ( - body_params.tuple_key.user - ) - except: - pass - - try: - if "authorization_model_id" in local_var_params: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = local_var_params["authorization_model_id"] - elif body_params.authorization_model_id: - telemetry_attributes[ - TelemetryAttributes.fga_client_request_model_id - ] = body_params.authorization_model_id - except: - pass - return self.api_client.call_api( "/stores/{store_id}/authorization-models".replace("{store_id}", store_id), "POST", diff --git a/openfga_sdk/telemetry/__init__.py b/openfga_sdk/telemetry/__init__.py index e032d23..5da1611 100644 --- a/openfga_sdk/telemetry/__init__.py +++ b/openfga_sdk/telemetry/__init__.py @@ -1,9 +1,11 @@ from openfga_sdk.telemetry.attributes import TelemetryAttribute, TelemetryAttributes from openfga_sdk.telemetry.configuration import ( TelemetryConfiguration, + TelemetryConfigurations, + TelemetryConfigurationType, TelemetryMetricConfiguration, TelemetryMetricsConfiguration, ) from openfga_sdk.telemetry.histograms import TelemetryHistogram, TelemetryHistograms -from openfga_sdk.telemetry.metrics import MetricsTelemetry +from openfga_sdk.telemetry.metrics import TelemetryMetrics from openfga_sdk.telemetry.telemetry import Telemetry diff --git a/openfga_sdk/telemetry/attributes.py b/openfga_sdk/telemetry/attributes.py index 97d2712..dbe9890 100644 --- a/openfga_sdk/telemetry/attributes.py +++ b/openfga_sdk/telemetry/attributes.py @@ -1,43 +1,41 @@ import time import urllib -from typing import NamedTuple +from typing import NamedTuple, Optional from aiohttp import ClientResponse from urllib3 import HTTPResponse from openfga_sdk.credentials import Credentials +from openfga_sdk.exceptions import ApiException from openfga_sdk.rest import RESTResponse +from openfga_sdk.telemetry.utilities import ( + doesInstanceHaveCallable, +) class TelemetryAttribute(NamedTuple): name: str - format: str = None + format: str = "string" class TelemetryAttributes: fga_client_request_client_id: TelemetryAttribute = TelemetryAttribute( name="fga-client.request.client_id", - format="string", ) fga_client_request_method: TelemetryAttribute = TelemetryAttribute( name="fga-client.request.method", - format="string", ) fga_client_request_model_id: TelemetryAttribute = TelemetryAttribute( name="fga-client.request.model_id", - format="string", ) fga_client_request_store_id: TelemetryAttribute = TelemetryAttribute( name="fga-client.request.store_id", - format="string", ) fga_client_response_model_id: TelemetryAttribute = TelemetryAttribute( name="fga-client.response.model_id", - format="string", ) fga_client_user: TelemetryAttribute = TelemetryAttribute( name="fga-client.user", - format="string", ) http_client_request_duration: TelemetryAttribute = TelemetryAttribute( name="http.client.request.duration", @@ -45,11 +43,9 @@ class TelemetryAttributes: ) http_host: TelemetryAttribute = TelemetryAttribute( name="http.host", - format="string", ) http_request_method: TelemetryAttribute = TelemetryAttribute( name="http.request.method", - format="string", ) http_request_resend_count: TelemetryAttribute = TelemetryAttribute( name="http.request.resend_count", @@ -65,21 +61,49 @@ class TelemetryAttributes: ) url_scheme: TelemetryAttribute = TelemetryAttribute( name="url.scheme", - format="string", ) url_full: TelemetryAttribute = TelemetryAttribute( name="url.full", - format="string", ) user_agent_original: TelemetryAttribute = TelemetryAttribute( name="user_agent.original", - format="string", ) + _attributes: list[TelemetryAttribute] = [ + fga_client_request_client_id, + fga_client_request_method, + fga_client_request_model_id, + fga_client_request_store_id, + fga_client_response_model_id, + fga_client_user, + http_client_request_duration, + http_host, + http_request_method, + http_request_resend_count, + http_response_status_code, + http_server_request_duration, + url_scheme, + url_full, + user_agent_original, + ] + + @staticmethod + def get( + name: Optional[str] = None, + ) -> list[TelemetryAttribute] | TelemetryAttribute | None: + if name is None: + return TelemetryAttributes._attributes + + for attribute in TelemetryAttributes._attributes: + if attribute.name == name: + return attribute + + return None + + @staticmethod def prepare( - self, - attributes: dict[TelemetryAttribute | str, str | int] | None, - filter: list[TelemetryAttribute | str] | None = None, + attributes: dict[TelemetryAttribute, str | int] | None, + filter: list[TelemetryAttribute] | None = None, ) -> dict[str, str | int]: response = {} @@ -95,7 +119,9 @@ def prepare( attributeTranslated = ( attribute.lower().replace("-", "_").replace(".", "_") ) - attributeInstance = getattr(self, attributeTranslated, None) + attributeInstance = getattr( + TelemetryAttributes, attributeTranslated, None + ) if attributeInstance is None: raise ValueError("Invalid attribute specified: %s" % attribute) @@ -107,11 +133,7 @@ def prepare( "Invalid attribute specified: %s" % type(attribute) ) - if ( - filter is not None - and attribute.name not in filter - and attribute not in filter - ): + if filter is not None and attribute not in filter: continue if attribute.format == "string": @@ -131,13 +153,20 @@ def prepare( except ValueError: continue + if attribute.format == "float": + if not isinstance(value, float): + try: + value = float(value) + except ValueError: + continue + response[attribute.name] = value continue return response + @staticmethod def fromRequest( - self, user_agent: str = None, fga_method: str = None, http_method: str = None, @@ -145,90 +174,148 @@ def fromRequest( resend_count: int = None, start: float = None, credentials: Credentials = None, - attributes: dict[TelemetryAttribute | str, str | int] = None, - ) -> dict[TelemetryAttribute | str, str | int]: + attributes: dict[TelemetryAttribute, str | int] = None, + ) -> dict[TelemetryAttribute, str | int]: if attributes is None: attributes = {} - if fga_method is not None: - attributes[self.fga_client_request_method.name] = fga_method + if ( + TelemetryAttributes.fga_client_request_method not in attributes + and fga_method is not None + ): + fga_method = fga_method.rsplit("/", 1)[-1] + + if fga_method: + attributes[TelemetryAttributes.fga_client_request_method] = ( + fga_method.rsplit("/", 1)[-1] + ) + + if TelemetryAttributes.fga_client_request_method in attributes: + fga_method = attributes[TelemetryAttributes.fga_client_request_method] + fga_method = ( + fga_method.lower().replace("_", " ").title().replace(" ", "").strip() + ) + + if fga_method: + attributes[TelemetryAttributes.fga_client_request_method] = fga_method + else: + del attributes[TelemetryAttributes.fga_client_request_method] if user_agent is not None: - attributes[self.user_agent_original.name] = user_agent + attributes[TelemetryAttributes.user_agent_original] = user_agent if http_method is not None: - attributes[self.http_request_method.name] = http_method + attributes[TelemetryAttributes.http_request_method] = http_method if url is not None: - attributes[self.http_host.name] = urllib.parse.urlparse(url).hostname - attributes[self.url_scheme.name] = urllib.parse.urlparse(url).scheme - attributes[self.url_full.name] = url + attributes[TelemetryAttributes.http_host] = urllib.parse.urlparse( + url + ).hostname + attributes[TelemetryAttributes.url_scheme] = urllib.parse.urlparse( + url + ).scheme + attributes[TelemetryAttributes.url_full] = url if start is not None and start > 0: - attributes[self.http_client_request_duration.name] = int( + attributes[TelemetryAttributes.http_client_request_duration] = int( (time.time() - start) * 1000 ) - if resend_count is not None: - attributes[self.http_request_resend_count.name] = resend_count + if resend_count is not None and resend_count > 0: + attributes[TelemetryAttributes.http_request_resend_count] = resend_count if credentials is not None: if credentials.method == "client_credentials": - attributes[self.fga_client_request_client_id.name] = ( + attributes[TelemetryAttributes.fga_client_request_client_id] = ( credentials.configuration.client_id ) return attributes + @staticmethod def fromResponse( - self, - response: HTTPResponse | RESTResponse | ClientResponse = None, - credentials: Credentials = None, - attributes: dict[TelemetryAttribute | str, str | int] = None, - ) -> dict[TelemetryAttribute | str, str | int]: + response: Optional[ + HTTPResponse | RESTResponse | ClientResponse | ApiException + ] = None, + credentials: Optional[Credentials] = None, + attributes: Optional[dict[TelemetryAttribute, str | int]] = None, + ) -> dict[TelemetryAttribute, str | int]: response_model_id = None response_query_duration = None if attributes is None: attributes = {} + if isinstance(response, ApiException): + if response.status is not None: + attributes[TelemetryAttributes.http_response_status_code] = int( + response.status + ) + + if response.body is not None: + response_model_id = response.body.get( + "openfga-authorization-model-id" + ) or response.body.get("openfga_authorization_model_id") + response_query_duration = response.body.get("fga-query-duration-ms") + if response is not None: - if self.instanceHasAttribute(response, "status"): - attributes[self.http_response_status_code.name] = int(response.status) + if hasattr(response, "status"): + attributes[TelemetryAttributes.http_response_status_code] = int( + response.status + ) - if self.instanceHasCallable(response, "getheader"): + if doesInstanceHaveCallable(response, "getheader"): response_model_id = response.getheader("openfga-authorization-model-id") response_query_duration = response.getheader("fga-query-duration-ms") - if self.instanceHasCallable(response, "headers"): + if doesInstanceHaveCallable(response, "headers"): response_model_id = response.headers.get( "openfga-authorization-model-id" ) response_query_duration = response.headers.get("fga-query-duration-ms") - if response_model_id is not None: - attributes[self.fga_client_response_model_id.name] = response_model_id + if response_model_id is not None: + attributes[TelemetryAttributes.fga_client_response_model_id] = ( + response_model_id + ) - if response_query_duration is not None: - attributes[self.http_server_request_duration.name] = ( - response_query_duration - ) + if response_query_duration is not None: + attributes[TelemetryAttributes.http_server_request_duration] = ( + response_query_duration + ) - if isinstance(credentials, Credentials): - if credentials.method == "client_credentials": - attributes[self.fga_client_request_client_id.name] = ( - credentials.configuration.client_id - ) + if isinstance(credentials, Credentials): + if credentials.method == "client_credentials": + attributes[TelemetryAttributes.fga_client_request_client_id] = ( + credentials.configuration.client_id + ) return attributes - def instanceHasAttribute(self, instance: object, attributeName: str) -> bool: - return hasattr(instance, attributeName) - - def instanceHasCallable(self, instance: object, callableName: str) -> bool: - instanceCallable = getattr(instance, callableName, None) - - if instanceCallable is None: - return False - - return callable(instanceCallable) + @staticmethod + def coalesceAttributeValue( + attribute: TelemetryAttribute, + value: Optional[int | float] = None, + attributes: Optional[dict[TelemetryAttribute, str | int]] = None, + ) -> int | float | None: + if value is None: + if attribute in attributes: + value = attributes[attribute] + + if value is not None: + if attribute.format == "int": + try: + value = int(value) + except ValueError: + value = None + + if attribute.format == "float": + try: + value = float(value) + except ValueError: + value = None + + if attribute.format == "string": + value = str(value) + + return value diff --git a/openfga_sdk/telemetry/configuration.py b/openfga_sdk/telemetry/configuration.py index e746d8f..4822c33 100644 --- a/openfga_sdk/telemetry/configuration.py +++ b/openfga_sdk/telemetry/configuration.py @@ -1,168 +1,1099 @@ -from typing import Optional +from typing import NamedTuple, Optional -from openfga_sdk.telemetry.attributes import TelemetryAttributes +from openfga_sdk.telemetry.attributes import TelemetryAttribute, TelemetryAttributes +from openfga_sdk.telemetry.counters import TelemetryCounter, TelemetryCounters +from openfga_sdk.telemetry.histograms import TelemetryHistogram, TelemetryHistograms class TelemetryMetricConfiguration: - _enabled: bool + """ + Telemetry configuration for an individual histogram or counter. When instantiated directly, all attributes are disabled by default. + Use the `getDefaultTelemetryMetricConfiguration` method to get an instance of TelemetryMetricConfiguration pre-configured with reasonable defaults enabled. + """ + + _state: dict[TelemetryAttribute, bool] = {} + _valid: bool | None = None def __init__( self, - enabled: Optional[bool] = True, - attr_fga_client_request_client_id: Optional[bool] = True, - attr_fga_client_request_method: Optional[bool] = True, - attr_fga_client_request_model_id: Optional[bool] = True, - attr_fga_client_request_store_id: Optional[bool] = True, - attr_fga_client_response_model_id: Optional[bool] = True, - attr_fga_client_user: Optional[bool] = False, - attr_http_client_request_duration: Optional[bool] = False, - attr_http_host: Optional[bool] = True, - attr_http_request_method: Optional[bool] = True, - attr_http_request_resend_count: Optional[bool] = True, - attr_http_response_status_code: Optional[bool] = True, - attr_http_server_request_duration: Optional[bool] = False, - attr_http_url_scheme: Optional[bool] = True, - attr_http_url_full: Optional[bool] = True, - attr_user_agent_original: Optional[bool] = True, + config: Optional[dict[TelemetryAttribute, bool]] = None, + fga_client_request_client_id: Optional[bool] = None, + fga_client_request_method: Optional[bool] = None, + fga_client_request_model_id: Optional[bool] = None, + fga_client_request_store_id: Optional[bool] = None, + fga_client_response_model_id: Optional[bool] = None, + fga_client_user: Optional[bool] = None, + http_client_request_duration: Optional[bool] = None, + http_host: Optional[bool] = None, + http_request_method: Optional[bool] = None, + http_request_resend_count: Optional[bool] = None, + http_response_status_code: Optional[bool] = None, + http_server_request_duration: Optional[bool] = None, + url_scheme: Optional[bool] = None, + url_full: Optional[bool] = None, + user_agent_original: Optional[bool] = None, ): - self._enabled = enabled - self.attr_fga_client_request_client_id = attr_fga_client_request_client_id - self.attr_fga_client_request_method = attr_fga_client_request_method - self.attr_fga_client_request_model_id = attr_fga_client_request_model_id - self.attr_fga_client_request_store_id = attr_fga_client_request_store_id - self.attr_fga_client_response_model_id = attr_fga_client_response_model_id - self.attr_fga_client_user = attr_fga_client_user - self.attr_http_client_request_duration = attr_http_client_request_duration - self.attr_http_host = attr_http_host - self.attr_http_request_method = attr_http_request_method - self.attr_http_request_resend_count = attr_http_request_resend_count - self.attr_http_response_status_code = attr_http_response_status_code - self.attr_http_server_request_duration = attr_http_server_request_duration - self.attr_http_url_scheme = attr_http_url_scheme - self.attr_http_url_full = attr_http_url_full - self.attr_user_agent_original = attr_user_agent_original + """ + Initialize a new instance of the `TelemetryMetricConfiguration` class. + + :param config: A dictionary containing the configuration for the telemetry metrics. + :param fga_client_request_client_id: The `fga-client.request.client_id` attribute includes the client ID associated with the request, if any. + :param fga_client_request_method: The `fga-client.request.method` attribute includes the FGA method/action that was performed. + :param fga_client_request_model_id: The `fga-client.request.model_id` attribute includes the authorization model ID that was sent as part of the request, if any. + :param fga_client_request_store_id: The `fga-client.request.store_id` attribute includes the store ID that was sent as part of the request, if any. + :param fga_client_response_model_id: The `fga-client.response.model_id` attribute includes the authorization model ID that the FGA server used. + :param fga_client_user: The `fga-client.user` attribute includes the user associated with the request, if any. + :param http_client_request_duration: The `http.client.request.duration` attribute includes the duration of the request from the client's perspective. + :param http_host: The `http.host` attribute includes the host name of the request. + :param http_request_method: The `http.request.method` attribute includes the HTTP method of the request. + :param http_request_resend_count: The `http.request.resend_count` attribute includes the number of times the request was resent. + :param http_response_status_code: The `http.response.status_code` attribute includes the HTTP status code of the response. + :param http_server_request_duration: The `http.server.request.duration` attribute includes the duration of the request from the server's perspective. + :param url_scheme: The `url.scheme` attribute includes the scheme of the request URL. + :param url_full: The `url.full` attribute includes the full URL of the request. + :param user_agent_original: The `user_agent.original` attribute includes the original user agent string of the request. + """ + + self.configure( + config=config, + clear=True, + ) + + if fga_client_request_client_id is not None: + self._state[TelemetryAttributes.fga_client_request_client_id] = ( + fga_client_request_client_id + ) + + if fga_client_request_method is not None: + self._state[TelemetryAttributes.fga_client_request_method] = ( + fga_client_request_method + ) + + if fga_client_request_model_id is not None: + self._state[TelemetryAttributes.fga_client_request_model_id] = ( + fga_client_request_model_id + ) + + if fga_client_request_store_id is not None: + self._state[TelemetryAttributes.fga_client_request_store_id] = ( + fga_client_request_store_id + ) + + if fga_client_response_model_id is not None: + self._state[TelemetryAttributes.fga_client_response_model_id] = ( + fga_client_response_model_id + ) + + if fga_client_user is not None: + self._state[TelemetryAttributes.fga_client_user] = fga_client_user + + if http_client_request_duration is not None: + self._state[TelemetryAttributes.http_client_request_duration] = ( + http_client_request_duration + ) + + if http_host is not None: + self._state[TelemetryAttributes.http_host] = http_host + + if http_request_method is not None: + self._state[TelemetryAttributes.http_request_method] = http_request_method + + if http_request_resend_count is not None: + self._state[TelemetryAttributes.http_request_resend_count] = ( + http_request_resend_count + ) + + if http_response_status_code is not None: + self._state[TelemetryAttributes.http_response_status_code] = ( + http_response_status_code + ) + + if http_server_request_duration is not None: + self._state[TelemetryAttributes.http_server_request_duration] = ( + http_server_request_duration + ) + + if url_scheme is not None: + self._state[TelemetryAttributes.url_scheme] = url_scheme + + if url_full is not None: + self._state[TelemetryAttributes.url_full] = url_full + + if user_agent_original is not None: + self._state[TelemetryAttributes.user_agent_original] = user_agent_original + + self._valid = None # Reset the validation state + + @property + def fga_client_request_client_id(self) -> bool: + """ + Get the configuration for the `fga-client.request.client_id` attribute. + + :return: The configuration for the `fga-client.request.client_id` attribute. + """ + + return self._state[TelemetryAttributes.fga_client_request_client_id] + + @fga_client_request_client_id.setter + def fga_client_request_client_id(self, value: bool): + """ + Set the configuration for the `fga-client.request.client_id` attribute. + + :param value: The configuration for the `fga-client.request.client_id` attribute. + """ + + self._valid = None # Reset the validation state + self._state[TelemetryAttributes.fga_client_request_client_id] = value + + @property + def fga_client_request_method(self) -> bool: + """ + Get the configuration for the `fga-client.request.method` attribute. + + :return: The configuration for the `fga-client.request.method` attribute. + """ + + return self._state[TelemetryAttributes.fga_client_request_method] + + @fga_client_request_method.setter + def fga_client_request_method(self, value: bool): + """ + Set the configuration for the `fga-client.request.method` attribute. + + :param value: The configuration for the `fga-client.request.method` attribute. + """ + + self._valid = None # Reset the validation state + self._state[TelemetryAttributes.fga_client_request_method] = value + + @property + def fga_client_request_model_id(self) -> bool: + """ + Get the configuration for the `fga-client.request.model_id` attribute. + + :return: The configuration for the `fga-client.request.model_id` attribute. + """ + + return self._state[TelemetryAttributes.fga_client_request_model_id] + + @fga_client_request_model_id.setter + def fga_client_request_model_id(self, value: bool): + """ + Set the configuration for the `fga-client.request.model_id` attribute. + + :param value: The configuration for the `fga-client.request.model_id` attribute. + """ + + self._valid = None # Reset the validation state + self._state[TelemetryAttributes.fga_client_request_model_id] = value + + @property + def fga_client_request_store_id(self) -> bool: + """ + Get the configuration for the `fga-client.request.store_id` attribute. + + :return: The configuration for the `fga-client.request.store_id` attribute. + """ + + return self._state[TelemetryAttributes.fga_client_request_store_id] + + @fga_client_request_store_id.setter + def fga_client_request_store_id(self, value: bool): + """ + Set the configuration for the `fga-client.request.store_id` attribute. + + :param value: The configuration for the `fga-client.request.store_id` attribute. + """ + + self._valid = None # Reset the validation state + self._state[TelemetryAttributes.fga_client_request_store_id] = value + + @property + def fga_client_response_model_id(self) -> bool: + """ + Get the configuration for the `fga-client.response.model_id` attribute. + + :return: The configuration for the `fga-client.response.model_id` attribute. + """ + + return self._state[TelemetryAttributes.fga_client_response_model_id] + + @fga_client_response_model_id.setter + def fga_client_response_model_id(self, value: bool): + """ + Set the configuration for the `fga-client.response.model_id` attribute. + + :param value: The configuration for the `fga-client.response.model_id` attribute. + """ + + self._valid = None # Reset the validation state + self._state[TelemetryAttributes.fga_client_response_model_id] = value + + @property + def fga_client_user(self) -> bool: + """ + Get the configuration for the `fga-client.user` attribute. + + :return: The configuration for the `fga-client.user` attribute. + """ + + return self._state[TelemetryAttributes.fga_client_user] + + @fga_client_user.setter + def fga_client_user(self, value: bool): + """ + Set the configuration for the `fga-client.user` attribute. + + :param value: The configuration for the `fga-client.user` attribute. + """ + + self._valid = None # Reset the validation state + self._state[TelemetryAttributes.fga_client_user] = value + + @property + def http_client_request_duration(self) -> bool: + """ + Get the configuration for the `http.client.request.duration` attribute. + + :return: The configuration for the `http.client.request.duration` attribute. + """ + + return self._state[TelemetryAttributes.http_client_request_duration] + + @http_client_request_duration.setter + def http_client_request_duration(self, value: bool): + """ + Set the configuration for the `http.client.request.duration` attribute. + + :param value: The configuration for the `http.client.request.duration` attribute. + """ + + self._valid = None # Reset the validation state + self._state[TelemetryAttributes.http_client_request_duration] = value + + @property + def http_host(self) -> bool: + """ + Get the configuration for the `http.host` attribute. + + :return: The configuration for the `http.host` attribute. + """ + + return self._state[TelemetryAttributes.http_host] + + @http_host.setter + def http_host(self, value: bool): + """ + Set the configuration for the `http.host` attribute. + + :param value: The configuration for the `http.host` attribute. + """ + + self._valid = None # Reset the validation state + self._state[TelemetryAttributes.http_host] = value + + @property + def http_request_method(self) -> bool: + """ + Get the configuration for the `http.request.method` attribute. + + :return: The configuration for the `http.request.method` attribute. + """ + + return self._state[TelemetryAttributes.http_request_method] + + @http_request_method.setter + def http_request_method(self, value: bool): + """ + Set the configuration for the `http.request.method` attribute. + + :param value: The configuration for the `http.request.method` attribute. + """ + + self._valid = None # Reset the validation state + self._http_request_method = value + + @property + def http_request_resend_count(self) -> bool: + """ + Get the configuration for the `http.request.resend_count` attribute. + + :return: The configuration for the `http.request.resend_count` attribute. + """ + + return self._state[TelemetryAttributes.http_request_resend_count] + + @http_request_resend_count.setter + def http_request_resend_count(self, value: bool): + """ + Set the configuration for the `http.request.resend_count` attribute. + + :param value: The configuration for the `http.request.resend_count` attribute. + """ + + self._valid = None # Reset the validation state + self._state[TelemetryAttributes.http_request_resend_count] = value + + @property + def http_response_status_code(self) -> bool: + """ + Get the configuration for the `http.response.status_code` attribute. + + :return: The configuration for the `http.response.status_code` attribute. + """ + + return self._state[TelemetryAttributes.http_response_status_code] + + @http_response_status_code.setter + def http_response_status_code(self, value: bool): + """ + Set the configuration for the `http.response.status_code` attribute. + + :param value: The configuration for the `http.response.status_code` attribute. + """ + + self._valid = None # Reset the validation state + self._state[TelemetryAttributes.http_response_status_code] = value + + @property + def http_server_request_duration(self) -> bool: + """ + Get the configuration for the `http.server.request.duration` attribute. + + :return: The configuration for the `http.server.request.duration` attribute. + """ + + return self._state[TelemetryAttributes.http_server_request_duration] + + @http_server_request_duration.setter + def http_server_request_duration(self, value: bool): + """ + Set the configuration for the `http.server.request.duration` attribute. + + :param value: The configuration for the `http.server.request.duration` attribute. + """ + + self._valid = None # Reset the validation state + self._state[TelemetryAttributes.http_server_request_duration] = value + + @property + def url_scheme(self) -> bool: + """ + Get the configuration for the `url.scheme` attribute. + + :return: The configuration for the `url.scheme` attribute. + """ + + return self._state[TelemetryAttributes.url_scheme] + + @url_scheme.setter + def url_scheme(self, value: bool): + """ + Set the configuration for the `url.scheme` attribute. + + :param value: The configuration for the `url.scheme` attribute. + """ + + self._valid = None # Reset the validation state + self._state[TelemetryAttributes.url_scheme] = value @property - def enabled(self) -> bool: - return self._enabled + def url_full(self) -> bool: + """ + Get the configuration for the `url.full` attribute. + + :return: The configuration for the `url.full` attribute. + """ - @enabled.setter - def enabled(self, value: bool): - self._enabled = value + return self._state[TelemetryAttributes.url_full] - def attributes(self) -> dict[str]: - enabled = {} + @url_full.setter + def url_full(self, value: bool): + """ + Set the configuration for the `url.full` attribute. - if self.attr_fga_client_request_client_id: - enabled[TelemetryAttributes.fga_client_request_client_id.name] = True + :param value: The configuration for the `url.full` attribute. + """ - if self.attr_fga_client_request_method: - enabled[TelemetryAttributes.fga_client_request_method.name] = True + self._valid = None # Reset the validation state + self._state[TelemetryAttributes.url_full] = value + + @property + def user_agent_original(self) -> bool: + """ + Get the configuration for the `user_agent.original` attribute. - if self.attr_fga_client_request_model_id: - enabled[TelemetryAttributes.fga_client_request_model_id.name] = True + :return: The configuration for the `user_agent.original` attribute. + """ - if self.attr_fga_client_request_store_id: - enabled[TelemetryAttributes.fga_client_request_store_id.name] = True + return self._state[TelemetryAttributes.user_agent_original] - if self.attr_fga_client_response_model_id: - enabled[TelemetryAttributes.fga_client_response_model_id.name] = True + @user_agent_original.setter + def user_agent_original(self, value: bool): + """ + Set the configuration for the `user_agent.original` attribute. + + :param value: The configuration for the `user_agent.original` attribute. + """ + + self._valid = None # Reset the validation state + self._state[TelemetryAttributes.user_agent_original] = value + + def clear(self) -> None: + """ + Reset the configuration to the default state (all attributes disabled). + """ + + # Reset the configuration to the default state + self._state = { + TelemetryAttributes.fga_client_request_client_id: False, + TelemetryAttributes.fga_client_request_method: False, + TelemetryAttributes.fga_client_request_model_id: False, + TelemetryAttributes.fga_client_request_store_id: False, + TelemetryAttributes.fga_client_response_model_id: False, + TelemetryAttributes.fga_client_user: False, + TelemetryAttributes.http_client_request_duration: False, + TelemetryAttributes.http_host: False, + TelemetryAttributes.http_request_method: False, + TelemetryAttributes.http_request_resend_count: False, + TelemetryAttributes.http_response_status_code: False, + TelemetryAttributes.http_server_request_duration: False, + TelemetryAttributes.url_scheme: False, + TelemetryAttributes.url_full: False, + TelemetryAttributes.user_agent_original: False, + } + + # Reset the validation state + self._valid = True + + def configure( + self, + config: Optional[dict[TelemetryAttribute | str, bool]] = None, + clear: Optional[bool] = False, + ) -> None: + """ + Configure the telemetry metric. + """ - if self.attr_fga_client_user: - enabled[TelemetryAttributes.fga_client_user.name] = True + # Reset the configuration to the default state + if clear is True: + self.clear() - if self.attr_http_client_request_duration: - enabled[TelemetryAttributes.http_client_request_duration.name] = True + # Apply an incoming configuration, if provided + if isinstance(config, dict): + for attribute, enabled in config.items(): + if isinstance(attribute, str): + attribute = TelemetryAttributes.get(name=attribute) - if self.attr_http_host: - enabled[TelemetryAttributes.http_host.name] = True + if not isinstance(attribute, TelemetryAttribute): + raise ValueError( + f"Invalid attribute type provided in `TelemetryMetricConfiguration`; `TelemetryAttribute` expected, but `{type(attribute)}` was provided.", + attribute, + ) - if self.attr_http_request_method: - enabled[TelemetryAttributes.http_request_method.name] = True + if not isinstance(enabled, bool): + raise ValueError( + f"Invalid attribute value provided in `TelemetryMetricConfiguration`; `bool` expected, but `{type(enabled)}` was provided.", + attribute, + ) - if self.attr_http_request_resend_count: - enabled[TelemetryAttributes.http_request_resend_count.name] = True + if attribute not in self._state: + raise ValueError( + f"Invalid attribute provided in `TelemetryMetricConfiguration`; `{attribute.name}` is not a supported attribute type for this context.", + attribute, + ) - if self.attr_http_response_status_code: - enabled[TelemetryAttributes.http_response_status_code.name] = True + self._state[attribute] = enabled - if self.attr_http_server_request_duration: - enabled[TelemetryAttributes.http_server_request_duration.name] = True + # Reset the validation state + self._valid = None - if self.attr_http_url_scheme: - enabled[TelemetryAttributes.url_scheme.name] = True + def getAttributes( + self, filter_enabled: Optional[bool] = True + ) -> dict[TelemetryAttribute, bool]: + """ + Returns a list of supported attributes. If `filter_enabled` is `True`, only enabled attributes are returned. - if self.attr_http_url_full: - enabled[TelemetryAttributes.url_full.name] = True + :param filter_enabled: A boolean indicating whether to filter the list to only include enabled attributes. - if self.attr_user_agent_original: - enabled[TelemetryAttributes.user_agent_original.name] = True + :return: A list of enabled attributes. + """ - return enabled + attributes = self._state + + if filter_enabled is True: + return [ + attribute + for attribute, enabled in attributes.items() + if enabled is True + ] + + return attributes + + def isEnabled(self, attribute: Optional[TelemetryAttribute] = None) -> bool: + """ + Check if this metric is enabled for telemetry, based on whether any attributes are enabled. + + :return: A boolean indicating whether any attributes are enabled for the metric. + """ + + # If no attribute is specified, check if any attributes are enabled + if attribute is None: + return True if any(self.getAttributes(filter_enabled=True)) else False + + # Check if the specified attribute is enabled + if attribute in self._state: + return self._state[attribute] + + return False + + def isValid(self, raise_exception: Optional[bool] = False) -> bool: + """ + Validate the telemetry metric configuration. + + :param raise_exception: A boolean indicating whether to raise an exception if the configuration is invalid. + + :return: A boolean indicating whether the metric configuration is valid. + """ + + # Check if the validation state is already cached + if self._valid is not None: + return self._valid + + # Validate the configuration + self._valid = all([isinstance(value, bool) for value in self._state.values()]) + + # If requested, raise an exception if the configuration is invalid + if self._valid is False and raise_exception is True: + raise ValueError("Invalid TelemetryMetricConfiguration.") + + # Return the validation state + return self._valid + + @staticmethod + def getSdkDefaults() -> dict[TelemetryAttribute, bool]: + """ + Get the default SDK configuration for the telemetry metric. + + :return: The default SDK configuration for the telemetry metric. + """ + return { + TelemetryAttributes.fga_client_request_client_id: True, + TelemetryAttributes.fga_client_request_method: True, + TelemetryAttributes.fga_client_request_model_id: True, + TelemetryAttributes.fga_client_request_store_id: True, + TelemetryAttributes.fga_client_response_model_id: True, + TelemetryAttributes.fga_client_user: False, + TelemetryAttributes.http_client_request_duration: False, + TelemetryAttributes.http_host: True, + TelemetryAttributes.http_request_method: True, + TelemetryAttributes.http_request_resend_count: True, + TelemetryAttributes.http_response_status_code: True, + TelemetryAttributes.http_server_request_duration: False, + TelemetryAttributes.url_scheme: True, + TelemetryAttributes.url_full: True, + TelemetryAttributes.user_agent_original: True, + } class TelemetryMetricsConfiguration: - _counter_credentials_request: TelemetryMetricConfiguration - _histogram_request_duration: TelemetryMetricConfiguration - _histogram_query_duration: TelemetryMetricConfiguration + _state: dict[ + TelemetryHistogram | TelemetryCounter, TelemetryMetricConfiguration | None + ] = {} + _valid: bool | None = None def __init__( self, - counter_credentials_request: Optional[TelemetryMetricConfiguration] = None, - histogram_request_duration: Optional[TelemetryMetricConfiguration] = None, - histogram_query_duration: Optional[TelemetryMetricConfiguration] = None, + config: Optional[ + dict[ + TelemetryHistogram | TelemetryCounter, + TelemetryMetricConfiguration | None, + ] + ] = None, + fga_client_credentials_request: Optional[TelemetryMetricConfiguration] = None, + fga_client_request_duration: Optional[TelemetryMetricConfiguration] = None, + fga_client_query_duration: Optional[TelemetryMetricConfiguration] = None, ): - if counter_credentials_request is None: - counter_credentials_request = TelemetryMetricConfiguration() + """ + Initialize a new instance of the `TelemetryMetricsConfiguration` class. + + :param config: A dictionary containing the configuration for the telemetry metrics. + :param fga_client_credentials_request: The `fga-client.credentials.request` counter collects the number of times a new token is requested using ClientCredentials. + :param fga_client_request_duration: The `fga-client.query.duration` histogram tracks how long requests take to complete from the client's perspective. + :param fga_client_query_duration: The `fga-client.request.duration` histogram tracks how long requests take to process from the server's perspective. + """ + + # Instantiate with default state, and apply the incoming configuration, if one was provided + self.configure(config=config, clear=True) + + if fga_client_credentials_request is not None: + self._state[TelemetryCounters.fga_client_credentials_request] = ( + fga_client_credentials_request + ) - if histogram_request_duration is None: - histogram_request_duration = TelemetryMetricConfiguration() + if fga_client_request_duration is not None: + self._state[TelemetryHistograms.fga_client_request_duration] = ( + fga_client_request_duration + ) - if histogram_query_duration is None: - histogram_query_duration = TelemetryMetricConfiguration() + if fga_client_query_duration is not None: + self._state[TelemetryHistograms.fga_client_query_duration] = ( + fga_client_query_duration + ) - self._counter_credentials_request = counter_credentials_request - self._histogram_request_duration = histogram_request_duration - self._histogram_query_duration = histogram_query_duration + # Reset the validation state + self._valid = None @property - def counter_credentials_request(self) -> TelemetryMetricConfiguration: - return self._counter_credentials_request + def fga_client_credentials_request(self) -> TelemetryMetricConfiguration | None: + """ + Get the configuration for the `fga-client.credentials.request` counter. + + :return: The configuration for the `fga-client.credentials.request` counter. + """ + + return self._state[TelemetryCounters.fga_client_credentials_request] + + @fga_client_credentials_request.setter + def fga_client_credentials_request( + self, value: TelemetryMetricConfiguration | None + ): + """ + Set the configuration for the `fga-client.credentials.request` counter. - @counter_credentials_request.setter - def counter_credentials_request(self, value: TelemetryMetricConfiguration): - self._counter_credentials_request = value + :param value: The configuration for the `fga-client.credentials.request` counter. + """ + + self._valid = None # Reset the validation state + self._state[TelemetryCounters.fga_client_credentials_request] = value @property - def histogram_request_duration(self) -> TelemetryMetricConfiguration: - return self._histogram_request_duration + def fga_client_request_duration(self) -> TelemetryMetricConfiguration | None: + """ + Get the configuration for the `fga-client.query.duration` histogram. + + :return: The configuration for the `fga-client.query.duration` histogram. + """ + + return self._state[TelemetryHistograms.fga_client_request_duration] + + @fga_client_request_duration.setter + def fga_client_request_duration(self, value: TelemetryMetricConfiguration | None): + """ + Set the configuration for the `fga-client.query.duration` histogram. - @histogram_request_duration.setter - def histogram_request_duration(self, value: TelemetryMetricConfiguration): - self._histogram_request_duration = value + :param value: The configuration for the `fga-client.query.duration` histogram. + """ + + self._valid = None # Reset the validation state + self._state[TelemetryHistograms.fga_client_request_duration] = value @property - def histogram_query_duration(self) -> TelemetryMetricConfiguration: - return self._histogram_query_duration + def fga_client_query_duration(self) -> TelemetryMetricConfiguration | None: + """ + Get the configuration for the `fga-client.request.duration` histogram. + + :return: The configuration for the `fga-client.request.duration` histogram. + """ + + return self._state[TelemetryHistograms.fga_client_query_duration] + + @fga_client_query_duration.setter + def fga_client_query_duration(self, value: TelemetryMetricConfiguration | None): + """ + Set the configuration for the `fga-client.request.duration` histogram. + + :param value: The configuration for the `fga-client.request.duration` histogram. + """ + + self._valid = None # Reset the validation state + self._state[TelemetryHistograms.fga_client_query_duration] = value + + def clear(self) -> None: + """ + Reset the configuration to the default state (all attributes disabled). + """ + self._state = { + TelemetryCounters.fga_client_credentials_request: None, + TelemetryHistograms.fga_client_request_duration: None, + TelemetryHistograms.fga_client_query_duration: None, + } + self._valid = True + + def configure( + self, + config: Optional[ + dict[ + TelemetryHistogram | TelemetryCounter | str, + TelemetryMetricConfiguration | dict[TelemetryAttribute, bool] | None, + ] + ] = None, + clear: Optional[bool] = False, + ) -> None: + """ + Configure metrics reporting for telemetry. + """ + if clear is True: + self.clear() + + if isinstance(config, dict): + for metric, configuration in config.items(): + if isinstance(metric, str): + metric = TelemetryCounters.get( + name=metric + ) or TelemetryHistograms.get(name=metric) + + if not isinstance(metric, TelemetryCounter) and not isinstance( + metric, TelemetryHistogram + ): + raise ValueError( + f"Invalid metric type provided in `TelemetryMetricsConfiguration`; `TelemetryHistogram` or `TelemetryCounter` was expected, but `{type(metric)}` was provided.", + metric, + ) + + if isinstance(configuration, dict): + configuration = TelemetryMetricConfiguration(configuration) + + if ( + not isinstance(configuration, TelemetryMetricConfiguration) + and configuration is not None + ): + raise ValueError( + f"Invalid metric configuration type provided in `TelemetryMetricsConfiguration`; `TelemetryMetricConfiguration` or `None` was expected, but `{type(configuration)}` was provided.", + configuration, + ) + + if metric not in self._state: + raise ValueError( + f"Invalid metric provided in `TelemetryMetricsConfiguration`; `{metric.name}` is not a supported metric type for this context.", + metric, + ) + + self._state[metric] = configuration + + self._valid = None + + def getMetrics( + self, filter_enabled: Optional[bool] = True + ) -> dict[ + TelemetryHistogram | TelemetryCounter, TelemetryMetricConfiguration | None + ]: + """ + Returns a list of supported metrics. If `filter_enabled` is `True`, only enabled metrics are returned. + + :param filter_enabled: A boolean indicating whether to filter the list to only include enabled metrics. + + :return: A list of enabled metrics. + """ + + metrics = self._state + + if filter_enabled is True: + return [ + metric + for metric, configuration in metrics.items() + if configuration is not None and configuration.isEnabled() + ] + + return metrics + + def isEnabled( + self, metric: Optional[TelemetryCounter | TelemetryHistogram] = None + ) -> bool: + """ + Check if a metric is enabled for telemetry. + + :return: A boolean indicating whether any metric is enabled. + """ + + # If no metric is specified, check if any metrics are enabled + if metric is None: + return True if any(self.getMetrics(filter_enabled=True)) else False + + # Check if the specified metric is enabled + if metric in self._state: + if ( + self._state[metric] is not None + and self._state[metric].isEnabled() is True + ): + return True + + return False + + def isValid(self, raise_exception: Optional[bool] = False) -> bool: + """ + Validate the telemetry metrics configuration. + + :param raise_exception: A boolean indicating whether to raise an exception if the configuration is invalid. + + :return: A boolean indicating whether the metrics configuration is valid, including all sub-configurations. + """ + + # Check if the validation state is already cached + if self._valid is not None: + return self._valid + + enabled = self.getMetrics(filter_enabled=True) + + # Validate all sub-configurations and cache the result + self._valid = [configuration.isValid() for configuration in enabled.values()] - @histogram_query_duration.setter - def histogram_query_duration(self, value: TelemetryMetricConfiguration): - self._histogram_query_duration = value + # If requested, raise an exception if the configuration is invalid + if self._valid is False and raise_exception is True: + raise ValueError("Invalid TelemetryMetricsConfiguration.") + + # Return the validation state + return self._valid + + @staticmethod + def getSdkDefaults() -> ( + dict[TelemetryHistogram | TelemetryCounter, TelemetryMetricConfiguration | None] + ): + """ + Get the default SDK configuration for telemetry metrics. + + :return: The default SDK configuration for telemetry metrics. + """ + return { + TelemetryCounters.fga_client_credentials_request: TelemetryMetricConfiguration.getSdkDefaults(), + TelemetryHistograms.fga_client_query_duration: TelemetryMetricConfiguration.getSdkDefaults(), + TelemetryHistograms.fga_client_request_duration: TelemetryMetricConfiguration.getSdkDefaults(), + } + + +class TelemetryConfigurationType(NamedTuple): + name: str + configurationClass: object + + +class TelemetryConfigurations: + metrics: TelemetryConfigurationType = TelemetryConfigurationType( + name="metrics", + configurationClass=TelemetryMetricsConfiguration, + ) + + _configurations: list[TelemetryConfigurationType] = [metrics] + + @staticmethod + def get( + name: Optional[str] = None, + ) -> list[TelemetryConfigurationType] | TelemetryConfigurationType | None: + if name is None: + return TelemetryConfigurations._configurations + + for configuration in TelemetryConfigurations._configurations: + if configuration.name == name: + return configuration + + return None class TelemetryConfiguration: - _metrics: TelemetryMetricsConfiguration + _state: dict[str, TelemetryMetricsConfiguration | None] = {} + _valid: bool | None = None + + def __init__( + self, + config: Optional[dict[str, TelemetryMetricsConfiguration | None]] = None, + metrics: Optional[TelemetryMetricsConfiguration] = None, + ): + """ + Initialize a new instance of the `TelemetryConfiguration` class. + + :param config: A dictionary containing the configuration for telemetry. + :param metrics: Customize which metrics and attributes are included in telemetry collection. + """ - def __init__(self, metrics: Optional[TelemetryMetricsConfiguration] = None): - if metrics is None: - metrics = TelemetryMetricsConfiguration() + # Instantiate with default state, and apply the incoming configuration, if one was provided + self.configure(config=config, clear=True) - self._metrics = metrics + if metrics is not None: + self._state[TelemetryConfigurations.metrics] = metrics @property - def metrics(self) -> TelemetryMetricsConfiguration: - return self._metrics + def metrics(self) -> TelemetryMetricsConfiguration | None: + """ + Get the metrics configuration for telemetry. + + :return: The metrics configuration for telemetry. + """ + + return self._state[TelemetryConfigurations.metrics] @metrics.setter - def metrics(self, value: TelemetryMetricsConfiguration): - self._metrics = value + def metrics(self, value: TelemetryMetricsConfiguration | None): + """ + Set the metrics configuration for telemetry. + + :param value: The metrics configuration for telemetry. + """ + + if value is not None and not isinstance(value, TelemetryMetricsConfiguration): + raise ValueError( + "A `metrics` configuration must be an instance of `TelemetryMetricsConfiguration` or `None`." + ) + + self._valid = None + self._state[TelemetryConfigurations.metrics] = value + + def clear(self) -> None: + """ + Reset the configuration to the default state (all attributes disabled). + """ + self._state = { + TelemetryConfigurations.metrics: None, + } + self._valid = True + + def configure( + self, + config: Optional[ + dict[ + TelemetryConfigurationType | str, + TelemetryMetricsConfiguration + | dict[ + TelemetryHistogram | TelemetryCounter, + TelemetryMetricConfiguration + | dict[TelemetryAttribute, bool] + | None, + ] + | None, + ] + ] = None, + clear: Optional[bool] = False, + ) -> None: + """ + Configure telemetry reporting. + """ + if clear is True: + self.clear() + + if isinstance(config, dict): + for context, configuration in config.items(): + if isinstance(context, str): + context = TelemetryConfigurations.get(context) + + if not isinstance(context, TelemetryConfigurationType): + raise ValueError( + f"Invalid context provided in `TelemetryConfiguration`; a valid string or an `TelemetryConfigurationType` instance was expected, but `{type(context)}` was provided.", + context, + ) + + if isinstance(configuration, dict): + configuration = TelemetryMetricsConfiguration(configuration) + + if ( + not isinstance(configuration, context.configurationClass) + and configuration is not None + ): + raise ValueError( + f"Invalid context configuration provided in `TelemetryConfiguration`; a {type(context.configurationClass)} was expected, but `{type(configuration)}` was provided.", + configuration, + ) + + if context not in self._state: + raise ValueError( + f"Invalid context provided in `TelemetryConfiguration`; `{context.name}` is not a supported context type for this configuration context.", + context, + ) + + self._state[context] = configuration + + self._valid = None + + def getConfigurations( + self, filter_enabled: Optional[bool] = True + ) -> dict[TelemetryConfigurationType, TelemetryMetricsConfiguration | None]: + """ + Returns a list of supported reporting contexts. If `filter_enabled` is `True`, only enabled contexts are returned. + + :param filter_enabled: A boolean indicating whether to filter the list to only include enabled contexts. + + :return: A list of enabled contexts. + """ + + contexts = self._state + + if filter_enabled is True: + return { + context: configuration + for context, configuration in contexts.items() + if configuration is not None and configuration.isEnabled() + } + + return contexts + + def isEnabled( + self, configuration: Optional[TelemetryConfigurationType] = None + ) -> bool: + """ + Check if telemetry is enabled. + + :return: A boolean indicating whether telemetry is enabled. + """ + + if configuration is None: + return True if any(self.getConfigurations(filter_enabled=True)) else False + + if configuration in self._state: + if ( + self._state[configuration] is not None + or self._state[configuration].isEnabled() is True + ): + return self._state[configuration].isEnabled() + + return False + + def isValid(self, raise_exception: Optional[bool] = False) -> bool: + """ + Validate the telemetry configuration. + + :param raise_exception: A boolean indicating whether to raise an exception if the configuration is invalid. + + :return: A boolean indicating whether the telemetry configuration is valid, including all sub-configurations. + """ + + if self._valid is not None: + return self._valid + + enabled = self.getConfigurations(filter_enabled=True) + + self._valid = all( + [configuration.isValid() for configuration in enabled.values()] + ) + + if self._valid is False and raise_exception is True: + raise ValueError("Invalid TelemetryConfiguration.") + + return self._valid + + @staticmethod + def getSdkDefaults() -> ( + dict[TelemetryConfigurationType, TelemetryMetricsConfiguration | None] + ): + """ + Get the default SDK configuration for telemetry. + + :return: The default SDK configuration for telemetry. + """ + return { + TelemetryConfigurations.metrics: TelemetryMetricsConfiguration.getSdkDefaults(), + } + + +def isMetricEnabled( + config: TelemetryConfiguration | TelemetryMetricsConfiguration, + metric: TelemetryCounter | TelemetryHistogram, +) -> bool: + """ + Check if a particular metric is enabled for telemetry collection. + """ + + if config is not None and metric is not None: + if isinstance(config, TelemetryConfiguration): + config = config.metrics + + if isinstance(config, TelemetryMetricsConfiguration): + return config.isEnabled(metric) + + return False diff --git a/openfga_sdk/telemetry/counters.py b/openfga_sdk/telemetry/counters.py index bd85634..9a8aaec 100644 --- a/openfga_sdk/telemetry/counters.py +++ b/openfga_sdk/telemetry/counters.py @@ -1,4 +1,4 @@ -from typing import NamedTuple +from typing import NamedTuple, Optional class TelemetryCounter(NamedTuple): @@ -8,8 +8,25 @@ class TelemetryCounter(NamedTuple): class TelemetryCounters: - credentials_request: TelemetryCounter = TelemetryCounter( + fga_client_credentials_request: TelemetryCounter = TelemetryCounter( name="fga-client.credentials.request", unit="milliseconds", - description="The number of times an access token is requested.", + description="Total number of new token requests initiated using the Client Credentials flow.", ) + + _counters: list[TelemetryCounter] = [ + fga_client_credentials_request, + ] + + @staticmethod + def get( + name: Optional[str] = None, + ) -> list[TelemetryCounter] | TelemetryCounter | None: + if name is None: + return TelemetryCounters._counters + + for counter in TelemetryCounters._counters: + if counter.name == name: + return counter + + return None diff --git a/openfga_sdk/telemetry/histograms.py b/openfga_sdk/telemetry/histograms.py index b453908..1449c49 100644 --- a/openfga_sdk/telemetry/histograms.py +++ b/openfga_sdk/telemetry/histograms.py @@ -1,4 +1,4 @@ -from typing import NamedTuple +from typing import NamedTuple, Optional class TelemetryHistogram(NamedTuple): @@ -8,13 +8,31 @@ class TelemetryHistogram(NamedTuple): class TelemetryHistograms: - request_duration: TelemetryHistogram = TelemetryHistogram( + fga_client_request_duration: TelemetryHistogram = TelemetryHistogram( name="fga-client.request.duration", unit="milliseconds", - description="How long it took for a request to be fulfilled.", + description="Total request time for FGA requests, in milliseconds.", ) - query_duration: TelemetryHistogram = TelemetryHistogram( + fga_client_query_duration: TelemetryHistogram = TelemetryHistogram( name="fga-client.query.duration", unit="milliseconds", - description="How long it took to perform a query request.", + description="Time taken by the FGA server to process and evaluate the request, in milliseconds.", ) + + _histograms: list[TelemetryHistogram] = [ + fga_client_request_duration, + fga_client_query_duration, + ] + + @staticmethod + def get( + name: Optional[str] = None, + ) -> list[TelemetryHistogram] | TelemetryHistogram | None: + if name is None: + return TelemetryHistograms._histograms + + for histogram in TelemetryHistograms._histograms: + if histogram.name == name: + return histogram + + return None diff --git a/openfga_sdk/telemetry/metrics.py b/openfga_sdk/telemetry/metrics.py index cb41b61..e35e652 100644 --- a/openfga_sdk/telemetry/metrics.py +++ b/openfga_sdk/telemetry/metrics.py @@ -3,16 +3,19 @@ from opentelemetry.metrics import Counter, Histogram, Meter, get_meter from openfga_sdk import __version__ -from openfga_sdk.telemetry.attributes import TelemetryAttribute, TelemetryAttributes +from openfga_sdk.telemetry.attributes import ( + TelemetryAttribute, + TelemetryAttributes, +) from openfga_sdk.telemetry.configuration import ( TelemetryConfiguration, - TelemetryMetricsConfiguration, + isMetricEnabled, ) from openfga_sdk.telemetry.counters import TelemetryCounter, TelemetryCounters from openfga_sdk.telemetry.histograms import TelemetryHistogram, TelemetryHistograms -class MetricsTelemetry: +class TelemetryMetrics: _meter: Meter = None _histograms: dict[str, Histogram] = {} _counters: dict[str, Counter] = {} @@ -33,18 +36,7 @@ def meter(self) -> Meter: return self._meter - def counter( - self, - counter: str | TelemetryCounter, - value: int = None, - attributes: dict[TelemetryAttribute | str, str | int] | None = None, - ) -> Counter: - if isinstance(counter, str): - try: - counter = TelemetryCounters[counter] - except (KeyError, TypeError): - raise KeyError(f"Invalid counter key: {counter}") - + def counter(self, counter: TelemetryCounter) -> Counter: if not isinstance(counter, TelemetryCounter): raise ValueError( "counter must be a TelemetryCounter, or a string that is a key in TelemetryCounters" @@ -55,23 +47,9 @@ def counter( name=counter.name, unit=counter.unit, description=counter.description ) - if value is not None: - self._counters[counter.name].add(amount=value, attributes=attributes) - return self._counters[counter.name] - def histogram( - self, - histogram: str | TelemetryHistogram, - value: int | float = None, - attributes: dict[TelemetryAttribute | str, str | int] | None = None, - ) -> Histogram: - if isinstance(histogram, str): - try: - histogram = TelemetryHistograms[histogram] - except (KeyError, TypeError): - raise KeyError(f"Invalid histogram key: {histogram}") - + def histogram(self, histogram: TelemetryHistogram) -> Histogram: if not isinstance(histogram, TelemetryHistogram): raise ValueError( "histogram must be a TelemetryHistogram, or a string that is a key in TelemetryHistograms" @@ -84,120 +62,92 @@ def histogram( description=histogram.description, ) - if value is not None: - self._histograms[histogram.name].record(amount=value, attributes=attributes) - return self._histograms[histogram.name] def credentialsRequest( self, - value: int | float | None = None, - attributes: dict[TelemetryAttribute | str, str | int] | None = None, + value: int, + attributes: dict[TelemetryAttribute, str | int] | None = None, configuration: TelemetryConfiguration | None = None, ) -> Counter: - if configuration is None: - configuration = TelemetryConfiguration() + """ + Record a client credentials request made by the client. + """ + counter = self.counter(TelemetryCounters.fga_client_credentials_request) - if ( - isinstance(configuration, TelemetryMetricsConfiguration) is False - or configuration.metrics.counter_credentials_request.enabled is False - or configuration.metrics.counter_credentials_request.attributes() == {} + if isMetricEnabled( + configuration, TelemetryCounters.fga_client_credentials_request ): - return self.counter(TelemetryCounters.credentials_request) + attributes = TelemetryAttributes.prepare( + attributes, + filter=configuration.metrics.fga_client_credentials_request.getAttributes(), + ) - attributes = TelemetryAttributes().prepare( - attributes, - filter=configuration.metrics.counter_credentials_request.attributes(), - ) + if value is not None: + counter.add(amount=value, attributes=attributes) - return self.counter(TelemetryCounters.credentials_request, value, attributes) + return counter def requestDuration( self, value: int | float | None = None, - attributes: dict[TelemetryAttribute | str, str | int] | None = None, + attributes: dict[TelemetryAttribute, str | int] | None = None, configuration: TelemetryConfiguration | None = None, ) -> Histogram: - if configuration is None: - configuration = TelemetryConfiguration() + """ + Record the duration of a request made by the client. + """ + histogram = self.histogram(TelemetryHistograms.fga_client_request_duration) - if ( - isinstance(configuration, TelemetryConfiguration) is False - or configuration.metrics.histogram_request_duration.enabled is False - or configuration.metrics.histogram_request_duration.attributes() == {} + if isMetricEnabled( + configuration, TelemetryHistograms.fga_client_request_duration ): - return self.histogram(TelemetryHistograms.request_duration) - - if ( - value is None - and TelemetryAttributes.http_client_request_duration.name in attributes - ): - value = attributes[TelemetryAttributes.http_client_request_duration.name] - attributes.pop(TelemetryAttributes.http_client_request_duration.name, None) - - if value is not None: - try: - value = int(value) - attributes[TelemetryAttributes.http_client_request_duration.name] = ( - value + attributes[TelemetryAttributes.http_client_request_duration] = value = ( + TelemetryAttributes.coalesceAttributeValue( + TelemetryAttributes.http_client_request_duration, + value, + attributes, ) - except ValueError: - value = None - - attributes = TelemetryAttributes().prepare( - attributes, - filter=configuration.metrics.histogram_request_duration.attributes(), - ) + ) - if ( - value is None - and TelemetryAttributes.http_client_request_duration.name in attributes - ): - value = attributes[TelemetryAttributes.http_client_request_duration.name] + attributes = TelemetryAttributes.prepare( + attributes, + filter=configuration.metrics.fga_client_request_duration.getAttributes(), + ) - if value is None: - return self.histogram(TelemetryHistograms.request_duration) + if value is not None: + histogram.record(amount=value, attributes=attributes) - return self.histogram(TelemetryHistograms.request_duration, value, attributes) + return histogram def queryDuration( self, value: int | float | None = None, - attributes: dict[TelemetryAttribute | str, str | int] | None = None, + attributes: dict[TelemetryAttribute, str | int] | None = None, configuration: TelemetryConfiguration | None = None, ) -> Histogram: - if configuration is None: - configuration = TelemetryConfiguration() - - if ( - isinstance(configuration, TelemetryConfiguration) is False - or configuration.metrics.histogram_query_duration.enabled is False - or configuration.metrics.histogram_query_duration.attributes() == {} - ): - return self.histogram(TelemetryHistograms.query_duration) + """ + Record the duration of a query made by the client, as reported by the server. + """ + histogram = self.histogram(TelemetryHistograms.fga_client_query_duration) - if ( - value is None - and TelemetryAttributes.http_server_request_duration.name in attributes + if isMetricEnabled( + configuration, TelemetryHistograms.fga_client_query_duration ): - value = attributes[TelemetryAttributes.http_server_request_duration.name] - attributes.pop(TelemetryAttributes.http_server_request_duration.name, None) - - if value is not None: - try: - value = int(value) - attributes[TelemetryAttributes.http_server_request_duration.name] = ( - value + attributes[TelemetryAttributes.http_server_request_duration] = value = ( + TelemetryAttributes.coalesceAttributeValue( + TelemetryAttributes.http_server_request_duration, + value, + attributes, ) - except ValueError: - value = None + ) - attributes = TelemetryAttributes().prepare( - attributes, - filter=configuration.metrics.histogram_query_duration.attributes(), - ) + attributes = TelemetryAttributes.prepare( + attributes, + filter=configuration.metrics.fga_client_query_duration.getAttributes(), + ) - if value is None: - return self.histogram(TelemetryHistograms.query_duration) + if value is not None: + histogram.record(amount=value, attributes=attributes) - return self.histogram(TelemetryHistograms.query_duration, value, attributes) + return histogram diff --git a/openfga_sdk/telemetry/telemetry.py b/openfga_sdk/telemetry/telemetry.py index 019429f..7a23f55 100644 --- a/openfga_sdk/telemetry/telemetry.py +++ b/openfga_sdk/telemetry/telemetry.py @@ -1,11 +1,12 @@ -from openfga_sdk.telemetry.metrics import MetricsTelemetry +from openfga_sdk.telemetry.metrics import TelemetryMetrics class Telemetry: - _metrics: MetricsTelemetry = None + _metrics: TelemetryMetrics | None = None - def metrics(self) -> MetricsTelemetry: + @property + def metrics(self) -> TelemetryMetrics: if self._metrics is None: - self._metrics = MetricsTelemetry() + self._metrics = TelemetryMetrics() return self._metrics diff --git a/openfga_sdk/telemetry/utilities.py b/openfga_sdk/telemetry/utilities.py new file mode 100644 index 0000000..9203328 --- /dev/null +++ b/openfga_sdk/telemetry/utilities.py @@ -0,0 +1,7 @@ +def doesInstanceHaveCallable(instance: object, callableName: str) -> bool: + instanceCallable = getattr(instance, callableName, None) + + if instanceCallable is None: + return False + + return callable(instanceCallable) diff --git a/test/client/client_test.py b/test/client/client_test.py index eba82b8..92adae0 100644 --- a/test/client/client_test.py +++ b/test/client/client_test.py @@ -2412,8 +2412,6 @@ async def test_list_relations_unauthorized(self, mock_request): ) mock_request.assert_called() - self.assertEqual(mock_request.call_count, 3) - await api_client.close() @patch.object(rest.RESTClientObject, "request") diff --git a/test/sync/client/client_test.py b/test/sync/client/client_test.py index c358dea..dd1008b 100644 --- a/test/sync/client/client_test.py +++ b/test/sync/client/client_test.py @@ -2418,7 +2418,6 @@ def test_list_relations_unauthorized(self, mock_request): self.assertIsInstance(api_exception.exception, UnauthorizedException) mock_request.assert_called() - self.assertEqual(mock_request.call_count, 3) api_client.close() @patch.object(rest.RESTClientObject, "request") diff --git a/test/telemetry/attributes_test.py b/test/telemetry/attributes_test.py index 7074202..af001c1 100644 --- a/test/telemetry/attributes_test.py +++ b/test/telemetry/attributes_test.py @@ -6,7 +6,9 @@ from openfga_sdk.credentials import CredentialConfiguration, Credentials from openfga_sdk.rest import RESTResponse -from openfga_sdk.telemetry.attributes import TelemetryAttributes +from openfga_sdk.telemetry.attributes import ( + TelemetryAttributes, +) @pytest.fixture @@ -67,15 +69,16 @@ def test_from_request_with_all_params(telemetry_attributes): credentials=credentials, ) - assert attributes["fga-client.request.method"] == "FGA_METHOD" - assert attributes["user_agent.original"] == "TestAgent" - assert attributes["http.host"] == "example.com" - assert attributes["http.request.method"] == "POST" - assert attributes["url.scheme"] == "https" - assert attributes["url.full"] == "https://example.com/api" - assert "http.client.request.duration" in attributes - assert attributes["http.request.resend_count"] == 2 - assert attributes["fga-client.request.client_id"] == "client_123" + assert attributes[TelemetryAttributes.fga_client_request_method] == "FgaMethod" + assert attributes[TelemetryAttributes.user_agent_original] == "TestAgent" + assert attributes[TelemetryAttributes.http_host] == "example.com" + assert attributes[TelemetryAttributes.http_request_method] == "POST" + assert attributes[TelemetryAttributes.url_scheme] == "https" + assert attributes[TelemetryAttributes.url_full] == "https://example.com/api" + + assert TelemetryAttributes.http_client_request_duration in attributes + assert attributes[TelemetryAttributes.http_request_resend_count] == 2 + assert attributes[TelemetryAttributes.fga_client_request_client_id] == "client_123" def test_from_request_without_optional_params(telemetry_attributes): @@ -86,15 +89,16 @@ def test_from_request_without_optional_params(telemetry_attributes): url="http://minimal.com", ) - assert attributes["fga-client.request.method"] == "FGA_METHOD" - assert attributes["user_agent.original"] == "MinimalAgent" - assert attributes["http.host"] == "minimal.com" - assert attributes["http.request.method"] == "GET" - assert attributes["url.scheme"] == "http" - assert attributes["url.full"] == "http://minimal.com" - assert "http.client.request.duration" not in attributes - assert "http.request.resend_count" not in attributes - assert "fga-client.request.client_id" not in attributes + assert attributes[TelemetryAttributes.fga_client_request_method] == "FgaMethod" + assert attributes[TelemetryAttributes.user_agent_original] == "MinimalAgent" + assert attributes[TelemetryAttributes.http_host] == "minimal.com" + assert attributes[TelemetryAttributes.http_request_method] == "GET" + assert attributes[TelemetryAttributes.url_scheme] == "http" + assert attributes[TelemetryAttributes.url_full] == "http://minimal.com" + + assert TelemetryAttributes.http_client_request_duration not in attributes + assert TelemetryAttributes.http_request_resend_count not in attributes + assert TelemetryAttributes.fga_client_request_client_id not in attributes def test_from_response_with_http_response(telemetry_attributes): @@ -113,10 +117,10 @@ def test_from_response_with_http_response(telemetry_attributes): response=response, credentials=credentials ) - assert attributes["http.response.status_code"] == 200 - assert attributes["fga-client.response.model_id"] == "model_123" - assert attributes["http.server.request.duration"] == "50" - assert attributes["fga-client.request.client_id"] == "client_123" + assert attributes[TelemetryAttributes.http_response_status_code] == 200 + assert attributes[TelemetryAttributes.fga_client_response_model_id] == "model_123" + assert attributes[TelemetryAttributes.http_server_request_duration] == "50" + assert attributes[TelemetryAttributes.fga_client_request_client_id] == "client_123" def test_from_response_with_rest_response(telemetry_attributes): @@ -137,30 +141,7 @@ def test_from_response_with_rest_response(telemetry_attributes): response=response, credentials=credentials ) - assert attributes["http.response.status_code"] == 404 - assert attributes["fga-client.response.model_id"] == "model_404" - assert attributes["http.server.request.duration"] == "100" - assert attributes["fga-client.request.client_id"] == "client_456" - - -def test_instance_has_attribute(telemetry_attributes): - mock_instance = MagicMock(spec_set=["some_attribute"]) - mock_instance.some_attribute = "value" - assert telemetry_attributes.instanceHasAttribute(mock_instance, "some_attribute") - assert not telemetry_attributes.instanceHasAttribute( - mock_instance, "missing_attribute" - ) - - -def test_instance_has_callable(telemetry_attributes): - mock_instance = MagicMock(spec_set=["some_callable", "some_attribute"]) - mock_instance.some_callable = lambda: "I am callable" - - assert telemetry_attributes.instanceHasCallable(mock_instance, "some_callable") - - assert not telemetry_attributes.instanceHasCallable( - mock_instance, "missing_callable" - ) - - mock_instance.some_attribute = "not callable" - assert not telemetry_attributes.instanceHasCallable(mock_instance, "some_attribute") + assert attributes[TelemetryAttributes.http_response_status_code] == 404 + assert attributes[TelemetryAttributes.fga_client_response_model_id] == "model_404" + assert attributes[TelemetryAttributes.http_server_request_duration] == "100" + assert attributes[TelemetryAttributes.fga_client_request_client_id] == "client_456" diff --git a/test/telemetry/configuration_test.py b/test/telemetry/configuration_test.py index 6c6d9f0..61f3170 100644 --- a/test/telemetry/configuration_test.py +++ b/test/telemetry/configuration_test.py @@ -1,108 +1,197 @@ from openfga_sdk.telemetry.attributes import TelemetryAttributes from openfga_sdk.telemetry.configuration import ( TelemetryConfiguration, + TelemetryConfigurations, TelemetryMetricConfiguration, TelemetryMetricsConfiguration, ) +from openfga_sdk.telemetry.counters import TelemetryCounters +from openfga_sdk.telemetry.histograms import TelemetryHistograms def test_telemetry_metric_configuration_default_initialization(): config = TelemetryMetricConfiguration() - # Assert default enabled value - assert config.enabled is True - - # Assert all attributes are True by default - assert config.attr_fga_client_request_client_id is True - assert config.attr_fga_client_request_method is True - assert config.attr_fga_client_request_model_id is True - assert config.attr_fga_client_request_store_id is True - assert config.attr_fga_client_response_model_id is True - assert config.attr_fga_client_user is False - assert config.attr_http_client_request_duration is False - assert config.attr_http_host is True - assert config.attr_http_request_method is True - assert config.attr_http_request_resend_count is True - assert config.attr_http_response_status_code is True - assert config.attr_http_server_request_duration is False - assert config.attr_http_url_scheme is True - assert config.attr_http_url_full is True - assert config.attr_user_agent_original is True + assert config.fga_client_request_client_id is False + assert config.fga_client_request_method is False + assert config.fga_client_request_model_id is False + assert config.fga_client_request_store_id is False + assert config.fga_client_response_model_id is False + assert config.fga_client_user is False + assert config.http_client_request_duration is False + assert config.http_host is False + assert config.http_request_method is False + assert config.http_request_resend_count is False + assert config.http_response_status_code is False + assert config.http_server_request_duration is False + assert config.url_scheme is False + assert config.url_full is False + assert config.user_agent_original is False def test_telemetry_metric_configuration_custom_initialization(): config = TelemetryMetricConfiguration( - enabled=False, - attr_fga_client_request_client_id=False, - attr_http_request_method=False, + fga_client_request_client_id=False, + http_request_method=True, ) - # Assert custom initialization values - assert config.enabled is False - assert config.attr_fga_client_request_client_id is False - assert config.attr_http_request_method is False - # Check default values for other attributes - assert config.attr_fga_client_request_method is True - assert config.attr_http_host is True + assert config.fga_client_request_client_id is False + assert config.http_request_method is True + assert config.fga_client_request_method is False + assert config.user_agent_original is False -def test_telemetry_metric_configuration_attributes_method(): +def test_telemetry_metric_configuration_custom_dict_object_keys_initialization(): config = TelemetryMetricConfiguration( - enabled=True, - attr_fga_client_request_client_id=True, - attr_http_request_method=False, + { + "fga-client.request.client_id": False, + "http.request.method": True, + } ) - enabled_attributes = config.attributes() + assert config.fga_client_request_client_id is False + assert config.http_request_method is True + assert config.fga_client_request_method is False + assert config.user_agent_original is False + assert config.fga_client_response_model_id is False - # Check only enabled attributes are returned - assert ( - enabled_attributes[TelemetryAttributes.fga_client_request_client_id.name] - is True + +def test_telemetry_metric_configuration_custom_dict_string_keys_initialization(): + config = TelemetryMetricConfiguration( + { + TelemetryAttributes.fga_client_request_client_id: False, + TelemetryAttributes.http_request_method: True, + } + ) + + assert config.fga_client_request_client_id is False + assert config.http_request_method is True + assert config.fga_client_request_method is False + assert config.user_agent_original is False + assert config.fga_client_response_model_id is False + + +def test_telemetry_metric_configuration_custom_mixed_initialization(): + config = TelemetryMetricConfiguration( + { + TelemetryAttributes.fga_client_request_client_id: False, + "http.request.method": True, + }, + fga_client_response_model_id=True, + ) + + assert config.fga_client_request_client_id is False + assert config.http_request_method is True + assert config.fga_client_request_method is False + assert config.user_agent_original is False + assert config.fga_client_response_model_id is True + + +def test_telemetry_metric_configuration_custom_overwritten_initialization(): + config = TelemetryMetricConfiguration( + { + TelemetryAttributes.fga_client_request_client_id: True, + }, + fga_client_request_client_id=False, ) - assert TelemetryAttributes.http_request_method.name not in enabled_attributes + + assert config.fga_client_request_client_id is False + + +def test_telemetry_metric_configuration_attributes_method(): + config = TelemetryMetricConfiguration( + fga_client_request_client_id=True, + http_request_method=False, + ) + + enabled_attributes = config.getAttributes() + + assert len(enabled_attributes) == 1 + assert TelemetryAttributes.fga_client_request_client_id in enabled_attributes + assert TelemetryAttributes.http_request_method not in enabled_attributes def test_telemetry_metrics_configuration_default_initialization(): metrics_config = TelemetryMetricsConfiguration() - # Assert default metric configurations - assert isinstance( - metrics_config.counter_credentials_request, TelemetryMetricConfiguration + assert metrics_config.fga_client_credentials_request is None + assert metrics_config.fga_client_query_duration is None + assert metrics_config.fga_client_request_duration is None + + +def test_telemetry_metrics_configuration_custom_initialization(): + custom_config = TelemetryMetricConfiguration() + metrics_config = TelemetryMetricsConfiguration( + fga_client_credentials_request=custom_config ) - assert isinstance( - metrics_config.histogram_request_duration, TelemetryMetricConfiguration + + assert metrics_config.fga_client_credentials_request is custom_config + assert metrics_config.fga_client_request_duration is None + assert metrics_config.fga_client_query_duration is None + + +def test_telemetry_metrics_configuration_custom_dict_object_keys_initialization(): + custom_config = TelemetryMetricConfiguration() + metrics_config = TelemetryMetricsConfiguration( + {TelemetryHistograms.fga_client_query_duration: custom_config} ) - assert isinstance( - metrics_config.histogram_query_duration, TelemetryMetricConfiguration + + assert metrics_config.fga_client_credentials_request is None + assert metrics_config.fga_client_request_duration is None + assert metrics_config.fga_client_query_duration is custom_config + + +def test_telemetry_metrics_configuration_custom_dict_string_keys_initialization(): + custom_config = TelemetryMetricConfiguration() + metrics_config = TelemetryMetricsConfiguration( + {"fga-client.request.duration": custom_config} ) + assert metrics_config.fga_client_credentials_request is None + assert metrics_config.fga_client_request_duration is custom_config + assert metrics_config.fga_client_query_duration is None -def test_telemetry_metrics_configuration_custom_initialization(): - custom_config = TelemetryMetricConfiguration(enabled=False) + +def test_telemetry_metrics_configuration_custom_mixed_initialization(): + custom_config = TelemetryMetricConfiguration() + metrics_config = TelemetryMetricsConfiguration( + { + TelemetryHistograms.fga_client_query_duration: custom_config, + "fga-client.request.duration": custom_config, + }, + fga_client_credentials_request=custom_config, + ) + + assert metrics_config.fga_client_credentials_request is custom_config + assert metrics_config.fga_client_request_duration is custom_config + assert metrics_config.fga_client_query_duration is custom_config + + +def test_telemetry_metrics_configuration_custom_overwritten_initialization(): + custom_config = TelemetryMetricConfiguration() metrics_config = TelemetryMetricsConfiguration( - counter_credentials_request=custom_config + { + TelemetryHistograms.fga_client_query_duration: None, + }, + fga_client_query_duration=custom_config, ) - # Check that custom configuration is used - assert metrics_config.counter_credentials_request is custom_config - assert metrics_config.histogram_request_duration.enabled is True + assert metrics_config.fga_client_query_duration is custom_config def test_telemetry_metrics_configuration_setters(): metrics_config = TelemetryMetricsConfiguration() - new_config = TelemetryMetricConfiguration(enabled=False) - metrics_config.counter_credentials_request = new_config + new_config = TelemetryMetricConfiguration() + metrics_config.fga_client_credentials_request = new_config - assert metrics_config.counter_credentials_request is new_config + assert metrics_config.fga_client_credentials_request is new_config def test_telemetry_configuration_default_initialization(): telemetry_config = TelemetryConfiguration() - # Check the default metrics configuration is used - assert isinstance(telemetry_config.metrics, TelemetryMetricsConfiguration) + assert telemetry_config.metrics is None def test_telemetry_configuration_custom_initialization(): @@ -112,9 +201,111 @@ def test_telemetry_configuration_custom_initialization(): assert telemetry_config.metrics is custom_metrics +def test_telemetry_configuration_custom_dict_object_keys_initialization(): + custom_metrics = TelemetryMetricsConfiguration() + telemetry_config = TelemetryConfiguration( + {TelemetryConfigurations.metrics: custom_metrics} + ) + + assert telemetry_config.metrics is custom_metrics + + +def test_telemetry_configuration_custom_dict_string_keys_initialization(): + custom_metrics = TelemetryMetricsConfiguration() + telemetry_config = TelemetryConfiguration({"metrics": custom_metrics}) + + assert telemetry_config.metrics is custom_metrics + + +def test_telemetry_configuration_custom_overwritten_initialization(): + custom_metrics = TelemetryMetricsConfiguration() + telemetry_config = TelemetryConfiguration( + {TelemetryConfigurations.metrics: None}, metrics=custom_metrics + ) + + assert telemetry_config.metrics is custom_metrics + + +def test_telemetry_configuration_custom_full_initialization(): + telemetry_config = TelemetryConfiguration( + { + "metrics": { + "fga-client.credentials.request": { + "fga-client.request.client_id": False, + "http.request.method": True, + }, + "fga-client.request.duration": { + "fga-client.request.client_id": True, + "http.request.method": False, + }, + } + } + ) + + assert ( + telemetry_config.metrics.fga_client_credentials_request.fga_client_request_client_id + is False + ) + assert ( + telemetry_config.metrics.fga_client_credentials_request.http_request_method + is True + ) + assert ( + telemetry_config.metrics.fga_client_request_duration.fga_client_request_client_id + is True + ) + assert ( + telemetry_config.metrics.fga_client_request_duration.http_request_method + is False + ) + + def test_telemetry_configuration_setter(): telemetry_config = TelemetryConfiguration() new_metrics = TelemetryMetricsConfiguration() telemetry_config.metrics = new_metrics assert telemetry_config.metrics is new_metrics + + +def test_default_telemetry_configuration(): + config = TelemetryConfiguration.getSdkDefaults() + + assert isinstance(config, dict) + assert len(config) == 1 + + assert TelemetryConfigurations.metrics in config + + +def test_default_telemetry_metrics_configuration(): + metrics_config = TelemetryMetricsConfiguration.getSdkDefaults() + + assert isinstance(metrics_config, dict) + assert len(metrics_config) == 3 + + assert TelemetryCounters.fga_client_credentials_request in metrics_config + assert TelemetryHistograms.fga_client_query_duration in metrics_config + assert TelemetryHistograms.fga_client_request_duration in metrics_config + + +def test_default_telemetry_metric_configuration(): + metric_config = TelemetryMetricConfiguration.getSdkDefaults() + + assert isinstance(metric_config, dict) + assert len(metric_config) == 15 + + assert metric_config[TelemetryAttributes.fga_client_request_client_id] is True + assert metric_config[TelemetryAttributes.fga_client_request_method] is True + assert metric_config[TelemetryAttributes.fga_client_request_model_id] is True + assert metric_config[TelemetryAttributes.fga_client_request_store_id] is True + assert metric_config[TelemetryAttributes.fga_client_response_model_id] is True + assert metric_config[TelemetryAttributes.fga_client_user] is False + assert metric_config[TelemetryAttributes.http_client_request_duration] is False + assert metric_config[TelemetryAttributes.http_host] is True + assert metric_config[TelemetryAttributes.http_request_method] is True + assert metric_config[TelemetryAttributes.http_request_resend_count] is True + assert metric_config[TelemetryAttributes.http_response_status_code] is True + assert metric_config[TelemetryAttributes.http_server_request_duration] is False + assert metric_config[TelemetryAttributes.url_scheme] is True + assert metric_config[TelemetryAttributes.url_full] is True + assert metric_config[TelemetryAttributes.user_agent_original] is True diff --git a/test/telemetry/counters_test.py b/test/telemetry/counters_test.py index 8c0d39d..5d14ed7 100644 --- a/test/telemetry/counters_test.py +++ b/test/telemetry/counters_test.py @@ -16,11 +16,13 @@ def test_telemetry_counter_initialization(): def test_telemetry_counters_default_values(): counters = TelemetryCounters() - assert counters.credentials_request.name == "fga-client.credentials.request" - assert counters.credentials_request.unit == "milliseconds" assert ( - counters.credentials_request.description - == "The number of times an access token is requested." + counters.fga_client_credentials_request.name == "fga-client.credentials.request" + ) + assert counters.fga_client_credentials_request.unit == "milliseconds" + assert ( + counters.fga_client_credentials_request.description + == "Total number of new token requests initiated using the Client Credentials flow." ) diff --git a/test/telemetry/histograms_test.py b/test/telemetry/histograms_test.py index 9ed6153..1bcd05a 100644 --- a/test/telemetry/histograms_test.py +++ b/test/telemetry/histograms_test.py @@ -16,18 +16,18 @@ def test_telemetry_histogram_initialization(): def test_telemetry_histograms_default_values(): histograms = TelemetryHistograms() - assert histograms.request_duration.name == "fga-client.request.duration" - assert histograms.request_duration.unit == "milliseconds" + assert histograms.fga_client_request_duration.name == "fga-client.request.duration" + assert histograms.fga_client_request_duration.unit == "milliseconds" assert ( - histograms.request_duration.description - == "How long it took for a request to be fulfilled." + histograms.fga_client_request_duration.description + == "Total request time for FGA requests, in milliseconds." ) - assert histograms.query_duration.name == "fga-client.query.duration" - assert histograms.query_duration.unit == "milliseconds" + assert histograms.fga_client_query_duration.name == "fga-client.query.duration" + assert histograms.fga_client_query_duration.unit == "milliseconds" assert ( - histograms.query_duration.description - == "How long it took to perform a query request." + histograms.fga_client_query_duration.description + == "Time taken by the FGA server to process and evaluate the request, in milliseconds." ) diff --git a/test/telemetry/metrics_test.py b/test/telemetry/metrics_test.py index 9e0636e..ea9eaab 100644 --- a/test/telemetry/metrics_test.py +++ b/test/telemetry/metrics_test.py @@ -7,7 +7,7 @@ from openfga_sdk.telemetry.attributes import TelemetryAttributes from openfga_sdk.telemetry.counters import TelemetryCounters from openfga_sdk.telemetry.histograms import TelemetryHistograms -from openfga_sdk.telemetry.metrics import MetricsTelemetry +from openfga_sdk.telemetry.metrics import TelemetryMetrics @patch("openfga_sdk.telemetry.metrics.get_meter") @@ -15,7 +15,7 @@ def test_meter_lazy_initialization(mock_get_meter): mock_meter = MagicMock(spec=Meter) mock_get_meter.return_value = mock_meter - telemetry = MetricsTelemetry() + telemetry = TelemetryMetrics() # Ensure _meter is initially None assert telemetry._meter is None @@ -32,70 +32,62 @@ def test_meter_lazy_initialization(mock_get_meter): @patch("openfga_sdk.telemetry.metrics.get_meter") -def test_counter_creation_and_add(mock_get_meter): +def test_counter_creation(mock_get_meter): mock_meter = MagicMock(spec=Meter) mock_counter = MagicMock(spec=Counter) mock_get_meter.return_value = mock_meter mock_meter.create_counter.return_value = mock_counter - telemetry = MetricsTelemetry() + telemetry = TelemetryMetrics() attributes = { TelemetryAttributes.fga_client_request_model_id.name: "model_123", "custom_attribute": "custom_value", } - counter = telemetry.counter( - TelemetryCounters.credentials_request, value=5, attributes=attributes - ) + counter = telemetry.counter(TelemetryCounters.fga_client_credentials_request) assert counter == mock_counter telemetry._meter.create_counter.assert_called_once_with( - name=TelemetryCounters.credentials_request.name, - unit=TelemetryCounters.credentials_request.unit, - description=TelemetryCounters.credentials_request.description, + name=TelemetryCounters.fga_client_credentials_request.name, + unit=TelemetryCounters.fga_client_credentials_request.unit, + description=TelemetryCounters.fga_client_credentials_request.description, ) - mock_counter.add.assert_called_once_with(amount=5, attributes=attributes) - @patch("openfga_sdk.telemetry.metrics.get_meter") -def test_histogram_creation_and_record(mock_get_meter): +def test_histogram_creation(mock_get_meter): mock_meter = MagicMock(spec=Meter) mock_histogram = MagicMock(spec=Histogram) mock_get_meter.return_value = mock_meter mock_meter.create_histogram.return_value = mock_histogram - telemetry = MetricsTelemetry() + telemetry = TelemetryMetrics() attributes = { TelemetryAttributes.fga_client_request_model_id.name: "model_123", "custom_attribute": "custom_value", } - histogram = telemetry.histogram( - TelemetryHistograms.request_duration, value=200.5, attributes=attributes - ) + histogram = telemetry.histogram(TelemetryHistograms.fga_client_request_duration) assert histogram == mock_histogram telemetry._meter.create_histogram.assert_called_once_with( - name=TelemetryHistograms.request_duration.name, - unit=TelemetryHistograms.request_duration.unit, - description=TelemetryHistograms.request_duration.description, + name=TelemetryHistograms.fga_client_request_duration.name, + unit=TelemetryHistograms.fga_client_request_duration.unit, + description=TelemetryHistograms.fga_client_request_duration.description, ) - mock_histogram.record.assert_called_once_with(amount=200.5, attributes=attributes) - def test_invalid_counter_key(): - telemetry = MetricsTelemetry() - with pytest.raises(KeyError): + telemetry = TelemetryMetrics() + with pytest.raises(ValueError): telemetry.counter("invalid_counter_key") def test_invalid_histogram_key(): - telemetry = MetricsTelemetry() - with pytest.raises(KeyError): + telemetry = TelemetryMetrics() + with pytest.raises(ValueError): telemetry.histogram("invalid_histogram_key") diff --git a/test/telemetry/telemetry_test.py b/test/telemetry/telemetry_test.py index a25487f..a8424ed 100644 --- a/test/telemetry/telemetry_test.py +++ b/test/telemetry/telemetry_test.py @@ -1,13 +1,13 @@ from unittest.mock import patch from openfga_sdk.telemetry.metrics import ( - MetricsTelemetry, + TelemetryMetrics, ) def test_metrics_lazy_initialization(): with patch( - "openfga_sdk.telemetry.telemetry.MetricsTelemetry" + "openfga_sdk.telemetry.telemetry.TelemetryMetrics" ) as mock_metrics_telemetry: from openfga_sdk.telemetry import Telemetry # Import inside the patch context @@ -17,14 +17,14 @@ def test_metrics_lazy_initialization(): assert telemetry._metrics is None # Access the metrics property, which should trigger lazy initialization - metrics = telemetry.metrics() + metrics = telemetry.metrics - # Verify that a MetricsTelemetry object was created and returned + # Verify that a TelemetryMetrics object was created and returned assert metrics == mock_metrics_telemetry.return_value mock_metrics_telemetry.assert_called_once() # Access the metrics property again, no new instance should be created - metrics_again = telemetry.metrics() + metrics_again = telemetry.metrics assert metrics_again == metrics mock_metrics_telemetry.assert_called_once() # Should still be only called once @@ -35,11 +35,11 @@ def test_metrics_initialization_without_patch(): telemetry = Telemetry() # Access the metrics property, which should trigger lazy initialization - metrics = telemetry.metrics() + metrics = telemetry.metrics - # Verify that a real MetricsTelemetry object was created and returned - assert isinstance(metrics, MetricsTelemetry) + # Verify that a real TelemetryMetrics object was created and returned + assert isinstance(metrics, TelemetryMetrics) # Access the metrics property again, no new instance should be created - metrics_again = telemetry.metrics() + metrics_again = telemetry.metrics assert metrics_again == metrics diff --git a/test/telemetry/utilities_test.py b/test/telemetry/utilities_test.py new file mode 100644 index 0000000..80de9d2 --- /dev/null +++ b/test/telemetry/utilities_test.py @@ -0,0 +1,15 @@ +from mock import MagicMock + +from openfga_sdk.telemetry.utilities import doesInstanceHaveCallable + + +def test_instance_has_callable(): + mock_instance = MagicMock(spec_set=["some_callable", "some_attribute"]) + mock_instance.some_callable = lambda: "I am callable" + + assert doesInstanceHaveCallable(mock_instance, "some_callable") + + assert not doesInstanceHaveCallable(mock_instance, "missing_callable") + + mock_instance.some_attribute = "not callable" + assert not doesInstanceHaveCallable(mock_instance, "some_attribute")