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:
