Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 25 commits into from
Feb 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
4910874
OPIK-814 [SDK] Implement `opik healthcheck` CLI command / python func…
japdubengsub Feb 20, 2025
f44ce36
refactor config.is_misconfigured()
japdubengsub Feb 21, 2025
372d8e2
reorder exceptions catch
japdubengsub Feb 21, 2025
057450d
wip
japdubengsub Feb 21, 2025
7a9976a
wip 2
japdubengsub Feb 21, 2025
b0f6d6f
fix linter
japdubengsub Feb 21, 2025
0fffb0e
move get_installed_packages() to environment.py
japdubengsub Feb 26, 2025
e296acb
refactoring version func
japdubengsub Feb 26, 2025
bfa7a77
refactoring current config printing
japdubengsub Feb 26, 2025
fa92ff9
refactoring current config printing
japdubengsub Feb 26, 2025
e1f900b
improve imports
japdubengsub Feb 26, 2025
ade76ab
refactoring
japdubengsub Feb 26, 2025
4f48a70
refactoring
japdubengsub Feb 26, 2025
8762c19
refactoring
japdubengsub Feb 26, 2025
493161e
refactoring opik+config classes
japdubengsub Feb 26, 2025
b5bd9f0
code formatting
japdubengsub Feb 26, 2025
161372d
linter fix
japdubengsub Feb 26, 2025
b8c55cf
message colorization refactoring
japdubengsub Feb 26, 2025
9556f39
config methods refactoring
japdubengsub Feb 26, 2025
b7cc60a
config methods refactoring
japdubengsub Feb 26, 2025
2efaa73
small refactoring
japdubengsub Feb 26, 2025
e65b119
show connection error message related to installation type
japdubengsub Feb 27, 2025
634c64d
Merge remote-tracking branch 'refs/remotes/origin/main' into OPIK-814
japdubengsub Feb 27, 2025
24e4e6c
fix wrong condition check
japdubengsub Feb 27, 2025
81866bf
do not print sentry dsn
japdubengsub Feb 27, 2025
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
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