Skip to content

Conversation

@sethmlarson
Copy link
Contributor

@sethmlarson sethmlarson commented Aug 26, 2019

This builds on #240 and adds it to all request-making dispatchers. It can be added to ASGI and WSGI dispatchers as well but I'm less familiar with what information would be useful for debugging those use-cases.

This also adds a new environment variable HTTPX_DEBUG which can be turned on to automatically display these debug logs.

A new section Environment Variables has been added to the documentation with the intent of future expansion for all environment variables that HTTPX will react to.

Closes #221.

@florimondmanca
Copy link
Contributor

This is a great idea. I'll take a closer look later, but do you know about structlog? Your key=value logging style here made me think it could be worth considering it (or another structured logging library). :-)

@florimondmanca florimondmanca self-requested a review August 26, 2019 12:11
@sethmlarson
Copy link
Contributor Author

I initially thought to use structlog, mostly didn't want to add a dependency just so we can do debug logging on a client library. Are there other structured logging libraries out there?

@florimondmanca
Copy link
Contributor

florimondmanca commented Aug 26, 2019

I don't know of other structured logging libraries. But I think the real question here is, as you said: is adding another dependency for debug logs worth it? I'm not sure it is either. :-)

It's probably possible to build our own formatting helper, starting with something really simple.

There's a structured logging section in the Logging cookbook, which could helper us build a helper to be used like this:

from httpx.logging import structured
logger.debug(structured("log message", key="value"))

The cookbook recipe dumps the output as JSON, but we can take inspiration from structlog's rendering implementation here too to achieve the style you've got here.

@florimondmanca
Copy link
Contributor

I think all of this can be figured out later as a refactoring step for this PR, though — just jotting down ideas here, but it's not a blocker at all. :-)

Copy link
Contributor

@florimondmanca florimondmanca left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really excited about how helpful this will be for debugging requests! I left a couple of comments on details.

Here are a list of environment variables that HTTPX recognizes
and what function they serve:

HTTPX_DEBUG
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
HTTPX_DEBUG
`HTTPX_DEBUG`

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't make any display difference using mkdocs-material (I tried it) but we'll go with it anyways :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or really? I looked at the "Developer Interface" page and e.g. Client is rendered in a monospace font. Perhaps it's the use of === / --- instead of # and ## for headers?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked, and regardless of the header markup style (which I think we could/should convert to using shebangs since that's what we use everywhere else?) I do get monospace when using code fences:

Screenshot 2019-08-26 at 23 43 38

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc: @tomchristie potential mkdocs-material improvement? :)

@florimondmanca
Copy link
Contributor

Also, will this fix #33?

@sethmlarson
Copy link
Contributor Author

Not quite, that issue is way too broad for its own good. Maybe we should close it and open issues with the specific tasks it requires.

Copy link
Contributor

@florimondmanca florimondmanca left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple more polish suggestions!

@sethmlarson
Copy link
Contributor Author

@florimondmanca Incorporated your suggestions!

Copy link
Contributor

@florimondmanca florimondmanca left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One last suggestion I didn’t see earlier and we’ll be good to merge 🚀

f"send_request method={request.method!r} "
f"target={request.url.full_path!r} "
f"headers={request.headers!r}"
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably also want to log the sending of h11.Data and h11.EndOfMessage events, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I can add that!

@lovelydinosaur
Copy link
Contributor

Fantastic, I'm loving this!

Couple of comments worth taking a look at.

@lovelydinosaur
Copy link
Contributor

Possible later additions here:

  • INFO level - log responses in an access log like style - method, URL, status code. We'd probably want to include all redirect requests there too.
  • WARNING level - Dropped connections, invalid responses, timeouts etc. Basically anything where a request was started but no response was available to log.
INFO httpx.client GET https://example.com/redirect-to-home 304 <Location: https://example.com/>
INFO httpx.client GET https://example.com/ 200
WARN http.client GET https://example.com/ Failed with ReadTimeout(...)

@sethmlarson
Copy link
Contributor Author

