diff --git a/README.md b/README.md index 1b446c40..37ebdda3 100644 --- a/README.md +++ b/README.md @@ -564,6 +564,24 @@ options = StreamOptions( client.stream(fql('Product.all().eventSource()'), options) ``` +## Logging + +Logging is handled using Python's standard `logging` package under the `fauna` namespace. Logs include the HTTP request with body (excluding the `Authorization` header) and the full HTTP response. + +To enable logging: +```python +import logging +from fauna.client import Client +from fauna import fql + +logging.basicConfig( + level=logging.DEBUG +) +client = Client() +client.query(fql('42')) +``` +For configuration options or to set specific log levels, see Python's [Logging HOWTO](https://docs.python.org/3/howto/logging.html). + ## Setup ```bash diff --git a/fauna/client/client.py b/fauna/client/client.py index 8fc5024e..ffb0babe 100644 --- a/fauna/client/client.py +++ b/fauna/client/client.py @@ -1,3 +1,4 @@ +import logging from dataclasses import dataclass from datetime import timedelta from typing import Any, Dict, Iterator, Mapping, Optional, Union, List @@ -13,6 +14,8 @@ from fauna.http.http_client import HTTPClient from fauna.query import EventSource, Query, Page, fql +logger = logging.getLogger("fauna") + DefaultHttpConnectTimeout = timedelta(seconds=5) DefaultHttpReadTimeout: Optional[timedelta] = None DefaultHttpWriteTimeout = timedelta(seconds=5) @@ -216,7 +219,7 @@ def __init__( max_keepalive_connections=DefaultMaxIdleConnections, keepalive_expiry=idle_timeout_s, ), - )) + ), logger) fauna.global_http_client = c self._session = fauna.global_http_client diff --git a/fauna/http/httpx_client.py b/fauna/http/httpx_client.py index c317d7fa..0961054c 100644 --- a/fauna/http/httpx_client.py +++ b/fauna/http/httpx_client.py @@ -1,4 +1,5 @@ import json +import logging from contextlib import contextmanager from json import JSONDecodeError from typing import Mapping, Any, Optional, Iterator @@ -50,9 +51,12 @@ def close(self) -> None: class HTTPXClient(HTTPClient): - def __init__(self, client: httpx.Client): + def __init__(self, + client: httpx.Client, + logger: logging.Logger = logging.getLogger("fauna")): super(HTTPXClient, self).__init__() self._c = client + self._logger = logger def request( self, @@ -69,14 +73,29 @@ def request( json=data, headers=headers, ) + + if self._logger.isEnabledFor(logging.DEBUG): + headers_to_log = request.headers.copy() + headers_to_log.pop("Authorization") + self._logger.debug( + f"query.request method={request.method} url={request.url} headers={headers_to_log} data={data}" + ) + except httpx.InvalidURL as e: raise ClientError("Invalid URL Format") from e try: - return HTTPXResponse(self._c.send( + response = self._c.send( request, stream=False, - )) + ) + + if self._logger.isEnabledFor(logging.DEBUG): + self._logger.debug( + f"query.response status_code={response.status_code} headers={response.headers} data={response.text}" + ) + + return HTTPXResponse(response) except (httpx.HTTPError, httpx.InvalidURL) as e: raise NetworkError("Exception re-raised from HTTP request") from e @@ -87,14 +106,37 @@ def stream( headers: Mapping[str, str], data: Mapping[str, Any], ) -> Iterator[Any]: - with self._c.stream( - "POST", url=url, headers=headers, json=data) as response: + request = self._c.build_request( + method="POST", + url=url, + headers=headers, + json=data, + ) + + if self._logger.isEnabledFor(logging.DEBUG): + headers_to_log = request.headers.copy() + headers_to_log.pop("Authorization") + self._logger.debug( + f"stream.request method={request.method} url={request.url} headers={headers_to_log} data={data}" + ) + + response = self._c.send( + request=request, + stream=True, + ) + + try: yield self._transform(response) + finally: + response.close() def _transform(self, response): try: for line in response.iter_lines(): - yield json.loads(line) + loaded = json.loads(line) + if self._logger.isEnabledFor(logging.DEBUG): + self._logger.debug(f"stream.data data={loaded}") + yield loaded except httpx.ReadTimeout as e: raise NetworkError("Stream timeout") from e except (httpx.HTTPError, httpx.InvalidURL) as e: diff --git a/tests/integration/test_stream.py b/tests/integration/test_stream.py index 44e355b2..c3158f28 100644 --- a/tests/integration/test_stream.py +++ b/tests/integration/test_stream.py @@ -1,12 +1,11 @@ import threading -import time import httpx import pytest from fauna import fql from fauna.client import Client, StreamOptions -from fauna.errors import ClientError, NetworkError, RetryableFaunaException, QueryRuntimeError +from fauna.errors import ClientError, RetryableFaunaException, QueryRuntimeError, NetworkError from fauna.http.httpx_client import HTTPXClient @@ -107,11 +106,16 @@ def test_max_retries(scoped_secret): count = [0] - def stream_func(*args, **kwargs): + old_send = httpx_client.send + + def send_func(*args, **kwargs): + if not kwargs['stream']: + return old_send(*args, **kwargs) + count[0] += 1 raise NetworkError('foo') - httpx_client.stream = stream_func + httpx_client.send = send_func count[0] = 0 with pytest.raises(RetryableFaunaException):