Mit einem Skript oder sogar einem Subsystem einer Anwendung für ein Netzwerkprotokoll-Debugging ist es wünschenswert zu sehen, welche Anforderungs-Antwort-Paare genau sind, einschließlich effektiver URLs, Header, Nutzdaten und des Status. Und es ist normalerweise unpraktisch, individuelle Anfragen überall zu instrumentieren. Gleichzeitig gibt es Leistungsüberlegungen, die die Verwendung einzelner (oder weniger spezialisierter) Vorschläge vorschlagen. requests.Session
Im Folgenden wird daher davon ausgegangen, dass der Vorschlag vorliegt befolgt wird.
requests
unterstützt sogenannte Event-Hooks (ab 2.23 gibt es eigentlich nur noch Hooksresponse
). Es ist im Grunde ein Ereignis-Listener, und das Ereignis wird ausgegeben, bevor die Kontrolle von zurückgegeben wird requests.request
. In diesem Moment sind sowohl Anforderung als auch Antwort vollständig definiert und können daher protokolliert werden.
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)
Auf diese Weise werden im Grunde alle HTTP-Roundtrips einer Sitzung protokolliert.
Formatieren von HTTP-Roundtrip-Protokolldatensätzen
Damit die obige Protokollierung nützlich ist, kann es einen speziellen Protokollierungsformatierer geben , der die Protokollierungsdatensätze versteht req
und zusätzliche Funktionen bietet res
. Es kann so aussehen:
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])
Wenn Sie nun einige Anfragen mit dem folgenden Befehl ausführen session
:
session.get('https://httpbin.org/user-agent')
session.get('https://httpbin.org/status/200')
Die Ausgabe von stderr
sieht wie folgt aus.
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
Ein GUI-Weg
Wenn Sie viele Fragen haben, ist eine einfache Benutzeroberfläche und eine Möglichkeit zum Filtern von Datensätzen hilfreich. Ich werde zeigen, Chronologer dafür zu verwenden (von dem ich der Autor bin).
Zunächst muss der Hook neu geschrieben werden, um Datensätze zu erstellen, logging
die beim Senden über das Kabel serialisiert werden können. Es kann so aussehen:
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)
Zweitens muss die Protokollierungskonfiguration an die Verwendung angepasst werden logging.handlers.HTTPHandler
(was Chronologer versteht).
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)
Führen Sie abschließend die Chronologer-Instanz aus. zB mit 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
Führen Sie die Anforderungen erneut aus:
session.get('https://httpbin.org/user-agent')
session.get('https://httpbin.org/status/200')
Der Stream-Handler erzeugt:
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
Wenn Sie nun http: // localhost: 8080 / öffnen (verwenden Sie "logger" als Benutzernamen und leeres Kennwort für das grundlegende Authentifizierungs-Popup) und auf die Schaltfläche "Öffnen" klicken, sollten Sie Folgendes sehen: