Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions config/clients/python/CHANGELOG.md.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

## [Unreleased](https://github.com/openfga/python-sdk/compare/v{{packageVersion}}...HEAD)

### [0.9.2](https://github.com/openfga/python-sdk/compare/v0.9.1...v0.9.2) (2025-03-25)

- fix(telemetry): fixes for telemetry attributes and metrics tracking
- fix: REST client should not close after `stream` request

## v0.9.1

### [0.9.1](https://github.com/openfga/python-sdk/compare/v0.9.0...v0.9.1) (2025-01-23)
Expand Down
2 changes: 1 addition & 1 deletion config/clients/python/config.overrides.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"sdkId": "python",
"gitRepoId": "python-sdk",
"packageName": "openfga_sdk",
"packageVersion": "0.9.1",
"packageVersion": "0.9.2",
"packageDescription": "Python SDK for OpenFGA",
"packageDetailedDescription": "This is an autogenerated python SDK for OpenFGA. It provides a wrapper around the [OpenFGA API definition](https://openfga.dev/api).",
"fossaComplianceNoticeId": "2f8a8629-b46c-435e-b8cd-1174a674fb4b",
Expand Down
3 changes: 3 additions & 0 deletions config/clients/python/template/src/api_client.py.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ class ApiClient:
response=e.body.decode("utf-8"),
credentials=self.configuration.credentials,
attributes=_telemetry_attributes,
start=start,
)

self._telemetry.metrics.request(
Expand Down Expand Up @@ -307,6 +308,7 @@ class ApiClient:
response=e,
credentials=self.configuration.credentials,
attributes=_telemetry_attributes,
start=start,
)

self._telemetry.metrics.request(
Expand All @@ -333,6 +335,7 @@ class ApiClient:
response=response_data,
credentials=self.configuration.credentials,
attributes=_telemetry_attributes,
start=start,
)

self._telemetry.metrics.request(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

from {{packageName}}.configuration import Configuration
from {{packageName}}.exceptions import FgaValidationException
from {{packageName}}.telemetry.attributes import TelemetryAttribute
from {{packageName}}.telemetry.configuration import (
TelemetryConfigurationType,
TelemetryMetricConfiguration,
TelemetryMetricsConfiguration,
)
from {{packageName}}.telemetry.counters import TelemetryCounter
from {{packageName}}.telemetry.histograms import TelemetryHistogram
from {{packageName}}.validation import is_well_formed_ulid_string

class ClientConfiguration(Configuration):
Expand All @@ -20,8 +28,32 @@ class ClientConfiguration(Configuration):
ssl_ca_cert=None,
api_url=None, # TODO: restructure when removing api_scheme/api_host
timeout_millisec: int | None = None,
telemetry: (
dict[
TelemetryConfigurationType | str,
TelemetryMetricsConfiguration
| dict[
TelemetryHistogram | TelemetryCounter | str,
TelemetryMetricConfiguration
| dict[TelemetryAttribute | str, bool]
| None,
]
| None,
]
| None
) = None,
):
super().__init__(api_scheme, api_host, store_id, credentials, retry_params, ssl_ca_cert=ssl_ca_cert, api_url=api_url, timeout_millisec=timeout_millisec)
super().__init__(
api_scheme,
api_host,
store_id,
credentials,
retry_params,
ssl_ca_cert=ssl_ca_cert,
api_url=api_url,
timeout_millisec=timeout_millisec,
telemetry=telemetry,
)
self._authorization_model_id = authorization_model_id

def is_valid(self):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ class ApiClient:
response=e.body.decode("utf-8"),
credentials=self.configuration.credentials,
attributes=_telemetry_attributes,
start=start,
)

self._telemetry.metrics.request(
Expand Down Expand Up @@ -291,6 +292,7 @@ class ApiClient:
response=e,
credentials=self.configuration.credentials,
attributes=_telemetry_attributes,
start=start,
)

self._telemetry.metrics.request(
Expand All @@ -317,6 +319,7 @@ class ApiClient:
response=response_data,
credentials=self.configuration.credentials,
attributes=_telemetry_attributes,
start=start,
)

self._telemetry.metrics.request(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ class TelemetryAttributes:
) = None,
credentials: Credentials | None = None,
attributes: dict[TelemetryAttribute, str | bool | int | float] | None = None,
start: float | None = None,
) -> dict[TelemetryAttribute, str | bool | int | float]:
response_model_id = None
response_query_duration = None
Expand All @@ -286,6 +287,11 @@ class TelemetryAttributes:
if attributes is not None:
_attributes = attributes

