Skip to content

Commit 6bae3ef

Browse files
committed
process PR feedback
1 parent 5008ad8 commit 6bae3ef

File tree

21 files changed

+1169
-492
lines changed

21 files changed

+1169
-492
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,8 @@ jobs:
1515
runs-on: ubuntu-latest
1616
strategy:
1717
matrix:
18-
python: ["3.7", "3.8", "3.9", "3.10"]
18+
python: ["3.8", "3.9", "3.10"]
1919
django: ["3.2", "4.1"]
20-
exclude:
21-
- python: "3.7"
22-
django: "4.1"
2320

2421
name: Run the test suite (Python ${{ matrix.python }}, Django ${{ matrix.django }})
2522

README.rst

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,16 @@ To use this with your project you need to follow these steps:
9797
}
9898
9999
LOG_OUTGOING_REQUESTS_DB_SAVE = True # save logs enabled/disabled based on the boolean value
100-
LOG_OUTGOING_REQUESTS_SAVE_BODY = True # save request/response body
101-
LOG_OUTGOING_REQUESTS_LOG_BODY_TO_STDOUT = True # log request/response body to STDOUT
100+
LOG_OUTGOING_REQUESTS_DB_SAVE_BODY = True # save request/response body
101+
LOG_OUTGOING_REQUESTS_EMIT_BODY = True # log request/response body
102+
LOG_OUTGOING_REQUESTS_CONTENT_TYPES = [
103+
"text/*",
104+
"application/json",
105+
"application/xml",
106+
"application/soap+xml",
107+
] # save request/response bodies with matching content type
108+
LOG_OUTGOING_REQUESTS_MAX_CONTENT_LENGTH = 524_288 # maximal size (in bytes) for the request/response body
109+
LOG_OUTGOING_REQUESTS_LOG_BODY_TO_STDOUT = True
102110
103111
104112
#. Run the migrations
@@ -115,8 +123,9 @@ To use this with your project you need to follow these steps:
115123
res = requests.get("https://httpbin.org/json")
116124
print(res.json())
117125
118-
#. Check stdout for the printable output, and navigate to ``/admin/log_outgoing_requests/outgoingrequestslog/`` to see
119-
the saved log records. The settings for saving logs can by overridden under ``/admin/log_outgoing_requests/outgoingrequestslogconfig/``.
126+
#. Check stdout for the printable output, and navigate to ``Admin > Miscellaneous > Outgoing Requests Logs``
127+
to see the saved log records. In order to override the settings for saving logs, navigate to
128+
``Admin > Miscellaneous > Outgoing Requests Log Configuration``.
120129

121130

122131
Local development

docs/quickstart.rst

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,16 @@ Installation
5858
}
5959
6060
LOG_OUTGOING_REQUESTS_DB_SAVE = True # save logs enabled/disabled based on the boolean value
61-
LOG_OUTGOING_REQUESTS_SAVE_BODY = True # save request/response body
62-
LOG_OUTGOING_REQUESTS_LOG_BODY_TO_STDOUT = True # log request/response body to STDOUT
61+
LOG_OUTGOING_REQUESTS_DB_SAVE_BODY = True # save request/response body
62+
LOG_OUTGOING_REQUESTS_EMIT_BODY = True # log request/response body
63+
LOG_OUTGOING_REQUESTS_CONTENT_TYPES = [
64+
"text/*",
65+
"application/json",
66+
"application/xml",
67+
"application/soap+xml",
68+
] # save request/response bodies with matching content type
69+
LOG_OUTGOING_REQUESTS_MAX_CONTENT_LENGTH = 524_288 # maximal size (in bytes) for the request/response body
70+
LOG_OUTGOING_REQUESTS_LOG_BODY_TO_STDOUT = True
6371
6472
6573
#. Run ``python manage.py migrate`` to create the necessary database tables.

log_outgoing_requests/admin.py

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
1+
from django import forms
2+
from django.conf import settings
13
from django.contrib import admin
4+
from django.shortcuts import get_object_or_404
25
from django.utils.translation import gettext as _
36

4-
from solo.admin import SingletonModelAdmin
7+
from solo.admin import SingletonModelAdmin # type: ignore
58

69
from .models import OutgoingRequestsLog, OutgoingRequestsLogConfig
710

811

