Skip to content

Commit c60a7e4

Browse files
authored
Flask: Capture custom request/response headers as span attributes (#952)
* Capture request/response headers for flask * Update changelog and fixed lint errors
1 parent dbb35a2 commit c60a7e4

File tree

4 files changed

+123
-0
lines changed

4 files changed

+123
-0
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
- `opentelemetry-instrumentation-wsgi` Capture custom request/response headers in span attributes
1111
([#925])(https://github.com/open-telemetry/opentelemetry-python-contrib/pull/925)
1212

13+
- `opentelemetry-instrumentation-flask` Flask: Capture custom request/response headers in span attributes
14+
([#952])(https://github.com/open-telemetry/opentelemetry-python-contrib/pull/952)
15+
1316
### Added
1417

1518
- `opentelemetry-instrumentation-sqlalchemy` added experimental sql commenter capability

instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/__init__.py

+8
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,10 @@ def _start_response(status, response_headers, *args, **kwargs):
153153
otel_wsgi.add_response_attributes(
154154
span, status, response_headers
155155
)
156+
if span.kind == trace.SpanKind.SERVER:
157+
otel_wsgi.add_custom_response_headers(
158+
span, response_headers
159+
)
156160
else:
157161
_logger.warning(
158162
"Flask environ's OpenTelemetry span "
@@ -200,6 +204,10 @@ def _before_request():
200204
] = flask.request.url_rule.rule
201205
for key, value in attributes.items():
202206
span.set_attribute(key, value)
207+
if span.kind == trace.SpanKind.SERVER:
208+
otel_wsgi.add_custom_request_headers(
209+
span, flask_request_environ
210+
)
203211

204212
activation = trace.use_span(span, end_on_exit=True)
205213
activation.__enter__() # pylint: disable=E1101

instrumentation/opentelemetry-instrumentation-flask/tests/base_test.py

+14
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
from flask import Response
1516
from werkzeug.test import Client
1617
from werkzeug.wrappers import BaseResponse
1718

@@ -23,6 +24,16 @@ def _hello_endpoint(helloid):
2324
raise ValueError(":-(")
2425
return "Hello: " + str(helloid)
2526

27+
@staticmethod
28+
def _custom_response_headers():
29+
resp = Response("test response")
30+
resp.headers["content-type"] = "text/plain; charset=utf-8"
31+
resp.headers["content-length"] = "13"
32+
resp.headers[
33+
"my-custom-header"
34+
] = "my-custom-value-1,my-custom-header-2"
35+
return resp
36+
2637
def _common_initialization(self):
2738
def excluded_endpoint():
2839
return "excluded"
@@ -35,6 +46,9 @@ def excluded2_endpoint():
3546
self.app.route("/excluded/<int:helloid>")(self._hello_endpoint)
3647
self.app.route("/excluded")(excluded_endpoint)
3748
self.app.route("/excluded2")(excluded2_endpoint)
49+
self.app.route("/test_custom_response_headers")(
50+
self._custom_response_headers
51+
)
3852

3953
# pylint: disable=attribute-defined-outside-init
4054
self.client = Client(self.app, BaseResponse)

instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py

+98
Original file line numberDiff line numberDiff line change
@@ -442,3 +442,101 @@ def test_mark_span_internal_in_presence_of_span_from_other_framework(self):
442442
self.assertEqual(
443443
span_list[0].parent.span_id, span_list[1].context.span_id
444444
)
445+
446+
447+
class TestCustomRequestResponseHeaders(
448+
InstrumentationTest, TestBase, WsgiTestBase
449+
):
450+
def setUp(self):
451+
super().setUp()
452+
453+
self.env_patch = patch.dict(
454+
"os.environ",
455+
{
456+
"OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST": "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3",
457+
"OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE": "content-type,content-length,my-custom-header,invalid-header",
458+
},
459+
)
460+
self.env_patch.start()
461+
self.app = Flask(__name__)
462+
FlaskInstrumentor().instrument_app(self.app)
463+
464+
self._common_initialization()
465+
466+
def tearDown(self):
467+
super().tearDown()
468+
self.env_patch.stop()
469+
with self.disable_logging():
470+
FlaskInstrumentor().uninstrument_app(self.app)
471+
472+
def test_custom_request_header_added_in_server_span(self):
473+
headers = {
474+
"Custom-Test-Header-1": "Test Value 1",
475+
"Custom-Test-Header-2": "TestValue2,TestValue3",
476+
}
477+
resp = self.client.get("/hello/123", headers=headers)
478+
self.assertEqual(200, resp.status_code)
479+
span = self.memory_exporter.get_finished_spans()[0]
480+
expected = {
481+
"http.request.header.custom_test_header_1": ("Test Value 1",),
482+
"http.request.header.custom_test_header_2": (
483+
"TestValue2,TestValue3",
484+
),
485+
}
486+
self.assertEqual(span.kind, trace.SpanKind.SERVER)
487+
self.assertSpanHasAttributes(span, expected)
488+
489+
def test_custom_request_header_not_added_in_internal_span(self):
490+
tracer = trace.get_tracer(__name__)
491+
with tracer.start_as_current_span("test", kind=trace.SpanKind.SERVER):
492+
headers = {
493+
"Custom-Test-Header-1": "Test Value 1",
494+
"Custom-Test-Header-2": "TestValue2,TestValue3",
495+
}
496+
resp = self.client.get("/hello/123", headers=headers)
497+
self.assertEqual(200, resp.status_code)
498+
span = self.memory_exporter.get_finished_spans()[0]
499+
not_expected = {
500+
"http.request.header.custom_test_header_1": ("Test Value 1",),
501+
"http.request.header.custom_test_header_2": (
502+
"TestValue2,TestValue3",
503+
),
504+
}
505+
self.assertEqual(span.kind, trace.SpanKind.INTERNAL)
506+
for key, _ in not_expected.items():
507+
self.assertNotIn(key, span.attributes)
508+
509+
def test_custom_response_header_added_in_server_span(self):
510+
resp = self.client.get("/test_custom_response_headers")
511+
self.assertEqual(resp.status_code, 200)
512+
span = self.memory_exporter.get_finished_spans()[0]
513+
expected = {
514+
"http.response.header.content_type": (
515+
"text/plain; charset=utf-8",
516+
),
517+
"http.response.header.content_length": ("13",),
518+
"http.response.header.my_custom_header": (
519+
"my-custom-value-1,my-custom-header-2",
520+
),
521+
}
522+
self.assertEqual(span.kind, trace.SpanKind.SERVER)
523+
self.assertSpanHasAttributes(span, expected)
524+
525+
def test_custom_response_header_not_added_in_internal_span(self):
526+
tracer = trace.get_tracer(__name__)
527+
with tracer.start_as_current_span("test", kind=trace.SpanKind.SERVER):
528+
resp = self.client.get("/test_custom_response_headers")
529+
self.assertEqual(resp.status_code, 200)
530+
span = self.memory_exporter.get_finished_spans()[0]
531+
not_expected = {
532+
"http.response.header.content_type": (
533+
"text/plain; charset=utf-8",
534+
),
535+
"http.response.header.content_length": ("13",),
536+
"http.response.header.my_custom_header": (
537+
"my-custom-value-1,my-custom-header-2",
538+
),
539+
}
540+
self.assertEqual(span.kind, trace.SpanKind.INTERNAL)
541+
for key, _ in not_expected.items():
542+
self.assertNotIn(key, span.attributes)

0 commit comments

Comments
 (0)