Записувати всі запити з модуля python-request


96

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

Як я можу активувати реєстрацію всіх URL-адрес (+ параметрів), до яких Requestsздійснюється доступ?


Відповідь @yohann показує, як отримати ще більше вихідних даних, включаючи заголовки, які ви надсилаєте. Це має бути прийнята відповідь, а не відповідь Martijn, яка не відображає заголовки, які ви в кінцевому підсумку отримували через wireshark і натомість налаштовували запит вручну.
nealmcb

Відповіді:


92

Базова 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: Отримана відповідь як з довжиною вмісту, так і з кодуванням передачі під час обробки фрагментованої відповіді
  • DEBUG: Нові підключення (HTTP або HTTPS)
  • DEBUG: Перервані з'єднання
  • DEBUG: Деталі з’єднання: спосіб, шлях, версія HTTP, код стану та тривалість відповіді
  • DEBUG: Повторіть крок підрахунку

Сюди не входять заголовки чи тіло. urllib3використовує http.client.HTTPConnectionклас для роботи з grunt-роботою, але цей клас не підтримує реєстрацію, він зазвичай може бути налаштований лише на друк у stdout. Однак ви можете встановити його, щоб надіслати всю інформацію про налагодження в журнал, замість цього, ввівши 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показати заголовки теж? А орган запиту? Просто для спрощення: як я можу побачити ПОВНИЙ запит?
синій Швидкий

Боюсь, цього не можна зробити без латок. Найпоширеніший спосіб діагностувати такі проблеми - це проксі-сервер або реєстратор пакетів (я використовую wireshark, щоб самостійно фіксувати повні запити та відповіді). Я бачу, що ви задали нове запитання з цього приводу.
Мартін Пітерс

1
Звичайно, я зараз налагоджую wireshark, але у мене проблема: якщо я роблю http, я бачу повний вміст пакета, але Linkedin повертає 401, що очікується, оскільки Linkedin каже використовувати https. Але з https він теж не працює, і я не можу його налагодити, оскільки я не можу перевірити рівень TLS за допомогою wireshark.
синій Швидкий

1
@nealmcb: gah, так, встановлення атрибута глобального класу дійсно дозволить налагодження 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

У вас немає заголовка з вашим рішенням.
Йоханн,

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

45

Для тих, хто використовує 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

Як я можу змусити його працювати з файлом журналу? Здається, це працює лише для stdout. Приклад проблеми тут: stackoverflow.com/q/58738195/1090360
JackTheKnife

15

При спробі змусити систему ведення журналу 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 shoe-horn в систему реєстрації та потенційно захопити, наприклад, файл журналу.

Виберіть ' urllib3' logger not ' requests.packages.urllib3'

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

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

натомість вам потрібно:

log = logging.getLogger('urllib3')

Налагодження urllib3у файл журналу

Ось код, який реєструє urllib3роботу файлу журналу за допомогою системи Python logging:

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) .

Поєднайте два вищезазначені дані - максимально збільшіть усі можливі журнали до консолі

Щоб максимізувати всі можливі журналювання, вам слід погодитися на вивід консолі / 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 якийсь успіх у доступі до деталей друку до реєстратора?
Еріка

2

Я використовую python 3.4, запити 2.19.1:

'urllib3' - це реєстратор, який потрібно отримати зараз (більше не 'request.packages.urllib3'). Базовий журнал все одно відбуватиметься без встановлення http.client.HTTPConnection.debuglevel


1
Було б набагато краще, якби ви пояснили далі
Джеймі Ліндсі

2

Маючи сценарій або навіть підсистему програми для налагодження мережевого протоколу, бажано побачити, якими саме є пари запит-відповідь, включаючи ефективні 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

Графічний інтерфейс

Коли у вас багато запитів, просто користувальницький інтерфейс та спосіб фільтрування записів стане в нагоді. Я покажу використовувати 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 / (використовуйте "реєстратор" для імені користувача та порожній пароль для основного спливаючого вікна auth) і натискаєте кнопку "Відкрити", ви повинні побачити щось на зразок:

Знімок екрана Chronologer


0

Я використовую logger_config.yamlфайл для налаштування мого ведення журналу, і щоб ці журнали відображалися, все, що мені потрібно було зробити, це додати a disable_existing_loggers: Falseдо кінця.

Моє налаштування журналювання досить велике і заплутане, тому я навіть не знаю хорошого способу пояснити його тут, але якщо хтось також використовує файл YAML для налаштування свого ведення журналу, це може допомогти.

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.