Skip to content

Commit fba58dc

Browse files
authored
Merge branch 'main' into openai-v2-request-seed
2 parents 0d5b90f + fd79ae9 commit fba58dc

11 files changed

Lines changed: 455 additions & 13 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111
1212
## Unreleased
1313

14-
### Fixed
14+
### Fixed
1515

16+
- `opentelemetry-instrumentation-django`: Fix exemplars generation for `http.server.(request.)duration`
17+
([#3945](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3945))
1618
- `opentelemetry-util-http`, `opentelemetry-instrumentation-requests`, `opentelemetry-instrumentation-wsgi`, `opentelemetry-instrumentation-asgi`: normalize byte-valued user-agent headers before detecting synthetic sources so attributes are recorded reliably.
1719
([#4001](https://github.com/open-telemetry/opentelemetry-python-contrib/issues/4001))
1820

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
interactions:
2+
- request:
3+
body: |-
4+
{
5+
"messages": [
6+
{
7+
"role": "user",
8+
"content": "Say this is a test"
9+
}
10+
],
11+
"model": "gpt-4o-mini",
12+
"stream": true,
13+
"stream_options": {
14+
"include_usage": true
15+
}
16+
}
17+
headers:
18+
Accept:
19+
- application/json
20+
Accept-Encoding:
21+
- gzip, deflate
22+
Connection:
23+
- keep-alive
24+
Content-Length:
25+
- '148'
26+
Content-Type:
27+
- application/json
28+
Host:
29+
- api.openai.com
30+
User-Agent:
31+
- OpenAI/Python 1.109.1
32+
X-Stainless-Arch:
33+
- x64
34+
X-Stainless-Async:
35+
- 'false'
36+
X-Stainless-Lang:
37+
- python
38+
X-Stainless-OS:
39+
- Linux
40+
X-Stainless-Package-Version:
41+
- 1.109.1
42+
X-Stainless-Runtime:
43+
- CPython
44+
X-Stainless-Runtime-Version:
45+
- 3.12.12
46+
authorization:
47+
- Bearer test_openai_api_key
48+
x-stainless-read-timeout:
49+
- '600'
50+
x-stainless-retry-count:
51+
- '0'
52+
method: POST
53+
uri: https://api.openai.com/v1/chat/completions
54+
response:
55+
body:
56+
string: |+
57+
data: {"id":"chatcmpl-CnMGGNmSdY4yYxO9NmV7cSVxXZ5ci","object":"chat.completion.chunk","created":1765879680,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"yKkTGHRZw"}
58+
59+
data: {"id":"chatcmpl-CnMGGNmSdY4yYxO9NmV7cSVxXZ5ci","object":"chat.completion.chunk","created":1765879680,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{"content":"This"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"bFd3AyW"}
60+
61+
data: {"id":"chatcmpl-CnMGGNmSdY4yYxO9NmV7cSVxXZ5ci","object":"chat.completion.chunk","created":1765879680,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{"content":" is"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"LqISoMXL"}
62+
63+
data: {"id":"chatcmpl-CnMGGNmSdY4yYxO9NmV7cSVxXZ5ci","object":"chat.completion.chunk","created":1765879680,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{"content":" a"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"mUrlylOKo"}
64+
65+
data: {"id":"chatcmpl-CnMGGNmSdY4yYxO9NmV7cSVxXZ5ci","object":"chat.completion.chunk","created":1765879680,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{"content":" test"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"nGqQio"}
66+
67+
data: {"id":"chatcmpl-CnMGGNmSdY4yYxO9NmV7cSVxXZ5ci","object":"chat.completion.chunk","created":1765879680,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{"content":"."},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"gUhadpfoFR"}
68+
69+
data: {"id":"chatcmpl-CnMGGNmSdY4yYxO9NmV7cSVxXZ5ci","object":"chat.completion.chunk","created":1765879680,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{"content":" How"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"atLjmXM"}
70+
71+
data: {"id":"chatcmpl-CnMGGNmSdY4yYxO9NmV7cSVxXZ5ci","object":"chat.completion.chunk","created":1765879680,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{"content":" can"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"Ac67p8z"}
72+
73+
data: {"id":"chatcmpl-CnMGGNmSdY4yYxO9NmV7cSVxXZ5ci","object":"chat.completion.chunk","created":1765879680,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{"content":" I"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"tl4DibKXq"}
74+
75+
data: {"id":"chatcmpl-CnMGGNmSdY4yYxO9NmV7cSVxXZ5ci","object":"chat.completion.chunk","created":1765879680,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{"content":" assist"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"p5ei"}
76+
77+
data: {"id":"chatcmpl-CnMGGNmSdY4yYxO9NmV7cSVxXZ5ci","object":"chat.completion.chunk","created":1765879680,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{"content":" you"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"97qAT5t"}
78+
79+
data: {"id":"chatcmpl-CnMGGNmSdY4yYxO9NmV7cSVxXZ5ci","object":"chat.completion.chunk","created":1765879680,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{"content":" further"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"ZK4"}
80+
81+
data: {"id":"chatcmpl-CnMGGNmSdY4yYxO9NmV7cSVxXZ5ci","object":"chat.completion.chunk","created":1765879680,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{"content":"?"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"5iGjeBvFWd"}
82+
83+
data: {"id":"chatcmpl-CnMGGNmSdY4yYxO9NmV7cSVxXZ5ci","object":"chat.completion.chunk","created":1765879680,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"usage":null,"obfuscation":"SdS98"}
84+
85+
data: {"id":"chatcmpl-CnMGGNmSdY4yYxO9NmV7cSVxXZ5ci","object":"chat.completion.chunk","created":1765879680,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_644f11dd4d","choices":[],"usage":{"prompt_tokens":12,"completion_tokens":12,"total_tokens":24,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}},"obfuscation":"parVcHlnsA"}
86+
87+
data: [DONE]
88+
89+
headers:
90+
CF-RAY:
91+
- 9aed607a4c274be7-MXP
92+
Connection:
93+
- keep-alive
94+
Content-Type:
95+
- text/event-stream; charset=utf-8
96+
Date:
97+
- Tue, 16 Dec 2025 10:08:00 GMT
98+
Server:
99+
- cloudflare
100+
Set-Cookie: test_set_cookie
101+
Strict-Transport-Security:
102+
- max-age=31536000; includeSubDomains; preload
103+
Transfer-Encoding:
104+
- chunked
105+
X-Content-Type-Options:
106+
- nosniff
107+
access-control-expose-headers:
108+
- X-Request-ID
109+
alt-svc:
110+
- h3=":443"; ma=86400
111+
cf-cache-status:
112+
- DYNAMIC
113+
openai-organization: test_openai_org_id
114+
openai-processing-ms:
115+
- '274'
116+
openai-project:
117+
- proj_Pf1eM5R55Z35wBy4rt8PxAGq
118+
openai-version:
119+
- '2020-10-01'
120+
x-envoy-upstream-service-time:
121+
- '618'
122+
x-openai-proxy-wasm:
123+
- v0.1
124+
x-ratelimit-limit-requests:
125+
- '10000'
126+
x-ratelimit-limit-tokens:
127+
- '10000000'
128+
x-ratelimit-remaining-requests:
129+
- '9999'
130+
x-ratelimit-remaining-tokens:
131+
- '9999993'
132+
x-ratelimit-reset-requests:
133+
- 6ms
134+
x-ratelimit-reset-tokens:
135+
- 0s
136+
x-request-id:
137+
- req_50a8341c02184cbd8b364182eb40ed38
138+
status:
139+
code: 200
140+
message: OK
141+
version: 1

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

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -955,6 +955,58 @@ def test_chat_completion_with_content_span_unsampled(
955955
assert logs[0].log_record.trace_flags == logs[1].log_record.trace_flags
956956

957957

958+
@pytest.mark.vcr()
959+
def test_chat_completion_with_context_manager_streaming(
960+
span_exporter, log_exporter, openai_client, instrument_with_content
961+
):
962+
llm_model_value = "gpt-4o-mini"
963+
messages_value = [{"role": "user", "content": "Say this is a test"}]
964+
with openai_client.chat.completions.create(
965+
messages=messages_value,
966+
model=llm_model_value,
967+
stream=True,
968+
stream_options={"include_usage": True},
969+
) as response:
970+
message_content = ""
971+
for chunk in response:
972+
if chunk.choices:
973+
message_content += chunk.choices[0].delta.content or ""
974+
# get the last chunk
975+
if getattr(chunk, "usage", None):
976+
response_stream_usage = chunk.usage
977+
response_stream_model = chunk.model
978+
response_stream_id = chunk.id
979+
980+
spans = span_exporter.get_finished_spans()
981+
assert_all_attributes(
982+
spans[0],
983+
llm_model_value,
984+
response_stream_id,
985+
response_stream_model,
986+
response_stream_usage.prompt_tokens,
987+
response_stream_usage.completion_tokens,
988+
response_service_tier="default",
989+
)
990+
991+
logs = log_exporter.get_finished_logs()
992+
assert len(logs) == 2
993+
994+
user_message = {"content": messages_value[0]["content"]}
995+
assert_message_in_logs(
996+
logs[0], "gen_ai.user.message", user_message, spans[0]
997+
)
998+
999+
choice_event = {
1000+
"index": 0,
1001+
"finish_reason": "stop",
1002+
"message": {
1003+
"role": "assistant",
1004+
"content": message_content,
1005+
},
1006+
}
1007+
assert_message_in_logs(logs[1], "gen_ai.choice", choice_event, spans[0])
1008+
1009+
9581010
def chat_completion_multiple_tools_streaming(
9591011
span_exporter, log_exporter, openai_client, expect_content
9601012
):

instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware/otel_middleware.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -417,15 +417,6 @@ def process_response(self, request, response):
417417
except Exception: # pylint: disable=broad-exception-caught
418418
_logger.exception("Exception raised by response_hook")
419419

420-
if exception:
421-
activation.__exit__(
422-
type(exception),
423-
exception,
424-
getattr(exception, "__traceback__", None),
425-
)
426-
else:
427-
activation.__exit__(None, None, None)
428-
429420
if request_start_time is not None:
430421
duration_s = default_timer() - request_start_time
431422
if self._duration_histogram_old:
@@ -437,16 +428,29 @@ def process_response(self, request, response):
437428
if target:
438429
duration_attrs_old[SpanAttributes.HTTP_TARGET] = target
439430
self._duration_histogram_old.record(
440-
max(round(duration_s * 1000), 0), duration_attrs_old
431+
max(round(duration_s * 1000), 0),
432+
duration_attrs_old,
441433
)
442434
if self._duration_histogram_new:
443435
duration_attrs_new = _parse_duration_attrs(
444436
duration_attrs, _StabilityMode.HTTP
445437
)
446438
self._duration_histogram_new.record(
447-
max(duration_s, 0), duration_attrs_new
439+
max(duration_s, 0),
440+
duration_attrs_new,
448441
)
449442
self._active_request_counter.add(-1, active_requests_count_attrs)
443+
444+
if activation and span:
445+
if exception:
446+
activation.__exit__(
447+
type(exception),
448+
exception,
449+
getattr(exception, "__traceback__", None),
450+
)
451+
else:
452+
activation.__exit__(None, None, None)
453+
450454
if request.META.get(self._environ_token, None) is not None:
451455
detach(request.META.get(self._environ_token))
452456
request.META.pop(self._environ_token)

instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1104,3 +1104,66 @@ def test_http_custom_response_headers_not_in_span_attributes(self):
11041104
for key, _ in not_expected.items():
11051105
self.assertNotIn(key, span.attributes)
11061106
self.memory_exporter.clear()
1107+
1108+
1109+
class TestMiddlewareSpanActivationTiming(WsgiTestBase):
1110+
"""Test span activation timing relative to metrics recording."""
1111+
1112+
@classmethod
1113+
def setUpClass(cls):
1114+
conf.settings.configure(ROOT_URLCONF=modules[__name__])
1115+
super().setUpClass()
1116+
1117+
def setUp(self):
1118+
super().setUp()
1119+
setup_test_environment()
1120+
_django_instrumentor.instrument()
1121+
1122+
def tearDown(self):
1123+
super().tearDown()
1124+
teardown_test_environment()
1125+
_django_instrumentor.uninstrument()
1126+
1127+
@classmethod
1128+
def tearDownClass(cls):
1129+
super().tearDownClass()
1130+
conf.settings = conf.LazySettings()
1131+
1132+
def test_span_ended_after_metrics_recorded(self):
1133+
"""Span activation exits after metrics recording."""
1134+
Client().get("/traced/")
1135+
1136+
spans = self.memory_exporter.get_finished_spans()
1137+
self.assertEqual(len(spans), 1)
1138+
1139+
# Span properly finished
1140+
self.assertIsNotNone(spans[0].end_time)
1141+
1142+
# Metrics recorded
1143+
metrics_list = self.memory_metrics_reader.get_metrics_data()
1144+
histogram_found = any(
1145+
"duration" in metric.name
1146+
for rm in metrics_list.resource_metrics
1147+
for sm in rm.scope_metrics
1148+
for metric in sm.metrics
1149+
)
1150+
self.assertTrue(histogram_found)
1151+
1152+
def test_metrics_recorded_with_exception(self):
1153+
"""Metrics recorded even when request raises exception."""
1154+
with self.assertRaises(ValueError):
1155+
Client().get("/error/")
1156+
1157+
spans = self.memory_exporter.get_finished_spans()
1158+
self.assertEqual(len(spans), 1)
1159+
self.assertEqual(spans[0].status.status_code, StatusCode.ERROR)
1160+
1161+
# Metrics still recorded
1162+
metrics_list = self.memory_metrics_reader.get_metrics_data()
1163+
histogram_found = any(
1164+
"duration" in metric.name
1165+
for rm in metrics_list.resource_metrics
1166+
for sm in rm.scope_metrics
1167+
for metric in sm.metrics
1168+
)
1169+
self.assertTrue(histogram_found)

0 commit comments

Comments
 (0)