Skip to content

Commit 1b70258

Browse files
Add logging to pdfRest clients
- Introduced structured logging for request and response details, including retry attempts, delays, payloads, and errors. - Integrated sanitized logging for sensitive header information. - Improved debugging of request execution and retry behavior for better traceability. - Updated exception handling to include detailed log outputs. Assisted-by: Codex
1 parent 452cc26 commit 1b70258

1 file changed

Lines changed: 181 additions & 18 deletions

File tree

src/pdfrest/client.py

Lines changed: 181 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import asyncio
66
import importlib.metadata
77
import json
8+
import logging
89
import os
910
import random
1011
import time
@@ -98,6 +99,48 @@
9899
Query = Mapping[str, QueryParamValue]
99100
Body = Mapping[str, Any]
100101

102+
PDFREST_LOGGER = logging.getLogger("pdfrest")
103+
PDFREST_LOGGER.addHandler(logging.NullHandler())
104+
LOGGER = logging.getLogger("pdfrest.client")
105+
_PDFREST_HANDLER_IDS: set[int] = set()
106+
107+
108+
def _ensure_stream_handler(
109+
logger: logging.Logger, formatter: logging.Formatter
110+
) -> None:
111+
for handler in logger.handlers:
112+
if id(handler) in _PDFREST_HANDLER_IDS:
113+
return
114+
handler = logging.StreamHandler()
115+
handler.setFormatter(formatter)
116+
logger.addHandler(handler)
117+
_PDFREST_HANDLER_IDS.add(id(handler))
118+
119+
120+
def _configure_logging() -> None:
121+
level_name = os.getenv("PDFREST_LOG")
122+
if not level_name:
123+
return
124+
normalized = level_name.strip().lower()
125+
level_map = {"debug": logging.DEBUG, "info": logging.INFO}
126+
level = level_map.get(normalized)
127+
if level is None:
128+
return
129+
formatter = logging.Formatter("%(asctime)s %(levelname)s [%(name)s] %(message)s")
130+
pdfrest_logger = PDFREST_LOGGER
131+
pdfrest_logger.setLevel(level)
132+
_ensure_stream_handler(pdfrest_logger, formatter)
133+
pdfrest_logger.propagate = False
134+
135+
httpx_logger = logging.getLogger("httpx")
136+
httpx_logger.setLevel(level)
137+
_ensure_stream_handler(httpx_logger, formatter)
138+
httpx_logger.propagate = False
139+
140+
141+
_configure_logging()
142+
143+
101144
FileContent = IO[bytes] | bytes | str
102145
FileTuple2 = tuple[str | None, FileContent]
103146
FileTuple3 = tuple[str | None, FileContent, str | None]
@@ -369,6 +412,7 @@ def __init__(
369412
headers: AnyMapping | None = None,
370413
max_retries: int = DEFAULT_MAX_RETRIES,
371414
) -> None:
415+
self._logger = LOGGER
372416
if not isinstance(max_retries, int) or max_retries < 0:
373417
msg = "max_retries must be a non-negative integer."
374418
raise PdfRestConfigurationError(msg)
@@ -439,6 +483,42 @@ def _compute_backoff_delay(self, retry_number: int) -> float:
439483
delay = base_delay + jitter
440484
return delay if delay > 0 else 0.0
441485

