From 9cb35e29ee67d1d04cce33b85d3bd947b5f080f8 Mon Sep 17 00:00:00 2001 From: Mauricio Villegas <5780272+mauvilsa@users.noreply.github.com> Date: Wed, 31 Jan 2024 11:00:59 +0100 Subject: [PATCH] - Change json logger to use timestamp in iso format. - Add black and standard hooks to pre-commit config. - Replace pylint with ruff. --- .pre-commit-config.yaml | 30 +++-- reconplogger.py | 207 ++++++++++++++++++++-------------- reconplogger_tests.py | 244 +++++++++++++++++++++++----------------- setup.cfg | 2 +- setup.py | 42 ++++--- sphinx/conf.py | 184 +++++++++++++++--------------- 6 files changed, 408 insertions(+), 301 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8199a8c..3de846e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,6 +2,29 @@ fail_fast: true repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: check-added-large-files + - id: check-ast + - id: check-case-conflict + - id: check-docstring-first + - id: end-of-file-fixer + - id: mixed-line-ending + - id: trailing-whitespace + exclude: .bumpversion.cfg + +- repo: https://github.com/psf/black + rev: 23.12.1 + hooks: + - id: black + +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.1.13 + hooks: + - id: ruff + args: ["--fix"] + - repo: local hooks: @@ -18,13 +41,6 @@ repos: pass_filenames: false verbose: true - - name: pylint - id: pylint --errors-only --disable=no-member - entry: pylint --errors-only --disable=no-member - language: system - types: [python] - verbose: true - - name: tox id: tox --parallel entry: tox --parallel diff --git a/reconplogger.py b/reconplogger.py index c55d445..62d8c5c 100644 --- a/reconplogger.py +++ b/reconplogger.py @@ -11,78 +11,85 @@ import time -__version__ = '4.14.0' +__version__ = "4.14.0" try: # If flask is installed import the request context objects from flask import request, g, has_request_context + # If requests is installed patch the calls to add the correlation id import requests + def _request_patch(slf, *args, **kwargs): - headers = kwargs.pop('headers', {}) + headers = kwargs.pop("headers", {}) if has_request_context(): headers["Correlation-ID"] = g.correlation_id return slf.request_orig(*args, **kwargs, headers=headers) - setattr(requests.sessions.Session, 'request_orig', requests.sessions.Session.request) + + setattr( + requests.sessions.Session, "request_orig", requests.sessions.Session.request + ) requests.sessions.Session.request = _request_patch except ImportError: pass -reconplogger_format = '%(asctime)s\t%(levelname)s -- %(filename)s:%(lineno)s -- %(message)s' +reconplogger_format = ( + "%(asctime)s\t%(levelname)s -- %(filename)s:%(lineno)s -- %(message)s" +) reconplogger_default_cfg = { - 'version': 1, - 'formatters': { - 'plain': { - 'format': reconplogger_format, + "version": 1, + "formatters": { + "plain": { + "format": reconplogger_format, }, - 'json': { - 'format': reconplogger_format, - 'class': 'logmatic.jsonlogger.JsonFormatter' + "json": { + "format": reconplogger_format.replace("asctime", "timestamp"), + "class": "logmatic.JsonFormatter", }, }, - 'handlers': { - 'plain_handler': { - 'class': 'logging.StreamHandler', - 'formatter': 'plain', - 'level': 'WARNING', + "handlers": { + "plain_handler": { + "class": "logging.StreamHandler", + "formatter": "plain", + "level": "WARNING", + }, + "json_handler": { + "class": "logging.StreamHandler", + "formatter": "json", + "level": "WARNING", }, - 'json_handler': { - 'class': 'logging.StreamHandler', - 'formatter': 'json', - 'level': 'WARNING', + "null_handler": { + "class": "logging.NullHandler", }, - 'null_handler': { - 'class': 'logging.NullHandler', - } }, - 'loggers': { - 'plain_logger': { - 'level': 'DEBUG', - 'handlers': ['plain_handler'], + "loggers": { + "plain_logger": { + "level": "DEBUG", + "handlers": ["plain_handler"], }, - 'json_logger': { - 'level': 'DEBUG', - 'handlers': ['json_handler'], + "json_logger": { + "level": "DEBUG", + "handlers": ["json_handler"], }, - 'null_logger': { - 'handlers': ['null_handler'], + "null_logger": { + "handlers": ["null_handler"], }, }, } logging_levels = { - 'CRITICAL': CRITICAL, - 'ERROR': ERROR, - 'WARNING': WARNING, - 'INFO': INFO, - 'DEBUG': DEBUG, - 'NOTSET': NOTSET, + "CRITICAL": CRITICAL, + "ERROR": ERROR, + "WARNING": WARNING, + "INFO": INFO, + "DEBUG": DEBUG, + "NOTSET": NOTSET, } logging_levels.update({v: v for v in logging_levels.values()}) # Also accept int keys -null_logger = logging.Logger('null') +null_logger = logging.Logger("null") null_logger.addHandler(logging.NullHandler()) configs_loaded = set() @@ -97,14 +104,18 @@ def load_config(cfg: Optional[Union[str, dict]] = None, reload: bool = False): Returns: The logging package object. """ - if cfg is None or cfg in {'', 'reconplogger_default_cfg'} or (cfg in os.environ and os.environ[cfg] == 'reconplogger_default_cfg'): + if ( + cfg is None + or cfg in {"", "reconplogger_default_cfg"} + or (cfg in os.environ and os.environ[cfg] == "reconplogger_default_cfg") + ): cfg_dict = reconplogger_default_cfg elif isinstance(cfg, dict): cfg_dict = cfg elif isinstance(cfg, str): try: if os.path.isfile(cfg): - with open(cfg, 'r') as f: + with open(cfg, "r") as f: cfg_dict = yaml.safe_load(f.read()) elif cfg in os.environ: cfg_dict = yaml.safe_load(os.environ[cfg]) @@ -117,9 +128,10 @@ def load_config(cfg: Optional[Union[str, dict]] = None, reload: bool = False): raise ValueError except Exception: raise ValueError( - 'Received string which is neither a path to an existing file nor the name of an set environment variable nor a python dictionary string that can be consumed by logging.config.dictConfig.') + "Received string which is neither a path to an existing file nor the name of an set environment variable nor a python dictionary string that can be consumed by logging.config.dictConfig." + ) - cfg_dict['disable_existing_loggers'] = False + cfg_dict["disable_existing_loggers"] = False cfg_hash = yaml.safe_dump(cfg_dict).__hash__() if reload or cfg_hash not in configs_loaded: @@ -143,7 +155,7 @@ def replace_logger_handlers( if isinstance(logger, str): logger = get_logger(logger) if not isinstance(logger, logging.Logger): - raise ValueError('Expected logger to be logger name or Logger object.') + raise ValueError("Expected logger to be logger name or Logger object.") # Resolve handlers if isinstance(handlers, str): @@ -151,8 +163,7 @@ def replace_logger_handlers( elif isinstance(handlers, logging.Logger): handlers = handlers.handlers else: - raise ValueError( - 'Expected handlers to be list, logger name or Logger object.') + raise ValueError("Expected handlers to be list, logger name or Logger object.") ## Replace handlers ## logger.handlers = list(handlers) @@ -162,7 +173,7 @@ def add_file_handler( logger: logging.Logger, file_path: str, format: str = reconplogger_format, - level: Optional[str] = 'DEBUG', + level: Optional[str] = "DEBUG", ) -> logging.FileHandler: """Adds a file handler to a given logger. @@ -179,7 +190,7 @@ def add_file_handler( file_handler.setFormatter(logging.Formatter(format)) if level is not None: if level not in logging_levels: - raise ValueError('Invalid logging level: "'+str(level)+'".') + raise ValueError('Invalid logging level: "' + str(level) + '".') file_handler.setLevel(logging_levels[level]) logger.addHandler(file_handler) return file_handler @@ -187,9 +198,9 @@ def add_file_handler( def test_logger(logger: logging.Logger): """Logs one message to each debug, info and warning levels intended for testing.""" - logger.debug('reconplogger test debug message.') - logger.info('reconplogger test info message.') - logger.warning('reconplogger test warning message.') + logger.debug("reconplogger test debug message.") + logger.info("reconplogger test info message.") + logger.warning("reconplogger test warning message.") def get_logger(logger_name: str) -> logging.Logger: @@ -204,16 +215,19 @@ def get_logger(logger_name: str) -> logging.Logger: Raises: ValueError: If the logger does not exist. """ - if logger_name not in logging.Logger.manager.loggerDict and logger_name not in logging.root.manager.loggerDict: - raise ValueError('Logger "'+str(logger_name)+'" not defined.') + if ( + logger_name not in logging.Logger.manager.loggerDict + and logger_name not in logging.root.manager.loggerDict + ): + raise ValueError('Logger "' + str(logger_name) + '" not defined.') return logging.getLogger(logger_name) def logger_setup( - logger_name: str = 'plain_logger', + logger_name: str = "plain_logger", config: Optional[str] = None, level: Optional[str] = None, - env_prefix: str = 'LOGGER', + env_prefix: str = "LOGGER", reload: bool = False, parent: Optional[logging.Logger] = None, init_messages: bool = False, @@ -233,10 +247,10 @@ def logger_setup( The logger object. """ if not isinstance(env_prefix, str) or not env_prefix: - raise ValueError('env_prefix is required to be a non-empty string.') - env_cfg = env_prefix + '_CFG' - env_name = env_prefix + '_NAME' - env_level = env_prefix + '_LEVEL' + raise ValueError("env_prefix is required to be a non-empty string.") + env_cfg = env_prefix + "_CFG" + env_name = env_prefix + "_NAME" + env_level = env_prefix + "_LEVEL" # Configure logging load_config(os.getenv(env_cfg, config), reload=reload) @@ -244,9 +258,11 @@ def logger_setup( # Get logger name = os.getenv(env_name, logger_name) logger = get_logger(name) - if getattr(logger, '_reconplogger_setup', False) and not reload: + if getattr(logger, "_reconplogger_setup", False) and not reload: if parent or level or init_messages: - logger.debug(f'logger {name} already setup by reconplogger, ignoring overriding parameters.') + logger.debug( + f"logger {name} already setup by reconplogger, ignoring overriding parameters." + ) return logger # Override parent @@ -258,10 +274,10 @@ def logger_setup( if level: if isinstance(level, str): if level not in logging_levels: - raise ValueError('Invalid logging level: "'+str(level)+'".') + raise ValueError('Invalid logging level: "' + str(level) + '".') level = logging_levels[level] else: - raise ValueError('Expected level argument to be a string.') + raise ValueError("Expected level argument to be a string.") for handler in logger.handlers: if not isinstance(handler, logging.FileHandler): handler.setLevel(level) @@ -271,7 +287,7 @@ def logger_setup( # Log configured done and test logger if init_messages: - logger.info('reconplogger (v'+__version__+') logger configured.') + logger.info("reconplogger (v" + __version__ + ") logger configured.") test_logger(logger) logger._reconplogger_setup = True @@ -283,10 +299,10 @@ def logger_setup( def flask_app_logger_setup( flask_app, - logger_name: str = 'plain_logger', + logger_name: str = "plain_logger", config: Optional[str] = None, level: Optional[str] = None, - env_prefix: str = 'LOGGER', + env_prefix: str = "LOGGER", parent: Optional[logging.Logger] = None, ) -> logging.Logger: """Sets up logging configuration, configures flask to use it, and returns the logger. @@ -303,8 +319,14 @@ def flask_app_logger_setup( The logger object. """ # Configure logging and get logger - logger = logger_setup(logger_name=logger_name, config=config, level=level, env_prefix=env_prefix, parent=parent) - is_json_logger = logger.handlers[0].formatter.__class__.__name__ == 'JsonFormatter' + logger = logger_setup( + logger_name=logger_name, + config=config, + level=level, + env_prefix=env_prefix, + parent=parent, + ) + is_json_logger = logger.handlers[0].formatter.__class__.__name__ == "JsonFormatter" # Setup flask logger replace_logger_handlers(flask_app.logger, logger) @@ -313,9 +335,14 @@ def flask_app_logger_setup( # Add flask before and after request functions to augment the logs def _flask_logging_before_request(): - g.correlation_id = request.headers.get("Correlation-ID", str(uuid.uuid4())) # pylint: disable=assigning-non-slot + g.correlation_id = request.headers.get( + "Correlation-ID", str(uuid.uuid4()) + ) # pylint: disable=assigning-non-slot g.start_time = time.time() # pylint: disable=assigning-non-slot - flask_app.before_request_funcs.setdefault(None, []).append(_flask_logging_before_request) + + flask_app.before_request_funcs.setdefault(None, []).append( + _flask_logging_before_request + ) def _flask_logging_after_request(response): response.headers.set("Correlation-ID", g.correlation_id) @@ -324,7 +351,7 @@ def _flask_logging_after_request(response): message = "Request completed" else: message = ( - f'{request.remote_addr} {request.method} {request.path} ' + f"{request.remote_addr} {request.method} {request.path} " f'{request.environ.get("SERVER_PROTOCOL")} {response.status_code}' ) flask_app.logger.info( @@ -336,23 +363,27 @@ def _flask_logging_after_request(response): "http_response_size": response.calculate_content_length(), "http_input_payload_size": request.content_length or 0, "http_input_payload_type": request.content_type or "", - "http_response_time_ms": f'{1000*(time.time() - g.start_time):.0f}', - } + "http_response_time_ms": f"{1000*(time.time() - g.start_time):.0f}", + }, ) return response - flask_app.after_request_funcs.setdefault(None, []).append(_flask_logging_after_request) + + flask_app.after_request_funcs.setdefault(None, []).append( + _flask_logging_after_request + ) # Add correlation id filter flask_app.logger.addFilter(_CorrelationIdLoggingFilter()) # Setup werkzeug logger at least at WARNING level in case its server is used # since it also logs at INFO level after each request creating redundancy - werkzeug_logger = logging.getLogger('werkzeug') + werkzeug_logger = logging.getLogger("werkzeug") replace_logger_handlers(werkzeug_logger, logger) werkzeug_logger.setLevel(max(logger.level, WARNING)) werkzeug_logger.parent = logger.parent import werkzeug._internal + werkzeug._internal._logger = werkzeug_logger return logger @@ -374,9 +405,13 @@ def get_correlation_id() -> str: try: has_correlation_id = hasattr(g, "correlation_id") except RuntimeError: - raise RuntimeError("get_correlation_id used outside correlation_id_context or flask app context.") + raise RuntimeError( + "get_correlation_id used outside correlation_id_context or flask app context." + ) if not has_correlation_id: - raise RuntimeError("correlation_id not found in flask.g, probably flask app not yet setup.") + raise RuntimeError( + "correlation_id not found in flask.g, probably flask app not yet setup." + ) return g.correlation_id @@ -388,14 +423,19 @@ def set_correlation_id(correlation_id: str): RuntimeError: When run outside an application context or if flask app has not been setup. """ from flask import g + try: - hasattr(g, 'correlation_id') + hasattr(g, "correlation_id") except RuntimeError: - raise RuntimeError('set_correlation_id only intended to be used inside an application context.') + raise RuntimeError( + "set_correlation_id only intended to be used inside an application context." + ) g.correlation_id = str(correlation_id) # pylint: disable=assigning-non-slot -current_correlation_id: ContextVar[Optional[str]] = ContextVar('current_correlation_id', default=None) +current_correlation_id: ContextVar[Optional[str]] = ContextVar( + "current_correlation_id", default=None +) @contextmanager @@ -431,7 +471,7 @@ class RLoggerProperty: def __init__(self, *args, **kwargs): """Initializer for LoggerProperty class.""" super().__init__(*args, **kwargs) - if not hasattr(self, '_rlogger'): + if not hasattr(self, "_rlogger"): self.rlogger = True @property @@ -447,10 +487,7 @@ def rlogger(self): return self._rlogger @rlogger.setter - def rlogger( - self, - logger: Optional[Union[bool, logging.Logger]] - ): + def rlogger(self, logger: Optional[Union[bool, logging.Logger]]): if logger is None or (isinstance(logger, bool) and not logger): self._rlogger = null_logger elif isinstance(logger, bool) and logger: diff --git a/reconplogger_tests.py b/reconplogger_tests.py index 0c3ea3b..84bb8d8 100755 --- a/reconplogger_tests.py +++ b/reconplogger_tests.py @@ -13,9 +13,10 @@ from contextlib import ExitStack, contextmanager from io import StringIO from unittest.mock import patch -from testfixtures import LogCapture, compare, Comparison from typing import Iterator +from testfixtures import LogCapture, compare, Comparison + try: from flask import Flask, request except ImportError: @@ -38,89 +39,100 @@ def capture_logs(logger: logging.Logger) -> Iterator[StringIO]: class TestReconplogger(unittest.TestCase): - def setUp(self): reconplogger.configs_loaded = set() def test_default_logger(self): """Test load config with the default config and plain logger.""" - reconplogger.load_config('reconplogger_default_cfg') - logger = logging.getLogger('plain_logger') - info_msg = 'info message' - with LogCapture(names='plain_logger') as log: + reconplogger.load_config("reconplogger_default_cfg") + logger = logging.getLogger("plain_logger") + info_msg = "info message" + with LogCapture(names="plain_logger") as log: logger.info(info_msg) - log.check(('plain_logger', 'INFO', info_msg)) + log.check(("plain_logger", "INFO", info_msg)) def test_log_level(self): """Test load config with the default config and plain logger changing the log level.""" - logger = reconplogger.logger_setup(level='INFO', reload=True) + logger = reconplogger.logger_setup(level="INFO", reload=True) self.assertEqual(logger.handlers[0].level, logging.INFO) - logger = reconplogger.logger_setup(level='ERROR', reload=True) + logger = reconplogger.logger_setup(level="ERROR", reload=True) self.assertEqual(logger.handlers[0].level, logging.ERROR) - with patch.dict(os.environ, {'LOGGER_LEVEL': 'WARNING'}): - logger = reconplogger.logger_setup(level='INFO', env_prefix='LOGGER', reload=True) + with patch.dict(os.environ, {"LOGGER_LEVEL": "WARNING"}): + logger = reconplogger.logger_setup( + level="INFO", env_prefix="LOGGER", reload=True + ) self.assertEqual(logger.handlers[0].level, logging.WARNING) def test_default_logger_with_exception(self): """Test exception logging with the default config and json logger.""" - reconplogger.load_config('reconplogger_default_cfg') - logger = logging.getLogger('json_logger') - error_msg = 'error message' - exception = RuntimeError('Exception message') - with LogCapture(names='json_logger') as log: + reconplogger.load_config("reconplogger_default_cfg") + logger = logging.getLogger("json_logger") + error_msg = "error message" + exception = RuntimeError("Exception message") + with LogCapture(names="json_logger") as log: try: raise exception - except Exception as e: + except Exception: logger.error(error_msg, exc_info=True) compare(Comparison(exception), log.records[-1].exc_info[1]) - log.check(('json_logger', 'ERROR', error_msg)) + log.check(("json_logger", "ERROR", error_msg)) def test_plain_logger_setup(self): """Test logger_setup without specifying environment variable names.""" logger = reconplogger.logger_setup() - info_msg = 'info message' - with LogCapture(names='plain_logger') as log: + info_msg = "info message" + with LogCapture(names="plain_logger") as log: logger.info(info_msg) - log.check(('plain_logger', 'INFO', info_msg)) + log.check(("plain_logger", "INFO", info_msg)) def test_json_logger_setup(self): """Test logger_setup without specifying environment variable names but changing logger name.""" - logger = reconplogger.logger_setup(logger_name='json_logger') - info_msg = 'info message' - with LogCapture(names='json_logger') as log: + logger = reconplogger.logger_setup(logger_name="json_logger") + info_msg = "info message" + with LogCapture(names="json_logger") as log: logger.info(info_msg) - log.check(('json_logger', 'INFO', info_msg)) + log.check(("json_logger", "INFO", info_msg)) def test_replace_logger_handlers(self): - logger = logging.getLogger('test_replace_logger_handlers') - handlers1 = reconplogger.logger_setup(logger_name='plain_logger').handlers - handlers2 = reconplogger.logger_setup(logger_name='json_logger').handlers + logger = logging.getLogger("test_replace_logger_handlers") + handlers1 = reconplogger.logger_setup(logger_name="plain_logger").handlers + handlers2 = reconplogger.logger_setup(logger_name="json_logger").handlers self.assertNotEqual(logger.handlers, handlers1) self.assertNotEqual(logger.handlers, handlers2) - reconplogger.replace_logger_handlers('test_replace_logger_handlers', 'plain_logger') + reconplogger.replace_logger_handlers( + "test_replace_logger_handlers", "plain_logger" + ) self.assertEqual(logger.handlers, handlers1) self.assertNotEqual(logger.handlers, handlers2) - reconplogger.replace_logger_handlers('test_replace_logger_handlers', 'json_logger') + reconplogger.replace_logger_handlers( + "test_replace_logger_handlers", "json_logger" + ) self.assertEqual(logger.handlers, handlers2) self.assertNotEqual(logger.handlers, handlers1) self.assertRaises( - ValueError, lambda: reconplogger.replace_logger_handlers(logger, False)) + ValueError, lambda: reconplogger.replace_logger_handlers(logger, False) + ) self.assertRaises( - ValueError, lambda: reconplogger.replace_logger_handlers(False, False)) + ValueError, lambda: reconplogger.replace_logger_handlers(False, False) + ) def test_init_messages(self): logger = reconplogger.logger_setup(init_messages=True, reload=True) - with self.assertLogs(logger='plain_logger', level='WARNING') as log: + with self.assertLogs(logger="plain_logger", level="WARNING") as log: reconplogger.test_logger(logger) - self.assertTrue(any(['WARNING' in v and 'reconplogger' in v for v in log.output])) + self.assertTrue( + any(["WARNING" in v and "reconplogger" in v for v in log.output]) + ) - @patch.dict(os.environ, { - 'RECONPLOGGER_NAME': 'example_logger', - 'RECONPLOGGER_CFG': """{ + @patch.dict( + os.environ, + { + "RECONPLOGGER_NAME": "example_logger", + "RECONPLOGGER_CFG": """{ "version": 1, "formatters": { "verbose": { @@ -140,17 +152,18 @@ def test_init_messages(self): "level": "DEBUG" } } - }""" - }) + }""", + }, + ) def test_logger_setup_env_prefix(self): - logger = reconplogger.logger_setup(env_prefix='RECONPLOGGER') - info_msg = 'info message env logger' - with LogCapture(names='example_logger') as log: + logger = reconplogger.logger_setup(env_prefix="RECONPLOGGER") + info_msg = "info message env logger" + with LogCapture(names="example_logger") as log: logger.info(info_msg) - log.check(('example_logger', 'INFO', info_msg)) + log.check(("example_logger", "INFO", info_msg)) def test_logger_setup_env_prefix_invalid(self): - for env_prefix in [None, '']: + for env_prefix in [None, ""]: with self.subTest(env_prefix): with self.assertRaises(ValueError): reconplogger.logger_setup(env_prefix=env_prefix) @@ -158,22 +171,23 @@ def test_logger_setup_env_prefix_invalid(self): def test_undefined_logger(self): """Test setting up a logger not already defined.""" self.assertRaises( - ValueError, lambda: reconplogger.logger_setup('undefined_logger')) + ValueError, lambda: reconplogger.logger_setup("undefined_logger") + ) def test_logger_setup_invalid_level(self): with self.assertRaises(ValueError): - reconplogger.logger_setup(level='INVALID', reload=True) + reconplogger.logger_setup(level="INVALID", reload=True) with self.assertRaises(ValueError): reconplogger.logger_setup(level=True, reload=True) - @patch.dict(os.environ, {'LOGGER_NAME': 'json_logger'}) + @patch.dict(os.environ, {"LOGGER_NAME": "json_logger"}) def test_correlation_id_context(self): logger = reconplogger.logger_setup() correlation_id = str(uuid.uuid4()) with reconplogger.correlation_id_context(correlation_id): self.assertEqual(correlation_id, reconplogger.get_correlation_id()) with capture_logs(logger) as logs: - logger.error('error message') + logger.error("error message") self.assertIn(correlation_id, logs.getvalue()) def test_get_correlation_id_outside_of_context(self): @@ -184,84 +198,104 @@ def test_get_correlation_id_outside_of_context(self): self.assertIn("used outside correlation_id_context", str(ctx.exception)) @unittest.skipIf(not Flask, "flask package is required") - @patch.dict(os.environ, { - 'RECONPLOGGER_CFG': 'reconplogger_default_cfg', - 'RECONPLOGGER_NAME': 'json_logger', - }) + @patch.dict( + os.environ, + { + "RECONPLOGGER_CFG": "reconplogger_default_cfg", + "RECONPLOGGER_NAME": "json_logger", + }, + ) def test_flask_app_logger_setup(self): app = Flask(__name__) - reconplogger.flask_app_logger_setup( - env_prefix='RECONPLOGGER', flask_app=app) + reconplogger.flask_app_logger_setup(env_prefix="RECONPLOGGER", flask_app=app) assert app.logger.filters # pylint: disable=no-member assert app.before_request_funcs assert app.after_request_funcs - flask_msg = 'flask message' - werkzeug_msg = 'werkzeug message' - with LogCapture(names=(app.logger.name, 'werkzeug')) as log: + flask_msg = "flask message" + werkzeug_msg = "werkzeug message" + with LogCapture(names=(app.logger.name, "werkzeug")) as log: app.logger.warning(flask_msg) # pylint: disable=no-member - logging.getLogger('werkzeug').warning(werkzeug_msg) + logging.getLogger("werkzeug").warning(werkzeug_msg) log.check_present( - (app.logger.name, 'WARNING', flask_msg), - ('werkzeug', 'WARNING', werkzeug_msg), + (app.logger.name, "WARNING", flask_msg), + ("werkzeug", "WARNING", werkzeug_msg), ) @unittest.skipIf(not Flask, "flask package is required") - @patch.dict(os.environ, { - 'RECONPLOGGER_CFG': 'reconplogger_default_cfg', - 'RECONPLOGGER_NAME': 'json_logger', - }) + @patch.dict( + os.environ, + { + "RECONPLOGGER_CFG": "reconplogger_default_cfg", + "RECONPLOGGER_NAME": "json_logger", + }, + ) def test_flask_app_correlation_id(self): app = Flask(__name__) - flask_msg = 'flask message with correlation id' + flask_msg = "flask message with correlation id" - @app.route('/') + @app.route("/") def hello_world(): - if request.args.get('id') is None: + if request.args.get("id") is None: correlation_id = reconplogger.get_correlation_id() else: - correlation_id = request.args.get('id') + correlation_id = request.args.get("id") reconplogger.set_correlation_id(correlation_id) app.logger.info(flask_msg) # pylint: disable=no-member - return 'correlation_id='+correlation_id + return "correlation_id=" + correlation_id client = app.test_client() - with LogCapture(names=app.logger.name, attributes=('name', 'levelname')) as logs: + with LogCapture( + names=app.logger.name, attributes=("name", "levelname") + ) as logs: response = client.get("/") - logs.check((app.logger.name, 'ERROR')) + logs.check((app.logger.name, "ERROR")) self.assertEqual(response.status_code, 500) - reconplogger.flask_app_logger_setup(env_prefix='RECONPLOGGER', flask_app=app) + reconplogger.flask_app_logger_setup(env_prefix="RECONPLOGGER", flask_app=app) client = app.test_client() self.assertRaises(RuntimeError, lambda: reconplogger.get_correlation_id()) - self.assertRaises(RuntimeError, lambda: reconplogger.set_correlation_id('id')) + self.assertRaises(RuntimeError, lambda: reconplogger.set_correlation_id("id")) # Check correlation id propagation - with LogCapture(names=app.logger.name, attributes=('name', 'levelname', 'getMessage', 'correlation_id')) as logs: + with LogCapture( + names=app.logger.name, + attributes=("name", "levelname", "getMessage", "correlation_id"), + ) as logs: correlation_id = str(uuid.uuid4()) - response = client.get("/", headers={'Correlation-ID': correlation_id}) - self.assertEqual(response.data.decode('utf-8'), 'correlation_id='+correlation_id) + response = client.get("/", headers={"Correlation-ID": correlation_id}) + self.assertEqual( + response.data.decode("utf-8"), "correlation_id=" + correlation_id + ) logs.check( - (app.logger.name, 'INFO', flask_msg, correlation_id), - (app.logger.name, 'INFO', "Request completed", correlation_id), + (app.logger.name, "INFO", flask_msg, correlation_id), + (app.logger.name, "INFO", "Request completed", correlation_id), ) # Check correlation id creation - with LogCapture(names=app.logger.name, attributes=('name', 'levelname', 'getMessage', 'correlation_id')) as logs: + with LogCapture( + names=app.logger.name, + attributes=("name", "levelname", "getMessage", "correlation_id"), + ) as logs: client.get("/") correlation_id = logs.actual()[0][3] uuid.UUID(correlation_id) logs.check( - (app.logger.name, 'INFO', flask_msg, correlation_id), - (app.logger.name, 'INFO', "Request completed", correlation_id), + (app.logger.name, "INFO", flask_msg, correlation_id), + (app.logger.name, "INFO", "Request completed", correlation_id), ) # Check set correlation id - with LogCapture(names=app.logger.name, attributes=('name', 'levelname', 'getMessage', 'correlation_id')) as logs: + with LogCapture( + names=app.logger.name, + attributes=("name", "levelname", "getMessage", "correlation_id"), + ) as logs: correlation_id = str(uuid.uuid4()) - response = client.get("/?id="+correlation_id) - self.assertEqual(response.data.decode('utf-8'), 'correlation_id='+correlation_id) + response = client.get("/?id=" + correlation_id) + self.assertEqual( + response.data.decode("utf-8"), "correlation_id=" + correlation_id + ) logs.check( - (app.logger.name, 'INFO', flask_msg, correlation_id), - (app.logger.name, 'INFO', "Request completed", correlation_id), + (app.logger.name, "INFO", flask_msg, correlation_id), + (app.logger.name, "INFO", "Request completed", correlation_id), ) @unittest.skipIf(not Flask, "flask package is required") @@ -279,7 +313,7 @@ def get_id(): thread = threading.Thread(target=server.serve_forever, daemon=True) thread.start() - werkzeug_logger = logging.getLogger('werkzeug') + werkzeug_logger = logging.getLogger("werkzeug") self.assertEqual(werkzeug_logger.handlers, logger.handlers) self.assertGreaterEqual(werkzeug_logger.level, reconplogger.WARNING) @@ -288,19 +322,19 @@ def get_id(): reconplogger.set_correlation_id(correlation_id) response = requests.get(f"http://localhost:{port}/id") self.assertEqual(correlation_id, response.text) - self.assertEqual(correlation_id, response.headers['Correlation-ID']) + self.assertEqual(correlation_id, response.headers["Correlation-ID"]) server.shutdown() def test_add_file_handler(self): """Test the use of add_file_handler.""" - tmpdir = tempfile.mkdtemp(prefix='_reconplogger_test_') - error_msg = 'error message' - debug_msg = 'debug message' + tmpdir = tempfile.mkdtemp(prefix="_reconplogger_test_") + error_msg = "error message" + debug_msg = "debug message" - log_file = os.path.join(tmpdir, 'file1.log') - logger = reconplogger.logger_setup(logger_name='plain_logger', level='ERROR') - reconplogger.add_file_handler(logger, file_path=log_file, level='DEBUG') + log_file = os.path.join(tmpdir, "file1.log") + logger = reconplogger.logger_setup(logger_name="plain_logger", level="ERROR") + reconplogger.add_file_handler(logger, file_path=log_file, level="DEBUG") self.assertEqual(logger.handlers[0].level, logging.ERROR) self.assertEqual(logger.handlers[1].level, logging.DEBUG) logger.error(error_msg) @@ -309,19 +343,27 @@ def test_add_file_handler(self): self.assertTrue(any([error_msg in line for line in open(log_file).readlines()])) self.assertTrue(any([debug_msg in line for line in open(log_file).readlines()])) - log_file = os.path.join(tmpdir, 'file2.log') - logger = reconplogger.logger_setup(logger_name='plain_logger', level='DEBUG', reload=True) - reconplogger.add_file_handler(logger, file_path=log_file, level='ERROR') + log_file = os.path.join(tmpdir, "file2.log") + logger = reconplogger.logger_setup( + logger_name="plain_logger", level="DEBUG", reload=True + ) + reconplogger.add_file_handler(logger, file_path=log_file, level="ERROR") self.assertEqual(logger.handlers[0].level, logging.DEBUG) self.assertEqual(logger.handlers[1].level, logging.ERROR) logger.error(error_msg) logger.debug(debug_msg) logger.handlers[1].close() self.assertTrue(any([error_msg in line for line in open(log_file).readlines()])) - self.assertFalse(any([debug_msg in line for line in open(log_file).readlines()])) + self.assertFalse( + any([debug_msg in line for line in open(log_file).readlines()]) + ) self.assertRaises( - ValueError, lambda: reconplogger.add_file_handler(logger, file_path=log_file, level='INVALID')) + ValueError, + lambda: reconplogger.add_file_handler( + logger, file_path=log_file, level="INVALID" + ), + ) shutil.rmtree(tmpdir) @@ -334,7 +376,7 @@ class MyClass(reconplogger.RLoggerProperty): self.assertEqual(myclass.rlogger, reconplogger.null_logger) myclass.rlogger = True self.assertEqual(myclass.rlogger, reconplogger.logger_setup()) - logger = logging.Logger('test_logger_property') + logger = logging.Logger("test_logger_property") myclass.rlogger = logger self.assertEqual(myclass.rlogger, logger) @@ -345,5 +387,5 @@ def run_tests(): sys.exit(True) -if __name__ == '__main__': +if __name__ == "__main__": run_tests() diff --git a/setup.cfg b/setup.cfg index 6becad8..bc0f8d8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [options] py_modules = - reconplogger + reconplogger reconplogger_tests test_suite = reconplogger_tests python_requires = >=3.8 diff --git a/setup.py b/setup.py index ba7f7f9..07d87e0 100755 --- a/setup.py +++ b/setup.py @@ -7,50 +7,58 @@ import unittest -LONG_DESCRIPTION = re.sub(':class:|:func:|:ref:', '', open('README.rst').read()) +LONG_DESCRIPTION = re.sub(":class:|:func:|:ref:", "", open("README.rst").read()) CMDCLASS = {} ## test_coverage target ## class CoverageCommand(Command): - description = 'run test coverage and generate html or xml report' + description = "run test coverage and generate html or xml report" user_options = [] # type: ignore - def initialize_options(self): pass - def finalize_options(self): pass + + def initialize_options(self): + pass + + def finalize_options(self): + pass + def run(self): try: import coverage except ImportError: - print('error: coverage package not found, run_test_coverage requires it.') + print("error: coverage package not found, run_test_coverage requires it.") sys.exit(True) - cov = coverage.Coverage(source=['reconplogger']) + cov = coverage.Coverage(source=["reconplogger"]) cov.start() - tests = unittest.defaultTestLoader.loadTestsFromName('reconplogger_tests') + tests = unittest.defaultTestLoader.loadTestsFromName("reconplogger_tests") if not unittest.TextTestRunner(verbosity=2).run(tests).wasSuccessful(): sys.exit(True) cov.stop() cov.save() cov.report() - if 'TEST_COVERAGE_XML' in os.environ: - outfile = os.environ['TEST_COVERAGE_XML'] + if "TEST_COVERAGE_XML" in os.environ: + outfile = os.environ["TEST_COVERAGE_XML"] cov.xml_report(outfile=outfile) - print('\nSaved coverage report to '+outfile+'.') + print("\nSaved coverage report to " + outfile + ".") else: - cov.html_report(directory='htmlcov') - print('\nSaved html coverage report to htmlcov directory.') + cov.html_report(directory="htmlcov") + print("\nSaved html coverage report to htmlcov directory.") -CMDCLASS['test_coverage'] = CoverageCommand + +CMDCLASS["test_coverage"] = CoverageCommand ## build_sphinx target ## try: from sphinx.setup_command import BuildDoc - CMDCLASS['build_sphinx'] = BuildDoc # type: ignore + + CMDCLASS["build_sphinx"] = BuildDoc # type: ignore except Exception: - print('warning: sphinx package not found, build_sphinx target will not be available.') + print( + "warning: sphinx package not found, build_sphinx target will not be available." + ) ## Run setuptools setup ## -setup(long_description=LONG_DESCRIPTION, - cmdclass=CMDCLASS) +setup(long_description=LONG_DESCRIPTION, cmdclass=CMDCLASS) diff --git a/sphinx/conf.py b/sphinx/conf.py index b365f36..2d1144f 100644 --- a/sphinx/conf.py +++ b/sphinx/conf.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- # # reconplogger documentation build configuration file, created by # sphinx-quickstart on Tue Jul 31 18:01:01 2018. @@ -13,103 +12,103 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys import os +import sys # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath('../')) +sys.path.insert(0, os.path.abspath("../")) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.doctest', - 'sphinx.ext.intersphinx', - 'sphinx.ext.todo', - 'sphinx.ext.coverage', - 'sphinx.ext.mathjax', - 'sphinx.ext.viewcode', - 'sphinx.ext.napoleon', - 'autodocsumm', - 'sphinx_autodoc_typehints', + "sphinx.ext.autodoc", + "sphinx.ext.doctest", + "sphinx.ext.intersphinx", + "sphinx.ext.todo", + "sphinx.ext.coverage", + "sphinx.ext.mathjax", + "sphinx.ext.viewcode", + "sphinx.ext.napoleon", + "autodocsumm", + "sphinx_autodoc_typehints", ] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # source_suffix = ['.rst', '.md'] -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = 'reconplogger' -copyright = '2018-present, omni:us' -author = 'omni:us' +project = "reconplogger" +copyright = "2018-present, omni:us" +author = "omni:us" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '4.14.0' +version = "4.14.0" # The full version, including alpha/beta/rc tags. -release = '4.14.0' +release = "4.14.0" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = "en" # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build'] +exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all # documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False +# keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = True @@ -119,31 +118,31 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'sphinx_rtd_theme' +html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (relative to this directory) to use as a favicon of # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, @@ -153,122 +152,121 @@ # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. -#html_extra_path = [] +# html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr' -#html_search_language = 'en' +# html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # Now only 'ja' uses this config value -#html_search_options = {'type': 'default'} +# html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. -#html_search_scorer = 'scorer.js' +# html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. -htmlhelp_basename = 'reconploggerdoc' +htmlhelp_basename = "reconploggerdoc" # -- Options for LaTeX output --------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', - -# Latex figure (float) alignment -#'figure_align': 'htbp', + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + #'preamble': '', + # Latex figure (float) alignment + #'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'reconplogger.tex', 'reconplogger Documentation', - 'Mauricio Villegas', 'manual'), + ( + master_doc, + "reconplogger.tex", + "reconplogger Documentation", + "Mauricio Villegas", + "manual", + ), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'reconplogger', 'reconplogger Documentation', - [author], 1) -] +man_pages = [(master_doc, "reconplogger", "reconplogger Documentation", [author], 1)] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------- @@ -277,23 +275,29 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'reconplogger', 'reconplogger Documentation', - author, 'reconplogger', 'One line description of project.', - 'Miscellaneous'), + ( + master_doc, + "reconplogger", + "reconplogger Documentation", + author, + "reconplogger", + "One line description of project.", + "Miscellaneous", + ), ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False +# texinfo_no_detailmenu = False # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'https://docs.python.org/': None} +intersphinx_mapping = {"python": ("https://docs.python.org/3", None)}