With the latest change I tried to normalize the two sets of events being logged for HTTP/1.1 and HTTP/2 so they're similar to look at and debug.

HTTP/1.1

07:02:42.128 - httpx.dispatch.connection_pool - acquire_connection origin=Origin(scheme='https' host='google.com' port=443)
07:02:42.128 - httpx.dispatch.connection_pool - new_connection connection=HTTPConnection(origin=Origin(scheme='https' host='google.com' port=443))
07:02:42.133 - httpx.dispatch.connection - start_connect host='google.com' port=443 timeout=TimeoutConfig(timeout=5.0)
07:02:42.195 - httpx.dispatch.connection - connected http_version='HTTP/1.1'
07:02:42.195 - httpx.dispatch.http11 - send_headers method='GET' target='/' headers=Headers({'host': 'google.com', 'user-agent': 'python-httpx/0.7.1', 'accept': '*/*', 'accept-encoding': 'gzip, deflate, br', 'connection': 'keep-alive'})
07:02:42.195 - httpx.dispatch.http11 - receive_event event=NEED_DATA
07:02:42.239 - httpx.dispatch.http11 - receive_event event=Response(status_code=301, headers=[(b'location', b'https://www.google.com/'), (b'content-type', b'text/html; charset=UTF-8'), (b'date', b'Tue, 27 Aug 2019 12:02:42 GMT'), (b'expires', b'Thu, 26 Sep 2019 12:02:42 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,39"')], http_version=b'1.1', reason=b'Moved Permanently')
07:02:42.241 - httpx.dispatch.http11 - response_closed our_state=DONE their_state=SEND_BODY
07:02:42.242 - httpx.dispatch.http11 - send_event event=ConnectionClosed()
07:02:42.242 - httpx.dispatch.connection_pool - release_connection connection=HTTPConnection(origin=Origin(scheme='https' host='google.com' port=443))
07:02:42.242 - httpx.dispatch.connection_pool - acquire_connection origin=Origin(scheme='https' host='www.google.com' port=443)
07:02:42.243 - httpx.dispatch.connection_pool - new_connection connection=HTTPConnection(origin=Origin(scheme='https' host='www.google.com' port=443))
07:02:42.249 - httpx.dispatch.connection - start_connect host='www.google.com' port=443 timeout=TimeoutConfig(timeout=5.0)
07:02:42.309 - httpx.dispatch.connection - connected http_version='HTTP/1.1'
07:02:42.310 - httpx.dispatch.http11 - send_headers method='GET' target='/' headers=Headers({'host': 'www.google.com', 'user-agent': 'python-httpx/0.7.1', 'accept': '*/*', 'accept-encoding': 'gzip, deflate, br', 'connection': 'keep-alive'})
07:02:42.311 - httpx.dispatch.http11 - receive_event event=NEED_DATA
07:02:42.381 - httpx.dispatch.http11 - receive_event event=Response(status_code=200, headers=[(b'date', b'Tue, 27 Aug 2019 12:02:42 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'x-xss-protection', b'0'), (b'x-frame-options', b'SAMEORIGIN'), (b'set-cookie', b'1P_JAR=2019-08-27-12; expires=Thu, 26-Sep-2019 12:02:42 GMT; path=/; domain=.google.com; SameSite=none'), (b'set-cookie', b'NID=188=Gp8DK6pOzIS9u9wQ_WWtdgqgIYonZLumM5T_UnZ1GrYTwI_c6SJKlX2Ob8RTQMRTUpzinmnoCy5m0kA0gP6ISENfG0LIZ3-ZVBovXWA5kT_R_NubidkP5_haPb-qDGwTG_qrlLBA-kLsIpZNWKpD6t3_E2uOpJFC8jZ2m5I-iJ4; expires=Wed, 26-Feb-2020 12:02:42 GMT; path=/; domain=.google.com; HttpOnly'), (b'alt-svc', b'quic=":443"; ma=2592000; v="46,43,39"'), (b'transfer-encoding', b'chunked')], http_version=b'1.1', reason=b'OK')
07:02:42.383 - httpx.dispatch.http11 - receive_event event=Data(<1 bytes>)
07:02:42.384 - httpx.dispatch.http11 - receive_event event=Data(<1 bytes>)
07:02:42.384 - httpx.dispatch.http11 - receive_event event=Data(<1 bytes>)
07:02:42.385 - httpx.dispatch.http11 - receive_event event=Data(<1 bytes>)
07:02:42.385 - httpx.dispatch.http11 - receive_event event=Data(<1 bytes>)
07:02:42.386 - httpx.dispatch.http11 - receive_event event=Data(<1 bytes>)
07:02:42.387 - httpx.dispatch.http11 - receive_event event=Data(<1 bytes>)
07:02:42.387 - httpx.dispatch.http11 - receive_event event=Data(<1 bytes>)
07:02:42.387 - httpx.dispatch.http11 - receive_event event=Data(<1 bytes>)
07:02:42.388 - httpx.dispatch.http11 - receive_event event=Data(<1 bytes>)
07:02:42.388 - httpx.dispatch.http11 - receive_event event=Data(<1 bytes>)
07:02:42.389 - httpx.dispatch.http11 - receive_event event=Data(<1 bytes>)
07:02:42.389 - httpx.dispatch.http11 - receive_event event=Data(<1 bytes>)
07:02:42.390 - httpx.dispatch.http11 - receive_event event=Data(<1 bytes>)
07:02:42.390 - httpx.dispatch.http11 - receive_event event=Data(<1 bytes>)
07:02:42.391 - httpx.dispatch.http11 - receive_event event=Data(<1 bytes>)
07:02:42.391 - httpx.dispatch.http11 - receive_event event=Data(<2581 bytes>)
07:02:42.392 - httpx.dispatch.http11 - receive_event event=NEED_DATA
07:02:42.392 - httpx.dispatch.http11 - receive_event event=Data(<2439 bytes>)
07:02:42.393 - httpx.dispatch.http11 - receive_event event=EndOfMessage(headers=[])
07:02:42.393 - httpx.dispatch.http11 - response_closed our_state=DONE their_state=DONE
07:02:42.393 - httpx.dispatch.connection_pool - release_connection connection=HTTPConnection(origin=Origin(scheme='https' host='www.google.com' port=443))

HTTP/2

07:04:41.083 - httpx.dispatch.connection_pool - acquire_connection origin=Origin(scheme='https' host='google.com' port=443)
07:04:41.083 - httpx.dispatch.connection_pool - new_connection connection=HTTPConnection(origin=Origin(scheme='https' host='google.com' port=443))
07:04:41.088 - httpx.dispatch.connection - start_connect host='google.com' port=443 timeout=TimeoutConfig(timeout=5.0)
07:04:41.166 - httpx.dispatch.connection - connected http_version='HTTP/2'
07:04:41.166 - 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.1'), (b'accept', b'*/*'), (b'accept-encoding', b'gzip, deflate, br'), (b'connection', b'keep-alive')]
07:04:41.167 - httpx.dispatch.http2 - end_stream stream_id=1
07:04:41.191 - 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)}>
07:04:41.192 - httpx.dispatch.http2 - receive_event stream_id=0 event=<WindowUpdated stream_id:0, delta:983041>
07:04:41.193 - httpx.dispatch.http2 - receive_event stream_id=0 event=<SettingsAcknowledged changed_settings:{}>
07:04:41.213 - 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'Tue, 27 Aug 2019 12:04:41 GMT'), (b'expires', b'Thu, 26 Sep 2019 12:04:41 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,39"')]>
07:04:41.213 - httpx.dispatch.http2 - receive_event stream_id=1 event=<DataReceived stream_id:1, flow_controlled_length:220, data:3c48544d4c3e3c484541443e3c6d657461206874>
07:04:41.213 - httpx.dispatch.http2 - receive_event stream_id=1 event=<DataReceived stream_id:1, flow_controlled_length:0, data:>
07:04:41.214 - httpx.dispatch.http2 - receive_event stream_id=1 event=<StreamEnded stream_id:1>
07:04:41.214 - httpx.dispatch.http2 - receive_event stream_id=0 event=<PingReceived ping_data:0000000000000000>
07:04:41.217 - httpx.dispatch.connection_pool - release_connection connection=HTTPConnection(origin=Origin(scheme='https' host='google.com' port=443))
07:04:41.217 - httpx.dispatch.connection_pool - acquire_connection origin=Origin(scheme='https' host='www.google.com' port=443)
07:04:41.218 - httpx.dispatch.connection_pool - new_connection connection=HTTPConnection(origin=Origin(scheme='https' host='www.google.com' port=443))
07:04:41.238 - httpx.dispatch.connection - start_connect host='www.google.com' port=443 timeout=TimeoutConfig(timeout=5.0)
07:04:41.314 - httpx.dispatch.connection - connected http_version='HTTP/2'
07:04:41.315 - 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.1'), (b'accept', b'*/*'), (b'accept-encoding', b'gzip, deflate, br'), (b'connection', b'keep-alive')]
07:04:41.317 - httpx.dispatch.http2 - end_stream stream_id=1
07:04:41.338 - 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)}>
07:04:41.339 - httpx.dispatch.http2 - receive_event stream_id=0 event=<WindowUpdated stream_id:0, delta:983041>
07:04:41.339 - httpx.dispatch.http2 - receive_event stream_id=0 event=<SettingsAcknowledged changed_settings:{}>
07:04:41.392 - httpx.dispatch.http2 - receive_event stream_id=1 event=<ResponseReceived stream_id:1, headers:[(b':status', b'200'), (b'date', b'Tue, 27 Aug 2019 12:04:41 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'5048'), (b'x-xss-protection', b'0'), (b'x-frame-options', b'SAMEORIGIN'), (b'set-cookie', b'1P_JAR=2019-08-27-12; expires=Thu, 26-Sep-2019 12:04:41 GMT; path=/; domain=.google.com; SameSite=none'), (b'set-cookie', b'NID=188=prjg5iYy-bcIfsom2sUIFPTHtxxpHicUGQvEJwW2Zs49Sswu69FtIq_A35GRTus9xTLmFUJ66wgENfVEnePNlrx09nadujyCHzyMxS24ucqSkVee6YvAJJHLA6ih-MQMGw-HckU_6hVw4nUOlqIWiN5yqogYs2cG7b96qF4oFTY; expires=Wed, 26-Feb-2020 12:04:41 GMT; path=/; domain=.google.com; HttpOnly'), (b'alt-svc', b'quic=":443"; ma=2592000; v="46,43,39"')]>
07:04:41.395 - httpx.dispatch.http2 - receive_event stream_id=1 event=<DataReceived stream_id:1, flow_controlled_length:5157, data:1f8b08000000000002ffc55ae976dbb6b6fedfa7>
07:04:41.395 - httpx.dispatch.http2 - receive_event stream_id=1 event=<DataReceived stream_id:1, flow_controlled_length:196, data:>
07:04:41.395 - httpx.dispatch.http2 - receive_event stream_id=1 event=<StreamEnded stream_id:1>
07:04:41.395 - httpx.dispatch.http2 - receive_event stream_id=0 event=<PingReceived ping_data:0000000000000000>
07:04:41.397 - httpx.dispatch.connection_pool - release_connection connection=HTTPConnection(origin=Origin(scheme='https' host='www.google.com' port=443))

Copy link
Contributor

@florimondmanca florimondmanca left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍👏

@sethmlarson sethmlarson merged commit 6f4f186 into encode:master Aug 27, 2019
@sethmlarson sethmlarson deleted the logging branch August 27, 2019 16:43
@sethmlarson
Copy link
Contributor Author

Thanks for the review @florimondmanca and @tomchristie!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add DEBUG logging to Dispatchers and Client for easier debugging

3 participants