Skip to content

Commit d4c058d

Browse files
authored
feat: add support for async response log (#1733)
* feat: add support for async response log * fix whitespace * add await * add code coverage * fix lint * fix lint issues * address PR feedback * address PR feedback * link issue
1 parent ec8e035 commit d4c058d

File tree

6 files changed

+182
-12
lines changed

6 files changed

+182
-12
lines changed

google/auth/_helpers.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -443,15 +443,33 @@ def _parse_response(response: Any) -> Any:
443443
The parsed response. If the response contains valid JSON, the
444444
decoded JSON object (e.g., a dictionary or list) is returned.
445445
If the response does not have a `json()` method or if the JSON
446-
decoding fails, the original response object is returned.
446+
decoding fails, None is returned.
447447
"""
448448
try:
449449
json_response = response.json()
450450
return json_response
451-
except AttributeError:
452-
return response
453-
except json.JSONDecodeError:
454-
return response
451+
except Exception:
452+
# TODO(https://github.com/googleapis/google-auth-library-python/issues/1744):
453+
# Parse and return response payload as json based on different content types.
454+
return None
455+
456+
457+
def _response_log_base(logger: logging.Logger, parsed_response: Any) -> None:
458+
"""
459+
Logs a parsed HTTP response at the DEBUG level.
460+
461+
This internal helper function takes a parsed response and logs it
462+
using the provided logger. It also applies a hashing function to
463+
potentially sensitive information before logging.
464+
465+
Args:
466+
logger: The logging.Logger instance to use for logging.
467+
parsed_response: The parsed HTTP response object (e.g., a dictionary,
468+
list, or the original response if parsing failed).
469+
"""
470+
471+
logged_response = _hash_sensitive_info(parsed_response)
472+
logger.debug("Response received...", extra={"httpResponse": logged_response})
455473

456474

457475
def response_log(logger: logging.Logger, response: Any) -> None:
@@ -464,5 +482,4 @@ def response_log(logger: logging.Logger, response: Any) -> None:
464482
"""
465483
if is_logging_enabled(logger):
466484
json_response = _parse_response(response)
467-
logged_response = _hash_sensitive_info(json_response)
468-
logger.debug("Response received...", extra={"httpResponse": logged_response})
485+
_response_log_base(logger, json_response)

google/auth/aio/_helpers.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Copyright 2025 Google Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Helper functions for commonly used utilities."""
16+
17+
18+
import logging
19+
from typing import Any
20+
21+
from google.auth import _helpers
22+
23+
24+
async def _parse_response_async(response: Any) -> Any:
25+
"""
26+
Parses an async response, attempting to decode JSON.
27+
28+
Args:
29+
response: The response object to parse. This can be any type, but
30+
it is expected to have a `json()` method if it contains JSON.
31+
32+
Returns:
33+
The parsed response. If the response contains valid JSON, the
34+
decoded JSON object (e.g., a dictionary) is returned.
35+
If the response does not have a `json()` method or if the JSON
36+
decoding fails, None is returned.
37+
"""
38+
try:
39+
json_response = await response.json()
40+
return json_response
41+
except Exception:
42+
# TODO(https://github.com/googleapis/google-auth-library-python/issues/1745):
43+
# Parse and return response payload as json based on different content types.
44+
return None
45+
46+
47+
async def response_log_async(logger: logging.Logger, response: Any) -> None:
48+
"""
49+
Logs an Async HTTP response at the DEBUG level if logging is enabled.
50+
51+
Args:
52+
logger: The logging.Logger instance to use.
53+
response: The HTTP response object to log.
54+
"""
55+
if _helpers.is_logging_enabled(logger):
56+
json_response = await _parse_response_async(response)
57+
_helpers._response_log_base(logger, json_response)

google/auth/aio/transport/aiohttp.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
from google.auth import _helpers
3030
from google.auth import exceptions
31+
from google.auth.aio import _helpers as _helpers_async
3132
from google.auth.aio import transport
3233

3334
_LOGGER = logging.getLogger(__name__)
@@ -167,7 +168,7 @@ async def __call__(
167168
timeout=client_timeout,
168169
**kwargs,
169170
)
170-
# TODO(https://github.com/googleapis/google-auth-library-python/issues/1697): Add response log.
171+
await _helpers_async.response_log_async(_LOGGER, response)
171172
return Response(response)
172173

173174
except aiohttp.ClientError as caught_exc:

google/auth/transport/_aiohttp_requests.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from google.auth import _helpers
3131
from google.auth import exceptions
3232
from google.auth import transport
33+
from google.auth.aio import _helpers as _helpers_async
3334
from google.auth.transport import requests
3435

3536

@@ -191,7 +192,7 @@ async def __call__(
191192
response = await self.session.request(
192193
method, url, data=body, headers=headers, timeout=timeout, **kwargs
193194
)
194-
# TODO(https://github.com/googleapis/google-auth-library-python/issues/1697): Add response log.
195+
await _helpers_async.response_log_async(_LOGGER, response)
195196
return _CombinedResponse(response)
196197

197198
except aiohttp.ClientError as caught_exc:

tests/aio/test__helpers.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import json
16+
import logging
17+
from unittest import mock
18+
19+
import pytest # type: ignore
20+
21+
from google.auth.aio import _helpers
22+
23+
24+
@pytest.fixture
25+
def logger():
26+
"""Provides a basic logger instance for testing."""
27+
return logging.getLogger(__name__)
28+
29+
30+
@pytest.mark.asyncio
31+
async def test_response_log_debug_enabled(logger, caplog):
32+
logger.setLevel(logging.DEBUG)
33+
with mock.patch("google.auth._helpers.CLIENT_LOGGING_SUPPORTED", True):
34+
await _helpers.response_log_async(logger, {"payload": None})
35+
assert len(caplog.records) == 1
36+
record = caplog.records[0]
37+
assert record.message == "Response received..."
38+
assert record.httpResponse == "<class 'NoneType'>"
39+
40+
41+
@pytest.mark.asyncio
42+
async def test_response_log_debug_disabled(logger, caplog):
43+
logger.setLevel(logging.INFO)
44+
with mock.patch("google.auth._helpers.CLIENT_LOGGING_SUPPORTED", True):
45+
await _helpers.response_log_async(logger, "another_response")
46+
assert "Response received..." not in caplog.text
47+
48+
49+
@pytest.mark.asyncio
50+
async def test_response_log_debug_enabled_response_json(logger, caplog):
51+
class MockResponse:
52+
async def json(self):
53+
return {"key1": "value1", "key2": "value2", "key3": "value3"}
54+
55+
response = MockResponse()
56+
logger.setLevel(logging.DEBUG)
57+
with mock.patch("google.auth._helpers.CLIENT_LOGGING_SUPPORTED", True):
58+
await _helpers.response_log_async(logger, response)
59+
assert len(caplog.records) == 1
60+
record = caplog.records[0]
61+
assert record.message == "Response received..."
62+
assert record.httpResponse == {"key1": "value1", "key2": "value2", "key3": "value3"}
63+
64+
65+
@pytest.mark.asyncio
66+
async def test_parse_response_async_json_valid():
67+
class MockResponse:
68+
async def json(self):
69+
return {"data": "test"}
70+
71+
response = MockResponse()
72+
expected = {"data": "test"}
73+
assert await _helpers._parse_response_async(response) == expected
74+
75+
76+
@pytest.mark.asyncio
77+
async def test_parse_response_async_json_invalid():
78+
class MockResponse:
79+
def json(self):
80+
raise json.JSONDecodeError("msg", "doc", 0)
81+
82+
response = MockResponse()
83+
assert await _helpers._parse_response_async(response) is None
84+
85+
86+
@pytest.mark.asyncio
87+
async def test_parse_response_async_no_json_method():
88+
response = "plain text"
89+
assert await _helpers._parse_response_async(response) is None
90+
91+
92+
@pytest.mark.asyncio
93+
async def test_parse_response_async_none():
94+
assert await _helpers._parse_response_async(None) is None

tests/test__helpers.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@ def test_response_log_debug_enabled(logger, caplog):
398398
assert len(caplog.records) == 1
399399
record = caplog.records[0]
400400
assert record.message == "Response received..."
401-
assert record.httpResponse == {"payload": None}
401+
assert record.httpResponse == "<class 'NoneType'>"
402402

403403

404404
def test_response_log_debug_disabled(logger, caplog):
@@ -518,12 +518,12 @@ def json(self):
518518
raise json.JSONDecodeError("msg", "doc", 0)
519519

520520
response = MockResponse()
521-
assert _helpers._parse_response(response) == response
521+
assert _helpers._parse_response(response) is None
522522

523523

524524
def test_parse_response_no_json_method():
525525
response = "plain text"
526-
assert _helpers._parse_response(response) == "plain text"
526+
assert _helpers._parse_response(response) is None
527527

528528

529529
def test_parse_response_none():

0 commit comments

Comments
 (0)