Registra tutte le richieste dal modulo richieste python


95

Sto usando Python Requests . Ho bisogno di eseguire il debug di alcune OAuthattività e per questo vorrei che registrasse tutte le richieste eseguite. Potrei ottenere queste informazioni con ngrep, ma sfortunatamente non è possibile eseguire grep connessioni https (che sono necessarie per OAuth)

Come posso attivare la registrazione di tutti gli URL (+ parametri) che Requestsaccedono?


La risposta di @yohann mostra come ottenere ancora più output di registrazione, comprese le intestazioni che stai inviando. Dovrebbe essere la risposta accettata piuttosto che quella di Martijn, che non mostra le intestazioni che hai finito per ottenere tramite WireShark e personalizzare manualmente una richiesta.
nealmcb

Risposte:


91

La urllib3libreria sottostante registra tutte le nuove connessioni e URL con il loggingmodulo , ma non i POSTcorpi. Per le GETrichieste dovrebbe bastare:

import logging

logging.basicConfig(level=logging.DEBUG)

che ti offre l'opzione di registrazione più dettagliata; vedi il logging HOWTO per maggiori dettagli su come configurare i livelli di registrazione e le destinazioni.

Breve demo:

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

A seconda della versione esatta di urllib3, vengono registrati i seguenti messaggi:

  • INFO: Reindirizzamenti
  • WARN: Pool di connessioni pieno (se ciò accade spesso aumentare la dimensione del pool di connessioni)
  • WARN: Impossibile analizzare le intestazioni (intestazioni di risposta con formato non valido)
  • WARN: Nuovo tentativo di connessione
  • WARN: Il certificato non corrisponde al nome host previsto
  • WARN: Risposta ricevuta sia con Content-Length che con Transfer-Encoding, durante l'elaborazione di una risposta in blocchi
  • DEBUG: Nuove connessioni (HTTP o HTTPS)
  • DEBUG: Connessioni interrotte
  • DEBUG: Dettagli connessione: metodo, percorso, versione HTTP, codice di stato e lunghezza della risposta
  • DEBUG: Riprova a contare gli incrementi

Questo non include intestazioni o corpi. urllib3usa la http.client.HTTPConnectionclasse per eseguire il lavoro di grugnito, ma quella classe non supporta la registrazione, normalmente può essere configurata solo per stampare su stdout. Tuttavia, puoi impostarlo per inviare tutte le informazioni di debug alla registrazione invece introducendo un printnome alternativo in quel modulo:

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

La chiamata httpclient_logging_patch()fa sì che le http.clientconnessioni inviino tutte le informazioni di debug a un logger standard e quindi vengono rilevate da 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
Stranamente, non vedo access_tokennella richiesta OAuth. Linkedin si lamenta di una richiesta non autorizzata e voglio verificare se la libreria che sto utilizzando ( rautholtre a requests) sta inviando quel token con la richiesta. Mi aspettavo di vederlo come parametro di query, ma forse è nelle intestazioni della richiesta? Come posso forzare la urllib3visualizzazione anche delle intestazioni? E il corpo della richiesta? Tanto per semplificare: come posso vedere la richiesta COMPLETA ?
blueFast

Non puoi farlo senza rattoppare, temo. Il modo più comune per diagnosticare tali problemi è con un proxy o un registratore di pacchetti (utilizzo WireShark per acquisire personalmente le richieste e le risposte complete). Vedo però che hai fatto una nuova domanda sull'argomento.
Martijn Pieters

1
Certo, sto eseguendo il debug in questo momento con WireShark, ma ho un problema: se eseguo http, vedo l'intero contenuto del pacchetto, ma Linkedin restituisce 401, che è previsto, poiché Linkedin dice di usare https. Ma con https non funziona nemmeno e non posso eseguirne il debug poiché non riesco a ispezionare il livello TLS con WireShark.
blueFast

1
@nealmcb: gah, sì, l'impostazione di un attributo di classe globale consentirebbe effettivamente il debug in httplib. Vorrei invece che fosse usata quella libreria logging; l'output di debug viene scritto direttamente su stdout invece di permetterti di reindirizzarlo a una destinazione di log a tua scelta.
Martijn Pieters


111

È necessario abilitare il debug a httpliblivello ( requestsurllib3httplib).

Ecco alcune funzioni per attivare ( ..._on()e ..._off()) o per averlo temporaneamente attivato:

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

Uso dimostrativo:

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

Vedrai la RICHIESTA, inclusi INTESTAZIONI e DATI, e RISPOSTA con INTESTAZIONI ma senza DATI. L'unica cosa che manca sarà il response.body che non è registrato.

fonte


Grazie per le informazioni sull'utilizzo httplib.HTTPConnection.debuglevel = 1per ottenere le intestazioni - eccellente! Ma penso di ottenere gli stessi risultati usando solo logging.basicConfig(level=logging.DEBUG)al posto delle tue altre 5 linee. Mi sto perdendo qualcosa? Immagino che potrebbe essere un modo per impostare diversi livelli di registrazione per root rispetto a urllib3, se lo si desidera.
nealmcb

Non hai l'intestazione con la tua soluzione.
Yohann

