diff --git a/sdks/python/src/opik/api_objects/opik_client.py b/sdks/python/src/opik/api_objects/opik_client.py index 5d8d94d935..18de3b6a31 100644 --- a/sdks/python/src/opik/api_objects/opik_client.py +++ b/sdks/python/src/opik/api_objects/opik_client.py @@ -75,7 +75,7 @@ def __init__( api_key=api_key, ) - config_.is_misconfigured( + config_.check_for_known_misconfigurations( show_misconfiguration_message=_show_misconfiguration_message, ) self._config = config_ diff --git a/sdks/python/src/opik/config.py b/sdks/python/src/opik/config.py index f44c91cf6c..cf10a7888b 100644 --- a/sdks/python/src/opik/config.py +++ b/sdks/python/src/opik/config.py @@ -191,32 +191,25 @@ def config_file_fullpath(self) -> pathlib.Path: config_file_path = os.getenv("OPIK_CONFIG_PATH", CONFIG_FILE_PATH_DEFAULT) return pathlib.Path(config_file_path).expanduser() - def save_to_file(self) -> None: + @property + def config_file_exists(self) -> bool: """ - Save configuration to a file - - Raises: - OSError: If there is an issue writing to the file. + Determines whether the configuration file exists at the specified path. """ - config_file_content = configparser.ConfigParser() - - config_file_content["opik"] = { - "url_override": self.url_override, - "workspace": self.workspace, - } + return self.config_file_fullpath.exists() - if self.api_key is not None: - config_file_content["opik"]["api_key"] = self.api_key + @property + def is_cloud_installation(self) -> bool: + """ + Determine if the installation type is a cloud installation. + """ + return url_helpers.get_base_url(self.url_override) == url_helpers.get_base_url( + OPIK_URL_CLOUD + ) - try: - with open( - self.config_file_fullpath, mode="w+", encoding="utf-8" - ) as config_file: - config_file_content.write(config_file) - LOGGER.info(f"Configuration saved to file: {self.config_file_fullpath}") - except OSError as e: - LOGGER.error(f"Failed to save configuration: {e}") - raise + @property + def is_localhost_installation(self) -> bool: + return "localhost" in self.url_override @pydantic.model_validator(mode="after") def _set_url_override_from_api_key(self) -> "OpikConfig": @@ -238,35 +231,49 @@ def _set_url_override_from_api_key(self) -> "OpikConfig": return self - @property - def is_config_file_exists(self) -> bool: - """ - Determines whether the configuration file exists at the specified path. + def save_to_file(self) -> None: """ - return self.config_file_fullpath.exists() + Save configuration to a file - @property - def is_cloud_installation(self) -> bool: - """ - Determine if the installation type is a cloud installation. + Raises: + OSError: If there is an issue writing to the file. """ - return url_helpers.get_base_url(self.url_override) == url_helpers.get_base_url( - OPIK_URL_CLOUD - ) + config_file_content = configparser.ConfigParser() - def get_current_config_with_api_key_hidden(self) -> Dict[str, Any]: + config_file_content["opik"] = { + "url_override": self.url_override, + "workspace": self.workspace, + } + + if self.api_key is not None: + config_file_content["opik"]["api_key"] = self.api_key + + try: + with open( + self.config_file_fullpath, mode="w+", encoding="utf-8" + ) as config_file: + config_file_content.write(config_file) + LOGGER.info(f"Configuration saved to file: {self.config_file_fullpath}") + except OSError as e: + LOGGER.error(f"Failed to save configuration: {e}") + raise + + def as_dict(self, mask_api_key: bool) -> Dict[str, Any]: """ Retrieves the current configuration with the API key value masked. """ current_values = self.model_dump() - if current_values.get("api_key") is not None: + if current_values.get("api_key") is not None and mask_api_key: current_values["api_key"] = "*** HIDDEN ***" return current_values - def is_misconfigured(self, show_misconfiguration_message: bool = False) -> bool: + def check_for_known_misconfigurations( + self, show_misconfiguration_message: bool = False + ) -> bool: """ - Determines if Opik configuration is misconfigured and optionally displays + Attempts to detects if Opik is misconfigured and optionally displays a corresponding error message. + Works only for Opik cloud and OSS localhost installations. Parameters: show_misconfiguration_message : A flag indicating whether to display detailed error messages if the configuration @@ -274,7 +281,7 @@ def is_misconfigured(self, show_misconfiguration_message: bool = False) -> bool: """ is_misconfigured_flag, error_message = ( - self.get_misconfiguration_validation_results() + self.get_misconfiguration_detection_results() ) if is_misconfigured_flag: @@ -289,7 +296,33 @@ def is_misconfigured(self, show_misconfiguration_message: bool = False) -> bool: return False - def is_misconfigured_for_cloud(self) -> Tuple[bool, Optional[str]]: + def get_misconfiguration_detection_results(self) -> Tuple[bool, Optional[str]]: + """ + Tries detecting misconfigurations for either cloud or localhost environments. + The detection will not work for any other kind of installation. + + Returns: + Tuple[bool, Optional[str]]: A tuple where the first element indicates + whether the configuration is misconfigured (True for misconfigured, False for valid). + The second element is an optional string that contains + an error message if there is a configuration issue, or None if the + configuration is valid. + """ + is_misconfigured_for_cloud_flag, error_message = ( + self._is_misconfigured_for_cloud() + ) + if is_misconfigured_for_cloud_flag: + return True, error_message + + is_misconfigured_for_localhost_flag, error_message = ( + self._is_misconfigured_for_localhost() + ) + if is_misconfigured_for_localhost_flag: + return True, error_message + + return False, None + + def _is_misconfigured_for_cloud(self) -> Tuple[bool, Optional[str]]: """ Determines if the current Opik configuration is misconfigured for cloud logging. @@ -316,7 +349,7 @@ def is_misconfigured_for_cloud(self) -> Tuple[bool, Optional[str]]: return False, None - def is_misconfigured_for_local(self) -> Tuple[bool, Optional[str]]: + def _is_misconfigured_for_localhost(self) -> Tuple[bool, Optional[str]]: """ Determines if the current setup is misconfigured for a local open-source installation. @@ -325,14 +358,12 @@ def is_misconfigured_for_local(self) -> Tuple[bool, Optional[str]]: the configuration is misconfigured for local logging, and the second element is either an error message indicating the reason for misconfiguration or None. """ - localhost_installation = ( - "localhost" in self.url_override - ) # does not detect all OSS installations + workspace_is_default = self.workspace == OPIK_WORKSPACE_DEFAULT_NAME tracking_disabled = self.track_disable if ( - localhost_installation + self.is_localhost_installation and not workspace_is_default and not tracking_disabled ): @@ -346,35 +377,6 @@ def is_misconfigured_for_local(self) -> Tuple[bool, Optional[str]]: return False, None - def get_misconfiguration_validation_results(self) -> Tuple[bool, Optional[str]]: - """ - Validates the current configuration and identifies any misconfigurations - for either cloud or local environments. This method checks both cloud - and local configurations and determines the validity of each, returning - a boolean indicator of success or failure and an optional error message - if there is an issue. - - Returns: - Tuple[bool, Optional[str]]: A tuple where the first element indicates - whether the configuration is misconfigured (True for misconfigured, False for valid). - The second element is an optional string that contains - an error message if there is a configuration issue, or None if the - configuration is valid. - """ - is_misconfigured_for_cloud_flag, error_message = ( - self.is_misconfigured_for_cloud() - ) - if is_misconfigured_for_cloud_flag: - return True, error_message - - is_misconfigured_for_local_flag, error_message = ( - self.is_misconfigured_for_local() - ) - if is_misconfigured_for_local_flag: - return True, error_message - - return False, None - def update_session_config(key: str, value: Any) -> None: _SESSION_CACHE_DICT[key] = value diff --git a/sdks/python/src/opik/evaluation/metrics/base_metric.py b/sdks/python/src/opik/evaluation/metrics/base_metric.py index 731a176fc2..334500a06e 100644 --- a/sdks/python/src/opik/evaluation/metrics/base_metric.py +++ b/sdks/python/src/opik/evaluation/metrics/base_metric.py @@ -39,7 +39,7 @@ def __init__(self, name: str, track: bool = True) -> None: config = opik_config.OpikConfig() - if track and config.is_misconfigured() is False: + if track and config.check_for_known_misconfigurations() is False: self.score = opik.track(name=self.name)(self.score) # type: ignore self.ascore = opik.track(name=self.name)(self.ascore) # type: ignore diff --git a/sdks/python/src/opik/evaluation/models/litellm/opik_monitor.py b/sdks/python/src/opik/evaluation/models/litellm/opik_monitor.py index 017fed841b..bd51b44ccd 100644 --- a/sdks/python/src/opik/evaluation/models/litellm/opik_monitor.py +++ b/sdks/python/src/opik/evaluation/models/litellm/opik_monitor.py @@ -35,7 +35,7 @@ def enabled_in_config() -> bool: @functools.lru_cache def opik_is_misconfigured() -> bool: config_ = config.OpikConfig() - return config_.is_misconfigured() + return config_.check_for_known_misconfigurations() def _add_span_metadata_to_params(params: Dict[str, Any]) -> Dict[str, Any]: diff --git a/sdks/python/src/opik/healthcheck/__init__.py b/sdks/python/src/opik/healthcheck/__init__.py index afb68fb9b5..3170f66372 100644 --- a/sdks/python/src/opik/healthcheck/__init__.py +++ b/sdks/python/src/opik/healthcheck/__init__.py @@ -29,9 +29,15 @@ def run(show_installed_packages: bool = True) -> None: rich_representation.print_header("current configuration") rich_representation.print_current_config(config_obj) - rich_representation.print_header("current configuration validation") - is_misconfigured, err_msg = config_obj.get_misconfiguration_validation_results() - rich_representation.print_config_validation(not is_misconfigured, err_msg) + if config_obj.is_cloud_installation or config_obj.is_localhost_installation: + # Misconfigurations can be detected ONLY for localhost and cloud, not for other installations + rich_representation.print_header("Configuration scan") + misconfiguration_detected, err_msg = ( + config_obj.get_misconfiguration_detection_results() + ) + rich_representation.print_config_scan_results( + misconfiguration_detected=misconfiguration_detected, error_message=err_msg + ) rich_representation.print_header("backend workspace availability") is_available, err_msg = checks.get_backend_workspace_availability() diff --git a/sdks/python/src/opik/healthcheck/checks.py b/sdks/python/src/opik/healthcheck/checks.py index 88b907f05f..ea0e87c1db 100644 --- a/sdks/python/src/opik/healthcheck/checks.py +++ b/sdks/python/src/opik/healthcheck/checks.py @@ -20,7 +20,7 @@ def get_backend_workspace_availability() -> Tuple[bool, Optional[str]]: try: opik_obj.auth_check() is_available = True - except httpx.ConnectError as e: + except (httpx.ConnectError, httpx.TimeoutException) as e: err_msg = ( f"Error while checking backend workspace availability: {e}\n\n" "Can't connect to the backend service. " diff --git a/sdks/python/src/opik/healthcheck/rich_representation.py b/sdks/python/src/opik/healthcheck/rich_representation.py index 432a78be7d..f028ba9d14 100644 --- a/sdks/python/src/opik/healthcheck/rich_representation.py +++ b/sdks/python/src/opik/healthcheck/rich_representation.py @@ -55,7 +55,7 @@ def print_config_file_details(config: OpikConfig) -> None: file_path = make_value_text(str(config.config_file_fullpath)) is_exists_label = make_key_text("Config file exists:") - is_exists = make_value_text(str(config.is_config_file_exists)) + is_exists = make_value_text("yes" if config.config_file_exists else "no") console.print(file_path_label, file_path) console.print(is_exists_label, is_exists) @@ -66,7 +66,7 @@ def print_current_config(config: config.OpikConfig) -> None: table.add_column("Setting", style=DEFAULT_KEY_COLOR) table.add_column("Value", style=DEFAULT_VALUE_COLOR) - current_config_values = config.get_current_config_with_api_key_hidden() + current_config_values = config.as_dict(mask_api_key=True) for key, value in sorted(current_config_values.items()): if key != "sentry_dsn": table.add_row(key, str(value)) @@ -74,15 +74,18 @@ def print_current_config(config: config.OpikConfig) -> None: console.print(table) -def print_config_validation(is_valid: bool, error_message: Optional[str]) -> None: - is_valid_text = Text( - str(is_valid), style=DEFAULT_VALUE_COLOR if is_valid else DEFAULT_ERROR_COLOR +def print_config_scan_results( + misconfiguration_detected: bool, error_message: Optional[str] +) -> None: + is_misconfigured_text = Text( + "found" if misconfiguration_detected else "not found", + style=DEFAULT_ERROR_COLOR if misconfiguration_detected else DEFAULT_VALUE_COLOR, ) - is_valid_label = make_key_text("Current configuration is valid:") + issues_found_label = make_key_text("Configuration issues:") - console.print(is_valid_label, is_valid_text) + console.print(issues_found_label, is_misconfigured_text) - if is_valid: + if not misconfiguration_detected: return err_msg = Text(error_message, style=DEFAULT_ERROR_COLOR) @@ -94,7 +97,7 @@ def print_backend_workspace_availability( err_msg: Optional[str], ) -> None: is_available_text = Text( - str(is_available), + "yes" if is_available else "no", style=DEFAULT_VALUE_COLOR if is_available else DEFAULT_ERROR_COLOR, ) is_available_label = make_key_text("Backend workspace available:")