Регистрировать все запросы от модуля python-requests

98

Я использую запросы python . Мне нужно отладить некоторые OAuthдействия, и для этого я хотел бы, чтобы он регистрировал все выполняемые запросы. Я мог бы получить эту информацию ngrep, но, к сожалению, невозможно использовать grep https-соединения (которые необходимы для OAuth)

Как я могу активировать ведение журнала всех URL-адресов (+ параметров), Requestsк которым осуществляется доступ?

blueFast
источник
Ответ @yohann показывает, как получить еще больше выходных данных журнала, включая отправляемые вами заголовки. Это должен быть принятый ответ, а не ответ Martijn, который не показывает заголовки, которые вы в конечном итоге получили через wirehark и вместо этого вручную настроили запрос.
nealmcb 02

Ответы:

97

Базовая urllib3библиотека регистрирует все новые соединения и URL-адреса с loggingмодулем , но не POSTтела. Для GETзапросов этого должно хватить:

import logging

logging.basicConfig(level=logging.DEBUG)

что дает вам наиболее подробный вариант ведения журнала; см. HOWTO по ведению журнала для получения более подробной информации о том, как настроить уровни ведения журнала и места назначения.

Короткая демонстрация:

>>> import requests
>>> import logging
>>> logging.basicConfig(level=logging.DEBUG)
>>> r = requests.get('http://httpbin.org/get?foo=bar&baz=python')
DEBUG:urllib3.connectionpool:Starting new HTTP connection (1): httpbin.org:80
DEBUG:urllib3.connectionpool:http://httpbin.org:80 "GET /get?foo=bar&baz=python HTTP/1.1" 200 366

В зависимости от точной версии urllib3 регистрируются следующие сообщения:

  • INFO: Перенаправления
  • WARN: Пул соединений заполнен (если это часто случается, увеличивайте размер пула соединений)
  • WARN: Не удалось проанализировать заголовки (заголовки ответов в недопустимом формате)
  • WARN: Повторная попытка подключения
  • WARN: Сертификат не соответствует ожидаемому имени хоста
  • WARN: Получен ответ как с Content-Length, так и с Transfer-Encoding, при обработке фрагментированного ответа
  • DEBUG: Новые подключения (HTTP или HTTPS)
  • DEBUG: Разорванные соединения
  • DEBUG: Сведения о подключении: метод, путь, версия HTTP, код состояния и длина ответа
  • DEBUG: Увеличение числа повторов

Это не включает заголовки или тела. urllib3использует http.client.HTTPConnectionкласс для выполнения основной работы, но этот класс не поддерживает ведение журнала, обычно его можно настроить только для печати на стандартный вывод. Однако вы можете настроить его для отправки всей отладочной информации в журнал, вместо этого введя printв этот модуль альтернативное имя:

import logging
import http.client

httpclient_logger = logging.getLogger("http.client")

def httpclient_logging_patch(level=logging.DEBUG):
    """Enable HTTPConnection debug logging to the logging framework"""

    def httpclient_log(*args):
        httpclient_logger.log(level, " ".join(args))

    # mask the print() built-in in the http.client module to use
    # logging instead
    http.client.print = httpclient_log
    # enable debugging
    http.client.HTTPConnection.debuglevel = 1

Вызов httpclient_logging_patch()приводит http.clientк тому, что соединения выводят всю отладочную информацию в стандартное средство ведения журнала, и поэтому их собирают logging.basicConfig():

