Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion httpcore/_async/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,8 @@ def can_handle_request(self, origin: Origin) -> bool:

async def aclose(self) -> None:
if self._connection is not None:
await self._connection.aclose()
async with Trace("connection.close", None, {}):
await self._connection.aclose()

def is_available(self) -> bool:
if self._connection is None:
Expand Down
3 changes: 2 additions & 1 deletion httpcore/_sync/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,8 @@ def can_handle_request(self, origin: Origin) -> bool:

def close(self) -> None:
if self._connection is not None:
self._connection.close()
with Trace("connection.close", None, {}):
self._connection.close()

def is_available(self) -> bool:
if self._connection is None:
Expand Down
52 changes: 43 additions & 9 deletions httpcore/_trace.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,42 @@
import logging
from types import TracebackType
from typing import Any, Dict, Optional, Type

from ._models import Request

logger = logging.getLogger("httpcore")


class Trace:
def __init__(
self, name: str, request: Request, kwargs: Optional[Dict[str, Any]] = None
self,
name: str,
request: Optional[Request] = None,
kwargs: Optional[Dict[str, Any]] = None,
) -> None:
self.name = name
self.trace = request.extensions.get("trace")
self.trace_extension = (
None if request is None else request.extensions.get("trace")
)
self.debug = logger.isEnabledFor(logging.DEBUG)
self.kwargs = kwargs or {}
self.return_value: Any = None
self.should_trace = self.debug or self.trace_extension is not None

def trace(self, name: str, info: Dict[str, Any]) -> None:
if self.trace_extension is not None:
self.trace_extension(name, info)

if self.debug:
if not info or "return_value" in info and info["return_value"] is None:
message = name
else:
args = " ".join([f"{key}={value!r}" for key, value in info.items()])
message = f"{name} {args}"
logger.debug(message)

def __enter__(self) -> "Trace":
if self.trace is not None:
if self.should_trace:
info = self.kwargs
self.trace(f"{self.name}.started", info)
return self
Expand All @@ -25,18 +47,30 @@ def __exit__(
exc_value: Optional[BaseException] = None,
traceback: Optional[TracebackType] = None,
) -> None:
if self.trace is not None:
if self.should_trace:
if exc_value is None:
info = {"return_value": self.return_value}
self.trace(f"{self.name}.complete", info)
else:
info = {"exception": exc_value}
self.trace(f"{self.name}.failed", info)

async def atrace(self, name: str, info: Dict[str, Any]) -> None:
if self.trace_extension is not None:
await self.trace_extension(name, info)

if self.debug:
if not info or "return_value" in info and info["return_value"] is None:
message = name
else:
args = " ".join([f"{key}={value!r}" for key, value in info.items()])
message = f"{name} {args}"
logger.debug(message)

async def __aenter__(self) -> "Trace":
if self.trace is not None:
if self.should_trace:
info = self.kwargs
await self.trace(f"{self.name}.started", info)
await self.atrace(f"{self.name}.started", info)
return self

