Skip to content

Commit f1157db

Browse files
authored
Use standard logging style (#2547)
* Use standard logging style * Add docs for logging * Drop out-of-date HTTPX_LOG_LEVEL variable docs
1 parent 85c5898 commit f1157db

File tree

8 files changed

+163
-186
lines changed

8 files changed

+163
-186
lines changed

docs/environment_variables.md

Lines changed: 0 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -8,70 +8,6 @@ Environment variables are used by default. To ignore environment variables, `tru
88

99
Here is a list of environment variables that HTTPX recognizes and what function they serve:
1010

11-
## `HTTPX_LOG_LEVEL`
12-
13-
Valid values: `debug`, `trace` (case-insensitive)
14-
15-
If set to `debug`, then HTTP requests will be logged to `stderr`. This is useful for general purpose reporting of network activity.
16-
17-
If set to `trace`, then low-level details about the execution of HTTP requests will be logged to `stderr`, in addition to debug log lines. This can help you debug issues and see what's exactly being sent over the wire and to which location.
18-
19-
Example:
20-
21-
```python
22-
# test_script.py
23-
import httpx
24-
25-
with httpx.Client() as client:
26-
r = client.get("https://google.com")
27-
```
28-
29-
Debug output:
30-
31-
```console
32-
$ HTTPX_LOG_LEVEL=debug python test_script.py
33-
DEBUG [2019-11-06 19:11:24] httpx._client - HTTP Request: GET https://google.com "HTTP/1.1 301 Moved Permanently"
34-
DEBUG [2019-11-06 19:11:24] httpx._client - HTTP Request: GET https://www.google.com/ "HTTP/1.1 200 OK"
35-
```
36-
37-
Trace output:
38-
39-
```console
40-
$ HTTPX_LOG_LEVEL=trace python test_script.py
41-
TRACE [2019-11-06 19:18:56] httpx._dispatch.connection_pool - acquire_connection origin=Origin(scheme='https' host='google.com' port=443)
42-
TRACE [2019-11-06 19:18:56] httpx._dispatch.connection_pool - new_connection connection=HTTPConnection(origin=Origin(scheme='https' host='google.com' port=443))
43-
TRACE [2019-11-06 19:18:56] httpx._config - load_ssl_context verify=True cert=None trust_env=True http_versions=HTTPVersionConfig(['HTTP/1.1', 'HTTP/2'])
44-
TRACE [2019-11-06 19:18:56] httpx._config - load_verify_locations cafile=/Users/florimond/Developer/python-projects/httpx/venv/lib/python3.8/site-packages/certifi/cacert.pem
45-
TRACE [2019-11-06 19:18:56] httpx._dispatch.connection - start_connect host='google.com' port=443 timeout=Timeout(timeout=5.0)
46-
TRACE [2019-11-06 19:18:56] httpx._dispatch.connection - connected http_version='HTTP/2'
47-
TRACE [2019-11-06 19:18:56] httpx._dispatch.http2 - send_headers stream_id=1 method='GET' target='/' headers=[(b':method', b'GET'), (b':authority', b'google.com'), (b':scheme', b'https'), (b':path', b'/'), (b'user-agent', b'python-httpx/0.7.6'), (b'accept', b'*/*'), (b'accept-encoding', b'gzip, deflate, br'), (b'connection', b'keep-alive')]
48-
TRACE [2019-11-06 19:18:56] httpx._dispatch.http2 - end_stream stream_id=1
49-
TRACE [2019-11-06 19:18:56] httpx._dispatch.http2 - receive_event stream_id=0 event=<RemoteSettingsChanged changed_settings:{ChangedSetting(setting=SettingCodes.MAX_CONCURRENT_STREAMS, original_value=None, new_value=100), ChangedSetting(setting=SettingCodes.INITIAL_WINDOW_SIZE, original_value=65535, new_value=1048576), ChangedSetting(setting=SettingCodes.MAX_HEADER_LIST_SIZE, original_value=None, new_value=16384)}>
50-
TRACE [2019-11-06 19:18:56] httpx._dispatch.http2 - receive_event stream_id=0 event=<WindowUpdated stream_id:0, delta:983041>
51-
TRACE [2019-11-06 19:18:56] httpx._dispatch.http2 - receive_event stream_id=0 event=<SettingsAcknowledged changed_settings:{}>
52-
TRACE [2019-11-06 19:18:56] httpx._dispatch.http2 - receive_event stream_id=1 event=<ResponseReceived stream_id:1, headers:[(b':status', b'301'), (b'location', b'https://www.google.com/'), (b'content-type', b'text/html; charset=UTF-8'), (b'date', b'Wed, 06 Nov 2019 18:18:56 GMT'), (b'expires', b'Fri, 06 Dec 2019 18:18:56 GMT'), (b'cache-control', b'public, max-age=2592000'), (b'server', b'gws'), (b'content-length', b'220'), (b'x-xss-protection', b'0'), (b'x-frame-options', b'SAMEORIGIN'), (b'alt-svc', b'quic=":443"; ma=2592000; v="46,43",h3-Q050=":443"; ma=2592000,h3-Q049=":443"; ma=2592000,h3-Q048=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000')]>
53-
DEBUG [2019-11-06 19:18:56] httpx._client - HTTP Request: GET https://google.com "HTTP/2 301 Moved Permanently"
54-
TRACE [2019-11-06 19:18:56] httpx._dispatch.connection_pool - acquire_connection origin=Origin(scheme='https' host='www.google.com' port=443)
55-
TRACE [2019-11-06 19:18:56] httpx._dispatch.connection_pool - new_connection connection=HTTPConnection(origin=Origin(scheme='https' host='www.google.com' port=443))
56-
TRACE [2019-11-06 19:18:56] httpx._config - load_ssl_context verify=True cert=None trust_env=True http_versions=HTTPVersionConfig(['HTTP/1.1', 'HTTP/2'])
57-
TRACE [2019-11-06 19:18:56] httpx._config - load_verify_locations cafile=/Users/florimond/Developer/python-projects/httpx/venv/lib/python3.8/site-packages/certifi/cacert.pem
58-
TRACE [2019-11-06 19:18:56] httpx._dispatch.connection - start_connect host='www.google.com' port=443 timeout=Timeout(timeout=5.0)
59-
TRACE [2019-11-06 19:18:56] httpx._dispatch.connection - connected http_version='HTTP/2'
60-
TRACE [2019-11-06 19:18:56] httpx._dispatch.http2 - send_headers stream_id=1 method='GET' target='/' headers=[(b':method', b'GET'), (b':authority', b'www.google.com'), (b':scheme', b'https'), (b':path', b'/'), (b'user-agent', b'python-httpx/0.7.6'), (b'accept', b'*/*'), (b'accept-encoding', b'gzip, deflate, br'), (b'connection', b'keep-alive')]
61-
TRACE [2019-11-06 19:18:56] httpx._dispatch.http2 - end_stream stream_id=1
62-
TRACE [2019-11-06 19:18:56] httpx._dispatch.http2 - receive_event stream_id=0 event=<RemoteSettingsChanged changed_settings:{ChangedSetting(setting=SettingCodes.MAX_CONCURRENT_STREAMS, original_value=None, new_value=100), ChangedSetting(setting=SettingCodes.INITIAL_WINDOW_SIZE, original_value=65535, new_value=1048576), ChangedSetting(setting=SettingCodes.MAX_HEADER_LIST_SIZE, original_value=None, new_value=16384)}>
63-
TRACE [2019-11-06 19:18:56] httpx._dispatch.http2 - receive_event stream_id=0 event=<WindowUpdated stream_id:0, delta:983041>
64-
TRACE [2019-11-06 19:18:56] httpx._dispatch.http2 - receive_event stream_id=0 event=<SettingsAcknowledged changed_settings:{}>
65-
TRACE [2019-11-06 19:18:56] httpx._dispatch.http2 - receive_event stream_id=1 event=<ResponseReceived stream_id:1, headers:[(b':status', b'200'), (b'date', b'Wed, 06 Nov 2019 18:18:56 GMT'), (b'expires', b'-1'), (b'cache-control', b'private, max-age=0'), (b'content-type', b'text/html; charset=ISO-8859-1'), (b'p3p', b'CP="This is not a P3P policy! See g.co/p3phelp for more info."'), (b'content-encoding', b'gzip'), (b'server', b'gws'), (b'content-length', b'5073'), (b'x-xss-protection', b'0'), (b'x-frame-options', b'SAMEORIGIN'), (b'set-cookie', b'1P_JAR=2019-11-06-18; expires=Fri, 06-Dec-2019 18:18:56 GMT; path=/; domain=.google.com; SameSite=none'), (b'set-cookie', b'NID=190=m8G9qLxCz2_4HbZI02ON2HTJF4xTvOhoJiS57Hm-OJrNS2eY20LfXMR_u-mLjujeshW5-BTezI69OGpHksT4ZK2TCDsWeU0DF7AmDTjjXFOdj30eIUTpNq7r9aWRvI8UrqiwlIsLkE8Ee3t5PiIiVdSMUcji7dkavGlMUpkMXU8; expires=Thu, 07-May-2020 18:18:56 GMT; path=/; domain=.google.com; HttpOnly'), (b'alt-svc', b'quic=":443"; ma=2592000; v="46,43",h3-Q050=":443"; ma=2592000,h3-Q049=":443"; ma=2592000,h3-Q048=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000')]>
66-
DEBUG [2019-11-06 19:18:56] httpx._client - HTTP Request: GET https://www.google.com/ "HTTP/2 200 OK"
67-
TRACE [2019-11-06 19:18:56] httpx._dispatch.http2 - receive_event stream_id=1 event=<DataReceived stream_id:1, flow_controlled_length:5186, data:1f8b08000000000002ffc55af97adb4692ff3f4f>
68-
TRACE [2019-11-06 19:18:56] httpx._dispatch.http2 - receive_event stream_id=1 event=<DataReceived stream_id:1, flow_controlled_length:221, data:>
69-
TRACE [2019-11-06 19:18:56] httpx._dispatch.http2 - receive_event stream_id=1 event=<StreamEnded stream_id:1>
70-
TRACE [2019-11-06 19:18:56] httpx._dispatch.http2 - receive_event stream_id=0 event=<PingReceived ping_data:0000000000000000>
71-
TRACE [2019-11-06 19:18:56] httpx._dispatch.connection_pool - release_connection connection=HTTPConnection(origin=Origin(scheme='https' host='www.google.com' port=443))
72-
TRACE [2019-11-06 19:18:56] httpx._dispatch.connection - close_connection
73-
```
74-
7511
## `SSLKEYLOGFILE`
7612

7713
Valid values: a filename

docs/logging.md

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# Logging
2+
3+
If you need to inspect the internal behaviour of `httpx`, you can use Python's standard logging to output information about the underlying network behaviour.
4+
5+
For example, the following configuration...
6+
7+
```python
8+
import logging
9+
import httpx
10+
11+
logging.basicConfig(
12+
format="%(levelname)s [%(asctime)s] %(name)s - %(message)s",
13+
datefmt="%Y-%m-%d %H:%M:%S",
14+
level=logging.DEBUG
15+
)
16+
17+
httpx.get("https://www.example.com")
18+
```
19+
20+
Will send debug level output to the console, or wherever `stdout` is directed too...
21+
22+
```
23+
DEBUG [2023-03-16 14:36:20] httpx - load_ssl_context verify=True cert=None trust_env=True http2=False
24+
DEBUG [2023-03-16 14:36:20] httpx - load_verify_locations cafile='/Users/tomchristie/GitHub/encode/httpx/venv/lib/python3.10/site-packages/certifi/cacert.pem'
25+
DEBUG [2023-03-16 14:36:20] httpcore - connection.connect_tcp.started host='www.example.com' port=443 local_address=None timeout=5.0
26+
DEBUG [2023-03-16 14:36:20] httpcore - connection.connect_tcp.complete return_value=<httpcore.backends.sync.SyncStream object at 0x1068fd270>
27+
DEBUG [2023-03-16 14:36:20] httpcore - connection.start_tls.started ssl_context=<ssl.SSLContext object at 0x10689aa40> server_hostname='www.example.com' timeout=5.0
28+
DEBUG [2023-03-16 14:36:20] httpcore - connection.start_tls.complete return_value=<httpcore.backends.sync.SyncStream object at 0x1068fd240>
29+
DEBUG [2023-03-16 14:36:20] httpcore - http11.send_request_headers.started request=<Request [b'GET']>
30+
DEBUG [2023-03-16 14:36:20] httpcore - http11.send_request_headers.complete
31+
DEBUG [2023-03-16 14:36:20] httpcore - http11.send_request_body.started request=<Request [b'GET']>
32+
DEBUG [2023-03-16 14:36:20] httpcore - http11.send_request_body.complete
33+
DEBUG [2023-03-16 14:36:20] httpcore - http11.receive_response_headers.started request=<Request [b'GET']>
34+
DEBUG [2023-03-16 14:36:21] httpcore - http11.receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'OK', [(b'Content-Encoding', b'gzip'), (b'Accept-Ranges', b'bytes'), (b'Age', b'507675'), (b'Cache-Control', b'max-age=604800'), (b'Content-Type', b'text/html; charset=UTF-8'), (b'Date', b'Thu, 16 Mar 2023 14:36:21 GMT'), (b'Etag', b'"3147526947+ident"'), (b'Expires', b'Thu, 23 Mar 2023 14:36:21 GMT'), (b'Last-Modified', b'Thu, 17 Oct 2019 07:18:26 GMT'), (b'Server', b'ECS (nyb/1D2E)'), (b'Vary', b'Accept-Encoding'), (b'X-Cache', b'HIT'), (b'Content-Length', b'648')])
35+
INFO [2023-03-16 14:36:21] httpx - HTTP Request: GET https://www.example.com "HTTP/1.1 200 OK"
36+
DEBUG [2023-03-16 14:36:21] httpcore - http11.receive_response_body.started request=<Request [b'GET']>
37+
DEBUG [2023-03-16 14:36:21] httpcore - http11.receive_response_body.complete
38+
DEBUG [2023-03-16 14:36:21] httpcore - http11.response_closed.started
39+
DEBUG [2023-03-16 14:36:21] httpcore - http11.response_closed.complete
40+
DEBUG [2023-03-16 14:36:21] httpcore - connection.close.started
41+
DEBUG [2023-03-16 14:36:21] httpcore - connection.close.complete
42+
```
43+
44+
Logging output includes information from both the high-level `httpx` logger, and the network-level `httpcore` logger, which can be configured seperately.
45+
46+
For handling more complex logging configurations you might want to use the dictionary configuration style...
47+
48+
```python
49+
import logging.config
50+
import httpx
51+
52+
LOGGING_CONFIG = {
53+
"version": 1,
54+
"handlers": {
55+
"default": {
56+
"class": "logging.StreamHandler",
57+
"formatter": "http",
58+
"stream": "ext://sys.stderr"
59+
}
60+
},
61+
"formatters": {
62+
"http": {
63+
"format": "%(levelname)s [%(asctime)s] %(name)s - %(message)s",
64+
"datefmt": "%Y-%m-%d %H:%M:%S",
65+
}
66+
},
67+
'loggers': {
68+
'httpx': {
69+
'handlers': ['default'],
70+
'level': 'DEBUG',
71+
},
72+
'httpcore': {
73+
'handlers': ['default'],
74+
'level': 'DEBUG',
75+
},
76+
}
77+
}
78+
79+
logging.config.dictConfig(LOGGING_CONFIG)
80+
httpx.get('https://www.example.com')
81+
```
82+
83+
The exact formatting of the debug logging may be subject to change across different versions of `httpx` and `httpcore`. If you need to rely on a particular format it is recommended that you pin installation of these packages to fixed versions.

httpx/_client.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import datetime
22
import enum
3+
import logging
34
import typing
45
import warnings
56
from contextlib import asynccontextmanager, contextmanager
@@ -50,7 +51,6 @@
5051
Timer,
5152
URLPattern,
5253
get_environment_proxies,
53-
get_logger,
5454
is_https_redirect,
5555
same_origin,
5656
)
@@ -84,7 +84,7 @@ class UseClientDefault:
8484
USE_CLIENT_DEFAULT = UseClientDefault()
8585

8686

87-
logger = get_logger(__name__)
87+
logger = logging.getLogger("httpx")
8888

8989
USER_AGENT = f"python-httpx/{__version__}"
9090
ACCEPT_ENCODING = ", ".join(
@@ -1010,10 +1010,13 @@ def _send_single_request(self, request: Request) -> Response:
10101010
self.cookies.extract_cookies(response)
10111011
response.default_encoding = self._default_encoding
10121012

1013-
status = f"{response.status_code} {response.reason_phrase}"
1014-
response_line = f"{response.http_version} {status}"
1015-
logger.debug(
1016-
'HTTP Request: %s %s "%s"', request.method, request.url, response_line
1013+
logger.info(
1014+
'HTTP Request: %s %s "%s %d %s"',
1015+
request.method,
1016+
request.url,
1017+
response.http_version,
1018+
response.status_code,
1019+
response.reason_phrase,
10171020
)
10181021

10191022
return response

httpx/_config.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import logging
12
import os
23
import ssl
34
import sys
@@ -10,7 +11,7 @@
1011
from ._models import Headers
1112
from ._types import CertTypes, HeaderTypes, TimeoutTypes, URLTypes, VerifyTypes
1213
from ._urls import URL
13-
from ._utils import get_ca_bundle_from_env, get_logger
14+
from ._utils import get_ca_bundle_from_env
1415

1516
DEFAULT_CIPHERS = ":".join(
1617
[
@@ -32,7 +33,7 @@
3233
)
3334

3435

35-
logger = get_logger(__name__)
36+
logger = logging.getLogger("httpx")
3637

3738

3839
class UnsetType:
@@ -75,12 +76,12 @@ def __init__(
7576
self.ssl_context = self.load_ssl_context()
7677

7778
def load_ssl_context(self) -> ssl.SSLContext:
78-
logger.trace(
79-
f"load_ssl_context "
80-
f"verify={self.verify!r} "
81-
f"cert={self.cert!r} "
82-
f"trust_env={self.trust_env!r} "
83-
f"http2={self.http2!r}"
79+
logger.debug(
80+
"load_ssl_context verify=%r cert=%r trust_env=%r http2=%r",
81+
self.verify,
82+
self.cert,
83+
self.trust_env,
84+
self.http2,
8485
)
8586

8687
if self.verify:
@@ -141,11 +142,13 @@ def load_ssl_context_verify(self) -> ssl.SSLContext:
141142
pass
142143

143144
if ca_bundle_path.is_file():
144-
logger.trace(f"load_verify_locations cafile={ca_bundle_path!s}")
145-
context.load_verify_locations(cafile=str(ca_bundle_path))
145+
cafile = str(ca_bundle_path)
146+
logger.debug("load_verify_locations cafile=%r", cafile)
147+
context.load_verify_locations(cafile=cafile)
146148
elif ca_bundle_path.is_dir():
147-
logger.trace(f"load_verify_locations capath={ca_bundle_path!s}")
148-
context.load_verify_locations(capath=str(ca_bundle_path))
149+
capath = str(ca_bundle_path)
150+
logger.debug("load_verify_locations capath=%r", capath)
151+
context.load_verify_locations(capath=capath)
149152

150153
self._load_client_certs(context)
151154

httpx/_utils.py

Lines changed: 0 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
import codecs
22
import email.message
3-
import logging
43
import mimetypes
54
import os
65
import re
7-
import sys
86
import time
97
import typing
108
from pathlib import Path
@@ -196,50 +194,6 @@ def obfuscate_sensitive_headers(
196194
yield k, v
197195

198196

199-
_LOGGER_INITIALIZED = False
200-
TRACE_LOG_LEVEL = 5
201-
202-
203-
class Logger(logging.Logger):
204-
# Stub for type checkers.
205-
def trace(self, message: str, *args: typing.Any, **kwargs: typing.Any) -> None:
206-
... # pragma: no cover
207-
208-
209-
def get_logger(name: str) -> Logger:
210-
"""
211-
Get a `logging.Logger` instance, and optionally
212-
set up debug logging based on the HTTPX_LOG_LEVEL environment variable.
213-
"""
214-
global _LOGGER_INITIALIZED
215-
216-
if not _LOGGER_INITIALIZED:
217-
_LOGGER_INITIALIZED = True
218-
logging.addLevelName(TRACE_LOG_LEVEL, "TRACE")
219-
220-
log_level = os.environ.get("HTTPX_LOG_LEVEL", "").upper()
221-
if log_level in ("DEBUG", "TRACE"):
222-
logger = logging.getLogger("httpx")
223-
logger.setLevel(logging.DEBUG if log_level == "DEBUG" else TRACE_LOG_LEVEL)
224-
handler = logging.StreamHandler(sys.stderr)
225-
handler.setFormatter(
226-
logging.Formatter(
227-
fmt="%(levelname)s [%(asctime)s] %(name)s - %(message)s",
228-
datefmt="%Y-%m-%d %H:%M:%S",
229-
)
230-
)
231-
logger.addHandler(handler)
232-
233-
logger = logging.getLogger(name)
234-
235-
def trace(message: str, *args: typing.Any, **kwargs: typing.Any) -> None:
236-
logger.log(TRACE_LOG_LEVEL, message, *args, **kwargs)
237-
238-
logger.trace = trace # type: ignore
239-
240-
return typing.cast(Logger, logger)
241-
242-
243197
def port_or_default(url: "URL") -> typing.Optional[int]:
244198
if url.port is not None:
245199
return url.port

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ nav:
3131
- Guides:
3232
- Async Support: 'async.md'
3333
- HTTP/2 Support: 'http2.md'
34+
- Logging: 'logging.md'
3435
- Requests Compatibility: 'compatibility.md'
3536
- Troubleshooting: 'troubleshooting.md'
3637
- API Reference:

0 commit comments

Comments
 (0)