Skip to content

Commit

Permalink
feat: add per-HTTP request counter metric
Browse files Browse the repository at this point in the history
  • Loading branch information
evansims committed Oct 2, 2024
1 parent 41d5c1b commit 916442d
Show file tree
Hide file tree
Showing 10 changed files with 120 additions and 25 deletions.
1 change: 1 addition & 0 deletions docs/opentelemetry.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
22 changes: 22 additions & 0 deletions openfga_sdk/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -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,
Expand Down
5 changes: 2 additions & 3 deletions openfga_sdk/oauth2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
45 changes: 35 additions & 10 deletions openfga_sdk/sync/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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)
Expand All @@ -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,
Expand All @@ -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(
Expand Down
5 changes: 2 additions & 3 deletions openfga_sdk/sync/oauth2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
27 changes: 27 additions & 0 deletions openfga_sdk/telemetry/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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:
"""
Expand Down Expand Up @@ -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,
Expand Down
9 changes: 7 additions & 2 deletions openfga_sdk/telemetry/counters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 1 addition & 3 deletions openfga_sdk/telemetry/histograms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
)

Expand Down
24 changes: 23 additions & 1 deletion openfga_sdk/telemetry/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 0 additions & 3 deletions test/telemetry/counters_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."


Expand All @@ -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."
Expand Down

0 comments on commit 916442d

Please sign in to comment.