diff --git a/config/config.default.yaml b/config/config.default.yaml index 146429bbc1..58d00f4adb 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -11,6 +11,8 @@ tutorial: false # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#logging logging: level: INFO + console_level: WARNING + file_level: format: "%(levelname)s:%(name)s:%(message)s" # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#remote diff --git a/config/schema.default.json b/config/schema.default.json index 0468c33193..58820115c2 100644 --- a/config/schema.default.json +++ b/config/schema.default.json @@ -2848,7 +2848,7 @@ "properties": { "level": { "default": "INFO", - "description": "Restrict console outputs to all infos, warning or errors only", + "description": "Root log level. Messages below this level are discarded before reaching any handler.", "enum": [ "DEBUG", "INFO", @@ -2858,6 +2858,37 @@ ], "type": "string" }, + "console_level": { + "default": "WARNING", + "description": "Minimum level emitted to the console (stderr). Defaults to ``WARNING`` so parallel rule output stays readable; the full log is kept in the rule's log file.", + "enum": [ + "DEBUG", + "INFO", + "WARNING", + "ERROR", + "CRITICAL" + ], + "type": "string" + }, + "file_level": { + "anyOf": [ + { + "enum": [ + "DEBUG", + "INFO", + "WARNING", + "ERROR", + "CRITICAL" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Minimum level written to the rule log file. Defaults to ``level`` when unset." + }, "format": { "default": "%(levelname)s:%(name)s:%(message)s", "description": "Custom format for log messages. See `LogRecord `_ attributes.", @@ -8182,7 +8213,7 @@ "properties": { "level": { "default": "INFO", - "description": "Restrict console outputs to all infos, warning or errors only", + "description": "Root log level. Messages below this level are discarded before reaching any handler.", "enum": [ "DEBUG", "INFO", @@ -8192,6 +8223,37 @@ ], "type": "string" }, + "console_level": { + "default": "WARNING", + "description": "Minimum level emitted to the console (stderr). Defaults to ``WARNING`` so parallel rule output stays readable; the full log is kept in the rule's log file.", + "enum": [ + "DEBUG", + "INFO", + "WARNING", + "ERROR", + "CRITICAL" + ], + "type": "string" + }, + "file_level": { + "anyOf": [ + { + "enum": [ + "DEBUG", + "INFO", + "WARNING", + "ERROR", + "CRITICAL" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Minimum level written to the rule log file. Defaults to ``level`` when unset." + }, "format": { "default": "%(levelname)s:%(name)s:%(message)s", "description": "Custom format for log messages. See `LogRecord `_ attributes.", diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 305e48949a..20e99b2031 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -20,6 +20,8 @@ Release Notes * perf: Optimize dask settings for computing weather-dependent profiles (https://github.com/PyPSA/pypsa-eur/pull/2137). +* Split logging levels between console and log file: the console now defaults to ``WARNING`` while the rule log file still captures ``INFO``. Configure via ``logging.console_level`` and ``logging.file_level`` (https://github.com/PyPSA/pypsa-eur/pull/2144). + PyPSA-Eur v2026.02.0 (18th February 2026) ========================================= diff --git a/scripts/_helpers.py b/scripts/_helpers.py index 4c74e4b718..e734285779 100644 --- a/scripts/_helpers.py +++ b/scripts/_helpers.py @@ -262,6 +262,8 @@ def configure_logging(snakemake, skip_handlers=False): kwargs = snakemake.config.get("logging", dict()).copy() kwargs.setdefault("level", "INFO") + console_level = kwargs.pop("console_level", "WARNING") + file_level = kwargs.pop("file_level", None) or kwargs["level"] if skip_handlers is False: fallback_path = Path(__file__).parent.joinpath( @@ -270,16 +272,11 @@ def configure_logging(snakemake, skip_handlers=False): logfile = snakemake.log.get( "python", snakemake.log[0] if snakemake.log else fallback_path ) - kwargs.update( - { - "handlers": [ - # Prefer the 'python' log, otherwise take the first log for each - # Snakemake rule - logging.FileHandler(logfile), - logging.StreamHandler(), - ] - } - ) + file_handler = logging.FileHandler(logfile) + file_handler.setLevel(file_level) + stream_handler = logging.StreamHandler() + stream_handler.setLevel(console_level) + kwargs["handlers"] = [file_handler, stream_handler] logging.basicConfig(**kwargs) # Setup a function to handle uncaught exceptions and include them with their stacktrace into logfiles diff --git a/scripts/lib/validation/config/_schema.py b/scripts/lib/validation/config/_schema.py index 5adb8665f0..6461d20c5a 100644 --- a/scripts/lib/validation/config/_schema.py +++ b/scripts/lib/validation/config/_schema.py @@ -46,7 +46,15 @@ class LoggingConfig(ConfigModel): level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = Field( "INFO", - description="Restrict console outputs to all infos, warning or errors only", + description="Root log level. Messages below this level are discarded before reaching any handler.", + ) + console_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = Field( + "WARNING", + description="Minimum level emitted to the console (stderr). Defaults to ``WARNING`` so parallel rule output stays readable; the full log is kept in the rule's log file.", + ) + file_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = Field( + None, + description="Minimum level written to the rule log file. Defaults to ``level`` when unset.", ) format: str = Field( "%(levelname)s:%(name)s:%(message)s",