Catat semua permintaan dari modul python-request


96

Saya menggunakan Permintaan python . Saya perlu men-debug beberapa OAuthaktivitas, dan untuk itu saya ingin mencatat semua permintaan yang dilakukan. Saya bisa mendapatkan informasi ini dengan ngrep, tetapi sayangnya tidak mungkin untuk mendapatkan koneksi https (yang diperlukan untuk OAuth)

Bagaimana cara mengaktifkan pencatatan semua URL (+ parameter) yang Requestssedang mengakses?


Tanggapan oleh @yohann menunjukkan cara mendapatkan lebih banyak keluaran logging, termasuk header yang Anda kirim. Ini harus menjadi jawaban yang diterima daripada Martijn, yang tidak menampilkan tajuk yang akhirnya Anda dapatkan melalui wireshark dan sebagai gantinya menyesuaikan permintaan dengan tangan.
nealmcb

Jawaban:


92

urllib3Pustaka yang mendasari membuat log semua koneksi dan URL baru dengan loggingmodul , tetapi tidak POSTbadan. Untuk GETpermintaan ini sudah cukup:

import logging

logging.basicConfig(level=logging.DEBUG)

yang memberi Anda opsi pencatatan paling panjang; lihat HOWTO logging untuk detail lebih lanjut tentang cara mengkonfigurasi level dan tujuan logging.

Demo singkat:

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

Bergantung pada versi urllib3 yang tepat, pesan berikut akan dicatat:

  • INFO: Pengalihan
  • WARN: Kumpulan koneksi penuh (jika ini terjadi sering-seringlah memperbesar ukuran kumpulan koneksi)
  • WARN: Gagal mengurai header (header respons dengan format yang tidak valid)
  • WARN: Mencoba kembali koneksi
  • WARN: Sertifikat tidak cocok dengan nama host yang diharapkan
  • WARN: Menerima respons dengan Content-Length dan Transfer-Encoding, saat memproses respons yang dipotong
  • DEBUG: Koneksi baru (HTTP atau HTTPS)
  • DEBUG: Koneksi terputus
  • DEBUG: Detail koneksi: metode, jalur, versi HTTP, kode status, dan panjang respons
  • DEBUG: Coba lagi kenaikan hitungan

Ini tidak termasuk header atau badan. urllib3menggunakan http.client.HTTPConnectionkelas untuk melakukan pekerjaan kasar, tetapi kelas itu tidak mendukung logging, biasanya hanya dapat dikonfigurasi untuk mencetak ke stdout. Namun, Anda dapat menyesuaikannya untuk mengirim semua informasi debug ke logging dengan memasukkan printnama alternatif ke dalam modul itu:

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

Panggilan httpclient_logging_patch()menyebabkan http.clientkoneksi mengeluarkan semua informasi debug ke logger standar, dan diambil oleh 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
Anehnya, saya tidak melihat access_tokendi permintaan OAuth. Linkedin mengeluh tentang permintaan yang tidak sah, dan saya ingin memverifikasi apakah perpustakaan yang saya gunakan ( rauthdi atas requests) mengirim token itu dengan permintaan tersebut. Saya berharap untuk melihatnya sebagai parameter kueri, tetapi mungkin itu ada di header permintaan? Bagaimana cara memaksa urllib3untuk menampilkan header juga? Dan badan permintaan? Hanya untuk membuatnya sederhana: bagaimana saya bisa melihat permintaan FULL ?
blueFast

Anda tidak dapat melakukan itu tanpa menambal, saya khawatir. Cara paling umum untuk mendiagnosis masalah tersebut adalah dengan proxy atau logger paket (saya menggunakan wireshark untuk menangkap sendiri permintaan dan tanggapan lengkap). Saya melihat Anda mengajukan pertanyaan baru tentang masalah ini.
Martijn Pieters

1
Tentu, saya sedang men-debug sekarang dengan wireshark, tetapi saya memiliki masalah: jika saya melakukan http, saya melihat isi paket lengkap, tetapi Linkedin mengembalikan 401, yang diharapkan, karena Linkedin menyuruh untuk menggunakan https. Tetapi dengan https itu juga tidak berfungsi, dan saya tidak dapat men-debugnya karena saya tidak dapat memeriksa lapisan TLS dengan wireshark.
blueFast

