Si l'on dispose d'un script ou même d'un sous-système d'une application pour le débogage d'un protocole réseau, il est souhaitable de voir quelles sont exactement les paires demande-réponse, y compris les URL effectives, les en-têtes, les charges utiles et l'état. Et il n'est généralement pas pratique d'instrumenter des requêtes individuelles un peu partout. En même temps, il y a des considérations de performance qui suggèrent l'utilisation d'une seule (ou de quelques) méthode(s) spécialisée(s) d'instrumentation. requests.Session
donc ce qui suit suppose que la suggestion est suivie.
requests
soutient ce que l'on appelle crochets d'événements (à partir de la version 2.23, il n'y a en fait que des response
crochet). Il s'agit essentiellement d'un écouteur d'événements, et l'événement est émis avant le retour du contrôle de la commande requests.request
. À ce stade, la demande et la réponse sont entièrement définies et peuvent donc être enregistrées.
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)
C'est en gros la façon d'enregistrer tous les allers-retours HTTP d'une session.
Formatage des enregistrements du journal des allers-retours HTTP
Pour que l'enregistrement ci-dessus soit utile, il peut y avoir des services spécialisés. formateur de journalisme qui comprend req
et res
supplémentaires sur les registres d'enregistrement. Cela peut ressembler à ceci :
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])
Maintenant, si vous faites des requêtes en utilisant le session
comme :
session.get('https://httpbin.org/user-agent')
session.get('https://httpbin.org/status/200')
La sortie vers stderr
se présentera comme suit.
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
Une interface graphique
Lorsque vous avez beaucoup de requêtes, il est utile d'avoir une interface utilisateur simple et un moyen de filtrer les enregistrements. Je vais montrer comment utiliser Chronologer pour cela (dont je suis l'auteur).
Premièrement, le crochet doit être réécrit pour produire des enregistrements qui logging
peut être sérialisée lorsqu'elle est envoyée sur le fil. Cela peut ressembler à ça :
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)
Deuxièmement, la configuration de la journalisation doit être adaptée pour utiliser logging.handlers.HTTPHandler
(ce que le Chronologer comprend).
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)
Enfin, exécutez l'instance de Chronologer, par exemple en utilisant 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
Et relancez les requêtes :
session.get('https://httpbin.org/user-agent')
session.get('https://httpbin.org/status/200')
Le gestionnaire de flux va produire :
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
Maintenant, si vous ouvrez http://localhost:8080/ (utilisez "logger" comme nom d'utilisateur et un mot de passe vide pour la popup d'authentification de base) et cliquez sur le bouton "Ouvrir", vous devriez voir quelque chose comme ça :