diff --git a/AUTHORS.md b/AUTHORS.md index a531210e..c91699a1 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -13,6 +13,7 @@ The list of contributors in alphabetical order: - [Elena Gazzarrini](https://orcid.org/0000-0001-5772-5166) - [Giuseppe Steduto](https://orcid.org/0009-0002-1258-8553) - [Jan Okraska](https://orcid.org/0000-0002-1416-3244) +- [Jelizaveta Lemeševa](https://orcid.org/0009-0003-6606-9270) - [Kenyi Hurtado-Anampa](https://orcid.org/0000-0002-9779-3566) - [Marco Donadoni](https://orcid.org/0000-0003-2922-5505) - [Marco Vidal](https://orcid.org/0000-0002-9363-4971) diff --git a/reana_job_controller/factory.py b/reana_job_controller/factory.py index fd87f720..49077298 100644 --- a/reana_job_controller/factory.py +++ b/reana_job_controller/factory.py @@ -18,6 +18,7 @@ from reana_job_controller import config from reana_job_controller.spec import build_openapi_spec +from reana_job_controller.utils import MultilineFormatter @event.listens_for(db_engine, "checkin") @@ -55,7 +56,12 @@ def shutdown_session(response_or_exc): def create_app(config_mapping=None): """Create REANA-Job-Controller application.""" - logging.basicConfig(level=REANA_LOG_LEVEL, format=REANA_LOG_FORMAT) + handler = logging.StreamHandler() + handler.setFormatter(MultilineFormatter(REANA_LOG_FORMAT)) + logging.basicConfig( + level=REANA_LOG_LEVEL, format=REANA_LOG_FORMAT, handlers=[handler] + ) + app = Flask(__name__) app.secret_key = "mega secret key" app.session = Session diff --git a/reana_job_controller/utils.py b/reana_job_controller/utils.py index 2f6389f7..db5605a3 100644 --- a/reana_job_controller/utils.py +++ b/reana_job_controller/utils.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # # This file is part of REANA. -# Copyright (C) 2017, 2018, 2019, 2020, 2022, 2023 CERN. +# Copyright (C) 2017, 2018, 2019, 2020, 2022, 2023, 2024 CERN. # # REANA is free software; you can redistribute it and/or modify it # under the terms of the MIT License; see LICENSE file for more details. @@ -13,11 +13,37 @@ import socket import subprocess import sys +from logging import Formatter, LogRecord from reana_db.database import Session from reana_db.models import Workflow +class MultilineFormatter(Formatter): + """Logging formatter for multiline logs.""" + + def format(self, record: LogRecord): + """Format multiline log message. + + :param record: LogRecord object. + :type record: logging.LogRecord + + :return: Formatted log message. + :rtype: str + """ + save_msg = str(record.msg) + output = "" + lines = save_msg.splitlines() + for line in lines: + record.msg = line + output += super().format(record) + "\n" + output = output.strip() + record.msg = save_msg + record.message = output + + return output + + def singleton(cls): """Singelton decorator.""" instances = {} diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 00000000..69841e64 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +# +# This file is part of REANA. +# Copyright (C) 2024 CERN. +# +# REANA is free software; you can redistribute it and/or modify it +# under the terms of the MIT License; see LICENSE file for more details. + +import logging +import pytest + +from reana_job_controller.utils import MultilineFormatter + +"""REANA-Job-Controller utils tests.""" + + +@pytest.mark.parametrize( + "message,expected_output", + [ + ( + "test", + "name | INFO | test", + ), + ( + "test\n", + "name | INFO | test", + ), + ( + "test\ntest", + "name | INFO | test\nname | INFO | test", + ), + ( + "test\ntest\n\n\n", + "name | INFO | test\nname | INFO | test\nname | INFO | \nname | INFO |", + ), + ( + " test\ntest ", + "name | INFO | test\nname | INFO | test", + ), + ( + " t e s\tt\n t e s t ", + "name | INFO | t e s\tt\nname | INFO | t e s t", + ), + ], +) +def test_multiline_formatter_format(message, expected_output): + """Test MultilineFormatter formatting.""" + formatter = MultilineFormatter("%(name)s | " "%(levelname)s | %(message)s") + assert ( + formatter.format( + logging.LogRecord( + "name", + logging.INFO, + "pathname", + 1, + message, + None, + None, + ), + ) + == expected_output + )