async def __aexit__(
Expand All @@ -45,10 +79,10 @@ async def __aexit__(
exc_value: Optional[BaseException] = None,
traceback: Optional[TracebackType] = None,
) -> None:
if self.trace is not None:
if self.should_trace:
if exc_value is None:
info = {"return_value": self.return_value}
await self.trace(f"{self.name}.complete", info)
await self.atrace(f"{self.name}.complete", info)
else:
info = {"exception": exc_value}
await self.trace(f"{self.name}.failed", info)
await self.atrace(f"{self.name}.failed", info)
6 changes: 6 additions & 0 deletions httpcore/backends/mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ def start_tls(
def get_extra_info(self, info: str) -> typing.Any:
return MockSSLObject(http2=self._http2) if info == "ssl_object" else None

def __repr__(self) -> str:
return "<httpcore.MockStream>"


class MockBackend(NetworkBackend):
def __init__(self, buffer: typing.List[bytes], http2: bool = False) -> None:
Expand Down Expand Up @@ -98,6 +101,9 @@ async def start_tls(
def get_extra_info(self, info: str) -> typing.Any:
return MockSSLObject(http2=self._http2) if info == "ssl_object" else None

def __repr__(self) -> str:
return "<httpcore.AsyncMockStream>"


class AsyncMockBackend(AsyncNetworkBackend):
def __init__(self, buffer: typing.List[bytes], http2: bool = False) -> None:
Expand Down
69 changes: 69 additions & 0 deletions tests/_async/test_connection_pool.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import logging
from typing import List, Optional

import pytest
Expand Down Expand Up @@ -160,6 +161,74 @@ async def trace(name, kwargs):
]


@pytest.mark.anyio
async def test_debug_request(caplog):
"""
The 'trace' request extension allows for a callback function to inspect the
internal events that occur while sending a request.
"""
caplog.set_level(logging.DEBUG)

network_backend = AsyncMockBackend(
[
b"HTTP/1.1 200 OK\r\n",
b"Content-Type: plain/text\r\n",
b"Content-Length: 13\r\n",
b"\r\n",
b"Hello, world!",
]
)

async with AsyncConnectionPool(network_backend=network_backend) as pool:
await pool.request("GET", "http://example.com/")

assert caplog.record_tuples == [
(
"httpcore",
logging.DEBUG,
"connection.connect_tcp.started host='example.com' port=80 local_address=None timeout=None",
),
(
"httpcore",
logging.DEBUG,
"connection.connect_tcp.complete return_value=<httpcore.AsyncMockStream>",
),
(
"httpcore",
logging.DEBUG,
"http11.send_request_headers.started request=<Request [b'GET']>",
),
("httpcore", logging.DEBUG, "http11.send_request_headers.complete"),
(
"httpcore",
logging.DEBUG,
"http11.send_request_body.started request=<Request [b'GET']>",
),
("httpcore", logging.DEBUG, "http11.send_request_body.complete"),
(
"httpcore",
logging.DEBUG,
"http11.receive_response_headers.started request=<Request [b'GET']>",
),
(
"httpcore",
logging.DEBUG,
"http11.receive_response_headers.complete return_value="
"(b'HTTP/1.1', 200, b'OK', [(b'Content-Type', b'plain/text'), (b'Content-Length', b'13')])",
),
(
"httpcore",
logging.DEBUG,
"http11.receive_response_body.started request=<Request [b'GET']>",
),
("httpcore", logging.DEBUG, "http11.receive_response_body.complete"),
("httpcore", logging.DEBUG, "http11.response_closed.started"),
("httpcore", logging.DEBUG, "http11.response_closed.complete"),
("httpcore", logging.DEBUG, "connection.close.started"),
("httpcore", logging.DEBUG, "connection.close.complete"),
]


@pytest.mark.anyio
async def test_connection_pool_with_http_exception():
"""
Expand Down
69 changes: 69 additions & 0 deletions tests/_sync/test_connection_pool.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import logging
from typing import List, Optional

import pytest
Expand Down Expand Up @@ -161,6 +162,74 @@ def trace(name, kwargs):



def test_debug_request(caplog):
"""
The 'trace' request extension allows for a callback function to inspect the
internal events that occur while sending a request.
"""
caplog.set_level(logging.DEBUG)

network_backend = MockBackend(
[
b"HTTP/1.1 200 OK\r\n",
b"Content-Type: plain/text\r\n",
b"Content-Length: 13\r\n",
b"\r\n",
b"Hello, world!",
]
)

with ConnectionPool(network_backend=network_backend) as pool:
pool.request("GET", "http://example.com/")

assert caplog.record_tuples == [
(
"httpcore",
logging.DEBUG,
"connection.connect_tcp.started host='example.com' port=80 local_address=None timeout=None",
),
(
"httpcore",
logging.DEBUG,
"connection.connect_tcp.complete return_value=<httpcore.MockStream>",
),
(
"httpcore",
logging.DEBUG,
"http11.send_request_headers.started request=<Request [b'GET']>",
),
("httpcore", logging.DEBUG, "http11.send_request_headers.complete"),
(
"httpcore",
logging.DEBUG,
"http11.send_request_body.started request=<Request [b'GET']>",
),
("httpcore", logging.DEBUG, "http11.send_request_body.complete"),
(
"httpcore",
logging.DEBUG,
"http11.receive_response_headers.started request=<Request [b'GET']>",
),
(
"httpcore",
logging.DEBUG,
"http11.receive_response_headers.complete return_value="
"(b'HTTP/1.1', 200, b'OK', [(b'Content-Type', b'plain/text'), (b'Content-Length', b'13')])",
),
(
"httpcore",
logging.DEBUG,
"http11.receive_response_body.started request=<Request [b'GET']>",
),
("httpcore", logging.DEBUG, "http11.receive_response_body.complete"),
("httpcore", logging.DEBUG, "http11.response_closed.started"),
("httpcore", logging.DEBUG, "http11.response_closed.complete"),
("httpcore", logging.DEBUG, "connection.close.started"),
("httpcore", logging.DEBUG, "connection.close.complete"),
]



def test_connection_pool_with_http_exception():
"""
HTTP/1.1 requests that result in an exception during the connection should
Expand Down