Имея сценарий или даже подсистему приложения для отладки сетевого протокола, желательно видеть, какие именно пары запрос-ответ, включая эффективные URL-адреса, заголовки, полезные данные и статус. И, как правило, непрактично обрабатывать индивидуальные запросы повсюду. В то же время есть соображения производительности, которые предполагают использование одного (или нескольких специализированных) requests.Session
, поэтому ниже предполагается, что это предложение выполняется.
requests
поддерживает так называемые перехватчики событий (с 2.23 фактически только response
перехватчики). По сути, это прослушиватель событий, и событие генерируется перед возвратом управления из requests.request
. На данный момент и запрос, и ответ полностью определены, поэтому их можно регистрировать.
import logging
import requests
logger = logging.getLogger('httplogger')
def logRoundtrip(response, *args, **kwargs):
extra = {'req': response.request, 'res': response}
logger.debug('HTTP roundtrip', extra=extra)
session = requests.Session()
session.hooks['response'].append(logRoundtrip)
Это в основном способ регистрации всех HTTP-циклов сеанса.
Форматирование записей журнала приема-передачи HTTP
Для того, чтобы приведенное выше ведение журнала было полезным, может быть специализированное средство форматирования журнала, которое понимает req
и res
дополняет записи журнала. Это может выглядеть так:
import textwrap
class HttpFormatter(logging.Formatter):
def _formatHeaders(self, d):
return '\n'.join(f'{k}: {v}' for k, v in d.items())
def formatMessage(self, record):
result = super().formatMessage(record)
if record.name == 'httplogger':
result += textwrap.dedent('''
---------------- request ----------------
{req.method} {req.url}
{reqhdrs}
{req.body}
---------------- response ----------------
{res.status_code} {res.reason} {res.url}
{reshdrs}
{res.text}
''').format(
req=record.req,
res=record.res,
reqhdrs=self._formatHeaders(record.req.headers),
reshdrs=self._formatHeaders(record.res.headers),
)
return result
formatter = HttpFormatter('{asctime} {levelname} {name} {message}', style='{')
handler = logging.StreamHandler()
handler.setFormatter(formatter)
logging.basicConfig(level=logging.DEBUG, handlers=[handler])
Теперь, если вы выполните несколько запросов с помощью session
, например:
session.get('https://httpbin.org/user-agent')
session.get('https://httpbin.org/status/200')
Вывод stderr
будет выглядеть следующим образом.
2020-05-14 22:10:13,224 DEBUG urllib3.connectionpool Starting new HTTPS connection (1): httpbin.org:443
2020-05-14 22:10:13,695 DEBUG urllib3.connectionpool https://httpbin.org:443 "GET /user-agent HTTP/1.1" 200 45
2020-05-14 22:10:13,698 DEBUG httplogger HTTP roundtrip
---------------- request ----------------
GET https://httpbin.org/user-agent
User-Agent: python-requests/2.23.0
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive
None
---------------- response ----------------
200 OK https://httpbin.org/user-agent
Date: Thu, 14 May 2020 20:10:13 GMT
Content-Type: application/json
Content-Length: 45
Connection: keep-alive
Server: gunicorn/19.9.0
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
{
"user-agent": "python-requests/2.23.0"
}
2020-05-14 22:10:13,814 DEBUG urllib3.connectionpool https://httpbin.org:443 "GET /status/200 HTTP/1.1" 200 0
2020-05-14 22:10:13,818 DEBUG httplogger HTTP roundtrip
---------------- request ----------------
GET https://httpbin.org/status/200
User-Agent: python-requests/2.23.0
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive
None
---------------- response ----------------
200 OK https://httpbin.org/status/200
Date: Thu, 14 May 2020 20:10:13 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 0
Connection: keep-alive
Server: gunicorn/19.9.0
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
GUI способ
Когда у вас много запросов, вам пригодится простой пользовательский интерфейс и способ фильтрации записей. Я покажу, как использовать для этого Chronologer (автор которого я).
Во-первых, обработчик был переписан для создания записей, которые logging
могут сериализоваться при отправке по сети. Это может выглядеть так:
def logRoundtrip(response, *args, **kwargs):
extra = {
'req': {
'method': response.request.method,
'url': response.request.url,
'headers': response.request.headers,
'body': response.request.body,
},
'res': {
'code': response.status_code,
'reason': response.reason,
'url': response.url,
'headers': response.headers,
'body': response.text
},
}
logger.debug('HTTP roundtrip', extra=extra)
session = requests.Session()
session.hooks['response'].append(logRoundtrip)
Во-вторых, необходимо адаптировать конфигурацию журналирования для использования logging.handlers.HTTPHandler
(что понимает Chronologer).
import logging.handlers
chrono = logging.handlers.HTTPHandler(
'localhost:8080', '/api/v1/record', 'POST', credentials=('logger', ''))
handlers = [logging.StreamHandler(), chrono]
logging.basicConfig(level=logging.DEBUG, handlers=handlers)
Наконец, запустите экземпляр Chronologer. например, используя Docker:
docker run --rm -it -p 8080:8080 -v /tmp/db \
-e CHRONOLOGER_STORAGE_DSN=sqlite:////tmp/db/chrono.sqlite \
-e CHRONOLOGER_SECRET=example \
-e CHRONOLOGER_ROLES="basic-reader query-reader writer" \
saaj/chronologer \
python -m chronologer -e production serve -u www-data -g www-data -m
И снова запустим запросы:
session.get('https://httpbin.org/user-agent')
session.get('https://httpbin.org/status/200')
Обработчик потока выдаст:
DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): httpbin.org:443
DEBUG:urllib3.connectionpool:https://httpbin.org:443 "GET /user-agent HTTP/1.1" 200 45
DEBUG:httplogger:HTTP roundtrip
DEBUG:urllib3.connectionpool:https://httpbin.org:443 "GET /status/200 HTTP/1.1" 200 0
DEBUG:httplogger:HTTP roundtrip
Теперь, если вы откроете http: // localhost: 8080 / (используйте logger для имени пользователя и пустой пароль для основного всплывающего окна авторизации) и нажмете кнопку «Открыть», вы должны увидеть что-то вроде: