Ayant un script ou même 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 efficaces, les en-têtes, les charges utiles et l'état. Et il est généralement peu pratique d'instrumenter des demandes individuelles partout. Dans le même temps, il existe des considérations de performances qui suggèrent d'utiliser un seul (ou quelques spécialistes) requests.Session
, de sorte que ce qui suit suppose que la suggestion est suivie.
requests
prend en charge ce que l'on appelle les hooks d'événement (à partir de 2.23, il n'y a en fait qu'un response
hook). Il s'agit essentiellement d'un écouteur d'événement, et l'événement est émis avant de renvoyer le contrôle requests.request
. À ce moment, 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 essentiellement comment enregistrer tous les aller-retour HTTP d'une session.
Formatage des enregistrements de journal HTTP aller-retour
Pour que la journalisation ci-dessus soit utile, il peut y avoir un formateur de journalisation spécialisé qui comprend req
et des res
extras sur les enregistrements de journalisation. 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
ressemblera à ceci.
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 manière GUI
Lorsque vous avez beaucoup de requêtes, il est pratique de disposer d'une interface utilisateur simple et d'un moyen de filtrer les enregistrements. Je vais montrer comment utiliser Chronologer pour cela (dont je suis l'auteur).
Tout d'abord, le hook a été réécrit pour produire des enregistrements qui logging
peuvent être sérialisés lors de l'envoi sur le fil. Cela peut ressembler à ceci:
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 à l'utilisation logging.handlers.HTTPHandler
(ce que 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 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 exécutez à nouveau les requêtes:
session.get('https://httpbin.org/user-agent')
session.get('https://httpbin.org/status/200')
Le gestionnaire de flux produira:
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" pour le nom d'utilisateur et un mot de passe vide pour la fenêtre contextuelle d'authentification de base) et cliquez sur le bouton "Ouvrir", vous devriez voir quelque chose comme: