|
11 | 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
12 | 12 | # See the License for the specific language governing permissions and |
13 | 13 | # limitations under the License. |
| 14 | + |
14 | 15 | import json |
15 | 16 | import typing |
16 | 17 | from unittest import mock |
@@ -609,3 +610,344 @@ def test_no_op_tracer_provider(self): |
609 | 610 | response = self.perform_request(self.HTTP_URL) |
610 | 611 | self.assertEqual(b"Hello!", response.data) |
611 | 612 | self.assert_span(num_spans=0) |
| 613 | + |
| 614 | + def test_custom_response_headers_captured(self): |
| 615 | + URLLib3Instrumentor().uninstrument() |
| 616 | + URLLib3Instrumentor().instrument( |
| 617 | + captured_response_headers=["X-Custom-Header", "X-Another-Header"] |
| 618 | + ) |
| 619 | + |
| 620 | + response_headers = { |
| 621 | + "X-Custom-Header": "custom-value", |
| 622 | + "X-Another-Header": "another-value", |
| 623 | + } |
| 624 | + url = "http://mock//capture_headers" |
| 625 | + httpretty.register_uri( |
| 626 | + httpretty.GET, url, body="Hello!", adding_headers=response_headers |
| 627 | + ) |
| 628 | + self.perform_request(url) |
| 629 | + |
| 630 | + span = self.assert_span(num_spans=1) |
| 631 | + self.assertEqual( |
| 632 | + span.attributes["http.response.header.x_custom_header"], |
| 633 | + ("custom-value",), |
| 634 | + ) |
| 635 | + self.assertEqual( |
| 636 | + span.attributes["http.response.header.x_another_header"], |
| 637 | + ("another-value",), |
| 638 | + ) |
| 639 | + self.assertNotIn( |
| 640 | + "http.response.header.x_excluded_header", span.attributes |
| 641 | + ) |
| 642 | + |
| 643 | + def test_custom_headers_not_captured_when_not_configured(self): |
| 644 | + """Test that headers are not captured when env vars are not set.""" |
| 645 | + URLLib3Instrumentor().uninstrument() |
| 646 | + URLLib3Instrumentor().instrument() |
| 647 | + |
| 648 | + self.perform_request( |
| 649 | + self.HTTP_URL, |
| 650 | + headers={"X-Request-Header": "request-value"}, |
| 651 | + ) |
| 652 | + |
| 653 | + span = self.assert_span(num_spans=1) |
| 654 | + self.assertNotIn( |
| 655 | + "http.request.header.x_request_header", span.attributes |
| 656 | + ) |
| 657 | + self.assertNotIn( |
| 658 | + "http.response.header.x_response_header", span.attributes |
| 659 | + ) |
| 660 | + |
| 661 | + def test_sensitive_headers_sanitized(self): |
| 662 | + """Test that sensitive header values are redacted.""" |
| 663 | + URLLib3Instrumentor().uninstrument() |
| 664 | + URLLib3Instrumentor().instrument( |
| 665 | + captured_request_headers=["Authorization", "X-Api-Key"], |
| 666 | + captured_response_headers=["Set-Cookie", "X-Secret"], |
| 667 | + sensitive_headers=[ |
| 668 | + "Authorization", |
| 669 | + "X-Api-Key", |
| 670 | + "Set-Cookie", |
| 671 | + "X-Secret", |
| 672 | + ], |
| 673 | + ) |
| 674 | + |
| 675 | + response_headers = { |
| 676 | + "Set-Cookie": "session=abc123", |
| 677 | + "X-Secret": "secret", |
| 678 | + } |
| 679 | + url = "http://mock//capture_headers" |
| 680 | + httpretty.register_uri( |
| 681 | + httpretty.GET, url, body="Hello!", adding_headers=response_headers |
| 682 | + ) |
| 683 | + self.perform_request( |
| 684 | + url, |
| 685 | + headers={ |
| 686 | + "Authorization": "Bearer secret-token", |
| 687 | + "X-Api-Key": "secret-key", |
| 688 | + }, |
| 689 | + ) |
| 690 | + |
| 691 | + span = self.assert_span(num_spans=1) |
| 692 | + self.assertEqual( |
| 693 | + span.attributes["http.request.header.authorization"], |
| 694 | + ("[REDACTED]",), |
| 695 | + ) |
| 696 | + self.assertEqual( |
| 697 | + span.attributes["http.request.header.x_api_key"], |
| 698 | + ("[REDACTED]",), |
| 699 | + ) |
| 700 | + self.assertEqual( |
| 701 | + span.attributes["http.response.header.set_cookie"], |
| 702 | + ("[REDACTED]",), |
| 703 | + ) |
| 704 | + self.assertEqual( |
| 705 | + span.attributes["http.response.header.x_secret"], |
| 706 | + ("[REDACTED]",), |
| 707 | + ) |
| 708 | + |
| 709 | + def test_custom_headers_with_regex(self): |
| 710 | + """Test that header capture works with regex patterns.""" |
| 711 | + URLLib3Instrumentor().uninstrument() |
| 712 | + URLLib3Instrumentor().instrument( |
| 713 | + captured_request_headers=["X-Custom-Request-.*"], |
| 714 | + captured_response_headers=["X-Custom-Response-.*"], |
| 715 | + ) |
| 716 | + |
| 717 | + response_headers = { |
| 718 | + "X-Custom-Response-A": "value-A", |
| 719 | + "X-Custom-Response-B": "value-B", |
| 720 | + "X-Other-Response-Header": "other-value", |
| 721 | + } |
| 722 | + url = "http://mock//capture_headers" |
| 723 | + httpretty.register_uri( |
| 724 | + httpretty.GET, url, body="Hello!", adding_headers=response_headers |
| 725 | + ) |
| 726 | + self.perform_request( |
| 727 | + url, |
| 728 | + headers={ |
| 729 | + "X-Custom-Request-One": "value-one", |
| 730 | + "X-Custom-Request-Two": "value-two", |
| 731 | + "X-Other-Request-Header": "other-value", |
| 732 | + }, |
| 733 | + ) |
| 734 | + |
| 735 | + span = self.assert_span(num_spans=1) |
| 736 | + self.assertEqual( |
| 737 | + span.attributes["http.request.header.x_custom_request_one"], |
| 738 | + ("value-one",), |
| 739 | + ) |
| 740 | + self.assertEqual( |
| 741 | + span.attributes["http.request.header.x_custom_request_two"], |
| 742 | + ("value-two",), |
| 743 | + ) |
| 744 | + self.assertNotIn( |
| 745 | + "http.request.header.x_other_request_header", span.attributes |
| 746 | + ) |
| 747 | + self.assertEqual( |
| 748 | + span.attributes["http.response.header.x_custom_response_a"], |
| 749 | + ("value-A",), |
| 750 | + ) |
| 751 | + self.assertEqual( |
| 752 | + span.attributes["http.response.header.x_custom_response_b"], |
| 753 | + ("value-B",), |
| 754 | + ) |
| 755 | + self.assertNotIn( |
| 756 | + "http.response.header.x_other_response_header", span.attributes |
| 757 | + ) |
| 758 | + |
| 759 | + def test_custom_headers_case_insensitive(self): |
| 760 | + """Test that header capture is case-insensitive.""" |
| 761 | + URLLib3Instrumentor().uninstrument() |
| 762 | + URLLib3Instrumentor().instrument( |
| 763 | + captured_request_headers=["x-request-header"], |
| 764 | + captured_response_headers=["x-response-header"], |
| 765 | + ) |
| 766 | + |
| 767 | + response_headers = {"X-ReSPoNse-HeaDER": "custom-value"} |
| 768 | + url = "http://mock//capture_headers" |
| 769 | + httpretty.register_uri( |
| 770 | + httpretty.GET, url, body="Hello!", adding_headers=response_headers |
| 771 | + ) |
| 772 | + self.perform_request( |
| 773 | + url, |
| 774 | + headers={"X-ReQuESt-HeaDER": "custom-value"}, |
| 775 | + ) |
| 776 | + |
| 777 | + span = self.assert_span(num_spans=1) |
| 778 | + self.assertEqual( |
| 779 | + span.attributes["http.request.header.x_request_header"], |
| 780 | + ("custom-value",), |
| 781 | + ) |
| 782 | + self.assertEqual( |
| 783 | + span.attributes["http.response.header.x_response_header"], |
| 784 | + ("custom-value",), |
| 785 | + ) |
| 786 | + |
| 787 | + def test_standard_http_headers_captured(self): |
| 788 | + """Test that standard HTTP headers can be captured.""" |
| 789 | + URLLib3Instrumentor().uninstrument() |
| 790 | + URLLib3Instrumentor().instrument( |
| 791 | + captured_request_headers=["Content-Type", "Accept"], |
| 792 | + captured_response_headers=["Content-Type", "Server"], |
| 793 | + ) |
| 794 | + |
| 795 | + response_headers = { |
| 796 | + "Content-Type": "text/plain", |
| 797 | + "Server": "TestServer/1.0", |
| 798 | + } |
| 799 | + url = "http://mock//capture_headers" |
| 800 | + httpretty.register_uri( |
| 801 | + httpretty.GET, url, body="Hello!", adding_headers=response_headers |
| 802 | + ) |
| 803 | + self.perform_request( |
| 804 | + url, |
| 805 | + headers={ |
| 806 | + "Content-Type": "application/json", |
| 807 | + "Accept": "application/json", |
| 808 | + }, |
| 809 | + ) |
| 810 | + |
| 811 | + span = self.assert_span(num_spans=1) |
| 812 | + self.assertEqual( |
| 813 | + span.attributes["http.request.header.content_type"], |
| 814 | + ("application/json",), |
| 815 | + ) |
| 816 | + self.assertEqual( |
| 817 | + span.attributes["http.request.header.accept"], |
| 818 | + ("application/json",), |
| 819 | + ) |
| 820 | + self.assertEqual( |
| 821 | + span.attributes["http.response.header.content_type"], |
| 822 | + ("text/plain",), |
| 823 | + ) |
| 824 | + self.assertEqual( |
| 825 | + span.attributes["http.response.header.server"], |
| 826 | + ("TestServer/1.0",), |
| 827 | + ) |
| 828 | + |
| 829 | + def test_capture_all_request_headers(self): |
| 830 | + """Test that all request headers can be captured with .* pattern.""" |
| 831 | + URLLib3Instrumentor().uninstrument() |
| 832 | + URLLib3Instrumentor().instrument(captured_request_headers=[".*"]) |
| 833 | + |
| 834 | + self.perform_request( |
| 835 | + self.HTTP_URL, |
| 836 | + headers={ |
| 837 | + "X-Header-One": "value1", |
| 838 | + "X-Header-Two": "value2", |
| 839 | + "X-Header-Three": "value3", |
| 840 | + }, |
| 841 | + ) |
| 842 | + |
| 843 | + span = self.assert_span(num_spans=1) |
| 844 | + self.assertEqual( |
| 845 | + span.attributes["http.request.header.x_header_one"], |
| 846 | + ("value1",), |
| 847 | + ) |
| 848 | + self.assertEqual( |
| 849 | + span.attributes["http.request.header.x_header_two"], |
| 850 | + ("value2",), |
| 851 | + ) |
| 852 | + self.assertEqual( |
| 853 | + span.attributes["http.request.header.x_header_three"], |
| 854 | + ("value3",), |
| 855 | + ) |
| 856 | + |
| 857 | + def test_capture_all_response_headers(self): |
| 858 | + """Test that all response headers can be captured with .* pattern.""" |
| 859 | + URLLib3Instrumentor().uninstrument() |
| 860 | + URLLib3Instrumentor().instrument(captured_response_headers=[".*"]) |
| 861 | + |
| 862 | + response_headers = { |
| 863 | + "X-Response-One": "value1", |
| 864 | + "X-Response-Two": "value2", |
| 865 | + "X-Response-Three": "value3", |
| 866 | + } |
| 867 | + url = "http://mock//capture_headers" |
| 868 | + httpretty.register_uri( |
| 869 | + httpretty.GET, url, body="Hello!", adding_headers=response_headers |
| 870 | + ) |
| 871 | + self.perform_request(url) |
| 872 | + |
| 873 | + span = self.assert_span(num_spans=1) |
| 874 | + self.assertEqual( |
| 875 | + span.attributes["http.response.header.x_response_one"], |
| 876 | + ("value1",), |
| 877 | + ) |
| 878 | + self.assertEqual( |
| 879 | + span.attributes["http.response.header.x_response_two"], |
| 880 | + ("value2",), |
| 881 | + ) |
| 882 | + self.assertEqual( |
| 883 | + span.attributes["http.response.header.x_response_three"], |
| 884 | + ("value3",), |
| 885 | + ) |
| 886 | + |
| 887 | + def test_sanitize_with_regex_pattern(self): |
| 888 | + """Test that sanitization works with regex patterns.""" |
| 889 | + URLLib3Instrumentor().uninstrument() |
| 890 | + URLLib3Instrumentor().instrument( |
| 891 | + captured_request_headers=["X-Test.*"], |
| 892 | + sensitive_headers=[".*secret.*"], |
| 893 | + ) |
| 894 | + |
| 895 | + self.perform_request( |
| 896 | + self.HTTP_URL, |
| 897 | + headers={ |
| 898 | + "X-Test": "normal-value", |
| 899 | + "X-Test-Secret": "secret-value", |
| 900 | + }, |
| 901 | + ) |
| 902 | + |
| 903 | + span = self.assert_span(num_spans=1) |
| 904 | + self.assertEqual( |
| 905 | + span.attributes["http.request.header.x_test"], |
| 906 | + ("normal-value",), |
| 907 | + ) |
| 908 | + self.assertEqual( |
| 909 | + span.attributes["http.request.header.x_test_secret"], |
| 910 | + ("[REDACTED]",), |
| 911 | + ) |
| 912 | + |
| 913 | + @mock.patch.dict( |
| 914 | + "os.environ", |
| 915 | + { |
| 916 | + "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_REQUEST": "x-request-one,x-request-two", |
| 917 | + "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_CLIENT_RESPONSE": "x-response-one", |
| 918 | + "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS": "x-request-two", |
| 919 | + }, |
| 920 | + ) |
| 921 | + def test_capture_and_sanitize_environment_variables(self): |
| 922 | + URLLib3Instrumentor().uninstrument() |
| 923 | + URLLib3Instrumentor().instrument() |
| 924 | + |
| 925 | + response_headers = { |
| 926 | + "X-Response-One": "value1", |
| 927 | + "X-Response-Two": "value2", |
| 928 | + } |
| 929 | + url = "http://mock//capture_headers" |
| 930 | + httpretty.register_uri( |
| 931 | + httpretty.GET, url, body="Hello!", adding_headers=response_headers |
| 932 | + ) |
| 933 | + self.perform_request( |
| 934 | + url, headers={"x-request-one": "one", "x-request-two": "two"} |
| 935 | + ) |
| 936 | + |
| 937 | + span = self.assert_span(num_spans=1) |
| 938 | + self.assertEqual( |
| 939 | + span.attributes["http.request.header.x_request_one"], |
| 940 | + ("one",), |
| 941 | + ) |
| 942 | + self.assertEqual( |
| 943 | + span.attributes["http.request.header.x_request_two"], |
| 944 | + ("[REDACTED]",), |
| 945 | + ) |
| 946 | + self.assertEqual( |
| 947 | + span.attributes["http.response.header.x_response_one"], |
| 948 | + ("value1",), |
| 949 | + ) |
| 950 | + self.assertNotIn( |
| 951 | + "http.response.header.x_response_two", |
| 952 | + span.attributes, |
| 953 | + ) |
0 commit comments