Skip to content

Commit 6c7cb72

Browse files
committed
introduces capture_headers config option (#392)
fixes #365 closes #392
1 parent d215428 commit 6c7cb72

File tree

9 files changed

+99
-10
lines changed

9 files changed

+99
-10
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* Added `transaction.type` to errors (#391)
99
* Added parsing of `/proc/self/cgroup` to capture container meta data (#352)
1010
* Added option to configure logging for Flask using a log level (#344)
11+
* Added `capture_headers` config option (#392)
1112

1213
## v4.0.3
1314
[Check the diff](https://github.com/elastic/apm-agent-python/compare/v4.0.2...v4.0.3)

docs/configuration.asciidoc

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,24 @@ It contains the name of the field, and the name of the uploaded file, if provide
375375
WARNING: request bodies often contain sensitive values like passwords, credit card numbers etc.
376376
If your service handles data like this, we advise to only enable this feature with care.
377377

378+
[float]
379+
[[config-capture-headers]]
380+
==== `capture_headers`
381+
382+
|============
383+
| Environment | Django/Flask | Default
384+
| `ELASTIC_APM_CAPTURE_HEADERS` | `CAPTURE_HEADERS` | `true`
385+
|============
386+
387+
For transactions and errors that happen due to HTTP requests,
388+
the Python agent can optionally capture the request and response headers.
389+
390+
Possible values: `true`, `false`
391+
392+
WARNING: request headers often contain sensitive values like session IDs, cookies etc.
393+
See our documentation on <<sanitizing-data, sanitizing data>> for more information on how to
394+
filter such data.
395+
378396
[float]
379397
[[config-transaction-max-spans]]
380398
==== `transaction_max_spans`

docs/tuning.asciidoc

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,4 +95,17 @@ There are four settings you can modify to control this behavior:
9595

9696
As you can see, these settings are divided between app frames, which represent your application code, and library frames, which represent the code of your dependencies. Each of these categories are also split into separate error and span settings.
9797

98-
Reading source files inside a running application can cause a lot of disk I/O, and sending up source lines for each frame will have a network and storage cost that is quite high. Turning down these limits will help prevent excessive memory usage.
98+
Reading source files inside a running application can cause a lot of disk I/O, and sending up source lines for each frame will have a network and storage cost that is quite high. Turning down these limits will help prevent excessive memory usage.
99+
100+
[float]
101+
[[tuning-body-headers]]
102+
=== Collecting headers and request body
103+
104+
You can configure the Elastic APM agent to capture headers of both requests and responses (<<config-capture-headers,`capture_headers`>>),
105+
as well as request bodies (<<config-capture-body,`capture_body`>>).
106+
By default, capturing request bodies is disabled.
107+
Enabling it for transactions may introduce noticeable overhead, as well as increased storage use, depending on the nature of your POST requests.
108+
In most scenarios, we advise against enabling request body capturing for transactions, and only enable it if necessary for errors.
109+
110+
Capturing request/response headers has less overhead on the agent, but can have an impact on storage use.
111+
If storage use is a problem for you, it might be worth disabling.

elasticapm/conf/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ class Config(_ConfigBase):
251251
disable_send = _BoolConfigValue("DISABLE_SEND", default=False)
252252
instrument = _BoolConfigValue("DISABLE_INSTRUMENTATION", default=True)
253253
enable_distributed_tracing = _BoolConfigValue("ENABLE_DISTRIBUTED_TRACING", default=True)
254+
capture_headers = _BoolConfigValue("CAPTURE_HEADERS", default=True)
254255

255256

256257
def setup_logging(handler, exclude=("gunicorn", "south", "elasticapm.errors")):

elasticapm/contrib/django/client.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,11 +98,12 @@ def get_user_info(self, request):
9898
def get_data_from_request(self, request, capture_body=False):
9999
result = {
100100
"env": dict(get_environ(request.META)),
101-
"headers": dict(get_headers(request.META)),
102101
"method": request.method,
103102
"socket": {"remote_address": request.META.get("REMOTE_ADDR"), "encrypted": request.is_secure()},
104103
"cookies": dict(request.COOKIES),
105104
}
105+
if self.config.capture_headers:
106+
result["headers"] = dict(get_headers(request.META))
106107

107108
if request.method in constants.HTTP_WITH_BODY:
108109
content_type = request.META.get("CONTENT_TYPE")
@@ -141,7 +142,7 @@ def get_data_from_request(self, request, capture_body=False):
141142
def get_data_from_response(self, response):
142143
result = {"status_code": response.status_code}
143144

144-
if hasattr(response, "items"):
145+
if self.config.capture_headers and hasattr(response, "items"):
145146
result["headers"] = dict(response.items())
146147
return result
147148

elasticapm/contrib/flask/__init__.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,9 @@ def handle_exception(self, *args, **kwargs):
9393
exc_info=kwargs.get("exc_info"),
9494
context={
9595
"request": get_data_from_request(
96-
request, capture_body=self.client.config.capture_body in ("errors", "all")
96+
request,
97+
capture_body=self.client.config.capture_body in ("errors", "all"),
98+
capture_headers=self.client.config.capture_headers,
9799
)
98100
},
99101
custom={"app": self.app},
@@ -168,11 +170,15 @@ def request_finished(self, app, response):
168170
rule = build_name_with_http_method_prefix(rule, request)
169171
elasticapm.set_context(
170172
lambda: get_data_from_request(
171-
request, capture_body=self.client.config.capture_body in ("transactions", "all")
173+
request,
174+
capture_body=self.client.config.capture_body in ("transactions", "all"),
175+
capture_headers=self.client.config.capture_headers,
172176
),
173177
"request",
174178
)
175-
elasticapm.set_context(lambda: get_data_from_response(response), "response")
179+
elasticapm.set_context(
180+
lambda: get_data_from_response(response, capture_headers=self.client.config.capture_headers), "response"
181+
)
176182
if response.status_code:
177183
result = "HTTP {}xx".format(response.status_code // 100)
178184
else:

elasticapm/contrib/flask/utils.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@
55
from elasticapm.utils.wsgi import get_environ, get_headers
66

77

8-
def get_data_from_request(request, capture_body=False):
8+
def get_data_from_request(request, capture_body=False, capture_headers=True):
99
result = {
1010
"env": dict(get_environ(request.environ)),
11-
"headers": dict(get_headers(request.environ)),
1211
"method": request.method,
1312
"socket": {"remote_address": request.environ.get("REMOTE_ADDR"), "encrypted": request.is_secure},
1413
"cookies": request.cookies,
1514
}
15+
if capture_headers:
16+
result["headers"] = dict(get_headers(request.environ))
1617
if request.method in constants.HTTP_WITH_BODY:
1718
body = None
1819
if request.content_type == "application/x-www-form-urlencoded":
@@ -37,13 +38,13 @@ def get_data_from_request(request, capture_body=False):
3738
return result
3839

3940

40-
def get_data_from_response(response):
41+
def get_data_from_response(response, capture_headers=True):
4142
result = {}
4243

4344
if isinstance(getattr(response, "status_code", None), compat.integer_types):
4445
result["status_code"] = response.status_code
4546

46-
if getattr(response, "headers", None):
47+
if capture_headers and getattr(response, "headers", None):
4748
headers = response.headers
4849
result["headers"] = {key: ";".join(headers.getlist(key)) for key in compat.iterkeys(headers)}
4950
return result

tests/contrib/django/django_tests.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1358,6 +1358,26 @@ def test_capture_files(client, django_elasticapm_client):
13581358
assert error["context"]["request"]["body"] == "[REDACTED]"
13591359

13601360

1361+
@pytest.mark.parametrize(
1362+
"django_elasticapm_client", [{"capture_headers": "true"}, {"capture_headers": "false"}], indirect=True
1363+
)
1364+
def test_capture_headers(client, django_elasticapm_client):
1365+
with pytest.raises(MyException), override_settings(
1366+
**middleware_setting(django.VERSION, ["elasticapm.contrib.django.middleware.TracingMiddleware"])
1367+
):
1368+
client.post(reverse("elasticapm-raise-exc"), **{"HTTP_SOME_HEADER": "foo"})
1369+
error = django_elasticapm_client.events[ERROR][0]
1370+
transaction = django_elasticapm_client.events[TRANSACTION][0]
1371+
if django_elasticapm_client.config.capture_headers:
1372+
assert error["context"]["request"]["headers"]["some-header"] == "foo"
1373+
assert transaction["context"]["request"]["headers"]["some-header"] == "foo"
1374+
assert "headers" in transaction["context"]["response"]
1375+
else:
1376+
assert "headers" not in error["context"]["request"]
1377+
assert "headers" not in transaction["context"]["request"]
1378+
assert "headers" not in transaction["context"]["response"]
1379+
1380+
13611381
@pytest.mark.parametrize("django_elasticapm_client", [{"capture_body": "transactions"}], indirect=True)
13621382
def test_options_request(client, django_elasticapm_client):
13631383
with override_settings(

tests/contrib/flask/flask_tests.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,34 @@ def test_options_request(flask_apm_client):
258258
assert transactions[0]["context"]["request"]["method"] == "OPTIONS"
259259

260260

261+
@pytest.mark.parametrize(
262+
"elasticapm_client", [{"capture_headers": "true"}, {"capture_headers": "false"}], indirect=True
263+
)
264+
def test_capture_headers_errors(flask_apm_client):
265+
resp = flask_apm_client.app.test_client().post("/an-error/", headers={"some-header": "foo"})
266+
resp.close()
267+
error = flask_apm_client.client.events[ERROR][0]
268+
if flask_apm_client.client.config.capture_headers:
269+
assert error["context"]["request"]["headers"]["some-header"] == "foo"
270+
else:
271+
assert "headers" not in error["context"]["request"]
272+
273+
274+
@pytest.mark.parametrize(
275+
"elasticapm_client", [{"capture_headers": "true"}, {"capture_headers": "false"}], indirect=True
276+
)
277+
def test_capture_headers_transactions(flask_apm_client):
278+
resp = flask_apm_client.app.test_client().post("/users/", headers={"some-header": "foo"})
279+
resp.close()
280+
transaction = flask_apm_client.client.events[TRANSACTION][0]
281+
if flask_apm_client.client.config.capture_headers:
282+
assert transaction["context"]["request"]["headers"]["some-header"] == "foo"
283+
assert transaction["context"]["response"]["headers"]["foo"] == "bar;baz"
284+
else:
285+
assert "headers" not in transaction["context"]["request"]
286+
assert "headers" not in transaction["context"]["response"]
287+
288+
261289
def test_streaming_response(flask_apm_client):
262290
resp = flask_apm_client.app.test_client().get("/streaming/")
263291
assert resp.data == b"01234"

0 commit comments

Comments
 (0)