>>> httpclient_logging_patch()
>>> r = requests.get('http://httpbin.org/get?foo=bar&baz=python')
DEBUG:urllib3.connectionpool:Starting new HTTP connection (1): httpbin.org:80
DEBUG:http.client:send: b'GET /get?foo=bar&baz=python HTTP/1.1\r\nHost: httpbin.org\r\nUser-Agent: python-requests/2.22.0\r\nAccept-Encoding: gzip, deflate\r\nAccept: */*\r\nConnection: keep-alive\r\n\r\n'
DEBUG:http.client:reply: 'HTTP/1.1 200 OK\r\n'
DEBUG:http.client:header: Date: Tue, 04 Feb 2020 13:36:53 GMT
DEBUG:http.client:header: Content-Type: application/json
DEBUG:http.client:header: Content-Length: 366
DEBUG:http.client:header: Connection: keep-alive
DEBUG:http.client:header: Server: gunicorn/19.9.0
DEBUG:http.client:header: Access-Control-Allow-Origin: *
DEBUG:http.client:header: Access-Control-Allow-Credentials: true
DEBUG:urllib3.connectionpool:http://httpbin.org:80 "GET /get?foo=bar&baz=python HTTP/1.1" 200 366
Мартейн Питерс
источник
2
Как ни странно, я не вижу access_tokenв запросе OAuth. Linkedin жалуется на неавторизованный запрос, и я хочу проверить, отправляет ли библиотека, которую я использую ( rauthповерх requests), этот токен с запросом. Я ожидал увидеть это как параметр запроса, но, может быть, он находится в заголовках запроса? Как я могу заставить urllib3показывать и заголовки? А тело запроса? Чтобы было проще: как я могу увидеть ПОЛНЫЙ запрос?
blueFast
Боюсь, вы не сможете этого сделать без исправлений. Наиболее распространенный способ диагностики таких проблем - использование прокси-сервера или регистратора пакетов (я сам использую wirehark для сбора полных запросов и ответов). Я вижу, что вы задали новый вопрос по этому поводу.
Мартейн Питерс
1
Конечно, сейчас я выполняю отладку с помощью wirehark, но у меня проблема: если я использую http, я вижу полное содержимое пакета, но Linkedin возвращает 401, что и ожидается, поскольку Linkedin говорит использовать https. Но с https он тоже не работает, и я не могу отладить его, так как не могу проверить уровень TLS с помощью wirehark.
blueFast
1
@nealmcb: ага, да, установка атрибута глобального класса действительно позволит отладку в httplib. Я действительно хочу использовать эту библиотеку logging; вывод отладки записывается непосредственно в stdout, а не позволяет перенаправить его в место назначения журнала по вашему выбору.
Мартейн Питерс
112

Вам нужно включить отладку на httplibуровне ( requestsurllib3httplib).

Вот некоторые функции, позволяющие одновременно включать ( ..._on()и ..._off()) или временно включать:

import logging
import contextlib
try:
    from http.client import HTTPConnection # py3
except ImportError:
    from httplib import HTTPConnection # py2

def debug_requests_on():
    '''Switches on logging of the requests module.'''
    HTTPConnection.debuglevel = 1

    logging.basicConfig()
    logging.getLogger().setLevel(logging.DEBUG)
    requests_log = logging.getLogger("requests.packages.urllib3")
    requests_log.setLevel(logging.DEBUG)
    requests_log.propagate = True

def debug_requests_off():
    '''Switches off logging of the requests module, might be some side-effects'''
    HTTPConnection.debuglevel = 0

    root_logger = logging.getLogger()
    root_logger.setLevel(logging.WARNING)
    root_logger.handlers = []
    requests_log = logging.getLogger("requests.packages.urllib3")
    requests_log.setLevel(logging.WARNING)
    requests_log.propagate = False

@contextlib.contextmanager
def debug_requests():
    '''Use with 'with'!'''
    debug_requests_on()
    yield
    debug_requests_off()

Демо-использование:

>>> requests.get('http://httpbin.org/')
<Response [200]>