if start is not None and start > 0:
_attributes[TelemetryAttributes.http_client_request_duration] = int(
(time.time() - start) * 1000
)

if isinstance(response, ApiException):
if response.status is not None:
_attributes[TelemetryAttributes.http_response_status_code] = int(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ from {{packageName}}.telemetry.attributes import (
from {{packageName}}.telemetry.configuration import (
TelemetryConfiguration,
TelemetryMetricConfiguration,
TelemetryMetricsConfiguration,
isMetricEnabled,
)
from {{packageName}}.telemetry.counters import TelemetryCounter, TelemetryCounters
Expand Down Expand Up @@ -80,7 +81,7 @@ class TelemetryMetrics:

if (
isinstance(configuration, TelemetryConfiguration)
and isinstance(configuration.metrics, TelemetryMetricConfiguration)
and isinstance(configuration.metrics, TelemetryMetricsConfiguration)
and isinstance(
configuration.metrics.fga_client_request,
TelemetryMetricConfiguration,
Expand Down Expand Up @@ -117,7 +118,7 @@ class TelemetryMetrics:

if (
isinstance(configuration, TelemetryConfiguration)
and isinstance(configuration.metrics, TelemetryMetricConfiguration)
and isinstance(configuration.metrics, TelemetryMetricsConfiguration)
and isinstance(
configuration.metrics.fga_client_credentials_request,
TelemetryMetricConfiguration,
Expand Down Expand Up @@ -168,7 +169,7 @@ class TelemetryMetrics:

if (
isinstance(configuration, TelemetryConfiguration)
and isinstance(configuration.metrics, TelemetryMetricConfiguration)
and isinstance(configuration.metrics, TelemetryMetricsConfiguration)
and isinstance(
configuration.metrics.fga_client_request_duration,
TelemetryMetricConfiguration,
Expand Down Expand Up @@ -217,7 +218,7 @@ class TelemetryMetrics:

if (
isinstance(configuration, TelemetryConfiguration)
and isinstance(configuration.metrics, TelemetryMetricConfiguration)
and isinstance(configuration.metrics, TelemetryMetricsConfiguration)
and isinstance(
configuration.metrics.fga_client_query_duration,
TelemetryMetricConfiguration,
Expand Down
11 changes: 8 additions & 3 deletions config/clients/python/template/test/rest_test.py.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,9 @@ async def test_stream_exception_in_chunks():

class FakeContent:
async def iter_chunks(self):
raise ValueError("Boom!")
if True: # This ensures the coroutine is actually created and awaited
raise ValueError("Boom!")
yield (b"", None) # This line is never reached

mock_response = MagicMock()
mock_response.status = 200
Expand All @@ -346,8 +348,11 @@ async def test_stream_exception_in_chunks():
client.close = AsyncMock()

results = []
async for item in client.stream("GET", "http://example.com"):
results.append(item)
try:
async for item in client.stream("GET", "http://example.com"):
results.append(item)
except ValueError:
pass

assert results == []
client.handle_response_exception.assert_awaited_once()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ def test_from_request_without_optional_params(telemetry_attributes):


def test_from_response_with_http_response(telemetry_attributes):
start_time = time.time() - 5
response = MagicMock(spec=HTTPResponse)
response.status = 200
response.getheader.side_effect = lambda header: {
Expand All @@ -124,16 +125,21 @@ def test_from_response_with_http_response(telemetry_attributes):
configuration=CredentialConfiguration(client_id="client_123"),
)
attributes = telemetry_attributes.fromResponse(
response=response, credentials=credentials
response=response,
credentials=credentials,
start=start_time,
)

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"
assert TelemetryAttributes.http_client_request_duration in attributes
assert attributes[TelemetryAttributes.http_client_request_duration] > 0


def test_from_response_with_rest_response(telemetry_attributes):
start_time = time.time() - 5
response = MagicMock(spec=RESTResponse)
response.status = 404
response.headers = {
Expand All @@ -148,13 +154,17 @@ def test_from_response_with_rest_response(telemetry_attributes):
configuration=CredentialConfiguration(client_id="client_456"),
)
attributes = telemetry_attributes.fromResponse(
response=response, credentials=credentials
response=response,
credentials=credentials,
start=start_time,
)

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"
assert TelemetryAttributes.http_client_request_duration in attributes
assert attributes[TelemetryAttributes.http_client_request_duration] > 0

def test_from_body_with_batch_check(telemetry_attributes):
body = MagicMock(spec=BatchCheckRequest)
Expand Down