diff --git a/lib/galaxy/app.py b/lib/galaxy/app.py index 7668fb54be6a..dc69fdd91f1f 100644 --- a/lib/galaxy/app.py +++ b/lib/galaxy/app.py @@ -147,6 +147,7 @@ from galaxy.tools.data import ToolDataTableManager from galaxy.tools.data_manager.manager import DataManagers from galaxy.tools.error_reports import ErrorReports +from galaxy.tools.evaluation import ToolTemplatingException from galaxy.tools.search import ToolBoxSearch from galaxy.tools.special_tools import load_lib_tools from galaxy.tours import ( @@ -227,12 +228,34 @@ def configure_sentry_client(self): level=logging.INFO, # Capture info and above as breadcrumbs event_level=getattr(logging, event_level), # Send errors as events ) + + def before_send(event, hint): + if "exc_info" in hint: + exc_type, exc_value, tb = hint["exc_info"] + if isinstance(exc_value, ToolTemplatingException): + # We set a custom fingerprint that may look like: + # ["Error occurred while {action_str.lower()} for tool '{tool.id}'", + # "1.0", + # "cannot find 'file_name' while searching for 'species_chromosomes.file_name'"] + # If we don't do this issues are never properly grouped since by default the calling stack is inspected, + # and that is always unique in cheetah as it is dynamically generated. + event["fingerprint"] = [str(exc_value), str(exc_value.tool_version), str(exc_value.__cause__)] + event.setdefault("tags", {}).update( + { + "tool_is_latest": exc_value.is_latest, + "tool_id": str(exc_value.tool_id), + "tool_version": exc_value.tool_version, + } + ) + return event + self.sentry_client = sentry_sdk.init( self.config.sentry_dsn, release=f"{self.config.version_major}.{self.config.version_minor}", integrations=[sentry_logging], traces_sample_rate=self.config.sentry_traces_sample_rate, ca_certs=self.config.sentry_ca_certs, + before_send=before_send, ) diff --git a/lib/galaxy/tools/evaluation.py b/lib/galaxy/tools/evaluation.py index 07c04ab6055a..28a4b27c4b0d 100644 --- a/lib/galaxy/tools/evaluation.py +++ b/lib/galaxy/tools/evaluation.py @@ -12,6 +12,7 @@ Dict, List, Optional, + TYPE_CHECKING, Union, ) @@ -70,6 +71,9 @@ from galaxy.util.tree_dict import TreeDict from galaxy.work.context import WorkRequestContext +if TYPE_CHECKING: + from galaxy.tools import Tool + log = logging.getLogger(__name__) @@ -89,13 +93,27 @@ def add_error(self, file, phase, exception): global_tool_errors = ToolErrorLog() -def global_tool_logs(func, config_file, action_str): +class ToolTemplatingException(Exception): + + def __init__(self, *args: object, tool_id: Optional[str], tool_version: str, is_latest: bool) -> None: + super().__init__(*args) + self.tool_id = tool_id + self.tool_version = tool_version + self.is_latest = is_latest + + +def global_tool_logs(func, config_file: str, action_str: str, tool: "Tool"): try: return func() except Exception as e: # capture and log parsing errors global_tool_errors.add_error(config_file, action_str, e) - raise e + raise ToolTemplatingException( + f"Error occurred while {action_str.lower()} for tool '{tool.id}'", + tool_id=tool.id, + tool_version=tool.version, + is_latest=tool.is_latest_version, + ) from e DeferrableObjectsT = Union[ @@ -581,13 +599,18 @@ def build(self): """ config_file = self.tool.config_file global_tool_logs( - self._create_interactivetools_entry_points, config_file, "Building Interactive Tool Entry Points" + self._create_interactivetools_entry_points, config_file, "Building Interactive Tool Entry Points", self.tool + ) + global_tool_logs( + self._build_config_files, + config_file, + "Building Config Files", + self.tool, ) - global_tool_logs(self._build_config_files, config_file, "Building Config Files") - global_tool_logs(self._build_param_file, config_file, "Building Param File") - global_tool_logs(self._build_command_line, config_file, "Building Command Line") - global_tool_logs(self._build_version_command, config_file, "Building Version Command Line") - global_tool_logs(self._build_environment_variables, config_file, "Building Environment Variables") + global_tool_logs(self._build_param_file, config_file, "Building Param File", self.tool) + global_tool_logs(self._build_command_line, config_file, "Building Command Line", self.tool) + global_tool_logs(self._build_version_command, config_file, "Building Version Command Line", self.tool) + global_tool_logs(self._build_environment_variables, config_file, "Building Environment Variables", self.tool) return ( self.command_line, self.version_command_line, @@ -836,7 +859,7 @@ class PartialToolEvaluator(ToolEvaluator): def build(self): config_file = self.tool.config_file - global_tool_logs(self._build_environment_variables, config_file, "Building Environment Variables") + global_tool_logs(self._build_environment_variables, config_file, "Building Environment Variables", self.tool) return ( self.command_line, self.version_command_line, @@ -857,10 +880,10 @@ def execute_tool_hooks(self, inp_data, out_data, incoming): def build(self): config_file = self.tool.config_file - global_tool_logs(self._build_config_files, config_file, "Building Config Files") - global_tool_logs(self._build_param_file, config_file, "Building Param File") - global_tool_logs(self._build_command_line, config_file, "Building Command Line") - global_tool_logs(self._build_version_command, config_file, "Building Version Command Line") + global_tool_logs(self._build_config_files, config_file, "Building Config Files", self.tool) + global_tool_logs(self._build_param_file, config_file, "Building Param File", self.tool) + global_tool_logs(self._build_command_line, config_file, "Building Command Line", self.tool) + global_tool_logs(self._build_version_command, config_file, "Building Version Command Line", self.tool) return ( self.command_line, self.version_command_line,