1
@nealmcb: gah, ya, menyetel atribut kelas global memang akan memungkinkan debugging masuk httplib. Saya berharap perpustakaan itu digunakan loggingsebagai gantinya; keluaran debug ditulis langsung ke stdout daripada membiarkan Anda mengarahkannya ke tujuan log pilihan Anda.
Martijn Pieters


112

Anda perlu mengaktifkan debugging pada httpliblevel ( requestsurllib3httplib).

Berikut beberapa fungsi untuk mengaktifkan ( ..._on()dan ..._off()) atau untuk sementara:

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

Penggunaan demo:

>>> 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]>

Anda akan melihat PERMINTAAN, termasuk KEPALA dan DATA, dan TANGGAPAN dengan KEPALA tetapi tanpa DATA. Satu-satunya hal yang hilang adalah response.body yang belum dicatat.

Sumber


Terima kasih atas wawasan tentang cara menggunakan httplib.HTTPConnection.debuglevel = 1untuk mendapatkan tajuk - luar biasa! Tapi saya pikir saya mendapatkan hasil yang sama hanya dengan menggunakan logging.basicConfig(level=logging.DEBUG)5 baris lainnya. Apakah saya melewatkan sesuatu? Saya kira ini bisa menjadi cara untuk mengatur tingkat logging yang berbeda untuk root vs urllib3, jika diinginkan.
nealmcb

Anda tidak memiliki tajuk dengan solusi Anda.
Yohann

7
httplib.HTTPConnection.debuglevel = 2akan memungkinkan pencetakan badan POST juga.
Mandibula79

1
httplib.HTTPConnection.debuglevel = 1cukup @ Mandible79 $ curl https://raw.githubusercontent.com/python/cpython/master/Lib/http/client.py |grep debuglevelitu selaludebuglevel > 0
Yohann

3
Bagaimana cara mencegah konten yang dicatat dikirim ke keluaran standar?
yucer

45

Bagi yang menggunakan 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

Bagaimana saya bisa membuatnya bekerja dengan file log? Sepertinya hanya bekerja untuk stdout. Contoh masalah di sini: stackoverflow.com/q/58738195/1090360
JackTheKnife

15

Ketika mencoba membuat sistem logging Python ( import logging) mengeluarkan pesan log debug tingkat rendah, saya terkejut menemukan yang diberikan:

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

yang urllib3sebenarnya hanya menggunakan loggingsistem Python :

  • requests tidak
  • http.client.HTTPConnection tidak
  • urllib3 Iya

Tentu, Anda dapat mengekstrak pesan debug dari HTTPConnectiondengan menyetel:

HTTPConnection.debuglevel = 1

tetapi keluaran ini hanya dikeluarkan melalui printpernyataan. Untuk membuktikan ini, cukup grep client.pykode sumber Python 3.7 dan lihat sendiri pernyataan cetaknya (terima kasih @Yohann):

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

Agaknya mengarahkan stdout dalam beberapa cara mungkin berhasil untuk shoe-horn stdout ke dalam sistem logging dan berpotensi menangkap misalnya file log.

Pilih ' urllib3' logger bukan ' requests.packages.urllib3'

Untuk menangkap urllib3informasi debug melalui sistem Python 3 logging, bertentangan dengan banyak saran di internet, dan seperti yang ditunjukkan @MikeSmith, Anda tidak akan beruntung untuk mencegat:

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

sebagai gantinya Anda perlu:

log = logging.getLogger('urllib3')

Debugging urllib3ke file log

Berikut adalah beberapa kode yang mencatat urllib3pekerjaan ke file log menggunakan loggingsistem 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/')

hasil:

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

Mengaktifkan pernyataan HTTPConnection.debuglevelprint ()

Jika Anda mengatur HTTPConnection.debuglevel = 1

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

Anda akan mendapatkan output pernyataan cetak dari info level rendah berair tambahan:

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