>>> debug_requests_on()
>>> requests.get('http://httpbin.org/')
INFO:requests.packages.urllib3.connectionpool:Starting new HTTP connection (1): httpbin.org
DEBUG:requests.packages.urllib3.connectionpool:"GET / HTTP/1.1" 200 12150
send: 'GET / HTTP/1.1\r\nHost: httpbin.org\r\nConnection: keep-alive\r\nAccept-
Encoding: gzip, deflate\r\nAccept: */*\r\nUser-Agent: python-requests/2.11.1\r\n\r\n'
reply: 'HTTP/1.1 200 OK\r\n'
header: Server: nginx
...
<Response [200]>

>>> debug_requests_off()
>>> requests.get('http://httpbin.org/')
<Response [200]>

>>> with debug_requests():
...     requests.get('http://httpbin.org/')
INFO:requests.packages.urllib3.connectionpool:Starting new HTTP connection (1): httpbin.org
...
<Response [200]>

Вы увидите ЗАПРОС, включая ЗАГОЛОВКИ и ДАННЫЕ, и ОТВЕТ с ЗАГОЛОВКАМИ, но без ДАННЫХ. Единственное, чего не хватает, это response.body, который не регистрируется.

Источник

Йоханн
источник
Спасибо за понимание использования httplib.HTTPConnection.debuglevel = 1для получения заголовков - отлично! Но я думаю, что я получаю те же результаты, используя просто logging.basicConfig(level=logging.DEBUG)вместо ваших других 5 строк. Я что-то упускаю? Я предполагаю, что это может быть способ установить разные уровни ведения журнала для корня и urllib3, если это необходимо.
nealmcb 02
У вас нет заголовка с вашим решением.
Yohann
7
httplib.HTTPConnection.debuglevel = 2также позволит распечатать тело POST.
Mandible79
1
httplib.HTTPConnection.debuglevel = 1достаточно @ Mandible79, $ curl https://raw.githubusercontent.com/python/cpython/master/Lib/http/client.py |grep debuglevelэто всегдаdebuglevel > 0
Йоханн
3
Каким-то образом предотвратить отправку записанного содержимого на стандартный вывод?
yucer
47

Для тех, кто использует Python 3+

import requests
import logging
import http.client

http.client.HTTPConnection.debuglevel = 1

logging.basicConfig()
logging.getLogger().setLevel(logging.DEBUG)
requests_log = logging.getLogger("requests.packages.urllib3")
requests_log.setLevel(logging.DEBUG)
requests_log.propagate = True
Forrestj
источник
Как я могу заставить его работать с файлом журнала? Вроде работает только на stdout. Пример проблемы здесь: stackoverflow.com/q/58738195/1090360
JackTheKnife
16

При попытке заставить систему ведения журнала Python ( import logging) выдавать сообщения журнала отладки низкого уровня, я с удивлением обнаружил, что это:

requests --> urllib3 --> http.client.HTTPConnection

который urllib3фактически использует только loggingсистему Python :

  • requests нет
  • http.client.HTTPConnection нет
  • urllib3 да

Конечно, вы можете извлекать сообщения отладки HTTPConnection, установив:

HTTPConnection.debuglevel = 1

но эти выходные данные просто передаются через printоператор. Чтобы доказать это, просто выполните grep с client.pyисходным кодом Python 3.7 и просмотрите операторы печати самостоятельно (спасибо @Yohann):

curl https://raw.githubusercontent.com/python/cpython/3.7/Lib/http/client.py |grep -A1 debuglevel` 

Предположительно, перенаправление stdout каким-то образом могло бы сработать на stdout-рожок для обуви в систему регистрации и потенциально захватить, например, в файл журнала.

Выберите urllib3"регистратор не requests.packages.urllib3"

Чтобы записать urllib3отладочную информацию через систему Python 3 logging, вопреки многим советам в Интернете, и, как указывает @MikeSmith, вам не удастся перехватить:

log = logging.getLogger('requests.packages.urllib3')

вместо этого вам необходимо:

log = logging.getLogger('urllib3')

Отладка urllib3в файл журнала

Вот код, который записывает urllib3работу в файл журнала с помощью loggingсистемы Python :

import requests
import logging
from http.client import HTTPConnection  # py3

# log = logging.getLogger('requests.packages.urllib3')  # useless
log = logging.getLogger('urllib3')  # works

log.setLevel(logging.DEBUG)  # needed
fh = logging.FileHandler("requests.log")
log.addHandler(fh)

requests.get('http://httpbin.org/')

результат:

Starting new HTTP connection (1): httpbin.org:80
http://httpbin.org:80 "GET / HTTP/1.1" 200 3168

Включение HTTPConnection.debuglevelоператоров print ()

Если вы установите HTTPConnection.debuglevel = 1

from http.client import HTTPConnection  # py3
HTTPConnection.debuglevel = 1
requests.get('http://httpbin.org/')

вы получите вывод оператора печати с дополнительной сочной информацией низкого уровня:

send: b'GET / HTTP/1.1\r\nHost: httpbin.org\r\nUser-Agent: python- 
requests/2.22.0\r\nAccept-Encoding: gzip, deflate\r\nAccept: */*\r\nConnection: keep-alive\r\n\r\n'
reply: 'HTTP/1.1 200 OK\r\n'
header: Access-Control-Allow-Credentials header: Access-Control-Allow-Origin 
header: Content-Encoding header: Content-Type header: Date header: ...

