Skip to content

Commit 8a259f6

Browse files
authored
Add public telemetry tracer helper (#568)
* Add public telemetry tracer helper * Fix telemetry helper test typing
1 parent 3209ed5 commit 8a259f6

4 files changed

Lines changed: 160 additions & 2 deletions

File tree

src/mistralai/extra/observability/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from .telemetry import (
88
TelemetryConfigurationError,
99
configure_telemetry,
10+
get_telemetry_tracer,
1011
)
1112

1213
if TYPE_CHECKING:
@@ -47,6 +48,7 @@ def set_tracer_provider(
4748
__all__ = [
4849
"TelemetryConfigurationError",
4950
"configure_telemetry",
51+
"get_telemetry_tracer",
5052
"set_tracer_provider",
5153
"trace",
5254
]

src/mistralai/extra/observability/telemetry.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
from mistralai.client.utils import get_security_from_env
1313

14-
from .otel import OTEL_SERVICE_NAME
14+
from .otel import MISTRAL_SDK_OTEL_TRACER_NAME, OTEL_SERVICE_NAME
1515

1616
if TYPE_CHECKING:
1717
from opentelemetry.sdk.trace import TracerProvider as SDKTracerProvider
@@ -125,6 +125,44 @@ def configure_telemetry(
125125
return True
126126

127127

128+
def get_telemetry_tracer(
129+
client: "Mistral",
130+
name: str | None = None,
131+
) -> otel_trace.Tracer:
132+
"""Return a tracer from the telemetry provider configured for a client.
133+
134+
Custom and SDK-owned dedicated providers are used directly. Clients
135+
explicitly configured for global telemetry use the OpenTelemetry global
136+
provider. If telemetry is disabled or has not been configured for this
137+
client, a TelemetryConfigurationError is raised.
138+
"""
139+
hooks = getattr(client.sdk_configuration, "_hooks", None)
140+
if hooks is None:
141+
raise ValueError("Cannot get telemetry tracer: SDK hooks not initialised.")
142+
143+
hook = _get_tracing_hook(hooks)
144+
tracer_name = name or MISTRAL_SDK_OTEL_TRACER_NAME
145+
146+
if hook.tracer_provider is None and not hook._telemetry_use_global_provider:
147+
configure_telemetry_for_hook(
148+
hook,
149+
client.sdk_configuration,
150+
finalizer_owner=client,
151+
respect_global_provider=True,
152+
)
153+
154+
if hook.tracer_provider is not None:
155+
return hook.tracer_provider.get_tracer(tracer_name)
156+
if hook._telemetry_use_global_provider:
157+
return otel_trace.get_tracer(tracer_name)
158+
159+
raise TelemetryConfigurationError(
160+
"Telemetry is not configured for this client. Call configure_telemetry(client) "
161+
"or configure_telemetry(client, provider='global') before requesting a "
162+
"telemetry tracer."
163+
)
164+
165+
128166
def configure_telemetry_for_hook(
129167
hook: "TracingHook",
130168
sdk_config: "SDKConfiguration",

src/mistralai/extra/tests/test_otel_tracing.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2029,6 +2029,54 @@ async def _run(server_url: str):
20292029
class TestPerInstanceTracerProvider(unittest.TestCase):
20302030
"""Tests for per-instance tracer_provider support via set_tracer_provider."""
20312031

2032+
def test_get_telemetry_tracer_dedicated_provider_captures_app_spans(self):
2033+
from mistralai.extra.observability import (
2034+
configure_telemetry,
2035+
get_telemetry_tracer,
2036+
)
2037+
2038+
exporter = InMemorySpanExporter()
2039+
dedicated_provider = TracerProvider()
2040+
dedicated_provider.add_span_processor(SimpleSpanProcessor(exporter))
2041+
2042+
with patch.dict(os.environ, {}, clear=True):
2043+
with patch(
2044+
"mistralai.extra.observability.telemetry._create_telemetry_tracer_provider",
2045+
return_value=dedicated_provider,
2046+
):
2047+
with (
2048+
_ChatCompletionTestServer() as server,
2049+
httpx.Client() as http_client,
2050+
):
2051+
client = Mistral(
2052+
api_key="test-key",
2053+
client=http_client,
2054+
server_url=server.url,
2055+
)
2056+
configure_telemetry(client)
2057+
tracer = get_telemetry_tracer(client, "my-agent")
2058+
2059+
with tracer.start_as_current_span("invoke_agent"):
2060+
client.chat.complete(
2061+
model="mistral-small-latest",
2062+
messages=_make_user_messages("hello"),
2063+
)
2064+
2065+
with tracer.start_as_current_span(
2066+
"execute_tool web_search"
2067+
):
2068+
pass
2069+
2070+
spans = exporter.get_finished_spans()
2071+
invoke_span = next(s for s in spans if s.name == "invoke_agent")
2072+
chat_span = next(s for s in spans if s.name == "chat mistral-small-latest")
2073+
tool_span = next(s for s in spans if s.name == "execute_tool web_search")
2074+
2075+
self.assertEqual(chat_span.parent.span_id, invoke_span.context.span_id)
2076+
self.assertEqual(tool_span.parent.span_id, invoke_span.context.span_id)
2077+
self.assertEqual(chat_span.context.trace_id, invoke_span.context.trace_id)
2078+
self.assertEqual(tool_span.context.trace_id, invoke_span.context.trace_id)
2079+
20322080
def test_custom_provider_captures_spans(self):
20332081
"""Spans go to the instance-specific exporter, not the global provider."""
20342082
# Create a standalone provider with its own exporter

src/mistralai/extra/tests/test_telemetry.py

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@
1010
from mistralai.client.models import Security
1111
from mistralai.client.sdkconfiguration import SDKConfiguration
1212
from mistralai.client.utils.logger import get_default_logger
13-
from mistralai.extra.observability import configure_telemetry, set_tracer_provider
13+
from mistralai.extra.observability import (
14+
configure_telemetry,
15+
get_telemetry_tracer,
16+
set_tracer_provider,
17+
)
18+
from mistralai.extra.observability.otel import MISTRAL_SDK_OTEL_TRACER_NAME
1419
from mistralai.extra.observability.telemetry import (
1520
MISTRAL_TELEMETRY_ENDPOINT,
1621
MISTRAL_SDK_TELEMETRY_ENV,
@@ -64,6 +69,14 @@ def _configure_for_hook(
6469
class FakeProvider:
6570
def __init__(self):
6671
self.shutdown_called = False
72+
self.get_tracer_calls: list[str] = []
73+
self.tracers: dict[str, MagicMock] = {}
74+
75+
def get_tracer(self, name: str):
76+
self.get_tracer_calls.append(name)
77+
if name not in self.tracers:
78+
self.tracers[name] = MagicMock(name=f"tracer:{name}")
79+
return self.tracers[name]
6780

6881
def shutdown(self):
6982
self.shutdown_called = True
@@ -277,6 +290,63 @@ def test_configure_telemetry_dedicated_replaces_custom_without_shutdown(self):
277290
self.assertIs(hook.tracer_provider, dedicated_provider)
278291
self.assertIs(hook._auto_telemetry_provider, dedicated_provider)
279292

293+
def test_get_telemetry_tracer_dedicated_uses_auto_provider(self):
294+
provider = FakeProvider()
295+
296+
with patch.dict(os.environ, {}, clear=True):
297+
with patch(
298+
"mistralai.extra.observability.telemetry._create_telemetry_tracer_provider",
299+
return_value=provider,
300+
):
301+
client = _make_client(api_key="test-key")
302+
configure_telemetry(client)
303+
tracer = get_telemetry_tracer(client)
304+
305+
self.assertIs(tracer, provider.tracers[MISTRAL_SDK_OTEL_TRACER_NAME])
306+
self.assertEqual(provider.get_tracer_calls, [MISTRAL_SDK_OTEL_TRACER_NAME])
307+
308+
def test_get_telemetry_tracer_custom_provider_uses_custom_name(self):
309+
provider = FakeProvider()
310+
client = _make_client(api_key="test-key")
311+
312+
configure_telemetry(client, provider=cast(TracerProvider, provider))
313+
tracer = get_telemetry_tracer(client, "my-agent")
314+
315+
self.assertIs(tracer, provider.tracers["my-agent"])
316+
self.assertEqual(provider.get_tracer_calls, ["my-agent"])
317+
318+
def test_get_telemetry_tracer_global_uses_global_provider(self):
319+
client = _make_client(api_key="test-key")
320+
configure_telemetry(client, provider="global")
321+
tracer = MagicMock()
322+
323+
with patch(
324+
"mistralai.extra.observability.telemetry.otel_trace.get_tracer",
325+
return_value=tracer,
326+
) as get_tracer:
327+
result = get_telemetry_tracer(client, "my-agent")
328+
329+
self.assertIs(result, tracer)
330+
get_tracer.assert_called_once_with("my-agent")
331+
332+
def test_get_telemetry_tracer_disabled_raises_configuration_error(self):
333+
env_cases = ({}, {MISTRAL_SDK_TELEMETRY_ENV: "false"})
334+
335+
for env in env_cases:
336+
with self.subTest(env=env):
337+
with patch.dict(os.environ, env, clear=True):
338+
with patch(
339+
"mistralai.extra.observability.telemetry._create_telemetry_tracer_provider"
340+
) as create_provider:
341+
client = _make_client(api_key="test-key")
342+
with self.assertRaisesRegex(
343+
TelemetryConfigurationError,
344+
"Telemetry is not configured",
345+
):
346+
get_telemetry_tracer(client)
347+
348+
create_provider.assert_not_called()
349+
280350
def test_internal_explicit_false_overrides_env_dedicated(self):
281351
with patch.dict(
282352
os.environ, {MISTRAL_SDK_TELEMETRY_ENV: "dedicated"}, clear=True

0 commit comments

Comments
 (0)