Skip to content

Commit b1bf8d4

Browse files
sanketmehta28lzchensrikanthccv
authored
code change to add custom http and websocket request and response hea… (#1004)
* code change to add custom http and websocket request and response headers as span attributes. Issue: #919 * adding entry to changelog * changes after running "tox -e generate" locally * - added server_span.is_recording() in _get_otel_send() just to make sure the span is recording before adding the attributes to span. - changed span to current_span to make sure attributes are being added to proper span. * removed commented code Co-authored-by: Leighton Chen <[email protected]> Co-authored-by: Srikanth Chekuri <[email protected]>
1 parent f8b877e commit b1bf8d4

File tree

3 files changed

+342
-1
lines changed

3 files changed

+342
-1
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2222

2323
## [1.10.0-0.29b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.10.0-0.29b0) - 2022-03-10
2424

25+
- `opentelemetry-instrumentation-wsgi` Capture custom request/response headers in span attributes
26+
([#1004])(https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1004)
27+
2528
- `opentelemetry-instrumentation-wsgi` Capture custom request/response headers in span attributes
2629
([#925])(https://github.com/open-telemetry/opentelemetry-python-contrib/pull/925)
2730
- `opentelemetry-instrumentation-flask` Flask: Capture custom request/response headers in span attributes

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

+62-1
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,14 @@ def client_response_hook(span: Span, message: dict):
115115
from opentelemetry.semconv.trace import SpanAttributes
116116
from opentelemetry.trace import Span, set_span_in_context
117117
from opentelemetry.trace.status import Status, StatusCode
118-
from opentelemetry.util.http import remove_url_credentials
118+
from opentelemetry.util.http import (
119+
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST,
120+
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE,
121+
get_custom_headers,
122+
normalise_request_header_name,
123+
normalise_response_header_name,
124+
remove_url_credentials,
125+
)
119126

120127
_ServerRequestHookT = typing.Optional[typing.Callable[[Span, dict], None]]
121128
_ClientRequestHookT = typing.Optional[typing.Callable[[Span, dict], None]]
@@ -223,6 +230,41 @@ def collect_request_attributes(scope):
223230
return result
224231

225232

233+
def collect_custom_request_headers_attributes(scope):
234+
"""returns custom HTTP request headers to be added into SERVER span as span attributes
235+
Refer specification https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-request-and-response-headers"""
236+
237+
attributes = {}
238+
custom_request_headers = get_custom_headers(
239+
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST
240+
)
241+
242+
for header in custom_request_headers:
243+
values = asgi_getter.get(scope, header)
244+
if values:
245+
key = normalise_request_header_name(header)
246+
attributes.setdefault(key, []).extend(values)
247+
248+
return attributes
249+
250+
251+
def collect_custom_response_headers_attributes(message):
252+
"""returns custom HTTP response headers to be added into SERVER span as span attributes
253+
Refer specification https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-request-and-response-headers"""
254+
attributes = {}
255+
custom_response_headers = get_custom_headers(
256+
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE
257+
)
258+
259+
for header in custom_response_headers:
260+
values = asgi_getter.get(message, header)
261+
if values:
262+
key = normalise_response_header_name(header)
263+
attributes.setdefault(key, []).extend(values)
264+
265+
return attributes
266+
267+
226268
def get_host_port_url_tuple(scope):
227269
"""Returns (host, port, full_url) tuple."""
228270
server = scope.get("server") or ["0.0.0.0", 80]
@@ -342,6 +384,13 @@ async def __call__(self, scope, receive, send):
342384
for key, value in attributes.items():
343385
current_span.set_attribute(key, value)
344386

387+
if current_span.kind == trace.SpanKind.SERVER:
388+
custom_attributes = (
389+
collect_custom_request_headers_attributes(scope)
390+
)
391+
if len(custom_attributes) > 0:
392+
current_span.set_attributes(custom_attributes)
393+
345394
if callable(self.server_request_hook):
346395
self.server_request_hook(current_span, scope)
347396

@@ -395,6 +444,18 @@ async def otel_send(message):
395444
set_status_code(server_span, 200)
396445
set_status_code(send_span, 200)
397446
send_span.set_attribute("type", message["type"])
447+
if (
448+
server_span.is_recording()
449+
and server_span.kind == trace.SpanKind.SERVER
450+
and "headers" in message
451+
):
452+
custom_response_attributes = (
453+
collect_custom_response_headers_attributes(message)
454+
)
455+
if len(custom_response_attributes) > 0:
456+
server_span.set_attributes(
457+
custom_response_attributes
458+
)
398459

399460
propagator = get_global_response_propagator()
400461
if propagator:

instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py

+277
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@
3131
)
3232
from opentelemetry.test.test_base import TestBase
3333
from opentelemetry.trace import SpanKind, format_span_id, format_trace_id
34+
from opentelemetry.util.http import (
35+
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST,
36+
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE,
37+
)
3438

3539

3640
async def http_app(scope, receive, send):
@@ -62,6 +66,47 @@ async def websocket_app(scope, receive, send):
6266
break
6367

6468

69+
async def http_app_with_custom_headers(scope, receive, send):
70+
message = await receive()
71+
assert scope["type"] == "http"
72+
if message.get("type") == "http.request":
73+
await send(
74+
{
75+
"type": "http.response.start",
76+
"status": 200,
77+
"headers": [
78+
(b"Content-Type", b"text/plain"),
79+
(b"custom-test-header-1", b"test-header-value-1"),
80+
(b"custom-test-header-2", b"test-header-value-2"),
81+
],
82+
}
83+
)
84+
await send({"type": "http.response.body", "body": b"*"})
85+
86+
87+
async def websocket_app_with_custom_headers(scope, receive, send):
88+
assert scope["type"] == "websocket"
89+
while True:
90+
message = await receive()
91+
if message.get("type") == "websocket.connect":
92+
await send(
93+
{
94+
"type": "websocket.accept",
95+
"headers": [
96+
(b"custom-test-header-1", b"test-header-value-1"),
97+
(b"custom-test-header-2", b"test-header-value-2"),
98+
],
99+
}
100+
)
101+
102+
if message.get("type") == "websocket.receive":
103+
if message.get("text") == "ping":
104+
await send({"type": "websocket.send", "text": "pong"})
105+
106+
if message.get("type") == "websocket.disconnect":
107+
break
108+
109+
65110
async def simple_asgi(scope, receive, send):
66111
assert isinstance(scope, dict)
67112
if scope["type"] == "http":
@@ -583,5 +628,237 @@ async def wrapped_app(scope, receive, send):
583628
)
584629

585630

631+
@mock.patch.dict(
632+
"os.environ",
633+
{
634+
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3",
635+
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3",
636+
},
637+
)
638+
class TestCustomHeaders(AsgiTestBase, TestBase):
639+
def setUp(self):
640+
super().setUp()
641+
self.tracer_provider, self.exporter = TestBase.create_tracer_provider()
642+
self.tracer = self.tracer_provider.get_tracer(__name__)
643+
self.app = otel_asgi.OpenTelemetryMiddleware(
644+
simple_asgi, tracer_provider=self.tracer_provider
645+
)
646+
647+
def test_http_custom_request_headers_in_span_attributes(self):
648+
self.scope["headers"].extend(
649+
[
650+
(b"custom-test-header-1", b"test-header-value-1"),
651+
(b"custom-test-header-2", b"test-header-value-2"),
652+
]
653+
)
654+
self.seed_app(self.app)
655+
self.send_default_request()
656+
self.get_all_output()
657+
span_list = self.exporter.get_finished_spans()
658+
expected = {
659+
"http.request.header.custom_test_header_1": (
660+
"test-header-value-1",
661+
),
662+
"http.request.header.custom_test_header_2": (
663+
"test-header-value-2",
664+
),
665+
}
666+
for span in span_list:
667+
if span.kind == SpanKind.SERVER:
668+
self.assertSpanHasAttributes(span, expected)
669+
670+
def test_http_custom_request_headers_not_in_span_attributes(self):
671+
self.scope["headers"].extend(
672+
[
673+
(b"custom-test-header-1", b"test-header-value-1"),
674+
]
675+
)
676+
self.seed_app(self.app)
677+
self.send_default_request()
678+
self.get_all_output()
679+
span_list = self.exporter.get_finished_spans()
680+
expected = {
681+
"http.request.header.custom_test_header_1": (
682+
"test-header-value-1",
683+
),
684+
}
685+
not_expected = {
686+
"http.request.header.custom_test_header_2": (
687+
"test-header-value-2",
688+
),
689+
}
690+
for span in span_list:
691+
if span.kind == SpanKind.SERVER:
692+
self.assertSpanHasAttributes(span, expected)
693+
for key, _ in not_expected.items():
694+
self.assertNotIn(key, span.attributes)
695+
696+
def test_http_custom_response_headers_in_span_attributes(self):
697+
self.app = otel_asgi.OpenTelemetryMiddleware(
698+
http_app_with_custom_headers, tracer_provider=self.tracer_provider
699+
)
700+
self.seed_app(self.app)
701+
self.send_default_request()
702+
self.get_all_output()
703+
span_list = self.exporter.get_finished_spans()
704+
expected = {
705+
"http.response.header.custom_test_header_1": (
706+
"test-header-value-1",
707+
),
708+
"http.response.header.custom_test_header_2": (
709+
"test-header-value-2",
710+
),
711+
}
712+
for span in span_list:
713+
if span.kind == SpanKind.SERVER:
714+
self.assertSpanHasAttributes(span, expected)
715+
716+
def test_http_custom_response_headers_not_in_span_attributes(self):
717+
self.app = otel_asgi.OpenTelemetryMiddleware(
718+
http_app_with_custom_headers, tracer_provider=self.tracer_provider
719+
)
720+
self.seed_app(self.app)
721+
self.send_default_request()
722+
self.get_all_output()
723+
span_list = self.exporter.get_finished_spans()
724+
not_expected = {
725+
"http.response.header.custom_test_header_3": (
726+
"test-header-value-3",
727+
),
728+
}
729+
for span in span_list:
730+
if span.kind == SpanKind.SERVER:
731+
for key, _ in not_expected.items():
732+
self.assertNotIn(key, span.attributes)
733+
734+
def test_websocket_custom_request_headers_in_span_attributes(self):
735+
self.scope = {
736+
"type": "websocket",
737+
"http_version": "1.1",
738+
"scheme": "ws",
739+
"path": "/",
740+
"query_string": b"",
741+
"headers": [
742+
(b"custom-test-header-1", b"test-header-value-1"),
743+
(b"custom-test-header-2", b"test-header-value-2"),
744+
],
745+
"client": ("127.0.0.1", 32767),
746+
"server": ("127.0.0.1", 80),
747+
}
748+
self.seed_app(self.app)
749+
self.send_input({"type": "websocket.connect"})
750+
self.send_input({"type": "websocket.receive", "text": "ping"})
751+
self.send_input({"type": "websocket.disconnect"})
752+
753+
self.get_all_output()
754+
span_list = self.exporter.get_finished_spans()
755+
expected = {
756+
"http.request.header.custom_test_header_1": (
757+
"test-header-value-1",
758+
),
759+
"http.request.header.custom_test_header_2": (
760+
"test-header-value-2",
761+
),
762+
}
763+
for span in span_list:
764+
if span.kind == SpanKind.SERVER:
765+
self.assertSpanHasAttributes(span, expected)
766+
767+
def test_websocket_custom_request_headers_not_in_span_attributes(self):
768+
self.scope = {
769+
"type": "websocket",
770+
"http_version": "1.1",
771+
"scheme": "ws",
772+
"path": "/",
773+
"query_string": b"",
774+
"headers": [
775+
(b"Custom-Test-Header-1", b"test-header-value-1"),
776+
(b"Custom-Test-Header-2", b"test-header-value-2"),
777+
],
778+
"client": ("127.0.0.1", 32767),
779+
"server": ("127.0.0.1", 80),
780+
}
781+
self.seed_app(self.app)
782+
self.send_input({"type": "websocket.connect"})
783+
self.send_input({"type": "websocket.receive", "text": "ping"})
784+
self.send_input({"type": "websocket.disconnect"})
785+
786+
self.get_all_output()
787+
span_list = self.exporter.get_finished_spans()
788+
not_expected = {
789+
"http.request.header.custom_test_header_3": (
790+
"test-header-value-3",
791+
),
792+
}
793+
for span in span_list:
794+
if span.kind == SpanKind.SERVER:
795+
for key, _ in not_expected.items():
796+
self.assertNotIn(key, span.attributes)
797+
798+
def test_websocket_custom_response_headers_in_span_attributes(self):
799+
self.scope = {
800+
"type": "websocket",
801+
"http_version": "1.1",
802+
"scheme": "ws",
803+
"path": "/",
804+
"query_string": b"",
805+
"headers": [],
806+
"client": ("127.0.0.1", 32767),
807+
"server": ("127.0.0.1", 80),
808+
}
809+
self.app = otel_asgi.OpenTelemetryMiddleware(
810+
websocket_app_with_custom_headers,
811+
tracer_provider=self.tracer_provider,
812+
)
813+
self.seed_app(self.app)
814+
self.send_input({"type": "websocket.connect"})
815+
self.send_input({"type": "websocket.receive", "text": "ping"})
816+
self.send_input({"type": "websocket.disconnect"})
817+
self.get_all_output()
818+
span_list = self.exporter.get_finished_spans()
819+
expected = {
820+
"http.response.header.custom_test_header_1": (
821+
"test-header-value-1",
822+
),
823+
"http.response.header.custom_test_header_2": (
824+
"test-header-value-2",
825+
),
826+
}
827+
for span in span_list:
828+
if span.kind == SpanKind.SERVER:
829+
self.assertSpanHasAttributes(span, expected)
830+
831+
def test_websocket_custom_response_headers_not_in_span_attributes(self):
832+
self.scope = {
833+
"type": "websocket",
834+
"http_version": "1.1",
835+
"scheme": "ws",
836+
"path": "/",
837+
"query_string": b"",
838+
"headers": [],
839+
"client": ("127.0.0.1", 32767),
840+
"server": ("127.0.0.1", 80),
841+
}
842+
self.app = otel_asgi.OpenTelemetryMiddleware(
843+
websocket_app_with_custom_headers,
844+
tracer_provider=self.tracer_provider,
845+
)
846+
self.seed_app(self.app)
847+
self.send_input({"type": "websocket.connect"})
848+
self.send_input({"type": "websocket.receive", "text": "ping"})
849+
self.send_input({"type": "websocket.disconnect"})
850+
self.get_all_output()
851+
span_list = self.exporter.get_finished_spans()
852+
not_expected = {
853+
"http.response.header.custom_test_header_3": (
854+
"test-header-value-3",
855+
),
856+
}
857+
for span in span_list:
858+
if span.kind == SpanKind.SERVER:
859+
for key, _ in not_expected.items():
860+
self.assertNotIn(key, span.attributes)
861+
862+
586863
if __name__ == "__main__":
587864
unittest.main()

0 commit comments

Comments
 (0)