http_util.py 1.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657
  1. import logging
  2. from typing import Any
  3. import httpx
  4. logger = logging.getLogger("app.http")
  5. _MAX_BODY_LOG_LENGTH = 2000
  6. _BODY_PREVIEW_LENGTH = 500
  7. def _truncate(text: str, max_len: int = _MAX_BODY_LOG_LENGTH) -> str:
  8. if len(text) <= max_len:
  9. return text
  10. return text[:max_len] + f"... (truncated, total {len(text)} chars)"
  11. def _body_summary(text: str) -> str:
  12. if not text:
  13. return ""
  14. if len(text) > _MAX_BODY_LOG_LENGTH:
  15. return f"<omitted, body too large: {len(text)} chars>"
  16. return _truncate(text, _BODY_PREVIEW_LENGTH)
  17. def _on_request(request: httpx.Request) -> None:
  18. body = request.content.decode("utf-8", errors="replace") if request.content else ""
  19. logger.info(
  20. "[HTTP >>> ] %s %s | body(%d): %s",
  21. request.method,
  22. request.url,
  23. len(body),
  24. _body_summary(body),
  25. )
  26. def _on_response(response: httpx.Response) -> None:
  27. response.read()
  28. body = response.text
  29. logger.info(
  30. "[HTTP <<< ] %s %s | body(%d): %s",
  31. response.status_code,
  32. response.url,
  33. len(body),
  34. _body_summary(body),
  35. )
  36. def logged_client(**kwargs: Any) -> httpx.Client:
  37. """Create an ``httpx.Client`` that logs every request body and response body."""
  38. hooks = kwargs.pop("event_hooks", {})
  39. req_hooks = list(hooks.get("request", []))
  40. resp_hooks = list(hooks.get("response", []))
  41. req_hooks.append(_on_request)
  42. resp_hooks.append(_on_response)
  43. kwargs["event_hooks"] = {"request": req_hooks, "response": resp_hooks}
  44. return httpx.Client(**kwargs)