486+
@staticmethod
487+
def _sanitize_headers(headers: Mapping[str, Any] | None) -> dict[str, Any]:
488+
if not headers:
489+
return {}
490+
sanitized: dict[str, Any] = {}
491+
for key, value in headers.items():
492+
if key.lower() == API_KEY_HEADER_NAME.lower():
493+
sanitized[key] = "******"
494+
else:
495+
sanitized[key] = value
496+
return sanitized
497+
498+
def _log_request(self, request: _RequestModel) -> None:
499+
if not self._logger.isEnabledFor(logging.DEBUG):
500+
return
501+
sanitized_headers = self._sanitize_headers(request.headers)
502+
self._logger.debug(
503+
"Request %s %s params=%s timeout=%s headers=%s",
504+
request.method,
505+
request.endpoint,
506+
request.params,
507+
request.timeout,
508+
sanitized_headers,
509+
)
510+
if request.method in {"POST", "PUT", "PATCH"} and request.json_body is not None:
511+
self._logger.debug(
512+
"Request payload %s %s: %s",
513+
request.method,
514+
request.endpoint,
515+
request.json_body,
516+
)
517+
518+
@staticmethod
519+
def _describe_request(request: _RequestModel) -> str:
520+
return f"{request.method} {request.endpoint}"
521+
442522
@staticmethod
443523
def _base_url_requires_api_key(url: URL) -> bool:
444524
host = url.host or ""
@@ -567,19 +647,46 @@ def _compose_json_body(
567647
return payload
568648

569649
def _handle_response(self, response: httpx.Response) -> Any:
650+
request = response.request
651+
request_label = (
652+
f"{getattr(request, 'method', 'UNKNOWN')} {getattr(request, 'url', '')}"
653+
if request is not None
654+
else "UNKNOWN"
655+
)
570656
if response.is_success:
657+
if self._logger.isEnabledFor(logging.DEBUG):
658+
self._logger.debug(
659+
"Response %s status=%s", request_label, response.status_code
660+
)
571661
return self._decode_json(response)
572662

573663
message, error_payload = self._extract_error_details(response)
574664

575665
if response.status_code == 401:
576666
auth_message = message or "Authentication with pdfRest failed."
667+
if self._logger.isEnabledFor(logging.DEBUG):
668+
self._logger.debug(
669+
"Authentication error response %s status=%s message=%s payload=%s",
670+
request_label,
671+
response.status_code,
672+
auth_message,
673+
error_payload,
674+
)
577675
raise PdfRestAuthenticationError(
578676
response.status_code,
579677
message=auth_message,
580678
response_content=error_payload,
581679
)
582680

681+
if self._logger.isEnabledFor(logging.DEBUG):
682+
self._logger.debug(
683+
"Error response %s status=%s message=%s payload=%s",
684+
request_label,
685+
response.status_code,
686+
message,
687+
error_payload,
688+
)
689+
583690
raise PdfRestApiError(
584691
response.status_code, message=message, response_content=error_payload
585692
)
@@ -646,16 +753,29 @@ def __enter__(self) -> _SyncApiClient:
646753
def __exit__(self, exc_type: Any, exc: Any, traceback: Any) -> None:
647754
self.close()
648755

649-
def _execute_with_retry(self, func: Callable[[], ReturnType]) -> ReturnType:
650-
for attempt in range(self._max_retries + 1):
756+
def _execute_with_retry(
757+
self, func: Callable[[], ReturnType], *, operation: str
758+
) -> ReturnType:
759+
total_attempts = self._max_retries + 1
760+
for attempt in range(total_attempts):
651761
try:
652762
return func()
653763
except PdfRestError as exc:
654-
if attempt == self._max_retries or not self._should_retry_exception(
655-
exc
656-
):
764+
self._logger.debug(
765+
"Exception during %s attempt %d/%d: %s",
766+
operation,
767+
attempt + 1,
768+
total_attempts,
769+
exc,
770+
)
771+
should_retry = (
772+
attempt < self._max_retries and self._should_retry_exception(exc)
773+
)
774+
if not should_retry:
775+
self._logger.debug("No retry for %s; raising exception.", operation)
657776
raise
658777
delay = self._compute_backoff_delay(attempt)
778+
self._logger.debug("Retrying %s after %.2f seconds.", operation, delay)
659779
if delay > 0:
660780
time.sleep(delay)
661781
msg = "Retry loop exited unexpectedly."
@@ -664,12 +784,14 @@ def _execute_with_retry(self, func: Callable[[], ReturnType]) -> ReturnType:
664784
def _send_request(self, request: _RequestModel) -> Any:
665785
http_client = self._client
666786
return self._execute_with_retry(
667-
lambda: self._perform_request(http_client, request)
787+
lambda: self._perform_request(http_client, request),
788+
operation=self._describe_request(request),
668789
)
669790

670791
def _perform_request(
671792
self, http_client: httpx.Client, request: _RequestModel
672793
) -> Any:
794+
self._log_request(request)
673795
try:
674796
response = http_client.request(
675797
method=request.method,
@@ -682,7 +804,13 @@ def _perform_request(
682804
data=request.data,
683805
)
684806
except httpx.HTTPError as exc:
685-
raise translate_httpx_error(exc) from exc
807+
translated = translate_httpx_error(exc)
808+
self._logger.debug(
809+
"HTTPX exception for %s: %s",
810+
self._describe_request(request),
811+
translated,
812+
)
813+
raise translated from exc
686814
try:
687815
payload = self._handle_response(response)
688816
except PdfRestApiError:
@@ -758,10 +886,12 @@ def download_file(
758886
timeout=timeout,
759887
)
760888
return self._execute_with_retry(
761-
lambda: self._download_with_retry(request_model)
889+
lambda: self._download_with_retry(request_model),
890+
operation=f"{request_model.method} {request_model.endpoint} (download)",
762891
)
763892

764893
def _download_with_retry(self, request: _RequestModel) -> httpx.Response:
894+
self._log_request(request)
765895
http_request = self._client.build_request(
766896
request.method,
767897
request.endpoint,
@@ -778,7 +908,13 @@ def _download_with_retry(self, request: _RequestModel) -> httpx.Response:
778908
try:
779909
response = self._client.send(http_request, stream=True)
780910
except httpx.HTTPError as exc:
781-
raise translate_httpx_error(exc) from exc
911+
translated = translate_httpx_error(exc)
912+
self._logger.debug(
913+
"HTTPX exception for %s: %s",
914+
self._describe_request(request),
915+
translated,
916+
)
917+
raise translated from exc
782918
if response.is_success:
783919
return response
784920
try:
@@ -852,17 +988,28 @@ async def __aexit__(self, exc_type: Any, exc: Any, traceback: Any) -> None:
852988
await self.aclose()
853989

854990
async def _execute_with_retry(
855-
self, func: Callable[[], Awaitable[ReturnType]]
991+
self, func: Callable[[], Awaitable[ReturnType]], *, operation: str
856992
) -> ReturnType:
857-
for attempt in range(self._max_retries + 1):
993+
total_attempts = self._max_retries + 1
994+
for attempt in range(total_attempts):
858995
try:
859996
return await func()
860997
except PdfRestError as exc:
861-
if attempt == self._max_retries or not self._should_retry_exception(
862-
exc
863-
):
998+
self._logger.debug(
999+
"Exception during %s attempt %d/%d: %s",
1000+
operation,
1001+
attempt + 1,
1002+
total_attempts,
1003+
exc,
1004+
)
1005+
should_retry = (
1006+
attempt < self._max_retries and self._should_retry_exception(exc)
1007+
)
1008+
if not should_retry:
1009+
self._logger.debug("No retry for %s; raising exception.", operation)
8641010
raise
8651011
delay = self._compute_backoff_delay(attempt)
1012+
self._logger.debug("Retrying %s after %.2f seconds.", operation, delay)
8661013
if delay > 0:
8671014
await asyncio.sleep(delay)
8681015
msg = "Retry loop exited unexpectedly."
@@ -871,12 +1018,14 @@ async def _execute_with_retry(
8711018
async def _send_request(self, request: _RequestModel) -> Any:
8721019
http_client = self._client
8731020
return await self._execute_with_retry(
874-
lambda: self._perform_request(http_client, request)
1021+
lambda: self._perform_request(http_client, request),
1022+
operation=self._describe_request(request),
8751023
)
8761024

8771025
async def _perform_request(
8781026
self, http_client: httpx.AsyncClient, request: _RequestModel
8791027
) -> Any:
1028+
self._log_request(request)
8801029
try:
8811030
response = await http_client.request(
8821031
method=request.method,
@@ -889,7 +1038,13 @@ async def _perform_request(
8891038
data=request.data,
8901039
)
8911040
except httpx.HTTPError as exc:
892-
raise translate_httpx_error(exc) from exc
1041+
translated = translate_httpx_error(exc)
1042+
self._logger.debug(
1043+
"HTTPX exception for %s: %s",
1044+
self._describe_request(request),
1045+
translated,
1046+
)
1047+
raise translated from exc
8931048
try:
8941049
payload = self._handle_response(response)
8951050
except PdfRestApiError:
@@ -973,10 +1128,12 @@ async def download_file(
9731128
timeout=timeout,
9741129
)
9751130
return await self._execute_with_retry(
976-
lambda: self._download_with_retry(request_model)
1131+
lambda: self._download_with_retry(request_model),
1132+
operation=f"{request_model.method} {request_model.endpoint} (download)",
9771133
)
9781134

9791135
async def _download_with_retry(self, request: _RequestModel) -> httpx.Response:
1136+
self._log_request(request)
9801137
http_request = self._client.build_request(
9811138
request.method,
9821139
request.endpoint,
@@ -993,7 +1150,13 @@ async def _download_with_retry(self, request: _RequestModel) -> httpx.Response:
9931150
try:
9941151
response = await self._client.send(http_request, stream=True)
9951152
except httpx.HTTPError as exc:
996-
raise translate_httpx_error(exc) from exc
1153+
translated = translate_httpx_error(exc)
1154+
self._logger.debug(
1155+
"HTTPX exception for %s: %s",
1156+
self._describe_request(request),
1157+
translated,
1158+
)
1159+
raise translated from exc
9971160
if response.is_success:
9981161
return response
9991162
try:

0 commit comments

Comments
 (0)