7
httplib.HTTPConnection.debuglevel = 2consentirà anche la stampa del corpo del POST.
Mandible79

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

3
In qualche modo per impedire che il contenuto registrato venga inviato allo standard output?
yucer

45

Per coloro che utilizzano 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

Come posso farlo funzionare con il file di registro? Sembra funzionare solo per stdout. Problema esempio qui: stackoverflow.com/q/58738195/1090360
JackTheKnife

15

Quando import loggingho provato a far sì che il sistema di registrazione Python ( ) emettesse messaggi di registro di debug di basso livello, mi ha sorpreso scoprire che:

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

che urllib3utilizza effettivamente solo il loggingsistema Python :

  • requests no
  • http.client.HTTPConnection no
  • urllib3

Certo, puoi estrarre i messaggi di debug HTTPConnectionda impostando:

HTTPConnection.debuglevel = 1

ma queste uscite vengono semplicemente emesse tramite l' printistruzione. Per dimostrarlo, semplicemente grep il client.pycodice sorgente di Python 3.7 e visualizzare le istruzioni di stampa da soli (grazie @Yohann):

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

Presumibilmente, il reindirizzamento dello stdout in qualche modo potrebbe funzionare allo scarpone stdout nel sistema di registrazione e potenzialmente catturarlo, ad esempio, in un file di registro.

Scegli l' urllib3opzione "logger not requests.packages.urllib3"

Per acquisire urllib3informazioni di debug tramite il loggingsistema Python 3 , contrariamente a molti consigli su Internet e come sottolinea @MikeSmith, non avrai molta fortuna nell'intercettare:

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

invece devi:

log = logging.getLogger('urllib3')

Debug urllib3in un file di registro

Ecco del codice che registra il urllib3funzionamento in un file di registro utilizzando il loggingsistema 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/')

il risultato:

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

Abilitazione delle HTTPConnection.debuglevelistruzioni print ()

Se imposti HTTPConnection.debuglevel = 1

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

si otterrà la stampa di uscita prospetto delle informazioni aggiuntive succosa basso livello:

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

Ricorda che questo output utilizza printe non il loggingsistema Python , e quindi non può essere catturato usando un loggingflusso tradizionale o un gestore di file (sebbene possa essere possibile catturare l'output in un file reindirizzando lo stdout) .

Combina i due precedenti: massimizza tutte le registrazioni possibili sulla console

Per massimizzare tutte le registrazioni possibili, devi accontentarti dell'output di console / stdout con questo:

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

dando l'intera gamma di output:

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
E che dire del reindirizzamento dei dettagli di stampa al logger?
yucer

Hai avuto successo nel portare i dettagli di stampa al logger?
Erika Dsouza

3

Sto usando python 3.4, richieste 2.19.1:

"urllib3" è il logger da ottenere ora (non più "requests.packages.urllib3"). La registrazione di base verrà comunque eseguita senza impostare http.client.HTTPConnection.debuglevel


Sarebbe molto meglio se spiegassi ulteriormente
Jamie Lindsey

2

Avendo uno script o anche un sottosistema di un'applicazione per il debug di un protocollo di rete, si desidera vedere quali sono esattamente le coppie richiesta-risposta, inclusi gli URL, le intestazioni, i payload e lo stato effettivi. Ed è in genere poco pratico strumentare le richieste individuali ovunque. Allo stesso tempo, ci sono considerazioni sulle prestazioni che suggeriscono di utilizzare singoli (o pochi specializzati)requests.Session , quindi quanto segue presume che il suggerimento sia seguito.

requestssupporta i cosiddetti hook di eventi (a partire dalla 2.23 in realtà c'è soloresponse hook). È fondamentalmente un listener di eventi e l'evento viene emesso prima di restituire il controllo da requests.request. In questo momento sia la richiesta che la risposta sono completamente definite, quindi possono essere registrate.

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)

Questo è fondamentalmente come registrare tutti i round trip HTTP di una sessione.

Formattazione dei record del registro di andata e ritorno HTTP

Affinché la registrazione di cui sopra sia utile, ci può essere un formattatore di registrazione specializzato che comprende reqed resextra sui record di registrazione. Può assomigliare a questo:

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

Ora se esegui alcune richieste usando session, come:

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

L'output stderrapparirà come segue.

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

Un modo GUI

Quando hai molte query, avere una semplice interfaccia utente e un modo per filtrare i record è utile. Mostrerò di usare Chronologer per quello (di cui sono l'autore).

Innanzitutto, l'hook è stato riscritto per produrre record che loggingpossono essere serializzati durante l'invio via cavo. Può assomigliare a questo:

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)

In secondo luogo, la configurazione della registrazione deve essere adattata all'uso logging.handlers.HTTPHandler(cosa che Chronologer comprende).

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)

Infine, esegui l'istanza di Chronologer. ad es. utilizzando 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

Ed esegui di nuovo le richieste:

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

Il gestore di flusso produrrà:

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

Ora se apri http: // localhost: 8080 / (usa "logger" per nome utente e password vuota per il popup di autenticazione di base) e fai clic sul pulsante "Apri", dovresti vedere qualcosa del tipo:

Screenshot di Chronologer

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.