From 6158e2acfa519eabf830b52371729853b01d1e76 Mon Sep 17 00:00:00 2001 From: mike0sv Date: Mon, 17 Nov 2025 16:40:52 +0000 Subject: [PATCH 1/4] Tracing support --- tracely/src/tracely/_tracer_provider.py | 62 +++++++++++++++++++++---- 1 file changed, 52 insertions(+), 10 deletions(-) diff --git a/tracely/src/tracely/_tracer_provider.py b/tracely/src/tracely/_tracer_provider.py index 90b1bfd..ae8eb1d 100644 --- a/tracely/src/tracely/_tracer_provider.py +++ b/tracely/src/tracely/_tracer_provider.py @@ -27,6 +27,7 @@ _TRACE_COLLECTOR_PROJECT_ID, ) from .evidently_cloud_client import EvidentlyCloudClient +from .evidently_oss_client import EvidentlyOSSClient from .interceptors import Interceptor @@ -81,9 +82,35 @@ def _create_tracer_provider( "or EVIDENTLY_TRACE_COLLECTOR_PROJECT_ID env variable" ) + # Detect OSS mode by checking if /api/users/login endpoint exists + # Cloud has this endpoint, OSS doesn't + is_oss_mode = False if _exporter_type not in ("console", "inmemory"): - cloud = EvidentlyCloudClient(_address, _api_key) - datasets_response: requests.Response = cloud.request( + try: + # Try to check if cloud login endpoint exists + test_session = requests.Session() + login_url = urllib.parse.urljoin(_address, "/api/users/login") + response = test_session.get( + login_url, + headers={"X-Evidently-Token": _api_key or "test"}, + timeout=2, + ) + # If we get a response (even 401/403), the endpoint exists (Cloud mode) + # Only 404 means the endpoint doesn't exist (OSS mode) + if response.status_code == 404: + is_oss_mode = True + except (requests.exceptions.HTTPError, requests.exceptions.RequestException, requests.exceptions.Timeout): + # If request fails (network error, timeout, etc.), assume OSS mode + is_oss_mode = True + + if _exporter_type not in ("console", "inmemory"): + # Use same logic for both OSS and Cloud, only difference is the client + if is_oss_mode: + client = EvidentlyOSSClient(_address, _api_key) + else: + client = EvidentlyCloudClient(_address, _api_key) + + datasets_response: requests.Response = client.request( "/api/datasets", "GET", query_params={"project_id": _project_id, "source_type": ["tracing"]}, @@ -95,7 +122,7 @@ def _create_tracer_provider( _export_id = dataset["id"] break if _export_id is None: - resp: requests.Response = cloud.request( + resp: requests.Response = client.request( "/api/datasets/tracing", "POST", query_params={"project_id": _project_id}, @@ -104,9 +131,10 @@ def _create_tracer_provider( _export_id = resp.json()["dataset_id"] _data_context.export_id = uuid.UUID(_export_id) + _data_context.project_id = uuid.UUID(_project_id) else: _data_context.export_id = "" - _data_context.project_id = uuid.UUID(_project_id) + _data_context.project_id = uuid.UUID("00000000-0000-0000-0000-000000000000") _data_context.default_usage_details = default_usage_details _data_context.usage_details_by_model_id = usage_details_by_model_id _data_context.interceptors = interceptors or [] @@ -115,7 +143,7 @@ def _create_tracer_provider( resource=Resource.create( { "evidently.export_id": str(_data_context.export_id), - "evidently.project_id": str(_data_context.export_id), + "evidently.project_id": str(_data_context.project_id), } ) ) @@ -124,17 +152,31 @@ def _create_tracer_provider( if _exporter_type == "grpc": from opentelemetry.exporter.otlp.proto.grpc import trace_exporter as grpc_exporter + headers = [] + if _api_key: + if is_oss_mode: + headers = [("evidently-secret", _api_key)] + else: + headers = [("authorization", _api_key)] exporter = grpc_exporter.OTLPSpanExporter( _address, - headers=[] if _api_key is None else [("authorization", _api_key)], + headers=headers, ) elif _exporter_type == "http": from opentelemetry.exporter.otlp.proto.http import trace_exporter as http_exporter - exporter = http_exporter.OTLPSpanExporter( - urllib.parse.urljoin(_address, "/api/v1/traces"), - session=cloud.session(), - ) + if is_oss_mode: + oss_client = EvidentlyOSSClient(_address, _api_key) + exporter = http_exporter.OTLPSpanExporter( + urllib.parse.urljoin(_address, "/api/v1/traces"), + session=oss_client.session(), + ) + else: + cloud = EvidentlyCloudClient(_address, _api_key) + exporter = http_exporter.OTLPSpanExporter( + urllib.parse.urljoin(_address, "/api/v1/traces"), + session=cloud.session(), + ) elif _exporter_type == "console": from opentelemetry.sdk.trace.export import ConsoleSpanExporter From f4bbfb2f8b81f897b1376cec2aeed75b0143eb79 Mon Sep 17 00:00:00 2001 From: mike0sv Date: Mon, 17 Nov 2025 16:41:25 +0000 Subject: [PATCH 2/4] OSS support --- tracely/src/tracely/evidently_oss_client.py | 36 +++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 tracely/src/tracely/evidently_oss_client.py diff --git a/tracely/src/tracely/evidently_oss_client.py b/tracely/src/tracely/evidently_oss_client.py new file mode 100644 index 0000000..11c08b8 --- /dev/null +++ b/tracely/src/tracely/evidently_oss_client.py @@ -0,0 +1,36 @@ +import urllib.parse +from typing import Optional + +import requests + + +class EvidentlyOSSClient: + """Client for Evidently OSS (Open Source) that works with or without authentication.""" + + def __init__(self, url: str, token: Optional[str] = None): + self._base_url = url + self._token = token + self._session = requests.Session() + if token: + self._session.headers.update({"evidently-secret": token}) + + def request( + self, + path: str, + method: str, + headers: Optional[dict] = None, + query_params: Optional[dict] = None, + body: Optional[dict] = None, + ) -> requests.Response: + url = urllib.parse.urljoin(self._base_url, path) + req_headers = dict(self._session.headers) + if headers: + req_headers.update(headers) + req = requests.Request(method, url, params=query_params, headers=req_headers, json=body) + resp = self.session().send(req.prepare()) + resp.raise_for_status() + return resp + + def session(self) -> requests.Session: + return self._session + From ca18ffe9337381cc38381965737fd2e0ffbf89ed Mon Sep 17 00:00:00 2001 From: mike0sv Date: Mon, 17 Nov 2025 16:47:25 +0000 Subject: [PATCH 3/4] fix lint --- tracely/src/tracely/_tracer_provider.py | 2 +- tracely/src/tracely/evidently_oss_client.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/tracely/src/tracely/_tracer_provider.py b/tracely/src/tracely/_tracer_provider.py index ae8eb1d..62a6d1c 100644 --- a/tracely/src/tracely/_tracer_provider.py +++ b/tracely/src/tracely/_tracer_provider.py @@ -109,7 +109,7 @@ def _create_tracer_provider( client = EvidentlyOSSClient(_address, _api_key) else: client = EvidentlyCloudClient(_address, _api_key) - + datasets_response: requests.Response = client.request( "/api/datasets", "GET", diff --git a/tracely/src/tracely/evidently_oss_client.py b/tracely/src/tracely/evidently_oss_client.py index 11c08b8..d91cff1 100644 --- a/tracely/src/tracely/evidently_oss_client.py +++ b/tracely/src/tracely/evidently_oss_client.py @@ -33,4 +33,3 @@ def request( def session(self) -> requests.Session: return self._session - From 6e9a3e7c3390e7d9f2b363ea973c19924a1e6be9 Mon Sep 17 00:00:00 2001 From: mike0sv Date: Thu, 20 Nov 2025 17:37:39 +0000 Subject: [PATCH 4/4] fix mypy --- tracely/src/tracely/_tracer_provider.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tracely/src/tracely/_tracer_provider.py b/tracely/src/tracely/_tracer_provider.py index 62a6d1c..a2062dd 100644 --- a/tracely/src/tracely/_tracer_provider.py +++ b/tracely/src/tracely/_tracer_provider.py @@ -105,6 +105,7 @@ def _create_tracer_provider( if _exporter_type not in ("console", "inmemory"): # Use same logic for both OSS and Cloud, only difference is the client + client: Union[EvidentlyOSSClient, EvidentlyCloudClient] if is_oss_mode: client = EvidentlyOSSClient(_address, _api_key) else: