Skip to content

Commit d94d05b

Browse files
Merge branch 'main' into fix-django-duration-exemplars
2 parents 097aba8 + 185502b commit d94d05b

File tree

9 files changed

+123
-39
lines changed

9 files changed

+123
-39
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2828
- `opentelemetry-instrumentation-aiohttp-server`: add support for custom header captures via `OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST` and `OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`
2929
([#3916](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3916))
3030
- `opentelemetry-instrumentation-redis`: add support for `suppress_instrumentation` context manager for both sync and async Redis clients and pipelines
31+
- `opentelemetry-instrumentation-django`: improve docs for response_hook with examples of providing attributes from middlewares
32+
([#3923](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3923))
3133
- Update for Log SDK breaking changes. Rename InMemoryLogExporter to InMemoryLogRecordExporter in several tests
3234
([#3850](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3589))
3335

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ For more information about the maintainer role, see the [community repository](h
124124
- [Dylan Russell](https://github.com/dylanrussell), Google
125125
- [Emídio Neto](https://github.com/emdneto), PicPay
126126
- [Jeremy Voss](https://github.com/jeremydvoss), Microsoft
127+
- [Liudmila Molkova](https://github.com/lmolkova), Grafana Labs
127128
- [Owais Lone](https://github.com/owais), Splunk
128129
- [Pablo Collins](https://github.com/pmcollins), Splunk
129130
- [Sanket Mehta](https://github.com/sanketmehta28), Cisco

instrumentation-genai/opentelemetry-instrumentation-openai-v2/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## Unreleased
99

10+
- Fix service tier attribute names: use `GEN_AI_OPENAI_REQUEST_SERVICE_TIER` for request
11+
attributes and `GEN_AI_OPENAI_RESPONSE_SERVICE_TIER` for response attributes.
12+
([#3920](https://github.com/open-telemetry/opentelemetry-python-contrib/issues/3920))
1013
- Added support for OpenAI embeddings instrumentation
1114
([#3461](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3461))
1215
- Record prompt and completion events regardless of span sampling decision.

instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/patch.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,7 @@ def _set_response_attributes(
370370
if getattr(result, "service_tier", None):
371371
set_span_attribute(
372372
span,
373-
GenAIAttributes.GEN_AI_OPENAI_REQUEST_SERVICE_TIER,
373+
GenAIAttributes.GEN_AI_OPENAI_RESPONSE_SERVICE_TIER,
374374
result.service_tier,
375375
)
376376

instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/utils.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,8 +230,13 @@ def get_llm_request_attributes(
230230
GenAIAttributes.GEN_AI_OPENAI_REQUEST_RESPONSE_FORMAT
231231
] = response_format
232232

233+
# service_tier can be passed directly or in extra_body (in SDK 1.26.0 it's via extra_body)
233234
service_tier = kwargs.get("service_tier")
234-
attributes[GenAIAttributes.GEN_AI_OPENAI_RESPONSE_SERVICE_TIER] = (
235+
if service_tier is None:
236+
extra_body = kwargs.get("extra_body")
237+
if isinstance(extra_body, Mapping):
238+
service_tier = extra_body.get("service_tier")
239+
attributes[GenAIAttributes.GEN_AI_OPENAI_REQUEST_SERVICE_TIER] = (
235240
service_tier if service_tier != "auto" else None
236241
)
237242

instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_async_chat_completions.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,8 @@ async def test_async_chat_completion_extra_params(
183183
response.model,
184184
response.usage.prompt_tokens,
185185
response.usage.completion_tokens,
186+
request_service_tier="default",
187+
response_service_tier=getattr(response, "service_tier", None),
186188
)
187189
assert (
188190
spans[0].attributes[GenAIAttributes.GEN_AI_OPENAI_REQUEST_SEED] == 42

instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_chat_completions.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,8 @@ def test_chat_completion_extra_params(
221221
response.model,
222222
response.usage.prompt_tokens,
223223
response.usage.completion_tokens,
224+
request_service_tier="default",
225+
response_service_tier=getattr(response, "service_tier", None),
224226
)
225227
assert (
226228
spans[0].attributes[GenAIAttributes.GEN_AI_OPENAI_REQUEST_SEED] == 42

instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_utils.py

Lines changed: 33 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@
2525
)
2626

2727

28+
def _assert_optional_attribute(span, attribute_name, expected_value):
29+
"""Helper to assert optional span attributes."""
30+
if expected_value is not None:
31+
assert expected_value == span.attributes[attribute_name]
32+
else:
33+
assert attribute_name not in span.attributes
34+
35+
2836
def assert_all_attributes(
2937
span: ReadableSpan,
3038
request_model: str,
@@ -35,6 +43,8 @@ def assert_all_attributes(
3543
operation_name: str = "chat",
3644
server_address: str = "api.openai.com",
3745
server_port: int = 443,
46+
request_service_tier: Optional[str] = None,
47+
response_service_tier: Optional[str] = None,
3848
):
3949
assert span.name == f"{operation_name} {request_model}"
4050
assert (
@@ -49,44 +59,35 @@ def assert_all_attributes(
4959
request_model == span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL]
5060
)
5161

52-
if response_model:
53-
assert (
54-
response_model
55-
== span.attributes[GenAIAttributes.GEN_AI_RESPONSE_MODEL]
56-
)
57-
else:
58-
assert GenAIAttributes.GEN_AI_RESPONSE_MODEL not in span.attributes
59-
60-
if response_id:
61-
assert (
62-
response_id == span.attributes[GenAIAttributes.GEN_AI_RESPONSE_ID]
63-
)
64-
else:
65-
assert GenAIAttributes.GEN_AI_RESPONSE_ID not in span.attributes
66-
67-
if input_tokens:
68-
assert (
69-
input_tokens
70-
== span.attributes[GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS]
71-
)
72-
else:
73-
assert GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS not in span.attributes
74-
75-
if output_tokens:
76-
assert (
77-
output_tokens
78-
== span.attributes[GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS]
79-
)
80-
else:
81-
assert (
82-
GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS not in span.attributes
83-
)
62+
_assert_optional_attribute(
63+
span, GenAIAttributes.GEN_AI_RESPONSE_MODEL, response_model
64+
)
65+
_assert_optional_attribute(
66+
span, GenAIAttributes.GEN_AI_RESPONSE_ID, response_id
67+
)
68+
_assert_optional_attribute(
69+
span, GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS, input_tokens
70+
)
71+
_assert_optional_attribute(
72+
span, GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS, output_tokens
73+
)
8474

8575
assert server_address == span.attributes[ServerAttributes.SERVER_ADDRESS]
8676

8777
if server_port != 443 and server_port > 0:
8878
assert server_port == span.attributes[ServerAttributes.SERVER_PORT]
8979

80+
_assert_optional_attribute(
81+
span,
82+
GenAIAttributes.GEN_AI_OPENAI_REQUEST_SERVICE_TIER,
83+
request_service_tier,
84+
)
85+
_assert_optional_attribute(
86+
span,
87+
GenAIAttributes.GEN_AI_OPENAI_RESPONSE_SERVICE_TIER,
88+
response_service_tier,
89+
)
90+
9091

9192
def assert_log_parent(log, span):
9293
"""Assert that the log record has the correct parent span context"""

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

Lines changed: 73 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``.
4646
4747
Request attributes
48-
********************
48+
******************
4949
To extract attributes from Django's request object and use them as span attributes, set the environment variable
5050
``OTEL_PYTHON_DJANGO_TRACED_REQUEST_ATTRS`` to a comma delimited list of request attribute names.
5151
@@ -57,10 +57,10 @@
5757
5858
will extract the ``path_info`` and ``content_type`` attributes from every traced request and add them as span attributes.
5959
60-
Django Request object reference: https://docs.djangoproject.com/en/3.1/ref/request-response/#attributes
60+
* `Django Request object reference <https://docs.djangoproject.com/en/5.2/ref/request-response/#attributes>`_
6161
6262
Request and Response hooks
63-
***************************
63+
**************************
6464
This instrumentation supports request and response hooks. These are functions that get called
6565
right after a span is created for a request and right before the span is finished for the response.
6666
The hooks can be configured as follows:
@@ -77,8 +77,76 @@ def response_hook(span, request, response):
7777
7878
DjangoInstrumentor().instrument(request_hook=request_hook, response_hook=response_hook)
7979
80-
Django Request object: https://docs.djangoproject.com/en/3.1/ref/request-response/#httprequest-objects
81-
Django Response object: https://docs.djangoproject.com/en/3.1/ref/request-response/#httpresponse-objects
80+
* `Django Request object <https://docs.djangoproject.com/en/5.2/ref/request-response/#httprequest-objects>`_
81+
* `Django Response object <https://docs.djangoproject.com/en/5.2/ref/request-response/#httpresponse-objects>`_
82+
83+
Adding attributes from middleware context
84+
#########################################
85+
In many Django applications, certain request attributes become available only *after*
86+
specific middlewares have executed. For example:
87+
88+
- ``django.contrib.auth.middleware.AuthenticationMiddleware`` populates ``request.user``
89+
- ``django.contrib.sites.middleware.CurrentSiteMiddleware`` populates ``request.site``
90+
91+
Because the OpenTelemetry instrumentation creates the span **before** Django middlewares run,
92+
these attributes are **not yet available** in the ``request_hook`` stage.
93+
94+
Therefore, such attributes should be safely attached in the **response_hook**, which executes
95+
after Django finishes processing the request (and after all middlewares have completed).
96+
97+
Example: Attaching the authenticated user and current site to the span:
98+
99+
.. code:: python
100+
101+
def response_hook(span, request, response):
102+
# Attach user information if available
103+
if request.user.is_authenticated:
104+
span.set_attribute("enduser.id", request.user.pk)
105+
span.set_attribute("enduser.username", request.user.get_username())
106+
107+
# Attach current site (if provided by CurrentSiteMiddleware)
108+
if hasattr(request, "site"):
109+
span.set_attribute("site.id", getattr(request.site, "pk", None))
110+
span.set_attribute("site.domain", getattr(request.site, "domain", None))
111+
112+
DjangoInstrumentor().instrument(response_hook=response_hook)
113+
114+
This ensures that middleware-dependent context (like user or site information) is properly
115+
recorded once Django’s middleware stack has finished execution.
116+
117+
Custom Django middleware can also attach arbitrary data to the ``request`` object,
118+
which can later be included as span attributes in the ``response_hook``.
119+
120+
* `Django middleware reference <https://docs.djangoproject.com/en/5.2/topics/http/middleware/>`_
121+
122+
Best practices
123+
##############
124+
- Use **response_hook** (not request_hook) when accessing attributes added by Django middlewares.
125+
- Common middleware-provided attributes include:
126+
127+
- ``request.user`` (AuthenticationMiddleware)
128+
- ``request.site`` (CurrentSiteMiddleware)
129+
130+
- Avoid adding large or sensitive data (e.g., passwords, session tokens, PII) to spans.
131+
- Use **namespaced attribute keys**, e.g., ``enduser.*``, ``site.*``, or ``custom.*``, for clarity.
132+
- Hooks should execute quickly — avoid blocking or long-running operations.
133+
- Hooks can be safely combined with OpenTelemetry **Context propagation** or **Baggage**
134+
for consistent tracing across services.
135+
136+
* `OpenTelemetry semantic conventions <https://opentelemetry.io/docs/specs/semconv/http/http-spans/>`_
137+
138+
Middleware execution order
139+
##########################
140+
In Django’s request lifecycle, the OpenTelemetry `request_hook` is executed before
141+
the first middleware runs. Therefore:
142+
143+
- At `request_hook` time → only the bare `HttpRequest` object is available.
144+
- After middlewares → `request.user`, `request.site` etc. become available.
145+
- At `response_hook` time → all middlewares (including authentication and site middlewares)
146+
have already run, making it the correct place to attach these attributes.
147+
148+
Developers who need to trace attributes from middlewares should always use `response_hook`
149+
to ensure complete and accurate span data.
82150
83151
Capture HTTP request and response headers
84152
*****************************************

0 commit comments

Comments
 (0)