feat: configure header extraction for ASGI middleware via constructor params (#2026)
* feat: configure header extraction for ASGI middleware via constructor params * fix django middleware * lint * remove import * Fix lint * Update instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py
This commit is contained in:
parent
a93bd74dc3
commit
4b1a9c75db
|
|
@ -48,6 +48,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
([#1948](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1948))
|
([#1948](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1948))
|
||||||
- Added schema_url (`"https://opentelemetry.io/schemas/1.11.0"`) to all metrics and traces
|
- Added schema_url (`"https://opentelemetry.io/schemas/1.11.0"`) to all metrics and traces
|
||||||
([#1977](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1977))
|
([#1977](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1977))
|
||||||
|
- Add support for configuring ASGI middleware header extraction via runtime constructor parameters
|
||||||
|
([#2026](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2026))
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -189,11 +189,13 @@ API
|
||||||
---
|
---
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import typing
|
import typing
|
||||||
import urllib
|
import urllib
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from timeit import default_timer
|
from timeit import default_timer
|
||||||
from typing import Tuple
|
from typing import Any, Awaitable, Callable, Tuple, cast
|
||||||
|
|
||||||
from asgiref.compatibility import guarantee_single_callable
|
from asgiref.compatibility import guarantee_single_callable
|
||||||
|
|
||||||
|
|
@ -332,55 +334,28 @@ def collect_request_attributes(scope):
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def collect_custom_request_headers_attributes(scope):
|
def collect_custom_headers_attributes(
|
||||||
"""returns custom HTTP request headers to be added into SERVER span as span attributes
|
scope_or_response_message: dict[str, Any],
|
||||||
Refer specification https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-request-and-response-headers
|
sanitize: SanitizeValue,
|
||||||
|
header_regexes: list[str],
|
||||||
|
normalize_names: Callable[[str], str],
|
||||||
|
) -> dict[str, str]:
|
||||||
"""
|
"""
|
||||||
|
Returns custom HTTP request or response headers to be added into SERVER span as span attributes.
|
||||||
|
|
||||||
sanitize = SanitizeValue(
|
Refer specifications:
|
||||||
get_custom_headers(
|
- https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-request-and-response-headers
|
||||||
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS
|
"""
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Decode headers before processing.
|
# Decode headers before processing.
|
||||||
headers = {
|
headers: dict[str, str] = {
|
||||||
_key.decode("utf8"): _value.decode("utf8")
|
_key.decode("utf8"): _value.decode("utf8")
|
||||||
for (_key, _value) in scope.get("headers")
|
for (_key, _value) in scope_or_response_message.get("headers")
|
||||||
|
or cast("list[tuple[bytes, bytes]]", [])
|
||||||
}
|
}
|
||||||
|
|
||||||
return sanitize.sanitize_header_values(
|
return sanitize.sanitize_header_values(
|
||||||
headers,
|
headers,
|
||||||
get_custom_headers(
|
header_regexes,
|
||||||
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST
|
normalize_names,
|
||||||
),
|
|
||||||
normalise_request_header_name,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def collect_custom_response_headers_attributes(message):
|
|
||||||
"""returns custom HTTP response headers to be added into SERVER span as span attributes
|
|
||||||
Refer specification https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-request-and-response-headers
|
|
||||||
"""
|
|
||||||
|
|
||||||
sanitize = SanitizeValue(
|
|
||||||
get_custom_headers(
|
|
||||||
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Decode headers before processing.
|
|
||||||
headers = {
|
|
||||||
_key.decode("utf8"): _value.decode("utf8")
|
|
||||||
for (_key, _value) in message.get("headers")
|
|
||||||
}
|
|
||||||
|
|
||||||
return sanitize.sanitize_header_values(
|
|
||||||
headers,
|
|
||||||
get_custom_headers(
|
|
||||||
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE
|
|
||||||
),
|
|
||||||
normalise_response_header_name,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -493,6 +468,9 @@ class OpenTelemetryMiddleware:
|
||||||
tracer_provider=None,
|
tracer_provider=None,
|
||||||
meter_provider=None,
|
meter_provider=None,
|
||||||
meter=None,
|
meter=None,
|
||||||
|
http_capture_headers_server_request: list[str] | None = None,
|
||||||
|
http_capture_headers_server_response: list[str] | None = None,
|
||||||
|
http_capture_headers_sanitize_fields: list[str] | None = None,
|
||||||
):
|
):
|
||||||
self.app = guarantee_single_callable(app)
|
self.app = guarantee_single_callable(app)
|
||||||
self.tracer = trace.get_tracer(
|
self.tracer = trace.get_tracer(
|
||||||
|
|
@ -540,7 +518,41 @@ class OpenTelemetryMiddleware:
|
||||||
self.client_response_hook = client_response_hook
|
self.client_response_hook = client_response_hook
|
||||||
self.content_length_header = None
|
self.content_length_header = None
|
||||||
|
|
||||||
async def __call__(self, scope, receive, send):
|
# Environment variables as constructor parameters
|
||||||
|
self.http_capture_headers_server_request = (
|
||||||
|
http_capture_headers_server_request
|
||||||
|
or (
|
||||||
|
get_custom_headers(
|
||||||
|
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST
|
||||||
|
)
|
||||||
|
)
|
||||||
|
or None
|
||||||
|
)
|
||||||
|
self.http_capture_headers_server_response = (
|
||||||
|
http_capture_headers_server_response
|
||||||
|
or (
|
||||||
|
get_custom_headers(
|
||||||
|
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
or None
|
||||||
|
)
|
||||||
|
self.http_capture_headers_sanitize_fields = SanitizeValue(
|
||||||
|
http_capture_headers_sanitize_fields
|
||||||
|
or (
|
||||||
|
get_custom_headers(
|
||||||
|
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS
|
||||||
|
)
|
||||||
|
)
|
||||||
|
or []
|
||||||
|
)
|
||||||
|
|
||||||
|
async def __call__(
|
||||||
|
self,
|
||||||
|
scope: dict[str, Any],
|
||||||
|
receive: Callable[[], Awaitable[dict[str, Any]]],
|
||||||
|
send: Callable[[dict[str, Any]], Awaitable[None]],
|
||||||
|
) -> None:
|
||||||
"""The ASGI application
|
"""The ASGI application
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|
@ -583,7 +595,14 @@ class OpenTelemetryMiddleware:
|
||||||
|
|
||||||
if current_span.kind == trace.SpanKind.SERVER:
|
if current_span.kind == trace.SpanKind.SERVER:
|
||||||
custom_attributes = (
|
custom_attributes = (
|
||||||
collect_custom_request_headers_attributes(scope)
|
collect_custom_headers_attributes(
|
||||||
|
scope,
|
||||||
|
self.http_capture_headers_sanitize_fields,
|
||||||
|
self.http_capture_headers_server_request,
|
||||||
|
normalise_request_header_name,
|
||||||
|
)
|
||||||
|
if self.http_capture_headers_server_request
|
||||||
|
else {}
|
||||||
)
|
)
|
||||||
if len(custom_attributes) > 0:
|
if len(custom_attributes) > 0:
|
||||||
current_span.set_attributes(custom_attributes)
|
current_span.set_attributes(custom_attributes)
|
||||||
|
|
@ -658,7 +677,7 @@ class OpenTelemetryMiddleware:
|
||||||
expecting_trailers = False
|
expecting_trailers = False
|
||||||
|
|
||||||
@wraps(send)
|
@wraps(send)
|
||||||
async def otel_send(message):
|
async def otel_send(message: dict[str, Any]):
|
||||||
nonlocal expecting_trailers
|
nonlocal expecting_trailers
|
||||||
with self.tracer.start_as_current_span(
|
with self.tracer.start_as_current_span(
|
||||||
" ".join((server_span_name, scope["type"], "send"))
|
" ".join((server_span_name, scope["type"], "send"))
|
||||||
|
|
@ -685,7 +704,14 @@ class OpenTelemetryMiddleware:
|
||||||
and "headers" in message
|
and "headers" in message
|
||||||
):
|
):
|
||||||
custom_response_attributes = (
|
custom_response_attributes = (
|
||||||
collect_custom_response_headers_attributes(message)
|
collect_custom_headers_attributes(
|
||||||
|
message,
|
||||||
|
self.http_capture_headers_sanitize_fields,
|
||||||
|
self.http_capture_headers_server_response,
|
||||||
|
normalise_response_header_name,
|
||||||
|
)
|
||||||
|
if self.http_capture_headers_server_response
|
||||||
|
else {}
|
||||||
)
|
)
|
||||||
if len(custom_response_attributes) > 0:
|
if len(custom_response_attributes) > 0:
|
||||||
server_span.set_attributes(
|
server_span.set_attributes(
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from unittest import mock
|
import os
|
||||||
|
|
||||||
import opentelemetry.instrumentation.asgi as otel_asgi
|
import opentelemetry.instrumentation.asgi as otel_asgi
|
||||||
from opentelemetry.test.asgitestutil import AsgiTestBase
|
from opentelemetry.test.asgitestutil import AsgiTestBase
|
||||||
|
|
@ -72,21 +72,22 @@ async def websocket_app_with_custom_headers(scope, receive, send):
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
@mock.patch.dict(
|
|
||||||
"os.environ",
|
|
||||||
{
|
|
||||||
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*",
|
|
||||||
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*",
|
|
||||||
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
class TestCustomHeaders(AsgiTestBase, TestBase):
|
class TestCustomHeaders(AsgiTestBase, TestBase):
|
||||||
|
constructor_params = {}
|
||||||
|
__test__ = False
|
||||||
|
|
||||||
|
def __init_subclass__(cls) -> None:
|
||||||
|
if cls is not TestCustomHeaders:
|
||||||
|
cls.__test__ = True
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.tracer_provider, self.exporter = TestBase.create_tracer_provider()
|
self.tracer_provider, self.exporter = TestBase.create_tracer_provider()
|
||||||
self.tracer = self.tracer_provider.get_tracer(__name__)
|
self.tracer = self.tracer_provider.get_tracer(__name__)
|
||||||
self.app = otel_asgi.OpenTelemetryMiddleware(
|
self.app = otel_asgi.OpenTelemetryMiddleware(
|
||||||
simple_asgi, tracer_provider=self.tracer_provider
|
simple_asgi,
|
||||||
|
tracer_provider=self.tracer_provider,
|
||||||
|
**self.constructor_params,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_http_custom_request_headers_in_span_attributes(self):
|
def test_http_custom_request_headers_in_span_attributes(self):
|
||||||
|
|
@ -148,7 +149,9 @@ class TestCustomHeaders(AsgiTestBase, TestBase):
|
||||||
|
|
||||||
def test_http_custom_response_headers_in_span_attributes(self):
|
def test_http_custom_response_headers_in_span_attributes(self):
|
||||||
self.app = otel_asgi.OpenTelemetryMiddleware(
|
self.app = otel_asgi.OpenTelemetryMiddleware(
|
||||||
http_app_with_custom_headers, tracer_provider=self.tracer_provider
|
http_app_with_custom_headers,
|
||||||
|
tracer_provider=self.tracer_provider,
|
||||||
|
**self.constructor_params,
|
||||||
)
|
)
|
||||||
self.seed_app(self.app)
|
self.seed_app(self.app)
|
||||||
self.send_default_request()
|
self.send_default_request()
|
||||||
|
|
@ -175,7 +178,9 @@ class TestCustomHeaders(AsgiTestBase, TestBase):
|
||||||
|
|
||||||
def test_http_custom_response_headers_not_in_span_attributes(self):
|
def test_http_custom_response_headers_not_in_span_attributes(self):
|
||||||
self.app = otel_asgi.OpenTelemetryMiddleware(
|
self.app = otel_asgi.OpenTelemetryMiddleware(
|
||||||
http_app_with_custom_headers, tracer_provider=self.tracer_provider
|
http_app_with_custom_headers,
|
||||||
|
tracer_provider=self.tracer_provider,
|
||||||
|
**self.constructor_params,
|
||||||
)
|
)
|
||||||
self.seed_app(self.app)
|
self.seed_app(self.app)
|
||||||
self.send_default_request()
|
self.send_default_request()
|
||||||
|
|
@ -277,6 +282,7 @@ class TestCustomHeaders(AsgiTestBase, TestBase):
|
||||||
self.app = otel_asgi.OpenTelemetryMiddleware(
|
self.app = otel_asgi.OpenTelemetryMiddleware(
|
||||||
websocket_app_with_custom_headers,
|
websocket_app_with_custom_headers,
|
||||||
tracer_provider=self.tracer_provider,
|
tracer_provider=self.tracer_provider,
|
||||||
|
**self.constructor_params,
|
||||||
)
|
)
|
||||||
self.seed_app(self.app)
|
self.seed_app(self.app)
|
||||||
self.send_input({"type": "websocket.connect"})
|
self.send_input({"type": "websocket.connect"})
|
||||||
|
|
@ -317,6 +323,7 @@ class TestCustomHeaders(AsgiTestBase, TestBase):
|
||||||
self.app = otel_asgi.OpenTelemetryMiddleware(
|
self.app = otel_asgi.OpenTelemetryMiddleware(
|
||||||
websocket_app_with_custom_headers,
|
websocket_app_with_custom_headers,
|
||||||
tracer_provider=self.tracer_provider,
|
tracer_provider=self.tracer_provider,
|
||||||
|
**self.constructor_params,
|
||||||
)
|
)
|
||||||
self.seed_app(self.app)
|
self.seed_app(self.app)
|
||||||
self.send_input({"type": "websocket.connect"})
|
self.send_input({"type": "websocket.connect"})
|
||||||
|
|
@ -333,3 +340,46 @@ class TestCustomHeaders(AsgiTestBase, TestBase):
|
||||||
if span.kind == SpanKind.SERVER:
|
if span.kind == SpanKind.SERVER:
|
||||||
for key, _ in not_expected.items():
|
for key, _ in not_expected.items():
|
||||||
self.assertNotIn(key, span.attributes)
|
self.assertNotIn(key, span.attributes)
|
||||||
|
|
||||||
|
|
||||||
|
SANITIZE_FIELDS_TEST_VALUE = ".*my-secret.*"
|
||||||
|
SERVER_REQUEST_TEST_VALUE = "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*"
|
||||||
|
SERVER_RESPONSE_TEST_VALUE = "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*"
|
||||||
|
|
||||||
|
|
||||||
|
class TestCustomHeadersEnv(TestCustomHeaders):
|
||||||
|
def setUp(self):
|
||||||
|
os.environ.update(
|
||||||
|
{
|
||||||
|
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: SANITIZE_FIELDS_TEST_VALUE,
|
||||||
|
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: SERVER_REQUEST_TEST_VALUE,
|
||||||
|
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: SERVER_RESPONSE_TEST_VALUE,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
super().setUp()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
os.environ.pop(
|
||||||
|
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS, None
|
||||||
|
)
|
||||||
|
os.environ.pop(
|
||||||
|
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST, None
|
||||||
|
)
|
||||||
|
os.environ.pop(
|
||||||
|
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE, None
|
||||||
|
)
|
||||||
|
super().tearDown()
|
||||||
|
|
||||||
|
|
||||||
|
class TestCustomHeadersConstructor(TestCustomHeaders):
|
||||||
|
constructor_params = {
|
||||||
|
"http_capture_headers_sanitize_fields": SANITIZE_FIELDS_TEST_VALUE.split(
|
||||||
|
","
|
||||||
|
),
|
||||||
|
"http_capture_headers_server_request": SERVER_REQUEST_TEST_VALUE.split(
|
||||||
|
","
|
||||||
|
),
|
||||||
|
"http_capture_headers_server_response": SERVER_RESPONSE_TEST_VALUE.split(
|
||||||
|
","
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -983,18 +983,16 @@ class TestAsgiApplicationRaisingError(AsgiTestBase):
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@mock.patch(
|
def test_asgi_issue_1883(self):
|
||||||
"opentelemetry.instrumentation.asgi.collect_custom_request_headers_attributes",
|
|
||||||
side_effect=ValueError("whatever"),
|
|
||||||
)
|
|
||||||
def test_asgi_issue_1883(
|
|
||||||
self, mock_collect_custom_request_headers_attributes
|
|
||||||
):
|
|
||||||
"""
|
"""
|
||||||
Test that exception UnboundLocalError local variable 'start' referenced before assignment is not raised
|
Test that exception UnboundLocalError local variable 'start' referenced before assignment is not raised
|
||||||
See https://github.com/open-telemetry/opentelemetry-python-contrib/issues/1883
|
See https://github.com/open-telemetry/opentelemetry-python-contrib/issues/1883
|
||||||
"""
|
"""
|
||||||
app = otel_asgi.OpenTelemetryMiddleware(simple_asgi)
|
|
||||||
|
async def bad_app(_scope, _receive, _send):
|
||||||
|
raise ValueError("whatever")
|
||||||
|
|
||||||
|
app = otel_asgi.OpenTelemetryMiddleware(bad_app)
|
||||||
self.seed_app(app)
|
self.seed_app(app)
|
||||||
self.send_default_request()
|
self.send_default_request()
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -43,10 +43,17 @@ from opentelemetry.instrumentation.wsgi import wsgi_getter
|
||||||
from opentelemetry.semconv.trace import SpanAttributes
|
from opentelemetry.semconv.trace import SpanAttributes
|
||||||
from opentelemetry.trace import Span, SpanKind, use_span
|
from opentelemetry.trace import Span, SpanKind, use_span
|
||||||
from opentelemetry.util.http import (
|
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,
|
||||||
_parse_active_request_count_attrs,
|
_parse_active_request_count_attrs,
|
||||||
_parse_duration_attrs,
|
_parse_duration_attrs,
|
||||||
|
get_custom_headers,
|
||||||
get_excluded_urls,
|
get_excluded_urls,
|
||||||
get_traced_request_attrs,
|
get_traced_request_attrs,
|
||||||
|
normalise_request_header_name,
|
||||||
|
normalise_response_header_name,
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -91,10 +98,7 @@ else:
|
||||||
try:
|
try:
|
||||||
from opentelemetry.instrumentation.asgi import asgi_getter, asgi_setter
|
from opentelemetry.instrumentation.asgi import asgi_getter, asgi_setter
|
||||||
from opentelemetry.instrumentation.asgi import (
|
from opentelemetry.instrumentation.asgi import (
|
||||||
collect_custom_request_headers_attributes as asgi_collect_custom_request_attributes,
|
collect_custom_headers_attributes as asgi_collect_custom_headers_attributes,
|
||||||
)
|
|
||||||
from opentelemetry.instrumentation.asgi import (
|
|
||||||
collect_custom_response_headers_attributes as asgi_collect_custom_response_attributes,
|
|
||||||
)
|
)
|
||||||
from opentelemetry.instrumentation.asgi import (
|
from opentelemetry.instrumentation.asgi import (
|
||||||
collect_request_attributes as asgi_collect_request_attributes,
|
collect_request_attributes as asgi_collect_request_attributes,
|
||||||
|
|
@ -108,7 +112,6 @@ except ImportError:
|
||||||
set_status_code = None
|
set_status_code = None
|
||||||
_is_asgi_supported = False
|
_is_asgi_supported = False
|
||||||
|
|
||||||
|
|
||||||
_logger = getLogger(__name__)
|
_logger = getLogger(__name__)
|
||||||
_attributes_by_preference = [
|
_attributes_by_preference = [
|
||||||
[
|
[
|
||||||
|
|
@ -249,7 +252,18 @@ class _DjangoMiddleware(MiddlewareMixin):
|
||||||
)
|
)
|
||||||
if span.is_recording() and span.kind == SpanKind.SERVER:
|
if span.is_recording() and span.kind == SpanKind.SERVER:
|
||||||
attributes.update(
|
attributes.update(
|
||||||
asgi_collect_custom_request_attributes(carrier)
|
asgi_collect_custom_headers_attributes(
|
||||||
|
carrier,
|
||||||
|
SanitizeValue(
|
||||||
|
get_custom_headers(
|
||||||
|
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS
|
||||||
|
)
|
||||||
|
),
|
||||||
|
get_custom_headers(
|
||||||
|
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST
|
||||||
|
),
|
||||||
|
normalise_request_header_name,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
if span.is_recording() and span.kind == SpanKind.SERVER:
|
if span.is_recording() and span.kind == SpanKind.SERVER:
|
||||||
|
|
@ -336,8 +350,17 @@ class _DjangoMiddleware(MiddlewareMixin):
|
||||||
for key, value in response.items():
|
for key, value in response.items():
|
||||||
asgi_setter.set(custom_headers, key, value)
|
asgi_setter.set(custom_headers, key, value)
|
||||||
|
|
||||||
custom_res_attributes = (
|
custom_res_attributes = asgi_collect_custom_headers_attributes(
|
||||||
asgi_collect_custom_response_attributes(custom_headers)
|
custom_headers,
|
||||||
|
SanitizeValue(
|
||||||
|
get_custom_headers(
|
||||||
|
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS
|
||||||
|
)
|
||||||
|
),
|
||||||
|
get_custom_headers(
|
||||||
|
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE
|
||||||
|
),
|
||||||
|
normalise_response_header_name,
|
||||||
)
|
)
|
||||||
for key, value in custom_res_attributes.items():
|
for key, value in custom_res_attributes.items():
|
||||||
span.set_attribute(key, value)
|
span.set_attribute(key, value)
|
||||||
|
|
|
||||||
|
|
@ -467,15 +467,18 @@ class TestBaseWithCustomHeaders(TestBase):
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
@patch.dict(
|
|
||||||
"os.environ",
|
|
||||||
{
|
|
||||||
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*",
|
|
||||||
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*",
|
|
||||||
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
class TestHTTPAppWithCustomHeaders(TestBaseWithCustomHeaders):
|
class TestHTTPAppWithCustomHeaders(TestBaseWithCustomHeaders):
|
||||||
|
@patch.dict(
|
||||||
|
"os.environ",
|
||||||
|
{
|
||||||
|
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*",
|
||||||
|
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*",
|
||||||
|
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
def setUp(self) -> None:
|
||||||
|
super().setUp()
|
||||||
|
|
||||||
def test_custom_request_headers_in_span_attributes(self):
|
def test_custom_request_headers_in_span_attributes(self):
|
||||||
expected = {
|
expected = {
|
||||||
"http.request.header.custom_test_header_1": (
|
"http.request.header.custom_test_header_1": (
|
||||||
|
|
@ -590,15 +593,18 @@ class TestHTTPAppWithCustomHeaders(TestBaseWithCustomHeaders):
|
||||||
self.assertNotIn(key, server_span.attributes)
|
self.assertNotIn(key, server_span.attributes)
|
||||||
|
|
||||||
|
|
||||||
@patch.dict(
|
|
||||||
"os.environ",
|
|
||||||
{
|
|
||||||
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*",
|
|
||||||
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*",
|
|
||||||
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
class TestWebSocketAppWithCustomHeaders(TestBaseWithCustomHeaders):
|
class TestWebSocketAppWithCustomHeaders(TestBaseWithCustomHeaders):
|
||||||
|
@patch.dict(
|
||||||
|
"os.environ",
|
||||||
|
{
|
||||||
|
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*",
|
||||||
|
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,Regex-Test-Header-.*,Regex-Invalid-Test-Header-.*,.*my-secret.*",
|
||||||
|
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3,my-custom-regex-header-.*,invalid-regex-header-.*,.*my-secret.*",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
def setUp(self) -> None:
|
||||||
|
super().setUp()
|
||||||
|
|
||||||
def test_custom_request_headers_in_span_attributes(self):
|
def test_custom_request_headers_in_span_attributes(self):
|
||||||
expected = {
|
expected = {
|
||||||
"http.request.header.custom_test_header_1": (
|
"http.request.header.custom_test_header_1": (
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,13 @@
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from os import environ
|
from os import environ
|
||||||
from re import IGNORECASE as RE_IGNORECASE
|
from re import IGNORECASE as RE_IGNORECASE
|
||||||
from re import compile as re_compile
|
from re import compile as re_compile
|
||||||
from re import search
|
from re import search
|
||||||
from typing import Iterable, List, Optional
|
from typing import Callable, Iterable, Optional
|
||||||
from urllib.parse import urlparse, urlunparse
|
from urllib.parse import urlparse, urlunparse
|
||||||
|
|
||||||
from opentelemetry.semconv.trace import SpanAttributes
|
from opentelemetry.semconv.trace import SpanAttributes
|
||||||
|
|
@ -84,9 +86,12 @@ class SanitizeValue:
|
||||||
)
|
)
|
||||||
|
|
||||||
def sanitize_header_values(
|
def sanitize_header_values(
|
||||||
self, headers: dict, header_regexes: list, normalize_function: callable
|
self,
|
||||||
) -> dict:
|
headers: dict[str, str],
|
||||||
values = {}
|
header_regexes: list[str],
|
||||||
|
normalize_function: Callable[[str], str],
|
||||||
|
) -> dict[str, str]:
|
||||||
|
values: dict[str, str] = {}
|
||||||
|
|
||||||
if header_regexes:
|
if header_regexes:
|
||||||
header_regexes_compiled = re_compile(
|
header_regexes_compiled = re_compile(
|
||||||
|
|
@ -216,14 +221,14 @@ def sanitize_method(method: Optional[str]) -> Optional[str]:
|
||||||
return "UNKNOWN"
|
return "UNKNOWN"
|
||||||
|
|
||||||
|
|
||||||
def get_custom_headers(env_var: str) -> List[str]:
|
def get_custom_headers(env_var: str) -> list[str]:
|
||||||
custom_headers = environ.get(env_var, [])
|
custom_headers = environ.get(env_var, None)
|
||||||
if custom_headers:
|
if custom_headers:
|
||||||
custom_headers = [
|
return [
|
||||||
custom_headers.strip()
|
custom_headers.strip()
|
||||||
for custom_headers in custom_headers.split(",")
|
for custom_headers in custom_headers.split(",")
|
||||||
]
|
]
|
||||||
return custom_headers
|
return []
|
||||||
|
|
||||||
|
|
||||||
def _parse_active_request_count_attrs(req_attrs):
|
def _parse_active_request_count_attrs(req_attrs):
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue