mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-10-29 17:42:12 +08:00
Refactor bug reporting into plugins
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0"?>
|
||||
<bug_reports>
|
||||
<!-- Each element in this file corresponds to a destination for an error
|
||||
report.
|
||||
|
||||
If you supply the parameter ``user_submission``, and set it to
|
||||
True, this error plugin will only be activated when the user uses the
|
||||
bug report submission interface. Otherwise, it will be triggered
|
||||
without any user interaction (e.g. historical behaviour of sentry
|
||||
plugin)
|
||||
|
||||
If your plugin is ``user_submission=True``, then supplying
|
||||
``verbose=True`` will cause the plugin to display a message to the end
|
||||
user. E.g. the email plugin simply states "Your error report has been sent", or the JSON plugin
|
||||
informs the user "Wrote error report to ..."
|
||||
|
||||
The plugins below are listed with their default values of
|
||||
verbose/user_submission, but those are not necessary to provide.
|
||||
-->
|
||||
|
||||
<!-- The default Email bug reporter. By default, the standard configuration
|
||||
is taken from your galaxy.ini -->
|
||||
<email verbose="true" user_submission="true" />
|
||||
|
||||
<!-- Example JSON bug reporter implementation. This just writes the
|
||||
bug report as a JSON file to a specified directory and serves
|
||||
as an example for others to extend.
|
||||
-->
|
||||
<!--
|
||||
<json user_submission="false" directory="/tmp/reports/" />
|
||||
-->
|
||||
|
||||
<!-- Submit error reports to sentry. If a sentry_dsn is configured in your
|
||||
galaxy.ini, then Galaxy will submit the job error to Sentry. -->
|
||||
<sentry user_submission="false"/>
|
||||
|
||||
</bug_reports>
|
||||
@@ -27,6 +27,7 @@ from galaxy.tools.cache import (
|
||||
ToolShedRepositoryCache
|
||||
)
|
||||
from galaxy.jobs import metrics as job_metrics
|
||||
from galaxy.tools.error_reports import ErrorReports
|
||||
from galaxy.web.proxy import ProxyManager
|
||||
from galaxy.web.stack import application_stack_instance
|
||||
from galaxy.queue_worker import GalaxyQueueWorker
|
||||
@@ -102,6 +103,9 @@ class UniverseApplication( object, config.ConfiguresGalaxyMixin ):
|
||||
# config so per-destination modifications can be made.
|
||||
self.job_metrics = job_metrics.JobMetrics( self.config.job_metrics_config_file, app=self )
|
||||
|
||||
# Initialize error report sinks.
|
||||
self.error_reports = ErrorReports( self.config.error_report_file, app=self )
|
||||
|
||||
# Initialize the job management configuration
|
||||
self.job_config = jobs.JobConfiguration(self)
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ PATH_DEFAULTS = dict(
|
||||
job_config_file=['config/job_conf.xml', 'job_conf.xml'],
|
||||
tool_destinations_config_file=['config/tool_destinations.yml', 'config/tool_destinations.yml.sample'],
|
||||
job_metrics_config_file=['config/job_metrics_conf.xml', 'job_metrics_conf.xml', 'config/job_metrics_conf.xml.sample'],
|
||||
error_report_file=['config/error_report.xml', 'error_report.xml', 'config/error_report.xml.sample'],
|
||||
dependency_resolvers_config_file=['config/dependency_resolvers_conf.xml', 'dependency_resolvers_conf.xml'],
|
||||
job_resource_params_file=['config/job_resource_params_conf.xml', 'job_resource_params_conf.xml'],
|
||||
migrated_tools_config=['migrated_tools_conf.xml', 'config/migrated_tools_conf.xml'],
|
||||
|
||||
@@ -1028,7 +1028,7 @@ class JobWrapper( object, HasResourceParameters ):
|
||||
# the partial files to the object store regardless of whether job.state == DELETED
|
||||
self.__update_output(job, dataset, clean_only=True)
|
||||
|
||||
self._report_error_to_sentry()
|
||||
self._report_error()
|
||||
# Perform email action even on failure.
|
||||
for pja in [pjaa.post_job_action for pjaa in job.post_job_actions if pjaa.post_job_action.action_type == "EmailAction"]:
|
||||
ActionBox.execute(self.app, self.sa_session, pja, job)
|
||||
@@ -1437,7 +1437,7 @@ class JobWrapper( object, HasResourceParameters ):
|
||||
self.sa_session.flush()
|
||||
log.debug( 'job %d ended (finish() executed in %s)' % (self.job_id, finish_timer) )
|
||||
if job.state == job.states.ERROR:
|
||||
self._report_error_to_sentry()
|
||||
self._report_error()
|
||||
cleanup_job = self.cleanup_job
|
||||
delete_files = cleanup_job == 'always' or ( job.state == job.states.OK and cleanup_job == 'onsuccess' )
|
||||
self.cleanup( delete_files=delete_files )
|
||||
@@ -1809,27 +1809,11 @@ class JobWrapper( object, HasResourceParameters ):
|
||||
return self.tool.requires_setting_metadata
|
||||
return False
|
||||
|
||||
def _report_error_to_sentry( self ):
|
||||
def _report_error( self ):
|
||||
job = self.get_job()
|
||||
tool = self.app.toolbox.get_tool(job.tool_id, tool_version=job.tool_version) or None
|
||||
if self.app.sentry_client and job.state == job.states.ERROR:
|
||||
self.app.sentry_client.capture(
|
||||
'raven.events.Message',
|
||||
message="Galaxy Job Error: %s v.%s" % (job.tool_id, job.tool_version),
|
||||
extra={
|
||||
'info' : job.info,
|
||||
'id' : job.id,
|
||||
'command_line' : job.command_line,
|
||||
'stderr' : job.stderr,
|
||||
'traceback': job.traceback,
|
||||
'exit_code': job.exit_code,
|
||||
'stdout': job.stdout,
|
||||
'handler': job.handler,
|
||||
'user': self.user,
|
||||
'tool_version': job.tool_version,
|
||||
'tool_xml': tool.config_file if tool else None
|
||||
}
|
||||
)
|
||||
for dataset in job.output_datasets:
|
||||
self.app.error_reports.default_error_sink.submit_report(dataset, job, tool, user_submission=False)
|
||||
|
||||
|
||||
class TaskWrapper(JobWrapper):
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
"""This module defines the error reporting framework for Galaxy jobs.
|
||||
"""
|
||||
import collections
|
||||
import logging
|
||||
import os
|
||||
|
||||
from galaxy import util
|
||||
from galaxy.util import plugin_config
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ErrorReports(object):
|
||||
"""Load and store a collection of :class:`ErrorSink` objects."""
|
||||
|
||||
def __init__(self, conf_file=None, **kwargs):
|
||||
"""Load :class:`ErrorSink` objects from specified configuration file."""
|
||||
self.plugin_classes = self.__plugins_dict()
|
||||
self.default_error_sink = ErrorSink.from_file(self.plugin_classes, conf_file, **kwargs)
|
||||
self.error_sinks = collections.defaultdict(lambda: self.default_error_sink)
|
||||
|
||||
def __plugins_dict(self):
|
||||
import galaxy.tools.error_reports.sinks
|
||||
return plugin_config.plugins_dict(galaxy.tools.error_reports.sinks, 'plugin_type')
|
||||
|
||||
|
||||
class NullErrorSink(object):
|
||||
|
||||
def submit_report(self, dataset, job, tool, **kwargs):
|
||||
return "Submitted Bug Report"
|
||||
|
||||
|
||||
NULL_ERROR_SINK = NullErrorSink()
|
||||
|
||||
|
||||
class ErrorSink(object):
|
||||
|
||||
def __init__(self, plugin_classes, plugins_source, **kwargs):
|
||||
self.extra_kwargs = kwargs
|
||||
self.plugin_classes = plugin_classes
|
||||
self.plugins = self.__plugins_from_source(plugins_source)
|
||||
|
||||
def submit_report(self, dataset, job, tool, user_submission=False, **kwargs):
|
||||
responses = []
|
||||
for plugin in self.plugins:
|
||||
if user_submission == plugin.user_submission:
|
||||
try:
|
||||
response = plugin.submit_report(dataset, job, tool, **kwargs)
|
||||
log.debug("Bug report plugin %s generated response %s", plugin, response)
|
||||
if plugin.verbose and response:
|
||||
responses.append(response)
|
||||
except Exception:
|
||||
log.exception("Failed to generate submit_report commands for plugin %s", plugin)
|
||||
return responses
|
||||
|
||||
def __plugins_from_source(self, plugins_source):
|
||||
return plugin_config.load_plugins(self.plugin_classes, plugins_source, self.extra_kwargs)
|
||||
|
||||
@staticmethod
|
||||
def from_file(plugin_classes, conf_file, **kwargs):
|
||||
if not conf_file or not os.path.exists(conf_file):
|
||||
return NULL_ERROR_SINK
|
||||
plugins_source = plugin_config.plugin_source_from_path(conf_file)
|
||||
return ErrorSink(plugin_classes, plugins_source, **kwargs)
|
||||
@@ -0,0 +1,26 @@
|
||||
"""This module describes the abstract interface for :class:`InstrumentPlugin`.
|
||||
|
||||
These are responsible for collecting and formatting a coherent set of metrics.
|
||||
"""
|
||||
import os.path
|
||||
from abc import (
|
||||
ABCMeta,
|
||||
abstractmethod
|
||||
)
|
||||
|
||||
import six
|
||||
|
||||
|
||||
@six.add_metaclass(ABCMeta)
|
||||
class ErrorSinkPlugin( object ):
|
||||
"""Describes how to send bug reports to various locations."""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def plugin_type( self ):
|
||||
"""Short string providing labelling this plugin """
|
||||
|
||||
def submit_report( self, dataset, job, tool, user_submission=False, **kwargs ):
|
||||
"""Submit the bug report and render a string to be displayed to the user.
|
||||
"""
|
||||
return None
|
||||
@@ -0,0 +1,34 @@
|
||||
"""The module describes the ``email`` error sink plugin."""
|
||||
from __future__ import absolute_import
|
||||
|
||||
import logging
|
||||
|
||||
from ..sinks import ErrorSinkPlugin
|
||||
from galaxy.tools.errors import EmailErrorReporter
|
||||
from galaxy.util import string_as_bool
|
||||
|
||||
log = logging.getLogger( __name__ )
|
||||
|
||||
|
||||
class EmailPlugin( ErrorSinkPlugin ):
|
||||
"""Send error report as an email
|
||||
"""
|
||||
plugin_type = "email"
|
||||
|
||||
def __init__( self, **kwargs ):
|
||||
self.app = kwargs['app']
|
||||
self.verbose = string_as_bool(kwargs.get('verbose', True))
|
||||
self.user_submission = string_as_bool(kwargs.get('user_submission', True))
|
||||
|
||||
def submit_report( self, dataset, job, tool, **kwargs ):
|
||||
"""Send report as an email
|
||||
"""
|
||||
try:
|
||||
error_reporter = EmailErrorReporter( dataset.id, self.app )
|
||||
error_reporter.send_report( user=job.get_user(), email=kwargs.get('email', None), message=kwargs.get('message', None) )
|
||||
return ( "Your error report has been sent", "done" )
|
||||
except Exception as e:
|
||||
return ( "An error occurred sending the report by email: %s" % str( e ), "error" )
|
||||
|
||||
|
||||
__all__ = ( 'EmailPlugin', )
|
||||
@@ -0,0 +1,56 @@
|
||||
"""The module describes the ``json`` error sink plugin."""
|
||||
from __future__ import absolute_import
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from ..sinks import ErrorSinkPlugin
|
||||
from galaxy.util import string_as_bool
|
||||
|
||||
log = logging.getLogger( __name__ )
|
||||
|
||||
|
||||
class JsonPlugin( ErrorSinkPlugin ):
|
||||
"""Write error report to a JSON file.
|
||||
"""
|
||||
plugin_type = "json"
|
||||
|
||||
def __init__( self, **kwargs ):
|
||||
self.app = kwargs['app']
|
||||
self.verbose = string_as_bool(kwargs.get('verbose', False))
|
||||
self.user_submission = string_as_bool(kwargs.get('user_submission', False))
|
||||
self.report_directory = kwargs.get( "directory", tempfile.gettempdir() )
|
||||
if not os.path.exists(self.report_directory):
|
||||
os.makedirs(self.report_directory)
|
||||
|
||||
def submit_report( self, dataset, job, tool, **kwargs ):
|
||||
"""Write the report to a json file.
|
||||
"""
|
||||
path = os.path.join( self.report_directory, str(dataset.id))
|
||||
with open( path, 'w' ) as handle:
|
||||
data = {
|
||||
'info' : job.info,
|
||||
'id' : job.id,
|
||||
'command_line' : job.command_line,
|
||||
'stderr' : job.stderr,
|
||||
'traceback': job.traceback,
|
||||
'exit_code': job.exit_code,
|
||||
'stdout': job.stdout,
|
||||
'handler': job.handler,
|
||||
'user': job.get_user().to_dict(),
|
||||
'tool_version': job.tool_version,
|
||||
'tool_xml': str(tool.config_file) if tool else None
|
||||
}
|
||||
if 'email' in kwargs:
|
||||
data['email'] = kwargs['email']
|
||||
|
||||
if 'message' in kwargs:
|
||||
data['message'] = kwargs['message']
|
||||
|
||||
json.dump(data, handle, indent=2)
|
||||
return ( 'Wrote error report to %s' % path, 'done' )
|
||||
|
||||
|
||||
__all__ = ( 'JsonPlugin', )
|
||||
@@ -0,0 +1,50 @@
|
||||
"""The module describes the ``sentry`` error sink plugin."""
|
||||
import logging
|
||||
|
||||
from ..sinks import ErrorSinkPlugin
|
||||
from galaxy.util import string_as_bool
|
||||
|
||||
log = logging.getLogger( __name__ )
|
||||
|
||||
|
||||
class SentryPlugin( ErrorSinkPlugin ):
|
||||
"""Send error report to Sentry.
|
||||
"""
|
||||
plugin_type = "sentry"
|
||||
|
||||
def __init__( self, **kwargs ):
|
||||
self.app = kwargs['app']
|
||||
self.verbose = string_as_bool(kwargs.get('verbose', False))
|
||||
self.user_submission = string_as_bool(kwargs.get('user_submission', False))
|
||||
|
||||
def submit_report( self, dataset, job, tool, **kwargs ):
|
||||
"""Submit the error report to sentry
|
||||
"""
|
||||
if self.app.sentry_client and job.state == job.states.ERROR:
|
||||
extra = {
|
||||
'info' : job.info,
|
||||
'id' : job.id,
|
||||
'command_line' : job.command_line,
|
||||
'stderr' : job.stderr,
|
||||
'traceback': job.traceback,
|
||||
'exit_code': job.exit_code,
|
||||
'stdout': job.stdout,
|
||||
'handler': job.handler,
|
||||
'user': job.get_user(),
|
||||
'tool_version': job.tool_version,
|
||||
'tool_xml': tool.config_file if tool else None
|
||||
}
|
||||
if 'email' in kwargs:
|
||||
extra['email'] = kwargs['email']
|
||||
|
||||
if 'message' in kwargs:
|
||||
extra['message'] = kwargs['message']
|
||||
|
||||
self.app.sentry_client.capture(
|
||||
'raven.events.Message',
|
||||
message="Galaxy Job Error: %s v.%s" % (job.tool_id, job.tool_version),
|
||||
extra=extra,
|
||||
)
|
||||
|
||||
|
||||
__all__ = ( 'SentryPlugin', )
|
||||
@@ -853,6 +853,18 @@ class GalaxyWebTransaction( base.DefaultWebTransaction,
|
||||
"""
|
||||
return self.fill_template( "message.mako", status=type, message=message, refresh_frames=refresh_frames, cont=cont, use_panels=use_panels, active_view=active_view )
|
||||
|
||||
def show_messages( self, messages, refresh_frames=[], cont=None, use_panels=False, active_view="" ):
|
||||
"""
|
||||
Convenience method for displaying a simple page with a multiple messages.
|
||||
|
||||
`messages`: list of tuple (message, type) where type is one of "error",
|
||||
"warning", "info", or "done"
|
||||
|
||||
`refresh_frames`: names of frames in the interface that should be
|
||||
refreshed when the message is displayed
|
||||
"""
|
||||
return self.fill_template( "messages.mako", messages=messages, refresh_frames=refresh_frames, cont=cont, use_panels=use_panels, active_view=active_view )
|
||||
|
||||
def show_error_message( self, message, refresh_frames=[], use_panels=False, active_view="" ):
|
||||
"""
|
||||
Convenience method for displaying an error message. See `show_message`.
|
||||
|
||||
@@ -165,12 +165,12 @@ class DatasetInterface( BaseUIController, UsesAnnotations, UsesItemRatings, Uses
|
||||
biostar_report = 'biostar' in str( kwd.get( 'submit_error_report') ).lower()
|
||||
if biostar_report:
|
||||
return trans.response.send_redirect( url_for( controller='biostar', action='biostar_tool_bug_report', hda=id, email=email, message=message ) )
|
||||
try:
|
||||
error_reporter = EmailErrorReporter( id, trans.app )
|
||||
error_reporter.send_report( user=trans.user, email=email, message=message )
|
||||
return trans.show_ok_message( "Your error report has been sent" )
|
||||
except Exception as e:
|
||||
return trans.show_error_message( "An error occurred sending the report by email: %s" % str( e ) )
|
||||
|
||||
dataset = trans.sa_session.query( trans.app.model.HistoryDatasetAssociation ).get( self.decode_id( id ) )
|
||||
job = self._get_job_for_dataset( trans, id )
|
||||
tool = trans.app.toolbox.get_tool(job.tool_id, tool_version=job.tool_version) or None
|
||||
messages = trans.app.error_reports.default_error_sink.submit_report(dataset, job, tool, user_submission=True, email=email, message=message)
|
||||
return trans.show_messages(messages)
|
||||
|
||||
@web.expose
|
||||
def default(self, trans, dataset_id=None, **kwd):
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
<%!
|
||||
def inherit(context):
|
||||
if context.get('use_panels'):
|
||||
if context.get('webapp'):
|
||||
app_name = context.get('webapp')
|
||||
elif context.get('app'):
|
||||
app_name = context.get('app').name
|
||||
else:
|
||||
app_name = 'galaxy'
|
||||
return '/webapps/%s/base_panels.mako' % app_name
|
||||
else:
|
||||
return '/base.mako'
|
||||
%>
|
||||
<%inherit file="${inherit(context)}"/>
|
||||
|
||||
<%namespace file="/refresh_frames.mako" import="handle_refresh_frames" />
|
||||
|
||||
<% _=n_ %>
|
||||
|
||||
<%def name="init()">
|
||||
<%
|
||||
self.has_left_panel=False
|
||||
self.has_right_panel=False
|
||||
self.active_view=active_view
|
||||
self.message_box_visible=False
|
||||
%>
|
||||
</%def>
|
||||
|
||||
<%def name="javascripts()">
|
||||
${parent.javascripts()}
|
||||
${handle_refresh_frames()}
|
||||
<script type="text/javascript">
|
||||
if ( parent.handle_minwidth_hint )
|
||||
{
|
||||
parent.handle_minwidth_hint( -1 );
|
||||
}
|
||||
</script>
|
||||
</%def>
|
||||
|
||||
##
|
||||
## Override methods from base.mako and base_panels.mako
|
||||
##
|
||||
|
||||
<%def name="center_panel()">
|
||||
%for (message, status) in messages:
|
||||
${render_large_message( message, status )}
|
||||
%endfor
|
||||
</%def>
|
||||
|
||||
<%def name="body()">
|
||||
%for (message, status) in messages:
|
||||
${render_large_message( message, status )}
|
||||
%endfor
|
||||
</%def>
|
||||
|
||||
## Render large message.
|
||||
<%def name="render_large_message( message, status )">
|
||||
<div class="${status}messagelarge" style="margin: 1em">${_(message)}</div>
|
||||
</%def>
|
||||
|
||||
## Render a message
|
||||
<%def name="render_msg( msg, status='done' )">
|
||||
<div class="${status}message">${_(msg)}</div>
|
||||
<br/>
|
||||
</%def>
|
||||
|
||||
Reference in New Issue
Block a user