Skip to content

Commit 7ea6a3e

Browse files
committed
feat: Add simple descriptive logger plugin and use by default
The logger plugin registers a derived class of snakemake's DefaultFormatter which shows the bold message in front of the regular job info block.
1 parent 50b2fbe commit 7ea6a3e

File tree

5 files changed

+171
-0
lines changed

5 files changed

+171
-0
lines changed

envs/environment.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,4 @@ dependencies:
7070
- highspy
7171
- tsam>=2.3.1
7272
- entsoe-py
73+
- ../plugins/snakemake-logger-plugin-descriptive
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# snakemake-logger-descriptive
2+
3+
A descriptive logger plugin for Snakemake that provides detailed, human-readable output.
4+
5+
## Installation
6+
7+
Install in development mode:
8+
9+
```bash
10+
pip install -e .
11+
```
12+
13+
Or with uv:
14+
15+
```bash
16+
uv pip install -e .
17+
```
18+
19+
## Usage
20+
21+
Use this logger plugin with Snakemake by specifying it via the `--logger` option:
22+
23+
```bash
24+
snakemake --logger descriptive
25+
```
26+
27+
## Features
28+
29+
- Detailed, descriptive logging output
30+
31+
## Development
32+
33+
This package follows the Snakemake logger plugin interface defined in
34+
`snakemake-interface-logger-plugins`.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[build-system]
2+
requires = ["setuptools>=61.0", "wheel"]
3+
build-backend = "setuptools.build_meta"
4+
5+
[project]
6+
name = "snakemake-logger-plugin-descriptive"
7+
version = "0.1.0"
8+
description = "A descriptive logger plugin for Snakemake"
9+
readme = "README.md"
10+
requires-python = ">=3.10"
11+
license = {text = "MIT"}
12+
authors = [
13+
{name = "Your Name", email = "your.email@example.com"}
14+
]
15+
dependencies = [
16+
"snakemake-interface-logger-plugins>=1.2.0",
17+
]
18+
19+
[project.entry-points."snakemake.plugins.logger"]
20+
descriptive = "snakemake_logger_plugin_descriptive:LogHandler"
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
"""Snakemake descriptive logger plugin."""
2+
3+
import sys
4+
import textwrap
5+
from dataclasses import dataclass
6+
7+
from snakemake.logging import (
8+
ColorizingTextHandler,
9+
DefaultFormatter,
10+
timestamp,
11+
)
12+
from snakemake_interface_logger_plugins.base import LogHandlerBase
13+
from snakemake_interface_logger_plugins.settings import LogHandlerSettingsBase
14+
15+
16+
@dataclass
17+
class LogHandlerSettings(LogHandlerSettingsBase):
18+
pass
19+
20+
21+
class DescriptiveFormatter(DefaultFormatter):
22+
BOLD_SEQ = "\033[1m"
23+
NON_BOLD_SEQ = "\033[21m"
24+
25+
def format_job_info(self, msg):
26+
"""Format for job_info log."""
27+
output = []
28+
29+
output.append(timestamp())
30+
if msg["rule_msg"]:
31+
output.append(msg["rule_msg"])
32+
output.append("\n".join(self._format_job_info(msg)))
33+
34+
if msg.get("indent", False):
35+
return textwrap.indent("\n".join(output), " ")
36+
return "\n".join(output)
37+
38+
39+
class LogHandler(LogHandlerBase, ColorizingTextHandler):
40+
def __post_init__(self) -> None:
41+
# initialize additional attributes
42+
# Do not overwrite the __init__ method as this is kept in control of the base
43+
# class in order to simplify the update process.
44+
# See https://github.com/snakemake/snakemake-interface-logger-plugins/blob/main/src/snakemake_interface_logger_plugins/base.py # noqa: E501
45+
# for attributes of the base class.
46+
# In particular, the settings of above LogHandlerSettings class are accessible via
47+
# self.settings.
48+
# You also have access to self.common_settings here, which are logging settings supplied by the caller in the form of OutputSettingsLoggerInterface. # noqa: E501
49+
# See https://github.com/snakemake/snakemake-interface-logger-plugins/blob/main/src/snakemake_interface_logger_plugins/settings.py for more details # noqa: E501
50+
ColorizingTextHandler.__init__(
51+
self,
52+
nocolor=self.common_settings.nocolor,
53+
stream=sys.stdout if self.common_settings.stdout else sys.stderr,
54+
)
55+
self.setFormatter(
56+
DescriptiveFormatter(
57+
self.common_settings.quiet, self.common_settings.show_failed_logs
58+
)
59+
)
60+
61+
# Here you can override logging.Handler methods to customize logging behavior.
62+
# Only an implementation of the emit() method is required. See the Python logging
63+
# documentation for details:
64+
# https://docs.python.org/3/library/logging.html#handler-objects
65+
66+
# LogRecords from Snakemake carry contextual information in the record's attributes
67+
# Of particular interest is the 'event' attribute, which indicates the type of log information contained
68+
# See https://github.com/snakemake/snakemake-interface-logger-plugins/blob/2ab84cb31f0b92cf0b7ee3026e15d1209729d197/src/snakemake_interface_logger_plugins/common.py#L33 # noqa: E501
69+
# For examples on parsing LogRecords, see https://github.com/cademirch/snakemake-logger-plugin-snkmt/blob/main/src/snakemake_logger_plugin_snkmt/parsers.py # noqa: E501
70+
71+
def emit(self, record):
72+
# Actually emit the record. Typically this will call self.format(record) to
73+
# convert the record to a formatted string. The result could then be written to
74+
# a stream or file.
75+
ColorizingTextHandler.emit(self, record)
76+
77+
@property
78+
def writes_to_stream(self) -> bool:
79+
# Whether this plugin writes to stderr/stdout.
80+
# If your plugin writes to stderr/stdout, return
81+
# true so that Snakemake disables its stderr logging.
82+
return True
83+
84+
@property
85+
def writes_to_file(self) -> bool:
86+
# Whether this plugin writes to a file.
87+
# If your plugin writes log output to a file, return
88+
# true so that Snakemake can report your logfile path at workflow end.
89+
# NOTE: Handlers that return True must provide a baseFilename attribute
90+
# containing the file path.
91+
return False
92+
93+
@property
94+
def has_filter(self) -> bool:
95+
# Whether this plugin attaches its own filter.
96+
# Return true if your plugin provides custom log filtering logic.
97+
# If false is returned, Snakemake's DefaultFilter will be attached see: https://github.com/snakemake/snakemake/blob/960f6a89eaa31da6014e810dfcf08f635ac03a6e/src/snakemake/logging.py#L372 # noqa: E501
98+
# See https://docs.python.org/3/library/logging.html#filter-objects for info on how to define and attach a Filter
99+
return False
100+
101+
@property
102+
def has_formatter(self) -> bool:
103+
# Whether this plugin attaches its own formatter.
104+
# Return true if your plugin provides custom log formatting logic.
105+
# If false is returned, Snakemake's Defaultformatter will be attached see: https://github.com/snakemake/snakemake/blob/960f6a89eaa31da6014e810dfcf08f635ac03a6e/src/snakemake/logging.py#L132 # noqa: E501
106+
# See https://docs.python.org/3/library/logging.html#formatter-objects for info on how to define and attach a Formatter
107+
return True
108+
109+
@property
110+
def needs_rulegraph(self) -> bool:
111+
# Whether this plugin requires the DAG rulegraph.
112+
# Return true if your plugin needs access to the workflow's
113+
# directed acyclic graph for logging purposes.
114+
return False

profiles/default/config.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
logger:
2+
descriptive

0 commit comments

Comments
 (0)