9-
@admin.display(description="Response body")
10-
def response_body(obj):
11-
return f"{obj}".upper()
12-
13-
1412
@admin.register(OutgoingRequestsLog)
1513
class OutgoingRequestsLogAdmin(admin.ModelAdmin):
1614
fields = (
@@ -26,8 +24,6 @@ class OutgoingRequestsLogAdmin(admin.ModelAdmin):
2624
"res_content_type",
2725
"req_headers",
2826
"res_headers",
29-
"req_body",
30-
"res_body",
3127
"trace",
3228
)
3329
readonly_fields = fields
@@ -40,10 +36,11 @@ class OutgoingRequestsLogAdmin(admin.ModelAdmin):
4036
"response_ms",
4137
"timestamp",
4238
)
43-
list_filter = ("method", "status_code", "hostname", "timestamp")
39+
list_filter = ("method", "timestamp", "status_code", "hostname")
4440
search_fields = ("url", "params", "hostname")
4541
date_hierarchy = "timestamp"
4642
show_full_result_count = False
43+
change_form_template = "log_outgoing_requests/change_form.html"
4744

4845
def has_add_permission(self, request):
4946
return False
@@ -54,21 +51,41 @@ def has_change_permission(self, request, obj=None):
5451
def query_params(self, obj):
5552
return obj.query_params
5653

57-
query_params.short_description = _("Query parameters")
54+
def change_view(self, request, object_id, extra_context=None):
55+
"""
56+
Add log object to to context for use in template.
57+
"""
58+
log = get_object_or_404(OutgoingRequestsLog, id=object_id)
59+
60+
extra_context = extra_context or {}
61+
extra_context["log"] = log
62+
63+
return super().change_view(request, object_id, extra_context=extra_context)
64+
65+
query_params.short_description = _("Query parameters") # type: ignore
5866

5967
class Media:
6068
css = {
6169
"all": ("log_outgoing_requests/css/admin.css",),
6270
}
6371

6472

73+
class ConfigAdminForm(forms.ModelForm):
74+
class Meta:
75+
model = OutgoingRequestsLogConfig
76+
fields = "__all__"
77+
widgets = {"allowed_content_types": forms.CheckboxSelectMultiple}
78+
help_texts = {
79+
"save_to_db": _(
80+
"Whether request logs should be saved to the database (default: {default})."
81+
).format(default=settings.LOG_OUTGOING_REQUESTS_DB_SAVE),
82+
"save_body": _(
83+
"Wheter the body of the request and response should be logged (default: "
84+
"{default})."
85+
).format(default=settings.LOG_OUTGOING_REQUESTS_DB_SAVE_BODY),
86+
}
87+
88+
6589
@admin.register(OutgoingRequestsLogConfig)
6690
class OutgoingRequestsLogConfigAdmin(SingletonModelAdmin):
67-
fields = (
68-
"save_to_db",
69-
"save_body",
70-
)
71-
list_display = (
72-
"save_to_db",
73-
"save_body",
74-
)
91+
form = ConfigAdminForm

log_outgoing_requests/constants.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from django.db import models
2+
from django.utils.translation import gettext_lazy as _
3+
4+
5+
class SaveLogsChoice(models.TextChoices):
6+
use_default = "use_default", _("Use default")
7+
yes = "yes", _("Yes")
8+
no = "no", _("No")
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
"""
2+
Datastructure(s) for use in settings.py
3+
4+
Note: do not place any Django-specific imports in this file, as
5+
it must be imported in settings.py.
6+
"""
7+
8+
from dataclasses import dataclass
9+
10+
11+
@dataclass
12+
class ContentType:
13+
"""
14+
Data class for keeping track of content types and associated default encodings
15+
"""
16+
17+
pattern: str
18+
default_encoding: str

log_outgoing_requests/formatters.py

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,32 +8,35 @@ class HttpFormatter(logging.Formatter):
88
def _formatHeaders(self, d):
99
return "\n".join(f"{k}: {v}" for k, v in d.items())
1010

11-
def _formatBody(self, content: dict, request_or_response: str) -> str:
12-
if settings.LOG_OUTGOING_REQUESTS_LOG_BODY_TO_STDOUT:
11+
def _formatBody(self, content: str, request_or_response: str) -> str:
12+
if settings.LOG_OUTGOING_REQUESTS_EMIT_BODY:
1313
return f"\n{request_or_response} body:\n{content}"
1414
return ""
1515

