Skip to content

Commit 5c5ac8a

Browse files
committed
Add WSGIRequest support to OpenTelemetry attributes
1 parent 1d56f29 commit 5c5ac8a

File tree

3 files changed

+104
-1
lines changed

3 files changed

+104
-1
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616
([#4695](https://github.com/open-telemetry/opentelemetry-python/pull/4695)).
1717
- docs: linked the examples with their github source code location and added Prometheus example
1818
([#4728](https://github.com/open-telemetry/opentelemetry-python/pull/4728))
19+
- opentelemetry-api: Fix for issue #4732 - Invalid type WSGIRequest for attribute 'request' value opentelemetry
20+
([#4733](https://github.com/open-telemetry/opentelemetry-python/pull/4733))
1921

2022
## Version 1.36.0/0.57b0 (2025-07-29)
2123

opentelemetry-api/src/opentelemetry/attributes/__init__.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,23 @@
2020

2121
from opentelemetry.util import types
2222

23+
24+
def _get_wsgi_request_type():
25+
"""Get WSGIRequest type if Django is available, otherwise return None."""
26+
try:
27+
# pylint: disable=import-outside-toplevel
28+
from django.core.handlers.wsgi import WSGIRequest # type: ignore
29+
30+
return WSGIRequest
31+
except ImportError:
32+
return None
33+
34+
2335
# bytes are accepted as a user supplied value for attributes but
2436
# decoded to strings internally.
2537
_VALID_ATTR_VALUE_TYPES = (bool, str, bytes, int, float)
2638
# AnyValue possible values
39+
_WSGIRequest = _get_wsgi_request_type()
2740
_VALID_ANY_VALUE_TYPES = (
2841
type(None),
2942
bool,
@@ -33,7 +46,7 @@
3346
str,
3447
Sequence,
3548
Mapping,
36-
)
49+
) + ((_WSGIRequest,) if _WSGIRequest is not None else ())
3750

3851

3952
_logger = logging.getLogger(__name__)
@@ -121,12 +134,25 @@ def _clean_attribute(
121134
def _clean_extended_attribute_value(
122135
value: types.AnyValue, max_len: Optional[int]
123136
) -> types.AnyValue:
137+
# pylint: disable=too-many-branches
124138
# for primitive types just return the value and eventually shorten the string length
125139
if value is None or isinstance(value, _VALID_ATTR_VALUE_TYPES):
126140
if max_len is not None and isinstance(value, str):
127141
value = value[:max_len]
128142
return value
129143

144+
if _WSGIRequest is not None and isinstance(value, _WSGIRequest):
145+
wsgi_data = {
146+
"method": getattr(value, "method", None),
147+
"path": getattr(value, "path", None),
148+
"path_info": getattr(value, "path_info", None),
149+
"content_type": getattr(value, "content_type", None),
150+
"user": str(getattr(value, "user", None))
151+
if hasattr(value, "user")
152+
else None,
153+
}
154+
return {k: v for k, v in wsgi_data.items() if v is not None}
155+
130156
if isinstance(value, Mapping):
131157
cleaned_dict: dict[str, types.AnyValue] = {}
132158
for key, element in value.items():

opentelemetry-api/tests/attributes/test_attributes.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,35 @@
1414

1515
# type: ignore
1616

17+
import io
1718
import unittest
1819
from typing import MutableSequence
1920

2021
from opentelemetry.attributes import (
2122
BoundedAttributes,
2223
_clean_attribute,
2324
_clean_extended_attribute,
25+
_get_wsgi_request_type,
2426
)
2527

2628

29+
def _get_django_settings():
30+
"""Get Django settings if available, otherwise return None."""
31+
try:
32+
# pylint: disable=import-outside-toplevel
33+
from django.conf import (
34+
settings, # pyright: ignore[reportMissingImports]
35+
)
36+
37+
return settings
38+
except ImportError:
39+
return None
40+
41+
42+
_WSGIRequest = _get_wsgi_request_type()
43+
_settings = _get_django_settings()
44+
45+
2746
class TestAttributes(unittest.TestCase):
2847
# pylint: disable=invalid-name
2948
def assertValid(self, value, key="k"):
@@ -182,6 +201,62 @@ def test_mapping(self):
182201
_clean_extended_attribute("headers", mapping, None), expected
183202
)
184203

204+
def test_wsgi_request_attribute(self):
205+
if _WSGIRequest is None or _settings is None:
206+
self.skipTest("Django not available")
207+
208+
if not _settings.configured:
209+
_settings.configure(
210+
DEBUG=True,
211+
SECRET_KEY="test-secret-key",
212+
USE_TZ=True,
213+
ROOT_URLCONF=[],
214+
MIDDLEWARE=[],
215+
)
216+
217+
# Create a minimal WSGI environ dict
218+
environ = {
219+
"REQUEST_METHOD": "GET",
220+
"PATH_INFO": "/test",
221+
"QUERY_STRING": "",
222+
"CONTENT_TYPE": "",
223+
"CONTENT_LENGTH": "",
224+
"HTTP_HOST": "testserver",
225+
"wsgi.version": (1, 0),
226+
"wsgi.url_scheme": "http",
227+
"wsgi.input": io.StringIO(),
228+
"wsgi.errors": io.StringIO(),
229+
"wsgi.multithread": False,
230+
"wsgi.multiprocess": False,
231+
"wsgi.run_once": False,
232+
"SERVER_NAME": "testserver",
233+
"SERVER_PORT": "80",
234+
}
235+
236+
# Create a WSGIRequest object
237+
wsgi_request = _WSGIRequest(environ)
238+
expected_cleaned = {
239+
"method": "GET",
240+
"path": "/test",
241+
"path_info": "/test",
242+
"content_type": "",
243+
}
244+
245+
cleaned_value = _clean_extended_attribute(
246+
"request", wsgi_request, None
247+
)
248+
self.assertEqual(cleaned_value, expected_cleaned)
249+
250+
cleaned_sequence = _clean_extended_attribute(
251+
"requests", [wsgi_request], None
252+
)
253+
self.assertEqual(cleaned_sequence, (expected_cleaned,))
254+
255+
cleaned_mapping = _clean_extended_attribute(
256+
"data", {"request": wsgi_request}, None
257+
)
258+
self.assertEqual(cleaned_mapping, {"request": expected_cleaned})
259+
185260

186261
class TestBoundedAttributes(unittest.TestCase):
187262
# pylint: disable=consider-using-dict-items

0 commit comments

Comments
 (0)