diff --git a/docs/opentelemetry.md b/docs/opentelemetry.md index 3467ee8..8a75043 100644 --- a/docs/opentelemetry.md +++ b/docs/opentelemetry.md @@ -26,6 +26,7 @@ If you configure the OpenTelemetry SDK, these metrics will be exported and sent | `fga-client.request.duration` | Histogram | Yes | Total request time for FGA requests, in milliseconds | | `fga-client.query.duration` | Histogram | Yes | Time taken by the FGA server to process and evaluate the request, in milliseconds | | `fga-client.credentials.request` | Counter | Yes | Total number of new token requests initiated using the Client Credentials flow | +| `fga-client.request` | Counter | No | Total number of requests made to the FGA server | ### Supported Attributes diff --git a/openfga_sdk/api_client.py b/openfga_sdk/api_client.py index 5c4d144..1de86ed 100644 --- a/openfga_sdk/api_client.py +++ b/openfga_sdk/api_client.py @@ -269,6 +269,7 @@ async def __call_api( 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( @@ -283,6 +284,17 @@ async def __call_api( ) except (RateLimitExceededError, ServiceException) as e: if retry < max_retry and e.status != 501: + _telemetry_attributes = TelemetryAttributes.fromResponse( + response=e.body.decode("utf-8"), + credentials=self.configuration.credentials, + attributes=_telemetry_attributes, + ) + + self._telemetry.metrics.request( + attributes=_telemetry_attributes, + configuration=self.configuration.telemetry, + ) + await asyncio.sleep(random_time(retry, min_wait_in_ms)) continue @@ -309,6 +321,11 @@ async def __call_api( attributes=_telemetry_attributes, ) + self._telemetry.metrics.request( + attributes=_telemetry_attributes, + configuration=self.configuration.telemetry, + ) + self._telemetry.metrics.queryDuration( attributes=_telemetry_attributes, configuration=self.configuration.telemetry, @@ -330,6 +347,11 @@ async def __call_api( attributes=_telemetry_attributes, ) + self._telemetry.metrics.request( + attributes=_telemetry_attributes, + configuration=self.configuration.telemetry, + ) + self._telemetry.metrics.queryDuration( attributes=_telemetry_attributes, configuration=self.configuration.telemetry, diff --git a/openfga_sdk/oauth2.py b/openfga_sdk/oauth2.py index 37354a2..256eff1 100644 --- a/openfga_sdk/oauth2.py +++ b/openfga_sdk/oauth2.py @@ -127,11 +127,10 @@ async def _obtain_token(self, client): ) self._access_token = api_response.get("access_token") self._telemetry.metrics.credentialsRequest( - 1, - { + attributes={ TelemetryAttributes.fga_client_request_client_id: configuration.client_id }, - self.configuration.telemetry, + configuration=self.configuration.telemetry, ) break diff --git a/openfga_sdk/sync/api_client.py b/openfga_sdk/sync/api_client.py index 756a9f5..b5e6b32 100644 --- a/openfga_sdk/sync/api_client.py +++ b/openfga_sdk/sync/api_client.py @@ -253,7 +253,21 @@ 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 = self.request( @@ -268,7 +282,19 @@ def __call_api( ) except (RateLimitExceededError, ServiceException) as e: if retry < max_retry and e.status != 501: + _telemetry_attributes = TelemetryAttributes.fromResponse( + response=e.body.decode("utf-8"), + credentials=self.configuration.credentials, + attributes=_telemetry_attributes, + ) + + self._telemetry.metrics.request( + attributes=_telemetry_attributes, + configuration=self.configuration.telemetry, + ) + time.sleep(random_time(retry, min_wait_in_ms)) + continue e.body = e.body.decode("utf-8") response_type = response_types_map.get(e.status, None) @@ -293,6 +319,11 @@ def __call_api( attributes=_telemetry_attributes, ) + self._telemetry.metrics.request( + attributes=_telemetry_attributes, + configuration=self.configuration.telemetry, + ) + self._telemetry.metrics.queryDuration( attributes=_telemetry_attributes, configuration=self.configuration.telemetry, @@ -308,21 +339,15 @@ def __call_api( 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, + _telemetry_attributes = TelemetryAttributes.fromResponse( + response=response_data, credentials=self.configuration.credentials, attributes=_telemetry_attributes, ) - _telemetry_attributes = TelemetryAttributes.fromResponse( - response=response_data, - credentials=self.configuration.credentials, + self._telemetry.metrics.request( attributes=_telemetry_attributes, + configuration=self.configuration.telemetry, ) self._telemetry.metrics.queryDuration( diff --git a/openfga_sdk/sync/oauth2.py b/openfga_sdk/sync/oauth2.py index bb5b342..9bde71c 100644 --- a/openfga_sdk/sync/oauth2.py +++ b/openfga_sdk/sync/oauth2.py @@ -127,11 +127,10 @@ def _obtain_token(self, client): ) self._access_token = api_response.get("access_token") self._telemetry.metrics.credentialsRequest( - 1, - { + attributes={ TelemetryAttributes.fga_client_request_client_id: configuration.client_id }, - self.configuration.telemetry, + configuration=self.configuration.telemetry, ) break diff --git a/openfga_sdk/telemetry/configuration.py b/openfga_sdk/telemetry/configuration.py index 4822c33..2ff5c1c 100644 --- a/openfga_sdk/telemetry/configuration.py +++ b/openfga_sdk/telemetry/configuration.py @@ -613,6 +613,7 @@ def __init__( fga_client_credentials_request: Optional[TelemetryMetricConfiguration] = None, fga_client_request_duration: Optional[TelemetryMetricConfiguration] = None, fga_client_query_duration: Optional[TelemetryMetricConfiguration] = None, + fga_client_request: Optional[TelemetryMetricConfiguration] = None, ): """ Initialize a new instance of the `TelemetryMetricsConfiguration` class. @@ -621,6 +622,7 @@ def __init__( :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. + :param fga_client_request: The `fga-client.request` counter collects the number of requests made to the FGA server. """ # Instantiate with default state, and apply the incoming configuration, if one was provided @@ -641,9 +643,33 @@ def __init__( fga_client_query_duration ) + if fga_client_request is not None: + self._state[TelemetryCounters.fga_client_request] = fga_client_request + # Reset the validation state self._valid = None + @property + def fga_client_request(self) -> TelemetryMetricConfiguration | None: + """ + Get the configuration for the `fga-client.request` counter. + + :return: The configuration for the `fga-client.request` counter. + """ + + return self._state[TelemetryCounters.fga_client_request] + + @fga_client_request.setter + def fga_client_request(self, value: TelemetryMetricConfiguration | None): + """ + Set the configuration for the `fga-client.request` counter. + + :param value: The configuration for the `fga-client.request` counter. + """ + + self._valid = None # Reset the validation state + self._state[TelemetryCounters.fga_client_request] = value + @property def fga_client_credentials_request(self) -> TelemetryMetricConfiguration | None: """ @@ -714,6 +740,7 @@ def clear(self) -> None: Reset the configuration to the default state (all attributes disabled). """ self._state = { + TelemetryCounters.fga_client_request: None, TelemetryCounters.fga_client_credentials_request: None, TelemetryHistograms.fga_client_request_duration: None, TelemetryHistograms.fga_client_query_duration: None, diff --git a/openfga_sdk/telemetry/counters.py b/openfga_sdk/telemetry/counters.py index 9a8aaec..f1eef05 100644 --- a/openfga_sdk/telemetry/counters.py +++ b/openfga_sdk/telemetry/counters.py @@ -3,19 +3,24 @@ class TelemetryCounter(NamedTuple): name: str - unit: str description: str + unit: str = "" class TelemetryCounters: fga_client_credentials_request: TelemetryCounter = TelemetryCounter( name="fga-client.credentials.request", - unit="milliseconds", description="Total number of new token requests initiated using the Client Credentials flow.", ) + fga_client_request: TelemetryCounter = TelemetryCounter( + name="fga-client.request", + description="Total number of requests made to the FGA server.", + ) + _counters: list[TelemetryCounter] = [ fga_client_credentials_request, + fga_client_request, ] @staticmethod diff --git a/openfga_sdk/telemetry/histograms.py b/openfga_sdk/telemetry/histograms.py index 1449c49..d3ffdc5 100644 --- a/openfga_sdk/telemetry/histograms.py +++ b/openfga_sdk/telemetry/histograms.py @@ -3,19 +3,17 @@ class TelemetryHistogram(NamedTuple): name: str - unit: str description: str + unit: str = "milliseconds" class TelemetryHistograms: fga_client_request_duration: TelemetryHistogram = TelemetryHistogram( name="fga-client.request.duration", - unit="milliseconds", description="Total request time for FGA requests, in milliseconds.", ) fga_client_query_duration: TelemetryHistogram = TelemetryHistogram( name="fga-client.query.duration", - unit="milliseconds", description="Time taken by the FGA server to process and evaluate the request, in milliseconds.", ) diff --git a/openfga_sdk/telemetry/metrics.py b/openfga_sdk/telemetry/metrics.py index c48819d..f33b562 100644 --- a/openfga_sdk/telemetry/metrics.py +++ b/openfga_sdk/telemetry/metrics.py @@ -63,9 +63,31 @@ def histogram(self, histogram: TelemetryHistogram) -> Histogram: return self._histograms[histogram.name] + def request( + self, + value: int = 1, + attributes: dict[TelemetryAttribute, str | int] | None = None, + configuration: TelemetryConfiguration | None = None, + ) -> Counter: + """ + Record a request made by the client. + """ + counter = self.counter(TelemetryCounters.fga_client_request) + + if isMetricEnabled(configuration, TelemetryCounters.fga_client_request): + attributes = TelemetryAttributes.prepare( + attributes, + filter=configuration.metrics.fga_client_request.getAttributes(), + ) + + if value is not None: + counter.add(amount=value, attributes=attributes) + + return counter + def credentialsRequest( self, - value: int, + value: int = 1, attributes: dict[TelemetryAttribute, str | int] | None = None, configuration: TelemetryConfiguration | None = None, ) -> Counter: diff --git a/test/telemetry/counters_test.py b/test/telemetry/counters_test.py index 5d14ed7..ae5372e 100644 --- a/test/telemetry/counters_test.py +++ b/test/telemetry/counters_test.py @@ -4,12 +4,10 @@ def test_telemetry_counter_initialization(): counter = TelemetryCounter( name="fga-client.test.counter", - unit="seconds", description="A test counter for unit testing.", ) assert counter.name == "fga-client.test.counter" - assert counter.unit == "seconds" assert counter.description == "A test counter for unit testing." @@ -19,7 +17,6 @@ def test_telemetry_counters_default_values(): assert ( 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."