Refactor bug reporting into plugins

This commit is contained in:
E Rasche
2017-08-10 17:53:00 +02:00
parent 5e43fcef3c
commit 70a64b304e
12 changed files with 361 additions and 27 deletions
+37
View File
@@ -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>
+4
View File
@@ -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)
+1
View File
@@ -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'],
+5 -21
View File
@@ -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', )
+12
View File
@@ -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):
+66
View File
@@ -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>