1616
def formatMessage(self, record):
1717
result = super().formatMessage(record)
18-
if record.name == "requests":
19-
result += textwrap.dedent(
20-
"""
21-
---------------- request ----------------
22-
{req.method} {req.url}
23-
{reqhdrs} {request_body}
2418

25-
---------------- response ----------------
26-
{res.status_code} {res.reason} {res.url}
27-
{reshdrs} {response_body}
19+
if record.name != "requests":
20+
return result
2821

22+
result += textwrap.dedent(
2923
"""
30-
).format(
31-
req=record.req,
32-
res=record.res,
33-
reqhdrs=self._formatHeaders(record.req.headers),
34-
reshdrs=self._formatHeaders(record.res.headers),
35-
request_body=self._formatBody(record.req.body, "Request"),
36-
response_body=self._formatBody(record.res.json(), "Response"),
37-
)
24+
---------------- request ----------------
25+
{req.method} {req.url}
26+
{reqhdrs} {request_body}
27+
28+
---------------- response ----------------
29+
{res.status_code} {res.reason} {res.url}
30+
{reshdrs} {response_body}
31+
32+
"""
33+
).format(
34+
req=record.req,
35+
res=record.res,
36+
reqhdrs=self._formatHeaders(record.req.headers),
37+
reshdrs=self._formatHeaders(record.res.headers),
38+
request_body=self._formatBody(record.req.body, "Request"),
39+
response_body=self._formatBody(record.res.content, "Response"),
40+
)
3841

3942
return result

log_outgoing_requests/handlers.py

Lines changed: 36 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,70 +2,71 @@
22
import traceback
33
from urllib.parse import urlparse
44

5-
from django.conf import settings
6-
7-
ALLOWED_CONTENT_TYPES = [
8-
"application/json",
9-
"multipart/form-data",
10-
"text/html",
11-
"text/plain",
12-
"",
13-
None,
14-
]
5+
from .utils import (
6+
check_content_length,
7+
check_content_type,
8+
get_default_encoding,
9+
parse_content_type_header,
10+
)
1511

1612

1713
class DatabaseOutgoingRequestsHandler(logging.Handler):
1814
def emit(self, record):
19-
from .models import OutgoingRequestsLogConfig
15+
from .models import OutgoingRequestsLog, OutgoingRequestsLogConfig
2016

2117
config = OutgoingRequestsLogConfig.get_solo()
2218

23-
if config.save_to_db or settings.LOG_OUTGOING_REQUESTS_DB_SAVE:
24-
from .models import OutgoingRequestsLog
25-
26-
trace = None
19+
if config.save_logs_enabled:
20+
trace = ""
2721

2822
# skip requests not coming from the library requests
2923
if not record or not record.getMessage() == "Outgoing request":
3024
return
3125

32-
# skip requests with non-allowed content
33-
request_content_type = record.req.headers.get("Content-Type", "")
34-
response_content_type = record.res.headers.get("Content-Type", "")
35-
36-
if not (
37-
request_content_type in ALLOWED_CONTENT_TYPES
38-
and response_content_type in ALLOWED_CONTENT_TYPES
39-
):
40-
return
41-
42-
safe_req_headers = record.req.headers.copy()
26+
scrubbed_req_headers = record.req.headers.copy()
4327

44-
if "Authorization" in safe_req_headers:
45-
safe_req_headers["Authorization"] = "***hidden***"
28+
if "Authorization" in scrubbed_req_headers:
29+
scrubbed_req_headers["Authorization"] = "***hidden***"
4630

4731
if record.exc_info:
4832
trace = traceback.format_exc()
4933

5034
parsed_url = urlparse(record.req.url)
5135
kwargs = {
5236
"url": record.req.url,
53-
"hostname": parsed_url.hostname,
37+
"hostname": parsed_url.netloc,
5438
"params": parsed_url.params,
5539
"status_code": record.res.status_code,
5640
"method": record.req.method,
57-
"req_content_type": record.req.headers.get("Content-Type", ""),
58-
"res_content_type": record.res.headers.get("Content-Type", ""),
5941
"timestamp": record.requested_at,
6042
"response_ms": int(record.res.elapsed.total_seconds() * 1000),
61-
"req_headers": self.format_headers(safe_req_headers),
43+
"req_headers": self.format_headers(scrubbed_req_headers),
6244
"res_headers": self.format_headers(record.res.headers),
6345
"trace": trace,
6446
}
6547

66-
if config.save_body or settings.LOG_OUTGOING_REQUESTS_SAVE_BODY:
67-
kwargs["req_body"] = (record.req.body,)
68-
kwargs["res_body"] = (record.res.json(),)
48+
if config.save_body_enabled:
49+
# check request
50+
content_type, encoding = parse_content_type_header(record.req)
51+
if check_content_type(content_type) and check_content_length(
52+
record.req, config
53+
):
54+
kwargs["req_content_type"] = content_type
55+
kwargs["req_body"] = record.req.body or b""
56+
kwargs["req_body_encoding"] = encoding or get_default_encoding(
57+
content_type
58+
)
59+
60+
# check response
61+
content_type, encoding = parse_content_type_header(record.res)
62+
if check_content_type(content_type) and check_content_length(
63+
record.res, config
64+
):
65+
kwargs["res_content_type"] = content_type
66+
kwargs["res_body"] = record.res.content or b""
67+
kwargs[
68+
"res_body_encoding"
69+
] = record.res.encoding or get_default_encoding(content_type)
6970

7071
OutgoingRequestsLog.objects.create(**kwargs)
7172

0 commit comments

Comments
 (0)