1515from collections import Counter
1616from http .server import BaseHTTPRequestHandler , ThreadingHTTPServer
1717import json
18+ import os
1819import threading
1920import unittest
2021from datetime import datetime , timezone
2122from typing import cast
22- from unittest .mock import MagicMock
23+ from unittest .mock import MagicMock , patch
2324
2425import httpx
2526from opentelemetry import context as context_api
@@ -118,9 +119,16 @@ def _make_httpx_response(body: dict, status_code: int = 200) -> httpx.Response:
118119 return resp
119120
120121
121- def _make_hook_context (operation_id : str ) -> HookContext :
122+ def _make_hook_context (
123+ operation_id : str ,
124+ telemetry : bool | str | None = "global" ,
125+ ) -> HookContext :
126+ config = MagicMock ()
127+ config .telemetry = telemetry
128+ config .client = None
129+ config .async_client = None
122130 return HookContext (
123- config = MagicMock () ,
131+ config = config ,
124132 base_url = "https://api.mistral.ai" ,
125133 operation_id = operation_id ,
126134 oauth2_scopes = None ,
@@ -254,6 +262,7 @@ def _run_hook_lifecycle(
254262 request_body ,
255263 response_body ,
256264 streaming : bool = False ,
265+ telemetry : bool | str | None = "global" ,
257266 ):
258267 """Drive the real TracingHook: before_request → after_success.
259268
@@ -266,7 +275,7 @@ def _run_hook_lifecycle(
266275 so the span is finalised before returning.
267276 """
268277 hook = TracingHook ()
269- hook_ctx = _make_hook_context (operation_id )
278+ hook_ctx = _make_hook_context (operation_id , telemetry = telemetry )
270279
271280 req_dict = (
272281 _dump (request_body ) if hasattr (request_body , "model_dump" ) else request_body
@@ -309,10 +318,11 @@ def _run_hook_error_lifecycle(
309318 response_body : dict ,
310319 status_code : int = 400 ,
311320 error : Exception | None = None ,
321+ telemetry : bool | str | None = "global" ,
312322 ):
313323 """Drive the real TracingHook: before_request → after_error."""
314324 hook = TracingHook ()
315- hook_ctx = _make_hook_context (operation_id )
325+ hook_ctx = _make_hook_context (operation_id , telemetry = telemetry )
316326
317327 req_dict = (
318328 _dump (request_body ) if hasattr (request_body , "model_dump" ) else request_body
@@ -343,6 +353,36 @@ def assertSpanAttributes(self, span, expected: dict):
343353 actual = {k : span .attributes [k ] for k in expected }
344354 self .assertEqual (expected , actual )
345355
356+ def test_global_provider_alone_does_not_enable_sdk_span (self ):
357+ request = ChatCompletionRequest (
358+ model = "mistral-small-latest" ,
359+ messages = [UserMessage (content = "Hello" )],
360+ )
361+ response = ChatCompletionResponse (
362+ id = "cmpl-no-telemetry-env" ,
363+ object = "chat.completion" ,
364+ model = "mistral-small-latest" ,
365+ created = 1700000015 ,
366+ choices = [
367+ ChatCompletionChoice (
368+ index = 0 ,
369+ message = AssistantMessage (content = "Hi!" , tool_calls = None ),
370+ finish_reason = "stop" ,
371+ ),
372+ ],
373+ usage = UsageInfo (prompt_tokens = 5 , completion_tokens = 2 , total_tokens = 7 ),
374+ )
375+
376+ with patch .dict (os .environ , {}, clear = True ):
377+ self ._run_hook_lifecycle (
378+ "chat_completion_v1_chat_completions_post" ,
379+ request ,
380+ response ,
381+ telemetry = None ,
382+ )
383+
384+ self .assertEqual (len (self ._get_finished_spans ()), 0 )
385+
346386 # -- Simple chat completion ------------------------------------------------
347387
348388 def test_simple_chat_completion (self ):
@@ -1754,6 +1794,7 @@ async def _mock_handler(request: httpx.Request) -> httpx.Response:
17541794 api_key = "test-key" ,
17551795 async_client = async_client ,
17561796 )
1797+ client .sdk_configuration .__dict__ ["telemetry" ] = "global"
17571798
17581799 async def _run ():
17591800 return await asyncio .gather (
@@ -1804,6 +1845,48 @@ async def _run():
18041845
18051846 # -- HTTPX auto-instrumentation parenting ---------------------------------
18061847
1848+ def test_app_otel_does_not_enable_mistral_span_without_mistral_telemetry (self ):
1849+ instrumentor = HTTPXClientInstrumentor ()
1850+ instrumentor .instrument ()
1851+ tracer = trace .get_tracer ("test-workflow-parenting" )
1852+
1853+ try :
1854+ with patch .dict (os .environ , {}, clear = True ):
1855+ with (
1856+ _ChatCompletionTestServer () as server ,
1857+ httpx .Client () as http_client ,
1858+ ):
1859+ client = Mistral (
1860+ api_key = "test-key" ,
1861+ client = http_client ,
1862+ server_url = server .url ,
1863+ )
1864+ with tracer .start_as_current_span (
1865+ "ExecuteActivity:generate_site_diagnostic"
1866+ ) as activity_span :
1867+ client .chat .complete (
1868+ model = "mistral-small-latest" ,
1869+ messages = _make_user_messages ("hello" ),
1870+ )
1871+
1872+ self .assertEqual (
1873+ trace .get_current_span ().get_span_context ().span_id ,
1874+ activity_span .get_span_context ().span_id ,
1875+ )
1876+ finally :
1877+ instrumentor .uninstrument ()
1878+
1879+ spans = self ._get_finished_spans ()
1880+ activity = next (
1881+ s for s in spans if s .name == "ExecuteActivity:generate_site_diagnostic"
1882+ )
1883+ genai_spans = [s for s in spans if s .name == "chat mistral-small-latest" ]
1884+ post_spans = [s for s in spans if s .name == "POST" ]
1885+
1886+ self .assertEqual (genai_spans , [])
1887+ self .assertEqual (len (post_spans ), 1 )
1888+ self .assertEqual (post_spans [0 ].parent .span_id , activity .context .span_id )
1889+
18071890 def test_httpx_auto_instrumented_span_is_child_of_genai_span (self ):
18081891 instrumentor = HTTPXClientInstrumentor ()
18091892 instrumentor .instrument ()
@@ -1816,6 +1899,7 @@ def test_httpx_auto_instrumented_span_is_child_of_genai_span(self):
18161899 client = http_client ,
18171900 server_url = server .url ,
18181901 )
1902+ client .sdk_configuration .__dict__ ["telemetry" ] = "global"
18191903 with tracer .start_as_current_span (
18201904 "ExecuteActivity:generate_site_diagnostic"
18211905 ) as activity_span :
@@ -1858,6 +1942,7 @@ def raise_connect_error(request: httpx.Request) -> httpx.Response:
18581942 client = http_client ,
18591943 server_url = "https://api.mistral.ai" ,
18601944 )
1945+ client .sdk_configuration .__dict__ ["telemetry" ] = "global"
18611946 with tracer .start_as_current_span (
18621947 "ExecuteActivity:generate_site_diagnostic"
18631948 ) as activity_span :
@@ -1901,6 +1986,7 @@ async def _run(server_url: str):
19011986 async_client = async_client ,
19021987 server_url = server_url ,
19031988 )
1989+ client .sdk_configuration .__dict__ ["telemetry" ] = "global"
19041990
19051991 with tracer .start_as_current_span (
19061992 "ExecuteActivity:generate_site_diagnostic"
@@ -1957,7 +2043,7 @@ def test_custom_provider_captures_spans(self):
19572043 hook = TracingHook ()
19582044 hook .tracer_provider = custom_provider
19592045
1960- hook_ctx = _make_hook_context ("chat_completion" )
2046+ hook_ctx = _make_hook_context ("chat_completion" , telemetry = None )
19612047
19622048 request_body = _dump (
19632049 ChatCompletionRequest (
@@ -2002,12 +2088,13 @@ def test_custom_provider_captures_spans(self):
20022088 ]
20032089 self .assertEqual (len (global_spans ), 0 )
20042090
2005- def test_fallback_to_global_provider (self ):
2006- """When tracer_provider is None (default) , spans go to the global provider."""
2091+ def test_global_telemetry_uses_global_provider (self ):
2092+ """When telemetry is set to global , spans go to the global provider."""
20072093 _EXPORTER .clear ()
20082094
20092095 hook = TracingHook ()
2010- # tracer_provider defaults to None — should use global provider
2096+ # tracer_provider defaults to None; telemetry="global" opts into the
2097+ # configured global provider.
20112098 self .assertIsNone (hook .tracer_provider )
20122099
20132100 hook_ctx = _make_hook_context ("chat_completion" )
0 commit comments