Skip to content

Commit 5f976f4

Browse files
authored
Merge branch 'main' into support_for_confluent_kafka_2_3
2 parents f42b573 + 4b1a9c7 commit 5f976f4

File tree

9 files changed

+215
-100
lines changed

9 files changed

+215
-100
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111
([#2119](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2119))
1212
- `opentelemetry-instrumentation-confluent-kafka` Add support for higher versions until 2.3.0 of confluent_kafka
1313
([#2132](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2132))
14+
- `opentelemetry-resource-detector-azure` Changed timeout to 4 seconds due to [timeout bug](https://github.com/open-telemetry/opentelemetry-python/issues/3644)
15+
([#2136](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2136))
1416

1517
## Version 1.22.0/0.43b0 (2023-12-14)
1618

@@ -48,6 +50,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4850
([#1948](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1948))
4951
- Added schema_url (`"https://opentelemetry.io/schemas/1.11.0"`) to all metrics and traces
5052
([#1977](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1977))
53+
- Add support for configuring ASGI middleware header extraction via runtime constructor parameters
54+
([#2026](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2026))
5155

5256
### Fixed
5357

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

+72-46
Original file line numberDiff line numberDiff line change
@@ -189,11 +189,13 @@ def client_response_hook(span: Span, message: dict):
189189
---
190190
"""
191191

192+
from __future__ import annotations
193+
192194
import typing
193195
import urllib
194196
from functools import wraps
195197
from timeit import default_timer
196-
from typing import Tuple
198+
from typing import Any, Awaitable, Callable, Tuple, cast
197199

198200
from asgiref.compatibility import guarantee_single_callable
199201

@@ -332,55 +334,28 @@ def collect_request_attributes(scope):
332334
return result
333335

334336

335-
def collect_custom_request_headers_attributes(scope):
336-
"""returns custom HTTP request headers to be added into SERVER span as span attributes
337-
Refer specification https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-request-and-response-headers
337+
def collect_custom_headers_attributes(
338+
scope_or_response_message: dict[str, Any],
339+
sanitize: SanitizeValue,
340+
header_regexes: list[str],
341+
normalize_names: Callable[[str], str],
342+
) -> dict[str, str]:
338343
"""
344+
Returns custom HTTP request or response headers to be added into SERVER span as span attributes.
339345
340-
sanitize = SanitizeValue(
341-
get_custom_headers(
342-
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS
343-
)
344-
)
345-
346-
# Decode headers before processing.
347-
headers = {
348-
_key.decode("utf8"): _value.decode("utf8")
349-
for (_key, _value) in scope.get("headers")
350-
}
351-
352-
return sanitize.sanitize_header_values(
353-
headers,
354-
get_custom_headers(
355-
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST
356-
),
357-
normalise_request_header_name,
358-
)
359-
360-
361-
def collect_custom_response_headers_attributes(message):
362-
"""returns custom HTTP response headers to be added into SERVER span as span attributes
363-
Refer specification https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-request-and-response-headers
346+
Refer specifications:
347+
- https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-request-and-response-headers
364348
"""
365-
366-
sanitize = SanitizeValue(
367-
get_custom_headers(
368-
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS
369-
)
370-
)
371-
372349
# Decode headers before processing.
373-
headers = {
350+
headers: dict[str, str] = {
374351
_key.decode("utf8"): _value.decode("utf8")
375-
for (_key, _value) in message.get("headers")
352+
for (_key, _value) in scope_or_response_message.get("headers")
353+
or cast("list[tuple[bytes, bytes]]", [])
376354
}
377-
378355
return sanitize.sanitize_header_values(
379356
headers,
380-
get_custom_headers(
381-
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE
382-
),
383-
normalise_response_header_name,
357+
header_regexes,
358+
normalize_names,
384359
)
385360

386361

@@ -493,6 +468,9 @@ def __init__(
493468
tracer_provider=None,
494469
meter_provider=None,
495470
meter=None,
471+
http_capture_headers_server_request: list[str] | None = None,
472+
http_capture_headers_server_response: list[str] | None = None,
473+
http_capture_headers_sanitize_fields: list[str] | None = None,
496474
):
497475
self.app = guarantee_single_callable(app)
498476
self.tracer = trace.get_tracer(
@@ -540,7 +518,41 @@ def __init__(
540518
self.client_response_hook = client_response_hook
541519
self.content_length_header = None
542520

543-
async def __call__(self, scope, receive, send):
521+
# Environment variables as constructor parameters
522+
self.http_capture_headers_server_request = (
523+
http_capture_headers_server_request
524+
or (
525+
get_custom_headers(
526+
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST
527+
)
528+
)
529+
or None
530+
)
531+
self.http_capture_headers_server_response = (
532+
http_capture_headers_server_response
533+
or (
534+
get_custom_headers(
535+
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE
536+
)
537+
)
538+
or None
539+
)
540+
self.http_capture_headers_sanitize_fields = SanitizeValue(
541+
http_capture_headers_sanitize_fields
542+
or (
543+
get_custom_headers(
544+
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS
545+
)
546+
)
547+
or []
548+
)
549+
550+
async def __call__(
551+
self,
552+
scope: dict[str, Any],
553+
receive: Callable[[], Awaitable[dict[str, Any]]],
554+
send: Callable[[dict[str, Any]], Awaitable[None]],
555+
) -> None:
544556
"""The ASGI application
545557
546558
Args:
@@ -583,7 +595,14 @@ async def __call__(self, scope, receive, send):
583595

584596
if current_span.kind == trace.SpanKind.SERVER:
585597
custom_attributes = (
586-
collect_custom_request_headers_attributes(scope)
598+
collect_custom_headers_attributes(
599+
scope,
600+
self.http_capture_headers_sanitize_fields,
601+
self.http_capture_headers_server_request,
602+
normalise_request_header_name,
603+
)
604+
if self.http_capture_headers_server_request
605+
else {}
587606
)
588607
if len(custom_attributes) > 0:
589608
current_span.set_attributes(custom_attributes)
@@ -658,7 +677,7 @@ def _get_otel_send(
658677
expecting_trailers = False
659678

660679
@wraps(send)
661-
async def otel_send(message):
680+
async def otel_send(message: dict[str, Any]):
662681
nonlocal expecting_trailers
663682
with self.tracer.start_as_current_span(
664683
" ".join((server_span_name, scope["type"], "send"))
@@ -685,7 +704,14 @@ async def otel_send(message):
685704
and "headers" in message
686705
):
687706
custom_response_attributes = (
688-
collect_custom_response_headers_attributes(message)
707+
collect_custom_headers_attributes(
708+
message,
709+
self.http_capture_headers_sanitize_fields,
710+
self.http_capture_headers_server_response,
711+
normalise_response_header_name,
712+
)
713+
if self.http_capture_headers_server_response
714+
else {}
689715
)
690716
if len(custom_response_attributes) > 0:
691717
server_span.set_attributes(

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

+62-12
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from unittest import mock
1+
import os
22

33
import opentelemetry.instrumentation.asgi as otel_asgi
44
from opentelemetry.test.asgitestutil import AsgiTestBase
@@ -72,21 +72,22 @@ async def websocket_app_with_custom_headers(scope, receive, send):
7272
break
7373

7474

75-
@mock.patch.dict(
76-
"os.environ",
77-
{
78-
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: ".*my-secret.*",
79-
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.*",
80-
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.*",
81-
},
82-
)
8375
class TestCustomHeaders(AsgiTestBase, TestBase):
76+
constructor_params = {}
77+
__test__ = False
78+
79+
def __init_subclass__(cls) -> None:
80+
if cls is not TestCustomHeaders:
81+
cls.__test__ = True
82+
8483
def setUp(self):
8584
super().setUp()
8685
self.tracer_provider, self.exporter = TestBase.create_tracer_provider()
8786
self.tracer = self.tracer_provider.get_tracer(__name__)
8887
self.app = otel_asgi.OpenTelemetryMiddleware(
89-
simple_asgi, tracer_provider=self.tracer_provider
88+
simple_asgi,
89+
tracer_provider=self.tracer_provider,
90+
**self.constructor_params,
9091
)
9192

9293
def test_http_custom_request_headers_in_span_attributes(self):
@@ -148,7 +149,9 @@ def test_http_custom_request_headers_not_in_span_attributes(self):
148149

149150
def test_http_custom_response_headers_in_span_attributes(self):
150151
self.app = otel_asgi.OpenTelemetryMiddleware(
151-
http_app_with_custom_headers, tracer_provider=self.tracer_provider
152+
http_app_with_custom_headers,
153+
tracer_provider=self.tracer_provider,
154+
**self.constructor_params,
152155
)
153156
self.seed_app(self.app)
154157
self.send_default_request()
@@ -175,7 +178,9 @@ def test_http_custom_response_headers_in_span_attributes(self):
175178

176179
def test_http_custom_response_headers_not_in_span_attributes(self):
177180
self.app = otel_asgi.OpenTelemetryMiddleware(
178-
http_app_with_custom_headers, tracer_provider=self.tracer_provider
181+
http_app_with_custom_headers,
182+
tracer_provider=self.tracer_provider,
183+
**self.constructor_params,
179184
)
180185
self.seed_app(self.app)
181186
self.send_default_request()
@@ -277,6 +282,7 @@ def test_websocket_custom_response_headers_in_span_attributes(self):
277282
self.app = otel_asgi.OpenTelemetryMiddleware(
278283
websocket_app_with_custom_headers,
279284
tracer_provider=self.tracer_provider,
285+
**self.constructor_params,
280286
)
281287
self.seed_app(self.app)
282288
self.send_input({"type": "websocket.connect"})
@@ -317,6 +323,7 @@ def test_websocket_custom_response_headers_not_in_span_attributes(self):
317323
self.app = otel_asgi.OpenTelemetryMiddleware(
318324
websocket_app_with_custom_headers,
319325
tracer_provider=self.tracer_provider,
326+
**self.constructor_params,
320327
)
321328
self.seed_app(self.app)
322329
self.send_input({"type": "websocket.connect"})
@@ -333,3 +340,46 @@ def test_websocket_custom_response_headers_not_in_span_attributes(self):
333340
if span.kind == SpanKind.SERVER:
334341
for key, _ in not_expected.items():
335342
self.assertNotIn(key, span.attributes)
343+
344+
345+
SANITIZE_FIELDS_TEST_VALUE = ".*my-secret.*"
346+
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.*"
347+
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.*"
348+
349+
350+
class TestCustomHeadersEnv(TestCustomHeaders):
351+
def setUp(self):
352+
os.environ.update(
353+
{
354+
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS: SANITIZE_FIELDS_TEST_VALUE,
355+
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: SERVER_REQUEST_TEST_VALUE,
356+
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: SERVER_RESPONSE_TEST_VALUE,
357+
}
358+
)
359+
super().setUp()
360+
361+
def tearDown(self):
362+
os.environ.pop(
363+
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS, None
364+
)
365+
os.environ.pop(
366+
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST, None
367+
)
368+
os.environ.pop(
369+
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE, None
370+
)
371+
super().tearDown()
372+
373+
374+
class TestCustomHeadersConstructor(TestCustomHeaders):
375+
constructor_params = {
376+
"http_capture_headers_sanitize_fields": SANITIZE_FIELDS_TEST_VALUE.split(
377+
","
378+
),
379+
"http_capture_headers_server_request": SERVER_REQUEST_TEST_VALUE.split(
380+
","
381+
),
382+
"http_capture_headers_server_response": SERVER_RESPONSE_TEST_VALUE.split(
383+
","
384+
),
385+
}

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

+6-8
Original file line numberDiff line numberDiff line change
@@ -983,18 +983,16 @@ class TestAsgiApplicationRaisingError(AsgiTestBase):
983983
def tearDown(self):
984984
pass
985985

986-
@mock.patch(
987-
"opentelemetry.instrumentation.asgi.collect_custom_request_headers_attributes",
988-
side_effect=ValueError("whatever"),
989-
)
990-
def test_asgi_issue_1883(
991-
self, mock_collect_custom_request_headers_attributes
992-
):
986+
def test_asgi_issue_1883(self):
993987
"""
994988
Test that exception UnboundLocalError local variable 'start' referenced before assignment is not raised
995989
See https://github.com/open-telemetry/opentelemetry-python-contrib/issues/1883
996990
"""
997-
app = otel_asgi.OpenTelemetryMiddleware(simple_asgi)
991+
992+
async def bad_app(_scope, _receive, _send):
993+
raise ValueError("whatever")
994+
995+
app = otel_asgi.OpenTelemetryMiddleware(bad_app)
998996
self.seed_app(app)
999997
self.send_default_request()
1000998
try:

0 commit comments

Comments
 (0)