Convert debug logs to trace logs (#500)
* Convert debug logs to trace logs * Update environment variables docs * Update logging test
This commit is contained in:
parent
717b34139b
commit
07586f97e8
@ -11,16 +11,14 @@ There are two ways to set `trust_env` to disable environment variables:
|
||||
Here is a list of environment variables that HTTPX recognizes
|
||||
and what function they serve:
|
||||
|
||||
`HTTPX_DEBUG`
|
||||
-----------
|
||||
`HTTPX_LOG_LEVEL`
|
||||
-----------------
|
||||
|
||||
Valid values: `1`, `true`
|
||||
Valid values: `debug`, `trace` (case-insensitive)
|
||||
|
||||
If this environment variable is set to a valid value then low-level
|
||||
details about the execution of HTTP requests will be logged to `stderr`.
|
||||
If set to `trace`, then low-level details about the execution of HTTP requests will be logged to `stderr`. This can help you debug issues and see what's exactly being sent over the wire and to which location.
|
||||
|
||||
This can help you debug issues and see what's exactly being sent
|
||||
over the wire and to which location.
|
||||
The `debug` log level is currently ignored, but is planned to issue high-level logs of HTTP requests.
|
||||
|
||||
Example:
|
||||
|
||||
@ -33,7 +31,7 @@ with httpx.Client() as client:
|
||||
```
|
||||
|
||||
```console
|
||||
user@host:~$ HTTPX_DEBUG=1 python test_script.py
|
||||
user@host:~$ HTTPX_LOG_LEVEL=trace python test_script.py
|
||||
20:54:17.585 - httpx.dispatch.connection_pool - acquire_connection origin=Origin(scheme='https' host='www.google.com' port=443)
|
||||
20:54:17.585 - httpx.dispatch.connection_pool - new_connection connection=HTTPConnection(origin=Origin(scheme='https' host='www.google.com' port=443))
|
||||
20:54:17.590 - httpx.dispatch.connection - start_connect host='www.google.com' port=443 timeout=TimeoutConfig(timeout=5.0)
|
||||
|
||||
@ -94,7 +94,7 @@ class SSLConfig:
|
||||
) -> ssl.SSLContext:
|
||||
http_versions = HTTPVersionConfig() if http_versions is None else http_versions
|
||||
|
||||
logger.debug(
|
||||
logger.trace(
|
||||
f"load_ssl_context "
|
||||
f"verify={self.verify!r} "
|
||||
f"cert={self.cert!r} "
|
||||
@ -163,10 +163,10 @@ class SSLConfig:
|
||||
pass
|
||||
|
||||
if ca_bundle_path.is_file():
|
||||
logger.debug(f"load_verify_locations cafile={ca_bundle_path!s}")
|
||||
logger.trace(f"load_verify_locations cafile={ca_bundle_path!s}")
|
||||
context.load_verify_locations(cafile=str(ca_bundle_path))
|
||||
elif ca_bundle_path.is_dir():
|
||||
logger.debug(f"load_verify_locations capath={ca_bundle_path!s}")
|
||||
logger.trace(f"load_verify_locations capath={ca_bundle_path!s}")
|
||||
context.load_verify_locations(capath=str(ca_bundle_path))
|
||||
|
||||
self._load_client_certs(context)
|
||||
|
||||
@ -84,10 +84,10 @@ class HTTPConnection(AsyncDispatcher):
|
||||
else:
|
||||
on_release = functools.partial(self.release_func, self)
|
||||
|
||||
logger.debug(f"start_connect host={host!r} port={port!r} timeout={timeout!r}")
|
||||
logger.trace(f"start_connect host={host!r} port={port!r} timeout={timeout!r}")
|
||||
stream = await self.backend.open_tcp_stream(host, port, ssl_context, timeout)
|
||||
http_version = stream.get_http_version()
|
||||
logger.debug(f"connected http_version={http_version!r}")
|
||||
logger.trace(f"connected http_version={http_version!r}")
|
||||
|
||||
if http_version == "HTTP/2":
|
||||
self.h2_connection = HTTP2Connection(
|
||||
@ -109,7 +109,7 @@ class HTTPConnection(AsyncDispatcher):
|
||||
)
|
||||
|
||||
async def close(self) -> None:
|
||||
logger.debug("close_connection")
|
||||
logger.trace("close_connection")
|
||||
if self.h2_connection is not None:
|
||||
await self.h2_connection.close()
|
||||
elif self.h11_connection is not None:
|
||||
|
||||
@ -128,7 +128,7 @@ class ConnectionPool(AsyncDispatcher):
|
||||
return response
|
||||
|
||||
async def acquire_connection(self, origin: Origin) -> HTTPConnection:
|
||||
logger.debug(f"acquire_connection origin={origin!r}")
|
||||
logger.trace(f"acquire_connection origin={origin!r}")
|
||||
connection = self.pop_connection(origin)
|
||||
|
||||
if connection is None:
|
||||
@ -143,16 +143,16 @@ class ConnectionPool(AsyncDispatcher):
|
||||
release_func=self.release_connection,
|
||||
trust_env=self.trust_env,
|
||||
)
|
||||
logger.debug(f"new_connection connection={connection!r}")
|
||||
logger.trace(f"new_connection connection={connection!r}")
|
||||
else:
|
||||
logger.debug(f"reuse_connection connection={connection!r}")
|
||||
logger.trace(f"reuse_connection connection={connection!r}")
|
||||
|
||||
self.active_connections.add(connection)
|
||||
|
||||
return connection
|
||||
|
||||
async def release_connection(self, connection: HTTPConnection) -> None:
|
||||
logger.debug(f"release_connection connection={connection!r}")
|
||||
logger.trace(f"release_connection connection={connection!r}")
|
||||
if connection.is_closed:
|
||||
self.active_connections.remove(connection)
|
||||
self.max_connections.release()
|
||||
|
||||
@ -65,7 +65,7 @@ class HTTP11Connection:
|
||||
async def close(self) -> None:
|
||||
event = h11.ConnectionClosed()
|
||||
try:
|
||||
logger.debug(f"send_event event={event!r}")
|
||||
logger.trace(f"send_event event={event!r}")
|
||||
self.h11_state.send(event)
|
||||
except h11.LocalProtocolError: # pragma: no cover
|
||||
# Premature client disconnect
|
||||
@ -78,7 +78,7 @@ class HTTP11Connection:
|
||||
"""
|
||||
Send the request method, URL, and headers to the network.
|
||||
"""
|
||||
logger.debug(
|
||||
logger.trace(
|
||||
f"send_headers method={request.method!r} "
|
||||
f"target={request.url.full_path!r} "
|
||||
f"headers={request.headers!r}"
|
||||
@ -99,7 +99,7 @@ class HTTP11Connection:
|
||||
try:
|
||||
# Send the request body.
|
||||
async for chunk in data:
|
||||
logger.debug(f"send_data data=Data(<{len(chunk)} bytes>)")
|
||||
logger.trace(f"send_data data=Data(<{len(chunk)} bytes>)")
|
||||
event = h11.Data(data=chunk)
|
||||
await self._send_event(event, timeout)
|
||||
|
||||
@ -164,9 +164,9 @@ class HTTP11Connection:
|
||||
event = self.h11_state.next_event()
|
||||
|
||||
if isinstance(event, h11.Data):
|
||||
logger.debug(f"receive_event event=Data(<{len(event.data)} bytes>)")
|
||||
logger.trace(f"receive_event event=Data(<{len(event.data)} bytes>)")
|
||||
else:
|
||||
logger.debug(f"receive_event event={event!r}")
|
||||
logger.trace(f"receive_event event={event!r}")
|
||||
|
||||
if event is h11.NEED_DATA:
|
||||
try:
|
||||
@ -182,7 +182,7 @@ class HTTP11Connection:
|
||||
return event
|
||||
|
||||
async def response_closed(self) -> None:
|
||||
logger.debug(
|
||||
logger.trace(
|
||||
f"response_closed "
|
||||
f"our_state={self.h11_state.our_state!r} "
|
||||
f"their_state={self.h11_state.their_state}"
|
||||
|
||||
@ -104,7 +104,7 @@ class HTTP2Connection:
|
||||
(b":path", request.url.full_path.encode("ascii")),
|
||||
] + [(k, v) for k, v in request.headers.raw if k != b"host"]
|
||||
|
||||
logger.debug(
|
||||
logger.trace(
|
||||
f"send_headers "
|
||||
f"stream_id={stream_id} "
|
||||
f"method={request.method!r} "
|
||||
@ -156,7 +156,7 @@ class HTTP2Connection:
|
||||
await self.stream.write(data_to_send, timeout)
|
||||
|
||||
async def end_stream(self, stream_id: int, timeout: TimeoutConfig = None) -> None:
|
||||
logger.debug(f"end_stream stream_id={stream_id}")
|
||||
logger.trace(f"end_stream stream_id={stream_id}")
|
||||
self.h2_state.end_stream(stream_id)
|
||||
data_to_send = self.h2_state.data_to_send()
|
||||
await self.stream.write(data_to_send, timeout)
|
||||
@ -207,7 +207,7 @@ class HTTP2Connection:
|
||||
events = self.h2_state.receive_data(data)
|
||||
for event in events:
|
||||
event_stream_id = getattr(event, "stream_id", 0)
|
||||
logger.debug(
|
||||
logger.trace(
|
||||
f"receive_event stream_id={event_stream_id} event={event!r}"
|
||||
)
|
||||
|
||||
|
||||
@ -86,12 +86,12 @@ class HTTPProxy(ConnectionPool):
|
||||
|
||||
async def acquire_connection(self, origin: Origin) -> HTTPConnection:
|
||||
if self.should_forward_origin(origin):
|
||||
logger.debug(
|
||||
logger.trace(
|
||||
f"forward_connection proxy_url={self.proxy_url!r} origin={origin!r}"
|
||||
)
|
||||
return await super().acquire_connection(self.proxy_url.origin)
|
||||
else:
|
||||
logger.debug(
|
||||
logger.trace(
|
||||
f"tunnel_connection proxy_url={self.proxy_url!r} origin={origin!r}"
|
||||
)
|
||||
return await self.tunnel_connection(origin)
|
||||
@ -143,7 +143,7 @@ class HTTPProxy(ConnectionPool):
|
||||
|
||||
# See if our tunnel has been opened successfully
|
||||
proxy_response = await connection.send(proxy_request)
|
||||
logger.debug(
|
||||
logger.trace(
|
||||
f"tunnel_response "
|
||||
f"proxy_url={self.proxy_url!r} "
|
||||
f"origin={origin!r} "
|
||||
@ -187,7 +187,7 @@ class HTTPProxy(ConnectionPool):
|
||||
ssl_context = await connection.get_ssl_context(ssl_config)
|
||||
assert ssl_context is not None
|
||||
|
||||
logger.debug(
|
||||
logger.trace(
|
||||
f"tunnel_start_tls "
|
||||
f"proxy_url={self.proxy_url!r} "
|
||||
f"origin={origin!r}"
|
||||
@ -196,7 +196,7 @@ class HTTPProxy(ConnectionPool):
|
||||
hostname=origin.host, ssl_context=ssl_context, timeout=timeout
|
||||
)
|
||||
http_version = stream.get_http_version()
|
||||
logger.debug(
|
||||
logger.trace(
|
||||
f"tunnel_tls_complete "
|
||||
f"proxy_url={self.proxy_url!r} "
|
||||
f"origin={origin!r} "
|
||||
|
||||
@ -176,20 +176,29 @@ def obfuscate_sensitive_headers(
|
||||
|
||||
|
||||
_LOGGER_INITIALIZED = False
|
||||
TRACE_LOG_LEVEL = 5
|
||||
|
||||
|
||||
def get_logger(name: str) -> logging.Logger:
|
||||
"""Gets a `logging.Logger` instance and optionally
|
||||
sets up debug logging if the user requests it via
|
||||
the `HTTPX_DEBUG=1` environment variable.
|
||||
class Logger(logging.Logger):
|
||||
# Stub for type checkers.
|
||||
def trace(self, message: str, *args: typing.Any, **kwargs: typing.Any) -> None:
|
||||
...
|
||||
|
||||
|
||||
def get_logger(name: str) -> Logger:
|
||||
"""
|
||||
Get a `logging.Logger` instance, and optionally
|
||||
set up debug logging based on the HTTPX_LOG_LEVEL environment variable.
|
||||
"""
|
||||
global _LOGGER_INITIALIZED
|
||||
|
||||
if not _LOGGER_INITIALIZED:
|
||||
_LOGGER_INITIALIZED = True
|
||||
if os.environ.get("HTTPX_DEBUG", "").lower() in ("1", "true"):
|
||||
|
||||
log_level = os.environ.get("HTTPX_LOG_LEVEL", "").upper()
|
||||
if log_level in ("DEBUG", "TRACE"):
|
||||
logger = logging.getLogger("httpx")
|
||||
logger.setLevel(logging.DEBUG)
|
||||
logger.setLevel(logging.DEBUG if log_level == "DEBUG" else TRACE_LOG_LEVEL)
|
||||
handler = logging.StreamHandler(sys.stderr)
|
||||
handler.setFormatter(
|
||||
logging.Formatter(
|
||||
@ -199,7 +208,14 @@ def get_logger(name: str) -> logging.Logger:
|
||||
)
|
||||
logger.addHandler(handler)
|
||||
|
||||
return logging.getLogger(name)
|
||||
logger = logging.getLogger(name)
|
||||
|
||||
def trace(message: str, *args: typing.Any, **kwargs: typing.Any) -> None:
|
||||
logger.log(TRACE_LOG_LEVEL, message, *args, **kwargs)
|
||||
|
||||
logger.trace = trace # type: ignore
|
||||
|
||||
return typing.cast(Logger, logger)
|
||||
|
||||
|
||||
def kv_format(**kwargs: typing.Any) -> str:
|
||||
@ -340,7 +356,7 @@ def asgi_message_with_placeholders(message: dict) -> dict:
|
||||
|
||||
|
||||
class MessageLoggerASGIMiddleware:
|
||||
def __init__(self, app: typing.Callable, logger: logging.Logger) -> None:
|
||||
def __init__(self, app: typing.Callable, logger: Logger) -> None:
|
||||
self.app = app
|
||||
self.logger = logger
|
||||
|
||||
@ -350,12 +366,12 @@ class MessageLoggerASGIMiddleware:
|
||||
async def inner_receive() -> dict:
|
||||
message = await receive()
|
||||
logged_message = asgi_message_with_placeholders(message)
|
||||
self.logger.debug(f"sent {kv_format(**logged_message)}")
|
||||
self.logger.trace(f"sent {kv_format(**logged_message)}")
|
||||
return message
|
||||
|
||||
async def inner_send(message: dict) -> None:
|
||||
logged_message = asgi_message_with_placeholders(message)
|
||||
self.logger.debug(f"received {kv_format(**logged_message)}")
|
||||
self.logger.trace(f"received {kv_format(**logged_message)}")
|
||||
await send(message)
|
||||
|
||||
logged_scope = dict(scope)
|
||||
@ -363,12 +379,12 @@ class MessageLoggerASGIMiddleware:
|
||||
logged_scope["headers"] = list(
|
||||
obfuscate_sensitive_headers(scope["headers"])
|
||||
)
|
||||
self.logger.debug(f"started {kv_format(**logged_scope)}")
|
||||
self.logger.trace(f"started {kv_format(**logged_scope)}")
|
||||
|
||||
try:
|
||||
await self.app(scope, inner_receive, inner_send)
|
||||
except BaseException as exc:
|
||||
self.logger.debug("raised_exception")
|
||||
self.logger.trace("raised_exception")
|
||||
raise exc from None
|
||||
else:
|
||||
self.logger.debug("completed")
|
||||
self.logger.trace("completed")
|
||||
|
||||
@ -103,9 +103,9 @@ def test_parse_header_links(value, expected):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("httpx_debug", ["0", "1", "True", "False"])
|
||||
async def test_httpx_debug_enabled_stderr_logging(server, capsys, httpx_debug):
|
||||
os.environ["HTTPX_DEBUG"] = httpx_debug
|
||||
@pytest.mark.parametrize("httpx_log_level", ["trace", "debug"])
|
||||
async def test_httpx_log_level_enabled_stderr_logging(server, capsys, httpx_log_level):
|
||||
os.environ["HTTPX_LOG_LEVEL"] = httpx_log_level
|
||||
|
||||
# Force a reload on the logging handlers
|
||||
utils._LOGGER_INITIALIZED = False
|
||||
@ -114,7 +114,7 @@ async def test_httpx_debug_enabled_stderr_logging(server, capsys, httpx_debug):
|
||||
async with httpx.AsyncClient() as client:
|
||||
await client.get(server.url)
|
||||
|
||||
if httpx_debug in ("1", "True"):
|
||||
if httpx_log_level == "trace":
|
||||
assert "httpx.dispatch.connection_pool" in capsys.readouterr().err
|
||||
else:
|
||||
assert "httpx.dispatch.connection_pool" not in capsys.readouterr().err
|
||||
|
||||
Loading…
Reference in New Issue
Block a user