From 6aa559f1f79b0f356aab7bea04d895943f682683 Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Thu, 18 Jul 2024 11:42:33 +0200 Subject: [PATCH] Group tool templating exceptions in sentry This will properly group tool templating exceptions. The added tags should make it easy to prioritize and distribute fixes. Also improves the exception if not using sentry: ``` ... Cheetah.Parser.ParseError: Some #directives are missing their corresponding #end ___ tag: for Line 3, column 4 Line|Cheetah Code ----|------------------------------------------------------------- 2 | moo 3 | ^ The above exception was the direct cause of the following exception: Traceback (most recent call last): File "/Users/mvandenb/src/galaxy/lib/galaxy/jobs/runners/__init__.py", line 297, in prepare_job job_wrapper.prepare() File "/Users/mvandenb/src/galaxy/lib/galaxy/jobs/__init__.py", line 1260, in prepare ) = tool_evaluator.build() File "/Users/mvandenb/src/galaxy/lib/galaxy/tools/evaluation.py", line 611, in build global_tool_logs(self._build_command_line, config_file, "Building Command Line", self.tool) File "/Users/mvandenb/src/galaxy/lib/galaxy/tools/evaluation.py", line 111, in global_tool_logs raise ToolTemplatingException( galaxy.tools.evaluation.ToolTemplatingException: Error occurred while building command line for tool 'cheetah_problem_syntax_error' ``` --- lib/galaxy/app.py | 23 ++++++++++++++++ lib/galaxy/tools/evaluation.py | 49 +++++++++++++++++++++++++--------- 2 files changed, 59 insertions(+), 13 deletions(-) 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,