diff --git a/docs/opentelemetry.md b/docs/opentelemetry.md index 8a75043..ce4ca6b 100644 --- a/docs/opentelemetry.md +++ b/docs/opentelemetry.md @@ -30,23 +30,24 @@ If you configure the OpenTelemetry SDK, these metrics will be exported and sent ### Supported Attributes -| Attribute Name | Type | Enabled by Default | Description | -| ------------------------------ | ------ | ------------------ | --------------------------------------------------------------------------------- | -| `fga-client.request.client_id` | string | Yes | Client ID associated with the request, if any | -| `fga-client.request.method` | string | Yes | FGA method/action that was performed (e.g., Check, ListObjects) in TitleCase | -| `fga-client.request.model_id` | string | Yes | Authorization model ID that was sent as part of the request, if any | -| `fga-client.request.store_id` | string | Yes | Store ID that was sent as part of the request | -| `fga-client.response.model_id` | string | Yes | Authorization model ID that the FGA server used | -| `fga-client.user` | string | No | User associated with the action of the request for check and list users | -| `http.client.request.duration` | int | No | Duration for the SDK to complete the request, in milliseconds | -| `http.host` | string | Yes | Host identifier of the origin the request was sent to | -| `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 | 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 | +| Attribute Name | Type | Enabled by Default | Description | +| ------------------------------------- | ------ | ------------------ | --------------------------------------------------------------------------------- | +| `fga-client.request.batch_check_size` | int | No | The total size of the `check` list in a `BatchCheck` call | +| `fga-client.request.client_id` | string | Yes | Client ID associated with the request, if any | +| `fga-client.request.method` | string | Yes | FGA method/action that was performed (e.g., Check, ListObjects) in TitleCase | +| `fga-client.request.model_id` | string | Yes | Authorization model ID that was sent as part of the request, if any | +| `fga-client.request.store_id` | string | Yes | Store ID that was sent as part of the request | +| `fga-client.response.model_id` | string | Yes | Authorization model ID that the FGA server used | +| `fga-client.user` | string | No | User associated with the action of the request for check and list users | +| `http.client.request.duration` | int | No | Duration for the SDK to complete the request, in milliseconds | +| `http.host` | string | Yes | Host identifier of the origin the request was sent to | +| `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 | 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 | ## Customizing Reporting diff --git a/openfga_sdk/api/open_fga_api.py b/openfga_sdk/api/open_fga_api.py index 8f4033c..dadde05 100644 --- a/openfga_sdk/api/open_fga_api.py +++ b/openfga_sdk/api/open_fga_api.py @@ -201,6 +201,11 @@ async def batch_check_with_http_info(self, body, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return await self.api_client.call_api( "/stores/{store_id}/batch-check".replace("{store_id}", store_id), "POST", @@ -375,6 +380,11 @@ async def check_with_http_info(self, body, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return await self.api_client.call_api( "/stores/{store_id}/check".replace("{store_id}", store_id), "POST", @@ -536,6 +546,11 @@ async def create_store_with_http_info(self, body, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return await self.api_client.call_api( "/stores", "POST", @@ -678,6 +693,11 @@ async def delete_store_with_http_info(self, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return await self.api_client.call_api( "/stores/{store_id}".replace("{store_id}", store_id), "DELETE", @@ -852,6 +872,11 @@ async def expand_with_http_info(self, body, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return await self.api_client.call_api( "/stores/{store_id}/expand".replace("{store_id}", store_id), "POST", @@ -1003,6 +1028,11 @@ async def get_store_with_http_info(self, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return await self.api_client.call_api( "/stores/{store_id}".replace("{store_id}", store_id), "GET", @@ -1178,6 +1208,11 @@ async def list_objects_with_http_info(self, body, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return await self.api_client.call_api( "/stores/{store_id}/list-objects".replace("{store_id}", store_id), "POST", @@ -1337,6 +1372,11 @@ async def list_stores_with_http_info(self, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return await self.api_client.call_api( "/stores", "GET", @@ -1512,6 +1552,11 @@ async def list_users_with_http_info(self, body, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return await self.api_client.call_api( "/stores/{store_id}/list-users".replace("{store_id}", store_id), "POST", @@ -1686,6 +1731,11 @@ async def read_with_http_info(self, body, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return await self.api_client.call_api( "/stores/{store_id}/read".replace("{store_id}", store_id), "POST", @@ -1856,6 +1906,11 @@ async def read_assertions_with_http_info(self, authorization_model_id, **kwargs) ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return await self.api_client.call_api( "/stores/{store_id}/assertions/{authorization_model_id}".replace( "{store_id}", store_id @@ -2024,6 +2079,11 @@ async def read_authorization_model_with_http_info(self, id, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return await self.api_client.call_api( "/stores/{store_id}/authorization-models/{id}".replace( "{store_id}", store_id @@ -2191,6 +2251,11 @@ async def read_authorization_models_with_http_info(self, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return await self.api_client.call_api( "/stores/{store_id}/authorization-models".replace("{store_id}", store_id), "GET", @@ -2362,6 +2427,11 @@ async def read_changes_with_http_info(self, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return await self.api_client.call_api( "/stores/{store_id}/changes".replace("{store_id}", store_id), "GET", @@ -2536,6 +2606,11 @@ async def write_with_http_info(self, body, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return await self.api_client.call_api( "/stores/{store_id}/write".replace("{store_id}", store_id), "POST", @@ -2723,6 +2798,11 @@ async def write_assertions_with_http_info( ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return await self.api_client.call_api( "/stores/{store_id}/assertions/{authorization_model_id}".replace( "{store_id}", store_id @@ -2900,6 +2980,11 @@ async def write_authorization_model_with_http_info(self, body, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return await self.api_client.call_api( "/stores/{store_id}/authorization-models".replace("{store_id}", store_id), "POST", diff --git a/openfga_sdk/sync/open_fga_api.py b/openfga_sdk/sync/open_fga_api.py index 0a5554e..114e7da 100644 --- a/openfga_sdk/sync/open_fga_api.py +++ b/openfga_sdk/sync/open_fga_api.py @@ -199,6 +199,11 @@ def batch_check_with_http_info(self, body, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return self.api_client.call_api( "/stores/{store_id}/batch-check".replace("{store_id}", store_id), "POST", @@ -373,6 +378,11 @@ def check_with_http_info(self, body, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return self.api_client.call_api( "/stores/{store_id}/check".replace("{store_id}", store_id), "POST", @@ -534,6 +544,11 @@ def create_store_with_http_info(self, body, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return self.api_client.call_api( "/stores", "POST", @@ -676,6 +691,11 @@ def delete_store_with_http_info(self, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return self.api_client.call_api( "/stores/{store_id}".replace("{store_id}", store_id), "DELETE", @@ -850,6 +870,11 @@ def expand_with_http_info(self, body, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return self.api_client.call_api( "/stores/{store_id}/expand".replace("{store_id}", store_id), "POST", @@ -1001,6 +1026,11 @@ def get_store_with_http_info(self, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return self.api_client.call_api( "/stores/{store_id}".replace("{store_id}", store_id), "GET", @@ -1176,6 +1206,11 @@ def list_objects_with_http_info(self, body, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return self.api_client.call_api( "/stores/{store_id}/list-objects".replace("{store_id}", store_id), "POST", @@ -1335,6 +1370,11 @@ def list_stores_with_http_info(self, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return self.api_client.call_api( "/stores", "GET", @@ -1510,6 +1550,11 @@ def list_users_with_http_info(self, body, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return self.api_client.call_api( "/stores/{store_id}/list-users".replace("{store_id}", store_id), "POST", @@ -1684,6 +1729,11 @@ def read_with_http_info(self, body, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return self.api_client.call_api( "/stores/{store_id}/read".replace("{store_id}", store_id), "POST", @@ -1852,6 +1902,11 @@ def read_assertions_with_http_info(self, authorization_model_id, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return self.api_client.call_api( "/stores/{store_id}/assertions/{authorization_model_id}".replace( "{store_id}", store_id @@ -2020,6 +2075,11 @@ def read_authorization_model_with_http_info(self, id, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return self.api_client.call_api( "/stores/{store_id}/authorization-models/{id}".replace( "{store_id}", store_id @@ -2187,6 +2247,11 @@ def read_authorization_models_with_http_info(self, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return self.api_client.call_api( "/stores/{store_id}/authorization-models".replace("{store_id}", store_id), "GET", @@ -2358,6 +2423,11 @@ def read_changes_with_http_info(self, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return self.api_client.call_api( "/stores/{store_id}/changes".replace("{store_id}", store_id), "GET", @@ -2532,6 +2602,11 @@ def write_with_http_info(self, body, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return self.api_client.call_api( "/stores/{store_id}/write".replace("{store_id}", store_id), "POST", @@ -2717,6 +2792,11 @@ def write_assertions_with_http_info(self, authorization_model_id, body, **kwargs ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return self.api_client.call_api( "/stores/{store_id}/assertions/{authorization_model_id}".replace( "{store_id}", store_id @@ -2894,6 +2974,11 @@ def write_authorization_model_with_http_info(self, body, **kwargs): ), } + telemetry_attributes = TelemetryAttributes.fromBody( + body=body_params, + attributes=telemetry_attributes, + ) + return self.api_client.call_api( "/stores/{store_id}/authorization-models".replace("{store_id}", store_id), "POST", diff --git a/openfga_sdk/telemetry/attributes.py b/openfga_sdk/telemetry/attributes.py index f09caf6..81360eb 100644 --- a/openfga_sdk/telemetry/attributes.py +++ b/openfga_sdk/telemetry/attributes.py @@ -1,6 +1,6 @@ import time import urllib -from typing import NamedTuple +from typing import Any, NamedTuple from aiohttp import ClientResponse from urllib3 import HTTPResponse @@ -19,6 +19,9 @@ class TelemetryAttribute(NamedTuple): class TelemetryAttributes: + fga_client_request_batch_check_size: TelemetryAttribute = TelemetryAttribute( + name="fga-client.request.batch_check_size", format="int" + ) fga_client_request_client_id: TelemetryAttribute = TelemetryAttribute( name="fga-client.request.client_id", ) @@ -70,6 +73,7 @@ class TelemetryAttributes: ) _attributes: list[TelemetryAttribute] = [ + fga_client_request_batch_check_size, fga_client_request_client_id, fga_client_request_method, fga_client_request_model_id, @@ -165,6 +169,23 @@ def prepare( return response + @staticmethod + def fromBody(body: Any, attributes: dict[TelemetryAttribute, str | int] = None): + from openfga_sdk.models.batch_check_request import BatchCheckRequest + + if attributes is None: + attributes = {} + + if ( + TelemetryAttributes.fga_client_request_batch_check_size not in attributes + and isinstance(body, BatchCheckRequest) + ): + attributes[TelemetryAttributes.fga_client_request_batch_check_size] = len( + body.checks + ) + + return attributes + @staticmethod def fromRequest( user_agent: str = None, diff --git a/openfga_sdk/telemetry/configuration.py b/openfga_sdk/telemetry/configuration.py index 7d780b6..3ba7701 100644 --- a/openfga_sdk/telemetry/configuration.py +++ b/openfga_sdk/telemetry/configuration.py @@ -32,6 +32,7 @@ def __init__( url_scheme: bool | None = None, url_full: bool | None = None, user_agent_original: bool | None = None, + fga_client_request_batch_check_size: bool | None = None, ): """ Initialize a new instance of the `TelemetryMetricConfiguration` class. @@ -52,6 +53,7 @@ def __init__( :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. + :param fga_client_request_batch_check_size: The `fga-client.request.batch_check_size` attribute includes the size of the `checks` list in a `BatchCheck` request. """ self.configure( @@ -59,6 +61,11 @@ def __init__( clear=True, ) + if fga_client_request_batch_check_size is not None: + self._state[TelemetryAttributes.fga_client_request_batch_check_size] = ( + fga_client_request_batch_check_size + ) + if fga_client_request_client_id is not None: self._state[TelemetryAttributes.fga_client_request_client_id] = ( fga_client_request_client_id @@ -124,6 +131,25 @@ def __init__( self._valid = None # Reset the validation state + @property + def fga_client_request_batch_check_size(self) -> bool: + """ + Get the configuration for the `fga_client_request_batch_check_size` attribute. + + :return: The configuration for the `fga_client_request_batch_check_size` attribute. + """ + return self._state[TelemetryAttributes.fga_client_request_batch_check_size] + + @fga_client_request_batch_check_size.setter + def fga_client_request_batch_check_size(self, value: bool): + """ + Set the configuration for the `fga_client_request_batch_check_size` attribute. + + :param value: The configuration for the `fga_client_request_batch_check_size` attribute. + """ + self._valid = None # Reset the validation state + self._state[TelemetryAttributes.fga_client_request_batch_check_size] = value + @property def fga_client_request_client_id(self) -> bool: """ @@ -446,6 +472,7 @@ def clear(self) -> None: # Reset the configuration to the default state self._state = { + TelemetryAttributes.fga_client_request_batch_check_size: False, TelemetryAttributes.fga_client_request_client_id: False, TelemetryAttributes.fga_client_request_method: False, TelemetryAttributes.fga_client_request_model_id: False, @@ -578,6 +605,7 @@ def getSdkDefaults() -> dict[TelemetryAttribute, bool]: :return: The default SDK configuration for the telemetry metric. """ return { + TelemetryAttributes.fga_client_request_batch_check_size: False, TelemetryAttributes.fga_client_request_client_id: True, TelemetryAttributes.fga_client_request_method: True, TelemetryAttributes.fga_client_request_model_id: True, diff --git a/test/telemetry/attributes_test.py b/test/telemetry/attributes_test.py index af001c1..14b854b 100644 --- a/test/telemetry/attributes_test.py +++ b/test/telemetry/attributes_test.py @@ -5,6 +5,8 @@ from urllib3 import HTTPResponse from openfga_sdk.credentials import CredentialConfiguration, Credentials +from openfga_sdk.models.batch_check_request import BatchCheckRequest +from openfga_sdk.models.check_request import CheckRequest from openfga_sdk.rest import RESTResponse from openfga_sdk.telemetry.attributes import ( TelemetryAttributes, @@ -20,14 +22,19 @@ def test_prepare_with_valid_attributes(telemetry_attributes): attributes = { telemetry_attributes.fga_client_request_client_id: "client_123", telemetry_attributes.http_request_method: "GET", + telemetry_attributes.fga_client_request_batch_check_size: 3, } - filter_attributes = [telemetry_attributes.fga_client_request_client_id] + filter_attributes = [ + telemetry_attributes.fga_client_request_client_id, + telemetry_attributes.fga_client_request_batch_check_size, + ] prepared = telemetry_attributes.prepare(attributes, filter=filter_attributes) # Assert that only filtered attributes are returned assert prepared == { "fga-client.request.client_id": "client_123", + "fga-client.request.batch_check_size": 3, } @@ -145,3 +152,20 @@ def test_from_response_with_rest_response(telemetry_attributes): 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" + + +def test_from_body_with_batch_check(telemetry_attributes): + body = MagicMock(spec=BatchCheckRequest) + body.checks = ["1", "2", "3"] + + attributes = telemetry_attributes.fromBody(body=body) + + assert attributes[TelemetryAttributes.fga_client_request_batch_check_size] == 3 + + +def test_from_body_with_other_body(telemetry_attributes): + body = MagicMock(spec=CheckRequest) + + attributes = telemetry_attributes.fromBody(body=body) + + assert attributes == {} diff --git a/test/telemetry/configuration_test.py b/test/telemetry/configuration_test.py index 61f3170..7b3761a 100644 --- a/test/telemetry/configuration_test.py +++ b/test/telemetry/configuration_test.py @@ -12,6 +12,7 @@ def test_telemetry_metric_configuration_default_initialization(): config = TelemetryMetricConfiguration() + assert config.fga_client_request_batch_check_size is False 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 @@ -292,8 +293,11 @@ def test_default_telemetry_metric_configuration(): metric_config = TelemetryMetricConfiguration.getSdkDefaults() assert isinstance(metric_config, dict) - assert len(metric_config) == 15 + assert len(metric_config) == 16 + assert ( + metric_config[TelemetryAttributes.fga_client_request_batch_check_size] is False + ) 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