Skip to content

Commit

Permalink
OPIK-814 [SDK] Implement opik healthcheck CLI command / python func…
Browse files Browse the repository at this point in the history
…tion to validate the current... (#1341)

* OPIK-814 [SDK] Implement `opik healthcheck` CLI command / python function to validate the current...

* refactor config.is_misconfigured()

* reorder exceptions catch

* wip

* wip 2

* fix linter

* move get_installed_packages() to environment.py

* refactoring version func

* refactoring current config printing

* refactoring current config printing

* improve imports

* refactoring

* refactoring

* refactoring

* refactoring opik+config classes

* code formatting

* linter fix

* message colorization refactoring

* config methods refactoring

* config methods refactoring

* small refactoring

* show connection error message related to installation type

* fix wrong condition check

* do not print sentry dsn
  • Loading branch information
japdubengsub authored Feb 27, 2025
1 parent 49f7ade commit d47c20d
Show file tree
Hide file tree
Showing 10 changed files with 369 additions and 67 deletions.
10 changes: 9 additions & 1 deletion sdks/python/src/opik/api_objects/opik_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ def __init__(
host: Optional[str] = None,
api_key: Optional[str] = None,
_use_batching: bool = False,
_show_misconfiguration_message: bool = True,
) -> None:
"""
Initialize an Opik object that can be used to log traces and spans manually to Opik server.
Expand All @@ -61,16 +62,22 @@ def __init__(
api_key: The API key for Opik. This parameter is ignored for local installations.
_use_batching: intended for internal usage in specific conditions only.
Enabling it is unsafe and can lead to data loss.
_show_misconfiguration_message: intended for internal usage in specific conditions only.
Print a warning message if the Opik server is not configured properly.
Returns:
None
"""

config_ = config.get_from_user_inputs(
project_name=project_name,
workspace=workspace,
url_override=host,
api_key=api_key,
)
config.is_misconfigured(config_, show_misconfiguration_message=True)

config_.is_misconfigured(
show_misconfiguration_message=_show_misconfiguration_message,
)
self._config = config_

self._workspace: str = config_.workspace
Expand Down Expand Up @@ -895,6 +902,7 @@ def create_prompt(
Parameters:
name: The name of the prompt.
prompt: The template content of the prompt.
metadata: Optional metadata to be included in the prompt.
Returns:
A Prompt object containing details of the created or retrieved prompt.
Expand Down
24 changes: 22 additions & 2 deletions sdks/python/src/opik/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@
from importlib import metadata

import click
from opik.configurator import configure as opik_configure
from opik.configurator import interactive_helpers
from rich.console import Console

from opik import healthcheck as opik_healthcheck
from opik.configurator import configure as opik_configure, interactive_helpers

console = Console()

LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -169,5 +173,21 @@ def proxy(
) # Reduce uvicorn logging to keep output clean


@cli.command(context_settings={"ignore_unknown_options": True})
@click.option(
"--show-installed-packages",
is_flag=True,
default=False,
help="Print the list of installed packages to the console.",
)
def healthcheck(show_installed_packages: bool = True) -> None:
"""
Performs a health check of the application, including validation of configuration,
verification of library installations, and checking the availability of the backend workspace.
Prints all relevant information to assist in debugging and diagnostics.
"""
opik_healthcheck.run(show_installed_packages)


if __name__ == "__main__":
cli()
182 changes: 131 additions & 51 deletions sdks/python/src/opik/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,72 +238,152 @@ 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.
"""
return self.config_file_fullpath.exists()

def update_session_config(key: str, value: Any) -> None:
_SESSION_CACHE_DICT[key] = value
@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
)

def get_current_config_with_api_key_hidden(self) -> 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:
current_values["api_key"] = "*** HIDDEN ***"
return current_values

def get_from_user_inputs(**user_inputs: Any) -> OpikConfig:
"""
Instantiates an OpikConfig using provided user inputs.
"""
cleaned_user_inputs = dict_utils.remove_none_from_dict(user_inputs)
def is_misconfigured(self, show_misconfiguration_message: bool = False) -> bool:
"""
Determines if Opik configuration is misconfigured and optionally displays
a corresponding error message.
return OpikConfig(**cleaned_user_inputs)
Parameters:
show_misconfiguration_message : A flag indicating whether to display detailed error messages if the configuration
is determined to be misconfigured. Defaults to False.
"""

is_misconfigured_flag, error_message = (
self.get_misconfiguration_validation_results()
)

def is_misconfigured(
config: OpikConfig,
show_misconfiguration_message: bool = False,
) -> bool:
"""
Determines if the provided Opik configuration is misconfigured and optionally displays
a corresponding error message.
Parameters:
config: The configuration object containing settings such as URL overrides, workspace, API key,
and tracking options to be validated for misconfiguration.
show_misconfiguration_message : A flag indicating whether to display detailed error messages if the configuration
is determined to be misconfigured. Defaults to False.
"""
if is_misconfigured_flag:
if show_misconfiguration_message:
print()
LOGGER.error(
"========================\n"
f"{error_message}\n"
"==============================\n"
)
return True

cloud_installation = url_helpers.get_base_url(
config.url_override
) == url_helpers.get_base_url(OPIK_URL_CLOUD)
localhost_installation = (
"localhost" in config.url_override
) # does not detect all OSS installations
workspace_is_default = config.workspace == OPIK_WORKSPACE_DEFAULT_NAME
api_key_configured = config.api_key is not None
tracking_disabled = config.track_disable

if (
cloud_installation
and (not api_key_configured or workspace_is_default)
and not tracking_disabled
):
if show_misconfiguration_message:
print()
LOGGER.error(
"========================\n"
return False

def is_misconfigured_for_cloud(self) -> Tuple[bool, Optional[str]]:
"""
Determines if the current Opik configuration is misconfigured for cloud logging.
Returns:
Tuple[bool, Optional[str]]: A tuple where the first element is a boolean indicating if
the configuration is misconfigured for cloud logging, and the second element is either
an error message indicating the reason for misconfiguration or None.
"""
workspace_is_default = self.workspace == OPIK_WORKSPACE_DEFAULT_NAME
api_key_configured = self.api_key is not None
tracking_disabled = self.track_disable

if (
self.is_cloud_installation
and (not api_key_configured or workspace_is_default)
and not tracking_disabled
):
error_message = (
"The workspace and API key must be specified to log data to https://www.comet.com/opik.\n"
"You can use `opik configure` CLI command to configure your environment for logging.\n"
"See the configuration details in the docs: https://www.comet.com/docs/opik/tracing/sdk_configuration.\n"
"==============================\n"
)
return True
return True, error_message

return False, None

def is_misconfigured_for_local(self) -> Tuple[bool, Optional[str]]:
"""
Determines if the current setup is misconfigured for a local open-source installation.
if localhost_installation and not workspace_is_default and not tracking_disabled:
if show_misconfiguration_message:
print()
LOGGER.error(
"========================\n"
Returns:
Tuple[bool, Optional[str]]: A tuple where the first element is a boolean indicating if
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
and not workspace_is_default
and not tracking_disabled
):
error_message = (
"Open source installations do not support workspace specification. Only `default` is available.\n"
"See the configuration details in the docs: https://www.comet.com/docs/opik/tracing/sdk_configuration\n"
"If you need advanced workspace management - you may consider using our cloud offer (https://www.comet.com/site/pricing/)\n"
"or contact our team for purchasing and setting up a self-hosted installation.\n"
"==============================\n"
)
return True
return True, error_message

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

return False

def update_session_config(key: str, value: Any) -> None:
_SESSION_CACHE_DICT[key] = value


def get_from_user_inputs(**user_inputs: Any) -> OpikConfig:
"""
Instantiates an OpikConfig using provided user inputs.
"""
cleaned_user_inputs = dict_utils.remove_none_from_dict(user_inputs)

return OpikConfig(**cleaned_user_inputs)
17 changes: 14 additions & 3 deletions sdks/python/src/opik/environment.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import functools
import getpass
import logging
import os
import platform
import socket
import sys
import functools

from typing import Literal
from importlib import metadata
from typing import Dict, Literal

import opik.config
from opik import url_helpers
Expand Down Expand Up @@ -129,3 +129,14 @@ def in_colab() -> bool:

ipy = IPython.get_ipython()
return "google.colab" in str(ipy)


@functools.lru_cache
def get_installed_packages() -> Dict[str, str]:
"""
Retrieve a dictionary of installed packages with their versions.
"""
installed_packages = {
pkg.metadata["Name"]: pkg.version for pkg in metadata.distributions()
}
return installed_packages
2 changes: 1 addition & 1 deletion sdks/python/src/opik/evaluation/metrics/base_metric.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def __init__(self, name: str, track: bool = True) -> None:

config = opik_config.OpikConfig()

if track and opik_config.is_misconfigured(config) is False:
if track and config.is_misconfigured() is False:
self.score = opik.track(name=self.name)(self.score) # type: ignore
self.ascore = opik.track(name=self.name)(self.ascore) # type: ignore

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def enabled_in_config() -> bool:
@functools.lru_cache
def opik_is_misconfigured() -> bool:
config_ = config.OpikConfig()
return config.is_misconfigured(config_)
return config_.is_misconfigured()


def _add_span_metadata_to_params(params: Dict[str, Any]) -> Dict[str, Any]:
Expand Down
16 changes: 8 additions & 8 deletions sdks/python/src/opik/forwarding_server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,25 +178,25 @@ async def chat_completions(request: Request) -> Any:
)
response = await client.chat.completions.create(**body)
return JSONResponse(response.model_dump())
except APIConnectionError as e:
except APITimeoutError as e:
forward_logger.error(
f"[dim]{request.state.request_id}[/] Failed to connect to LLM server: {str(e)}"
f"[dim]{request.state.request_id}[/] LLM server timeout: {str(e)}"
)
raise HTTPException(
status_code=503,
status_code=504,
detail={
"error": f"Failed to connect to LLM server. Is it running at {llm_server_host}?",
"error": "LLM server took too long to respond",
"request_id": request.state.request_id,
},
)
except APITimeoutError as e:
except APIConnectionError as e:
forward_logger.error(
f"[dim]{request.state.request_id}[/] LLM server timeout: {str(e)}"
f"[dim]{request.state.request_id}[/] Failed to connect to LLM server: {str(e)}"
)
raise HTTPException(
status_code=504,
status_code=503,
detail={
"error": "LLM server took too long to respond",
"error": f"Failed to connect to LLM server. Is it running at {llm_server_host}?",
"request_id": request.state.request_id,
},
)
Expand Down
Loading

0 comments on commit d47c20d

Please sign in to comment.