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

Slack #170

Merged
merged 10 commits into from Jan 16, 2020
2 changes: 1 addition & 1 deletion setup.py
Expand Up @@ -59,7 +59,7 @@
"psutil",
"nbformat"
],
extras_require={"reports": ["jinja2", "networkx", "pygments", "pygraphviz"]},
extras_require={"reports": ["jinja2", "networkx", "pygments", "pygraphviz"], "messaging": ["slacker"]},
classifiers=[
"Development Status :: 5 - Production/Stable",
"Environment :: Console",
Expand Down
25 changes: 19 additions & 6 deletions snakemake/__init__.py
Expand Up @@ -21,7 +21,7 @@
from snakemake.workflow import Workflow
from snakemake.dag import Batch
from snakemake.exceptions import print_exception, WorkflowError
from snakemake.logging import setup_logger, logger
from snakemake.logging import setup_logger, logger, SlackLogger
from snakemake.io import load_configfile
from snakemake.shell import shell
from snakemake.utils import update_config, available_cpu_count
Expand Down Expand Up @@ -117,7 +117,7 @@ def snakemake(
no_hooks=False,
overwrite_shellcmd=None,
updated_files=None,
log_handler=None,
log_handler=[],
keep_logger=False,
max_jobs_per_second=None,
max_status_checks_per_second=100,
Expand Down Expand Up @@ -148,6 +148,7 @@ def snakemake(
cluster_status=None,
export_cwl=None,
show_failed_logs=False,
messaging=None,
):
"""Run snakemake on a given snakefile.

Expand Down Expand Up @@ -253,7 +254,7 @@ def snakemake(
assume_shared_fs (bool): assume that cluster nodes share a common filesystem (default true).
cluster_status (str): status command for cluster execution. If None, Snakemake will rely on flag files. Otherwise, it expects the command to return "success", "failure" or "running" when executing with a cluster jobid as single argument.
export_cwl (str): Compile workflow to CWL and save to given file
log_handler (function): redirect snakemake output to this custom log handler, a function that takes a log message dictionary (see below) as its only argument (default None). The log message dictionary for the log handler has to following entries:
log_handler (list): redirect snakemake output to this list of custom log handler, each a function that takes a log message dictionary (see below) as its only argument (default []). The log message dictionary for the log handler has to following entries:

:level:
the log level ("info", "error", "debug", "progress", "job_info")
Expand Down Expand Up @@ -1502,6 +1503,15 @@ def get_argument_parser(profile=None):
"allowing to e.g. send notifications in the form of e.g. slack messages or emails.",
)

group_behavior.add_argument(
"--log-service",
default=None,
choices=["none", "slack"],
help="Set a specific messaging service for logging output."
"Snakemake will notify the service on errors and completed execution."
"Currently only slack is supported.",
)

group_cluster = parser.add_argument_group("CLUSTER")

# TODO extend below description to explain the wildcards that can be used
Expand Down Expand Up @@ -1966,6 +1976,7 @@ def open_browser():
# silently close
pass
else:
log_handler = []
if args.log_handler_script is not None:
if not os.path.exists(args.log_handler_script):
print(
Expand All @@ -1977,7 +1988,7 @@ def open_browser():
sys.exit(1)
log_script = SourceFileLoader("log", args.log_handler_script).load_module()
try:
log_handler = log_script.log_handler
log_handler.append(log_script.log_handler)
except:
print(
'Error: Invalid log handler script, {}. Expect python function "log_handler(msg)".'.format(
Expand All @@ -1986,8 +1997,10 @@ def open_browser():
file=sys.stderr,
)
sys.exit(1)
else:
log_handler = None

if args.log_service == "slack":
slack_logger = logging.SlackLogger()
log_handler.append(slack_logger.log_handler)

success = snakemake(
args.snakefile,
Expand Down
42 changes: 38 additions & 4 deletions snakemake/logging.py
Expand Up @@ -79,6 +79,42 @@ def decorate(self, record):
return "".join(message)


class SlackLogger:
def __init__(self):
from slacker import Slacker

self.token = os.getenv("SLACK_TOKEN")
if not self.token:
print(
"The use of slack logging requires the user to set a user specific slack legacy token to the SLACK_TOKEN environment variable. Set this variable by 'export SLACK_TOKEN=your_token'. To generate your token please visit https://api.slack.com/custom-integrations/legacy-tokens."
)
exit(-1)
self.slack = Slacker(self.token)
# Check for success
try:
auth = self.slack.auth.test().body
except Exception:
print(
"Slack connection failed. Please compare your provided slack token exported in the SLACK_TOKEN environment variable with your online token at https://api.slack.com/custom-integrations/legacy-tokens. A different token can be set up by 'export SLACK_TOKEN=your_token'."
)
exit(-1)
self.own_id = auth["user_id"]
self.error_occured = False

def log_handler(self, msg):
if msg["level"] == "error" and not self.error_occured:
self.slack.chat.post_message(
self.own_id, text="At least one error occured.", username="snakemake"
)
self.error_occured = True

if msg["level"] == "progress" and msg["done"] == msg["total"]:
# workflow finished
self.slack.chat.post_message(
self.own_id, text="Workflow complete.", username="snakemake"
)


class Logger:
def __init__(self):
self.logger = _logging.getLogger(__name__)
Expand Down Expand Up @@ -409,7 +445,7 @@ def format_resource_names(resources, omit_resources="_cores _nodes".split()):


def setup_logger(
handler=None,
handler=[],
quiet=False,
printshellcmds=False,
printreason=False,
Expand All @@ -421,9 +457,7 @@ def setup_logger(
mode=Mode.default,
show_failed_logs=False,
):
if handler is not None:
# custom log handler
logger.log_handler.append(handler)
logger.log_handler.extend(handler)

# console output only if no custom logger was specified
stream_handler = ColorizingStreamHandler(
Expand Down