Al tener un script o incluso un subsistema de una aplicación para la depuración de un protocolo de red, se desea ver qué pares de solicitud-respuesta son exactamente, incluidas las URL efectivas, los encabezados, las cargas útiles y el estado. Y normalmente no es práctico instrumentar solicitudes individuales en todo el lugar. Al mismo tiempo, existen consideraciones de desempeño que sugieren el uso de una sola (o pocas especializadas) requests.Session
, por lo que lo siguiente supone que se sigue la sugerencia .
requests
admite los llamados ganchos de eventos (a partir de 2.23 en realidad solo hay response
enganches). Es básicamente un detector de eventos y el evento se emite antes de devolver el control requests.request
. En este momento, tanto la solicitud como la respuesta están completamente definidas, por lo que se pueden registrar.
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)
Así es básicamente cómo registrar todos los viajes de ida y vuelta HTTP de una sesión.
Formateo de registros de registro de ida y vuelta HTTP
Para que el registro anterior sea útil, puede haber un formateador de registro especializado que comprenda req
y res
haga extras en los registros de registro. Puede verse así:
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])
Ahora, si haces algunas solicitudes usando el session
, como:
session.get('https://httpbin.org/user-agent')
session.get('https://httpbin.org/status/200')
La salida stderr
se verá de la siguiente manera.
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
Una forma de GUI
Cuando tienes muchas consultas, tener una interfaz de usuario simple y una forma de filtrar registros resulta útil. Mostraré el uso de Chronologer para eso (del cual soy autor).
Primero, el gancho se ha reescrito para producir registros que logging
puedan serializarse cuando se envían por cable. Puede verse así:
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)
En segundo lugar, la configuración de registro debe adaptarse para su uso logging.handlers.HTTPHandler
(lo que Chronologer entiende).
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)
Finalmente, ejecute la instancia de Chronologer. por ejemplo, usando 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
Y vuelva a ejecutar las solicitudes:
session.get('https://httpbin.org/user-agent')
session.get('https://httpbin.org/status/200')
El controlador de flujo producirá:
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
Ahora, si abre http: // localhost: 8080 / (use "logger" para el nombre de usuario y la contraseña vacía para la ventana emergente de autenticación básica) y hace clic en el botón "Abrir", debería ver algo como: