|
69 | 69 | from opentelemetry.test.mock_textmap import MockTextMapPropagator |
70 | 70 | from opentelemetry.test.test_base import TestBase |
71 | 71 | from opentelemetry.trace import StatusCode |
72 | | -from opentelemetry.util.http import get_excluded_urls |
| 72 | +from opentelemetry.util.http import ( |
| 73 | + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST, |
| 74 | + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE, |
| 75 | + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS, |
| 76 | + get_excluded_urls, |
| 77 | +) |
73 | 78 |
|
74 | 79 |
|
75 | 80 | class TransportMock: |
@@ -717,6 +722,227 @@ def test_if_headers_equals_none(self): |
717 | 722 | self.assertEqual(result.text, "Hello!") |
718 | 723 | self.assert_span() |
719 | 724 |
|
| 725 | + @mock.patch.dict( |
| 726 | + "os.environ", |
| 727 | + { |
| 728 | + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST: "X-Custom-Header,X-Another-Header", |
| 729 | + }, |
| 730 | + ) |
| 731 | + def test_custom_request_headers_captured(self): |
| 732 | + """Test that specified request headers are captured as span attributes.""" |
| 733 | + RequestsInstrumentor().uninstrument() |
| 734 | + RequestsInstrumentor().instrument() |
| 735 | + |
| 736 | + headers = { |
| 737 | + "X-Custom-Header": "custom-value", |
| 738 | + "X-Another-Header": "another-value", |
| 739 | + "X-Excluded-Header": "excluded-value", |
| 740 | + } |
| 741 | + httpretty.register_uri(httpretty.GET, self.URL, body="Hello!") |
| 742 | + result = requests.get(self.URL, headers=headers, timeout=5) |
| 743 | + self.assertEqual(result.text, "Hello!") |
| 744 | + |
| 745 | + span = self.assert_span() |
| 746 | + self.assertEqual( |
| 747 | + span.attributes["http.request.header.x_custom_header"], |
| 748 | + ("custom-value",), |
| 749 | + ) |
| 750 | + self.assertEqual( |
| 751 | + span.attributes["http.request.header.x_another_header"], |
| 752 | + ("another-value",), |
| 753 | + ) |
| 754 | + self.assertNotIn("http.request.x_excluded_header", span.attributes) |
| 755 | + |
| 756 | + @mock.patch.dict( |
| 757 | + "os.environ", |
| 758 | + { |
| 759 | + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE: "X-Custom-Header,X-Another-Header", |
| 760 | + }, |
| 761 | + ) |
| 762 | + def test_custom_response_headers_captured(self): |
| 763 | + """Test that specified request headers are captured as span attributes.""" |
| 764 | + RequestsInstrumentor().uninstrument() |
| 765 | + RequestsInstrumentor().instrument() |
| 766 | + |
| 767 | + headers = { |
| 768 | + "X-Custom-Header": "custom-value", |
| 769 | + "X-Another-Header": "another-value", |
| 770 | + "X-Excluded-Header": "excluded-value", |
| 771 | + } |
| 772 | + httpretty.register_uri( |
| 773 | + httpretty.GET, self.URL, body="Hello!", adding_headers=headers |
| 774 | + ) |
| 775 | + result = requests.get(self.URL, timeout=5) |
| 776 | + self.assertEqual(result.text, "Hello!") |
| 777 | + |
| 778 | + span = self.assert_span() |
| 779 | + self.assertEqual( |
| 780 | + span.attributes["http.response.header.x_custom_header"], |
| 781 | + ("custom-value",), |
| 782 | + ) |
| 783 | + self.assertEqual( |
| 784 | + span.attributes["http.response.header.x_another_header"], |
| 785 | + ("another-value",), |
| 786 | + ) |
| 787 | + self.assertNotIn("http.response.x_excluded_header", span.attributes) |
| 788 | + |
| 789 | + @mock.patch.dict("os.environ", {}) |
| 790 | + def test_custom_headers_not_captured_when_not_configured(self): |
| 791 | + """Test that headers are not captured when env vars are not set.""" |
| 792 | + RequestsInstrumentor().uninstrument() |
| 793 | + RequestsInstrumentor().instrument() |
| 794 | + headers = {"X-Request-Header": "request-value"} |
| 795 | + httpretty.register_uri( |
| 796 | + httpretty.GET, |
| 797 | + self.URL, |
| 798 | + body="Hello!", |
| 799 | + adding_headers={"X-Response-Header": "response-value"}, |
| 800 | + ) |
| 801 | + result = requests.get(self.URL, headers=headers, timeout=5) |
| 802 | + self.assertEqual(result.text, "Hello!") |
| 803 | + |
| 804 | + span = self.assert_span() |
| 805 | + self.assertNotIn( |
| 806 | + "http.request.header.x_request_header", span.attributes |
| 807 | + ) |
| 808 | + self.assertNotIn( |
| 809 | + "http.response.header.x_response_header", span.attributes |
| 810 | + ) |
| 811 | + |
| 812 | + @mock.patch.dict( |
| 813 | + "os.environ", |
| 814 | + { |
| 815 | + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE: "Set-Cookie,X-Secret", |
| 816 | + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST: "Authorization,X-Api-Key", |
| 817 | + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: "Authorization,X-Api-Key,Set-Cookie,X-Secret", |
| 818 | + }, |
| 819 | + ) |
| 820 | + def test_sensitive_headers_sanitized(self): |
| 821 | + """Test that sensitive header values are redacted.""" |
| 822 | + RequestsInstrumentor().uninstrument() |
| 823 | + RequestsInstrumentor().instrument() |
| 824 | + |
| 825 | + request_headers = { |
| 826 | + "Authorization": "Bearer secret-token", |
| 827 | + "X-Api-Key": "secret-key", |
| 828 | + } |
| 829 | + response_headers = { |
| 830 | + "Set-Cookie": "session=abc123", |
| 831 | + "X-Secret": "secret", |
| 832 | + } |
| 833 | + httpretty.register_uri( |
| 834 | + httpretty.GET, |
| 835 | + self.URL, |
| 836 | + body="Hello!", |
| 837 | + adding_headers=response_headers, |
| 838 | + ) |
| 839 | + result = requests.get(self.URL, headers=request_headers, timeout=5) |
| 840 | + self.assertEqual(result.text, "Hello!") |
| 841 | + |
| 842 | + span = self.assert_span() |
| 843 | + self.assertEqual( |
| 844 | + span.attributes["http.request.header.authorization"], |
| 845 | + ("[REDACTED]",), |
| 846 | + ) |
| 847 | + self.assertEqual( |
| 848 | + span.attributes["http.request.header.x_api_key"], |
| 849 | + ("[REDACTED]",), |
| 850 | + ) |
| 851 | + self.assertEqual( |
| 852 | + span.attributes["http.response.header.set_cookie"], |
| 853 | + ("[REDACTED]",), |
| 854 | + ) |
| 855 | + self.assertEqual( |
| 856 | + span.attributes["http.response.header.x_secret"], |
| 857 | + ("[REDACTED]",), |
| 858 | + ) |
| 859 | + |
| 860 | + @mock.patch.dict( |
| 861 | + "os.environ", |
| 862 | + { |
| 863 | + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE: "X-Custom-Response-.*", |
| 864 | + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST: "X-Custom-Request-.*", |
| 865 | + }, |
| 866 | + ) |
| 867 | + def test_custom_headers_with_regex(self): |
| 868 | + """Test that header capture works with regex patterns.""" |
| 869 | + RequestsInstrumentor().uninstrument() |
| 870 | + RequestsInstrumentor().instrument() |
| 871 | + request_headers = { |
| 872 | + "X-Custom-Request-One": "value-one", |
| 873 | + "X-Custom-Request-Two": "value-two", |
| 874 | + "X-Other-Request-Header": "other-value", |
| 875 | + } |
| 876 | + response_headers = { |
| 877 | + "X-Custom-Response-A": "value-A", |
| 878 | + "X-Custom-Response-B": "value-B", |
| 879 | + "X-Other-Response-Header": "other-value", |
| 880 | + } |
| 881 | + httpretty.register_uri( |
| 882 | + httpretty.GET, |
| 883 | + self.URL, |
| 884 | + body="Hello!", |
| 885 | + adding_headers=response_headers, |
| 886 | + ) |
| 887 | + result = requests.get(self.URL, headers=request_headers, timeout=5) |
| 888 | + self.assertEqual(result.text, "Hello!") |
| 889 | + |
| 890 | + span = self.assert_span() |
| 891 | + self.assertEqual( |
| 892 | + span.attributes["http.request.header.x_custom_request_one"], |
| 893 | + ("value-one",), |
| 894 | + ) |
| 895 | + self.assertEqual( |
| 896 | + span.attributes["http.request.header.x_custom_request_two"], |
| 897 | + ("value-two",), |
| 898 | + ) |
| 899 | + self.assertNotIn( |
| 900 | + "http.request.header.x_other_request_header", span.attributes |
| 901 | + ) |
| 902 | + self.assertEqual( |
| 903 | + span.attributes["http.response.header.x_custom_response_a"], |
| 904 | + ("value-A",), |
| 905 | + ) |
| 906 | + self.assertEqual( |
| 907 | + span.attributes["http.response.header.x_custom_response_b"], |
| 908 | + ("value-B",), |
| 909 | + ) |
| 910 | + self.assertNotIn( |
| 911 | + "http.response.header.x_other_response_header", span.attributes |
| 912 | + ) |
| 913 | + |
| 914 | + @mock.patch.dict( |
| 915 | + "os.environ", |
| 916 | + { |
| 917 | + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE: "x-response-header", |
| 918 | + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST: "x-request-header", |
| 919 | + }, |
| 920 | + ) |
| 921 | + def test_custom_headers_case_insensitive(self): |
| 922 | + """Test that header capture is case-insensitive.""" |
| 923 | + RequestsInstrumentor().uninstrument() |
| 924 | + RequestsInstrumentor().instrument() |
| 925 | + request_headers = {"X-ReQuESt-HeaDER": "custom-value"} |
| 926 | + response_headers = {"X-ReSPoNse-HeaDER": "custom-value"} |
| 927 | + httpretty.register_uri( |
| 928 | + httpretty.GET, |
| 929 | + self.URL, |
| 930 | + body="Hello!", |
| 931 | + adding_headers=response_headers, |
| 932 | + ) |
| 933 | + result = requests.get(self.URL, headers=request_headers, timeout=5) |
| 934 | + self.assertEqual(result.text, "Hello!") |
| 935 | + |
| 936 | + span = self.assert_span() |
| 937 | + self.assertEqual( |
| 938 | + span.attributes["http.request.header.x_request_header"], |
| 939 | + ("custom-value",), |
| 940 | + ) |
| 941 | + self.assertEqual( |
| 942 | + span.attributes["http.response.header.x_response_header"], |
| 943 | + ("custom-value",), |
| 944 | + ) |
| 945 | + |
720 | 946 |
|
721 | 947 | class TestRequestsIntegrationPreparedRequest( |
722 | 948 | RequestsIntegrationTestBase, TestBase |
|
0 commit comments