Ingat output ini menggunakan printdan bukan loggingsistem Python , dan karenanya tidak dapat ditangkap menggunakan loggingstream tradisional atau penangan file (meskipun dimungkinkan untuk menangkap output ke file dengan mengarahkan stdout) .

Gabungkan keduanya - maksimalkan semua kemungkinan logging ke konsol

Untuk memaksimalkan semua kemungkinan logging, Anda harus puas dengan keluaran konsol / stdout dengan ini:

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/')

memberikan hasil yang lengkap:

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
Dan bagaimana dengan mengarahkan detail cetak ke logger?
yucer

Apakah Anda berhasil mendapatkan detail cetak ke logger?
Erika Dsouza

2

Saya menggunakan python 3.4, permintaan 2.19.1:

'urllib3' adalah logger untuk mendapatkan sekarang (bukan lagi 'requests.packages.urllib3'). Pencatatan dasar masih akan terjadi tanpa menyetel http.client.HTTPConnection.debuglevel


1
Akan jauh lebih baik jika Anda menjelaskan lebih lanjut
Jamie Lindsey

2

Memiliki skrip atau bahkan subsistem aplikasi untuk debugging protokol jaringan, diinginkan untuk melihat apa pasangan permintaan-respons sebenarnya, termasuk URL, header, payloads dan status yang efektif. Dan biasanya tidak praktis untuk melengkapi permintaan individu di semua tempat. Pada saat yang sama ada pertimbangan kinerja yang menyarankan penggunaan tunggal (atau beberapa khusus) requests.Session, jadi berikut ini mengasumsikan bahwa saran itu diikuti.

requestsmendukung apa yang disebut event hooks (pada 2.23 sebenarnya hanya ada responsehook). Ini pada dasarnya adalah pendengar acara, dan acara tersebut dipancarkan sebelum mengembalikan kontrol dari requests.request. Pada saat ini baik permintaan maupun tanggapan telah ditentukan sepenuhnya, sehingga dapat dicatat.

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)

Itu pada dasarnya cara mencatat semua perjalanan bolak-balik HTTP dari sebuah sesi.

Memformat catatan log bolak-balik HTTP

Agar logging di atas bermanfaat, mungkin ada pemformat logging khusus yang memahami reqdan resekstra pada catatan logging. Ini bisa terlihat seperti ini:

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

Sekarang jika Anda melakukan beberapa permintaan menggunakan session, seperti:

session.get('https://httpbin.org/user-agent')
session.get('https://httpbin.org/status/200')

Outputnya stderrakan terlihat sebagai berikut.

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

Cara GUI

Saat Anda memiliki banyak kueri, memiliki UI sederhana dan cara untuk memfilter catatan akan berguna. Saya akan menunjukkan untuk menggunakan Chronologer untuk itu (saya adalah penulisnya).

Pertama, hook telah ditulis ulang untuk menghasilkan rekaman yang loggingdapat diserialisasi saat dikirim melalui kabel. Ini bisa terlihat seperti ini:

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)

Kedua, konfigurasi logging harus disesuaikan untuk digunakan logging.handlers.HTTPHandler(yang dipahami oleh 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)

Terakhir, jalankan instance Chronologer. misal menggunakan 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

Dan jalankan permintaan lagi:

session.get('https://httpbin.org/user-agent')
session.get('https://httpbin.org/status/200')

Pengendali aliran akan menghasilkan:

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

Sekarang jika Anda membuka http: // localhost: 8080 / (gunakan "logger" untuk nama pengguna dan kata sandi kosong untuk popup auth dasar) dan klik tombol "Open", Anda akan melihat sesuatu seperti:

Tangkapan layar dari Chronologer


0

Saya menggunakan logger_config.yamlfile untuk mengkonfigurasi logging saya, dan untuk menampilkan log tersebut, yang harus saya lakukan adalah menambahkan a disable_existing_loggers: Falsedi akhir file.

Pengaturan logging saya agak luas dan membingungkan, jadi saya bahkan tidak tahu cara yang baik untuk menjelaskannya di sini, tetapi jika seseorang juga menggunakan file YAML untuk mengkonfigurasi logging mereka, ini mungkin membantu.

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

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.