Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1465,6 +1465,8 @@ with Mistral(api_key=os.environ["MISTRAL_API_KEY"]) as client:
)
```

In dedicated mode, traces go to `/telemetry/v1/traces` on the client's `server_url` if one is set, otherwise to `https://api.mistral.ai`, including for the named `eu`/`us` servers. Set `MISTRAL_OTLP_TRACES_ENDPOINT` to send traces to a separate collector.

### Provider modes

`configure_telemetry(client, provider=...)` selects where spans go and who owns the export pipeline:
Expand Down Expand Up @@ -1552,7 +1554,7 @@ configure_telemetry(
| Variable | Description | Default |
| -------- | ----------- | ------- |
| `MISTRAL_SDK_TELEMETRY` | Auto-enable telemetry: `dedicated`, `global`, or `false`. | unset (disabled) |
| `MISTRAL_OTLP_TRACES_ENDPOINT` | Override the OTLP traces endpoint used in dedicated mode. | `https://api.mistral.ai/telemetry/v1/traces` |
| `MISTRAL_OTLP_TRACES_ENDPOINT` | Override the OTLP traces endpoint used in dedicated mode. | Client `server_url` + `/telemetry/v1/traces` if set, otherwise `https://api.mistral.ai/telemetry/v1/traces` |
| `MISTRAL_SDK_DEBUG_TRACING` | Set to `true` for verbose tracing logs. | `false` |
| `MISTRAL_API_KEY` | Used as the bearer token for the dedicated-mode exporter. | — |

Expand Down
25 changes: 17 additions & 8 deletions src/mistralai/extra/observability/telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from opentelemetry import trace as otel_trace

from mistralai.client.sdkconfiguration import SERVER_GLOBAL, SERVERS
from mistralai.client.utils import get_security_from_env

from .otel import MISTRAL_SDK_OTEL_TRACER_NAME, OTEL_SERVICE_NAME
Expand All @@ -27,7 +28,8 @@


MISTRAL_SDK_TELEMETRY_ENV = "MISTRAL_SDK_TELEMETRY"
MISTRAL_TELEMETRY_ENDPOINT = "https://api.mistral.ai/telemetry/v1/traces"
MISTRAL_TELEMETRY_PATH = "/telemetry/v1/traces"
MISTRAL_TELEMETRY_ENDPOINT = f"{SERVERS[SERVER_GLOBAL]}{MISTRAL_TELEMETRY_PATH}"
MISTRAL_OTLP_TRACES_ENDPOINT_ENV = "MISTRAL_OTLP_TRACES_ENDPOINT"
TELEMETRY_PROVIDER_DEDICATED: Final[Literal["dedicated"]] = "dedicated"
TELEMETRY_PROVIDER_GLOBAL: Final[Literal["global"]] = "global"
Expand Down Expand Up @@ -114,7 +116,10 @@ def configure_telemetry(
) -> bool:
"""Configure telemetry provider mode for a Mistral client.
By default, this creates an SDK-owned telemetry provider/exporter. Passing
By default, this creates an SDK-owned telemetry provider/exporter that sends
traces to /telemetry/v1/traces on the client's explicit server_url if set,
otherwise to https://api.mistral.ai (also for the named eu/us servers).
MISTRAL_OTLP_TRACES_ENDPOINT overrides that endpoint. Passing
provider="global" clears the per-client provider so SDK spans use the
global OpenTelemetry provider. Passing a TracerProvider attaches it to this
client without taking ownership of its lifecycle.
Expand Down Expand Up @@ -272,6 +277,7 @@ def configure_telemetry_for_hook(
api_key = _resolve_api_key_from_security(getattr(sdk_config, "security", None))
provider = _create_telemetry_tracer_provider(
api_key=api_key,
server_url=getattr(sdk_config, "server_url", None),
redaction=redaction,
)
_attach_telemetry_provider(hook, provider, finalizer_owner or sdk_config)
Expand Down Expand Up @@ -313,6 +319,7 @@ def _resolve_api_key_from_security(security: Any) -> str:
def _create_telemetry_tracer_provider(
*,
api_key: str | None,
server_url: str | None = None,
redaction: RedactionPolicyLike | bool = True,
) -> "SDKTracerProvider":
(
Expand All @@ -328,7 +335,11 @@ def _create_telemetry_tracer_provider(
"client or set MISTRAL_API_KEY."
)
exporter = otlp_span_exporter_cls(
endpoint=_resolve_mistral_telemetry_endpoint(),
# Only follow an explicit server_url (e.g. another deployment); the named
# regional servers don't serve the telemetry endpoint.
endpoint=_resolve_mistral_telemetry_endpoint(
server_url or SERVERS[SERVER_GLOBAL]
),
headers={"Authorization": _as_bearer_token(api_key)},
)
policy = _resolve_redaction(redaction)
Expand Down Expand Up @@ -359,11 +370,9 @@ def _load_otel_sdk():
return BatchSpanProcessor, OTLPSpanExporter, Resource, TracerProvider


def _resolve_mistral_telemetry_endpoint() -> str:
return os.getenv(
MISTRAL_OTLP_TRACES_ENDPOINT_ENV,
MISTRAL_TELEMETRY_ENDPOINT,
).strip() or MISTRAL_TELEMETRY_ENDPOINT
def _resolve_mistral_telemetry_endpoint(server_url: str) -> str:
override = os.getenv(MISTRAL_OTLP_TRACES_ENDPOINT_ENV, "").strip()
return override or f"{server_url.rstrip('/')}{MISTRAL_TELEMETRY_PATH}"


def _has_real_global_tracer_provider() -> bool:
Expand Down
155 changes: 152 additions & 3 deletions src/mistralai/extra/tests/test_telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
from typing import TYPE_CHECKING, cast
from unittest.mock import MagicMock, patch

import httpx2 as httpx
import pytest
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import SpanExportResult

from mistralai.client._hooks import SDKHooks
from mistralai.client._hooks.tracing import TracingHook
Expand Down Expand Up @@ -151,6 +153,7 @@ def test_env_dedicated_values_attach_provider(self):
self.assertTrue(configured)
create_provider.assert_called_once_with(
api_key="test-key",
server_url="",
redaction=True,
)
self.assertIs(_get_tracing_hook(client).tracer_provider, provider)
Expand Down Expand Up @@ -215,6 +218,7 @@ def test_configure_telemetry_attaches_per_client_provider(self):
self.assertTrue(configured)
create_provider.assert_called_once_with(
api_key="test-key",
server_url="",
redaction=True,
)
self.assertIs(_get_tracing_hook(client).tracer_provider, provider)
Expand All @@ -233,6 +237,7 @@ def test_configure_telemetry_accepts_explicit_dedicated_provider_mode(self):
self.assertTrue(configured)
create_provider.assert_called_once_with(
api_key="test-key",
server_url="",
redaction=True,
)
self.assertIs(_get_tracing_hook(client).tracer_provider, provider)
Expand Down Expand Up @@ -316,7 +321,9 @@ def test_configure_telemetry_dedicated_reconfigure_applies_new_redaction(self):
hook = _get_tracing_hook(client)
self.assertTrue(configured)
self.assertEqual(create_provider.call_count, 2)
create_provider.assert_any_call(api_key="test-key", redaction=False)
create_provider.assert_any_call(
api_key="test-key", server_url="", redaction=False
)
self.assertTrue(first_provider.shutdown_called)
self.assertIs(hook.tracer_provider, second_provider)
self.assertIs(hook._auto_telemetry_provider, second_provider)
Expand Down Expand Up @@ -462,6 +469,7 @@ def test_env_dedicated_uses_mistral_api_key_fallback(self):
self.assertTrue(configured)
create_provider.assert_called_once_with(
api_key="env-key",
server_url="",
redaction=True,
)
self.assertIs(_get_tracing_hook(client).tracer_provider, provider)
Expand Down Expand Up @@ -502,6 +510,7 @@ def test_env_dedicated_ignores_standard_otel_endpoint_env(self):
self.assertTrue(configured)
create_provider.assert_called_once_with(
api_key="test-key",
server_url="",
redaction=True,
)

Expand Down Expand Up @@ -679,6 +688,142 @@ def test_mistral_endpoint_env_overrides_default_endpoint(self):
)


class TestTelemetryEndpoint(unittest.TestCase):
@staticmethod
def _export_for_client(
client_options: dict[str, str],
auto_enable: bool,
endpoint_override: str | None = None,
) -> tuple[dict, str]:
from mistralai.client.sdk import Mistral

env = {MISTRAL_SDK_TELEMETRY_ENV: "dedicated"} if auto_enable else {}
if endpoint_override is not None:
env[MISTRAL_OTLP_TRACES_ENDPOINT_ENV] = endpoint_override
request_urls: list[str] = []

def handle_request(request: httpx.Request) -> httpx.Response:
request_urls.append(str(request.url))
return httpx.Response(200, json={"object": "list", "data": []})

with (
patch.dict(os.environ, env, clear=True),
patch(
"mistralai.extra.observability.telemetry._has_real_global_tracer_provider",
return_value=False,
),
patch(
"opentelemetry.exporter.otlp.proto.http.trace_exporter.OTLPSpanExporter"
) as exporter_class,
httpx.Client(transport=httpx.MockTransport(handle_request)) as http_client,
Mistral(
api_key="test-key",
client=http_client,
server=client_options.get("server"),
server_url=client_options.get("server_url"),
) as client,
):
exporter_class.return_value.export.return_value = SpanExportResult.SUCCESS
if not auto_enable:
configure_telemetry(client)
try:
client.models.list()
provider = _get_tracing_hook(client)._auto_telemetry_provider
assert provider is not None
assert provider.force_flush()
exporter_class.return_value.export.assert_called_once()
finally:
configure_telemetry(client, provider="global", redaction=False)

exporter_class.assert_called_once()
assert len(request_urls) == 1
return dict(exporter_class.call_args.kwargs), request_urls[0]

def test_telemetry_follows_explicit_server_url(self):
# (client options, expected API base URL, expected telemetry base URL)
cases: list[tuple[dict[str, str], str, str]] = [
({}, "https://api.mistral.ai", "https://api.mistral.ai"),
({"server": "global"}, "https://api.mistral.ai", "https://api.mistral.ai"),
# Named regional servers don't serve the telemetry endpoint.
({"server": "eu"}, "https://api.eu.mistral.ai", "https://api.mistral.ai"),
({"server": "us"}, "https://api.us.mistral.ai", "https://api.mistral.ai"),
(
{"server_url": "https://mistral.example.com"},
"https://mistral.example.com",
"https://mistral.example.com",
),
(
{"server_url": "https://llm.acme-corp.io/"},
"https://llm.acme-corp.io",
"https://llm.acme-corp.io",
),
(
{"server_url": "https://gateway.internal.net/mistral/"},
"https://gateway.internal.net/mistral",
"https://gateway.internal.net/mistral",
),
(
{"server_url": "http://localhost:8080"},
"http://localhost:8080",
"http://localhost:8080",
),
(
{"server": "eu", "server_url": "https://mistral.contoso.dev"},
"https://mistral.contoso.dev",
"https://mistral.contoso.dev",
),
]
for client_options, api_base_url, telemetry_base_url in cases:
for auto_enable in (False, True):
with self.subTest(
client_options=client_options, auto_enable=auto_enable
):
exporter_options, request_url = self._export_for_client(
client_options, auto_enable
)
self.assertEqual(request_url, f"{api_base_url}/v1/models")
self.assertEqual(
exporter_options,
{
"endpoint": f"{telemetry_base_url}/telemetry/v1/traces",
"headers": {"Authorization": "Bearer test-key"},
},
)

def test_blank_endpoint_override_uses_api_server(self):
for endpoint_override in (None, "", " "):
for auto_enable in (False, True):
with self.subTest(
endpoint_override=endpoint_override, auto_enable=auto_enable
):
exporter_options, _ = self._export_for_client(
{"server_url": "https://inference.foobar.ai"},
auto_enable,
endpoint_override,
)
self.assertEqual(
exporter_options["endpoint"],
"https://inference.foobar.ai/telemetry/v1/traces",
)

def test_endpoint_override_takes_precedence(self):
for auto_enable in (False, True):
with self.subTest(auto_enable=auto_enable):
exporter_options, request_url = self._export_for_client(
{"server_url": "https://proxy.widgets.org"},
auto_enable,
" https://collector.example.com/v1/traces ",
)
self.assertEqual(request_url, "https://proxy.widgets.org/v1/models")
self.assertEqual(
exporter_options,
{
"endpoint": "https://collector.example.com/v1/traces",
"headers": {"Authorization": "Bearer test-key"},
},
)


_TELEMETRY_LOGGER = "mistralai.extra.observability.telemetry"


Expand Down Expand Up @@ -751,7 +896,9 @@ def test_dedicated_mode_forwards_custom_redaction_to_provider(self):
policy = RegexRedactionPolicy()
configure_telemetry(client, provider="dedicated", redaction=policy)

create_provider.assert_called_once_with(api_key="test-key", redaction=policy)
create_provider.assert_called_once_with(
api_key="test-key", server_url="", redaction=policy
)

def test_dedicated_mode_forwards_default_redaction_to_provider(self):
# Wiring test
Expand All @@ -762,7 +909,9 @@ def test_dedicated_mode_forwards_default_redaction_to_provider(self):
client = _make_client(api_key="test-key")
configure_telemetry(client, provider="dedicated")

create_provider.assert_called_once_with(api_key="test-key", redaction=True)
create_provider.assert_called_once_with(
api_key="test-key", server_url="", redaction=True
)

def test_global_mode_warns_by_default(self, caplog):
client = _make_client(api_key="test-key")
Expand Down
Loading