Repository: open-telemetry/opentelemetry-python-contrib

Issue:
Add more features for adding HTTP request / response headers to spans.
I already have https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1172 open for this, and I'll be breaking it in to smaller pieces at @lzchen 's request.

**Is your feature request related to a problem?**
Currently, you can only provide a list of full HTTP request / response header names to be added to the span.

There is also no capacity for header value redaction.

**Describe the solution you'd like**
It would be nice to be able to specify a regex or "all" to get all headers.

Header value redaction is also a must-have for us.

**Describe alternatives you've considered**
I considered doing this in my application, but it makes more sense to add it here.



Discussion:

Resolving patch:
diff --git a/util/opentelemetry-util-http/src/opentelemetry/util/http/__init__.py b/util/opentelemetry-util-http/src/opentelemetry/util/http/__init__.py
--- a/util/opentelemetry-util-http/src/opentelemetry/util/http/__init__.py
+++ b/util/opentelemetry-util-http/src/opentelemetry/util/http/__init__.py
@@ -13,6 +13,7 @@
 # limitations under the License.
 
 from os import environ
+from re import IGNORECASE as RE_IGNORECASE
 from re import compile as re_compile
 from re import search
 from typing import Iterable, List
@@ -20,6 +21,9 @@
 
 from opentelemetry.semconv.trace import SpanAttributes
 
+OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS = (
+    "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS"
+)
 OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST = (
     "OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST"
 )
@@ -60,6 +64,22 @@ def url_disabled(self, url: str) -> bool:
         return bool(self._excluded_urls and search(self._regex, url))
 
 
+class SanitizeValue:
+    """Class to sanitize (remove sensitive data from) certain headers (given as a list of regexes)"""
+
+    def __init__(self, sanitized_fields: Iterable[str]):
+        self._sanitized_fields = sanitized_fields
+        if self._sanitized_fields:
+            self._regex = re_compile("|".join(sanitized_fields), RE_IGNORECASE)
+
+    def sanitize_header_value(self, header: str, value: str) -> str:
+        return (
+            "[REDACTED]"
+            if (self._sanitized_fields and search(self._regex, header))
+            else value
+        )
+
+
 _root = r"OTEL_PYTHON_{}"
 
 
@@ -90,7 +110,7 @@ def get_excluded_urls(instrumentation: str) -> ExcludeList:
 
 def parse_excluded_urls(excluded_urls: str) -> ExcludeList:
     """
-    Small helper to put an arbitrary url list inside of ExcludeList
+    Small helper to put an arbitrary url list inside an ExcludeList
     """
     if excluded_urls:
         excluded_url_list = [


Test patch:
diff --git a/util/opentelemetry-util-http/tests/test_capture_custom_headers.py b/util/opentelemetry-util-http/tests/test_capture_custom_headers.py
--- a/util/opentelemetry-util-http/tests/test_capture_custom_headers.py
+++ b/util/opentelemetry-util-http/tests/test_capture_custom_headers.py
@@ -16,8 +16,10 @@
 from unittest.mock import patch
 
 from opentelemetry.util.http import (
+    OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS,
     OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST,
     OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE,
+    SanitizeValue,
     get_custom_headers,
     normalise_request_header_name,
     normalise_response_header_name,
@@ -58,6 +60,48 @@ def test_get_custom_response_header(self):
             ],
         )
 
+    @patch.dict(
+        "os.environ",
+        {
+            OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: "My-Secret-Header,My-Secret-Header-2"
+        },
+    )
+    def test_get_custom_sanitize_header(self):
+        sanitized_fields = get_custom_headers(
+            OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS
+        )
+        self.assertEqual(
+            sanitized_fields,
+            ["My-Secret-Header", "My-Secret-Header-2"],
+        )
+
+    @patch.dict(
+        "os.environ",
+        {
+            OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: "My-Secret-Header,My-Secret-Header-2"
+        },
+    )
+    def test_sanitize(self):
+        sanitized_fields = get_custom_headers(
+            OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS
+        )
+
+        sanitize = SanitizeValue(sanitized_fields)
+
+        self.assertEqual(
+            sanitize.sanitize_header_value(
+                header="My-Secret-Header", value="My-Secret-Value"
+            ),
+            "[REDACTED]",
+        )
+
+        self.assertEqual(
+            sanitize.sanitize_header_value(
+                header="My-Not-Secret-Header", value="My-Not-Secret-Value"
+            ),
+            "My-Not-Secret-Value",
+        )
+
     def test_normalise_request_header_name(self):
         key = normalise_request_header_name("Test-Header")
         self.assertEqual(key, "http.request.header.test_header")