Помните, что этот вывод использует, printа не систему Python logging, и, следовательно, не может быть захвачен с помощью традиционного loggingпотока или обработчика файлов (хотя может быть возможно захватить вывод в файл, перенаправив stdout) .

Объедините два вышеупомянутых - максимизируйте все возможные журналы на консоли

Чтобы максимизировать все возможные журналы, вы должны согласиться на вывод console / stdout следующим образом:

import requests
import logging
from http.client import HTTPConnection  # py3

log = logging.getLogger('urllib3')
log.setLevel(logging.DEBUG)

# logging from urllib3 to console
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
log.addHandler(ch)

# print statements from `http.client.HTTPConnection` to console/stdout
HTTPConnection.debuglevel = 1

requests.get('http://httpbin.org/')

дающий полный спектр продукции:

Starting new HTTP connection (1): httpbin.org:80
send: b'GET / HTTP/1.1\r\nHost: httpbin.org\r\nUser-Agent: python-requests/2.22.0\r\nAccept-Encoding: gzip, deflate\r\nAccept: */*\r\nConnection: keep-alive\r\n\r\n'
reply: 'HTTP/1.1 200 OK\r\n'
http://httpbin.org:80 "GET / HTTP/1.1" 200 3168
header: Access-Control-Allow-Credentials header: Access-Control-Allow-Origin 
header: Content-Encoding header: ...
абулка
источник
3
А как насчет перенаправления деталей печати в регистратор?
yucer
yucer какие-либо успехи в передаче данных печати в регистратор?
Erika Dsouza
3

Имея сценарий или даже подсистему приложения для отладки сетевого протокола, желательно видеть, какие именно пары запрос-ответ, включая эффективные 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 для имени пользователя и пустой пароль для основного всплывающего окна авторизации) и нажмете кнопку «Открыть», вы должны увидеть что-то вроде:

Скриншот Chronologer

Saaj
источник
2

Я использую python 3.4, запросы 2.19.1:

"urllib3" - это средство ведения журнала, которое нужно получить сейчас (больше не "requests.packages.urllib3"). Базовое ведение журнала по-прежнему будет происходить без настройки http.client.HTTPConnection.debuglevel

Майк Смит
источник
1
Было бы намного лучше, если бы вы объяснили дальше
Джейми Линдси
0

Я использую logger_config.yamlфайл, чтобы настроить ведение журнала, и чтобы эти журналы отображались, все, что мне нужно было сделать, это добавить в disable_existing_loggers: Falseего конец.

Моя настройка ведения журнала довольно обширна и запутана, поэтому я даже не знаю, как это объяснить, но если кто-то также использует файл YAML для настройки ведения журнала, это может помочь.

https://docs.python.org/3/howto/logging.html#configuring-logging

Микаэль Ярняк
источник
0

Просто улучшаю этот ответ

У меня это сработало так:

import logging
import sys    
import requests
import textwrap
    
root = logging.getLogger('httplogger')


def logRoundtrip(response, *args, **kwargs):
    extra = {'req': response.request, 'res': response}
    root.debug('HTTP roundtrip', extra=extra)
    

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(sys.stdout)
handler.setFormatter(formatter)
root.addHandler(handler)
root.setLevel(logging.DEBUG)


session = requests.Session()
session.hooks['response'].append(logRoundtrip)
session.get('http://httpbin.org')
Кирилл
источник