def test_logging_filters_with_known_limitations( self, capfd: pytest.CaptureFixture, log_filters_input: str, mocker: MockerFixture, ) -> None: """Test known limitations of log message filters. - Filters in the input string should be separated with commas, not spaces. - Filters are applied with string matching, so a filter of `/health` will also filter out messages including `/healthy`. """ filters = logging_conf.LogFilter.set_filters(log_filters_input) mocker.patch.dict( logging_conf.LOGGING_CONFIG["filters"]["filter_log_message"], { "()": logging_conf.LogFilter, "filters": filters }, clear=True, ) logger = logging_conf.logging.getLogger( "test.logging_conf.output.filtererrors") logging_conf.configure_logging(logger=logger) logger.info(log_filters_input) logger.info("/healthy") captured = capfd.readouterr() assert log_filters_input not in captured.out if log_filters_input == "/health": assert "/healthy" not in captured.out else: assert "/healthy" in captured.out
def test_logging_filters( self, capfd: pytest.CaptureFixture, log_filters_input: str, log_filters_output: set[str], mocker: MockerFixture, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test that log message filters are applied as expected.""" monkeypatch.setenv("LOG_FILTERS", log_filters_input) mocker.patch.object(logging_conf, "LOG_FILTERS", logging_conf.LogFilter.set_filters()) mocker.patch.dict( logging_conf.LOGGING_CONFIG["filters"]["filter_log_message"], { "()": logging_conf.LogFilter, "filters": logging_conf.LOG_FILTERS }, clear=True, ) path_to_log = "/status" logger = logging_conf.logging.getLogger( "test.logging_conf.output.filters") logging_conf.configure_logging(logger=logger) logger.info(*self._uvicorn_access_log_args(path_to_log)) logger.info(log_filters_input) for log_filter in log_filters_output: logger.info(*self._uvicorn_access_log_args(log_filter)) captured = capfd.readouterr() assert logging_conf.LOG_FILTERS == log_filters_output assert path_to_log in captured.out for log_filter in log_filters_output: assert log_filter not in captured.out
def test_logging_output_default(self, capfd: pytest.CaptureFixture) -> None: """Test logger output with default format.""" logger = logging_conf.logging.getLogger() logging_conf.configure_logging() logger.info("Hello, World!") captured = capfd.readouterr() assert "INFO" in captured.out assert "Hello, World!" in captured.out
def test_configure_logging_module(self, logging_conf_module_path: str, mocker: MockerFixture) -> None: """Test logging configuration with correct logging config module path.""" logger = mocker.patch.object(logging_conf.logging, "root", autospec=True) logging_conf.configure_logging(logger=logger, logging_conf=logging_conf_module_path) logger.debug.assert_called_once_with( f"Logging dict config loaded from {logging_conf_module_path}.")
def test_configure_logging_tmp_file(self, logging_conf_tmp_file_path: Path, mocker: MockerFixture) -> None: """Test logging configuration with temporary logging config file path.""" logger = mocker.patch.object(logging_conf.logging, "root", autospec=True) logging_conf_file = f"{logging_conf_tmp_file_path}/tmp_log.py" logging_conf.configure_logging(logger=logger, logging_conf=logging_conf_file) logger.debug.assert_called_once_with( f"Logging dict config loaded from {logging_conf_file}.")
def test_configure_logging_module_incorrect(self, mocker: MockerFixture) -> None: """Test logging configuration with incorrect logging config module path.""" logger = mocker.patch.object(logging_conf.logging, "root", autospec=True) logger_error_msg = "Error when setting logging module" with pytest.raises(ModuleNotFoundError): logging_conf.configure_logging(logger=logger, logging_conf="no.module.here") assert logger_error_msg in logger.error.call_args.args[0] assert "ModuleNotFoundError" in logger.error.call_args.args[0]
def test_configure_logging_tmp_module( self, logging_conf_tmp_file_path: Path, mocker: MockerFixture, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test logging configuration with temporary logging config path.""" logger = mocker.patch.object(logging_conf.logging, "root", autospec=True) monkeypatch.syspath_prepend(logging_conf_tmp_file_path) monkeypatch.setenv("LOGGING_CONF", "tmp_log") assert os.getenv("LOGGING_CONF") == "tmp_log" logging_conf.configure_logging(logger=logger, logging_conf="tmp_log") logger.debug.assert_called_once_with( "Logging dict config loaded from tmp_log.")
def test_logging_output_custom_format( self, capfd: pytest.CaptureFixture, log_format: str, log_level_output: str, logging_conf_tmp_file_path: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test logger output with custom format.""" logging_conf_file = f"{logging_conf_tmp_file_path}/tmp_log.py" monkeypatch.setenv("LOG_FORMAT", "gunicorn") monkeypatch.setenv("LOG_LEVEL", "debug") logger = logging_conf.logging.getLogger() logging_conf.configure_logging(logging_conf=logging_conf_file) logger.debug("Hello, Customized World!") captured = capfd.readouterr() assert log_format not in captured.out assert log_level_output in captured.out assert f"Logging dict config loaded from {logging_conf_file}." in captured.out assert "Hello, Customized World!" in captured.out
def test_configure_logging_tmp_module_no_dict( self, logging_conf_tmp_path_no_dict: Path, mocker: MockerFixture, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test logging configuration with temporary logging config path. - Correct module name - No `LOGGING_CONFIG` object """ logger = mocker.patch.object(logging_conf.logging, "root", autospec=True) monkeypatch.syspath_prepend(logging_conf_tmp_path_no_dict) monkeypatch.setenv("LOGGING_CONF", "no_dict") logger_error_msg = "Error when setting logging module" attribute_error_msg = "No LOGGING_CONFIG in no_dict" assert os.getenv("LOGGING_CONF") == "no_dict" with pytest.raises(AttributeError): logging_conf.configure_logging(logger=logger, logging_conf="no_dict") logger.error.assert_called_once_with( f"{logger_error_msg}: AttributeError {attribute_error_msg}.")
def test_configure_logging_tmp_module_incorrect_type( self, logging_conf_tmp_path_incorrect_type: Path, mocker: MockerFixture, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test logging configuration with temporary logging config path. - Correct module name - `LOGGING_CONFIG` object with incorrect type """ logger = mocker.patch.object(logging_conf.logging, "root", autospec=True) monkeypatch.syspath_prepend(logging_conf_tmp_path_incorrect_type) monkeypatch.setenv("LOGGING_CONF", "incorrect_type") logger_error_msg = "Error when setting logging module" type_error_msg = "LOGGING_CONFIG is not a dictionary instance" assert os.getenv("LOGGING_CONF") == "incorrect_type" with pytest.raises(TypeError): logging_conf.configure_logging(logger=logger, logging_conf="incorrect_type") logger.error.assert_called_once_with( f"{logger_error_msg}: TypeError {type_error_msg}.")
def test_configure_logging_tmp_file_incorrect_extension( self, logging_conf_tmp_path_incorrect_extension: Path, mocker: MockerFixture, ) -> None: """Test logging configuration with incorrect temporary file type.""" logger = mocker.patch.object(logging_conf.logging, "root", autospec=True) incorrect_logging_conf = logging_conf_tmp_path_incorrect_extension.joinpath( "tmp_logging_conf") logger_error_msg = "Error when setting logging module" import_error_msg = f"Unable to import {incorrect_logging_conf}" with pytest.raises(ImportError) as e: logging_conf.configure_logging( logger=logger, logging_conf=str(incorrect_logging_conf), ) assert str(e.value) in import_error_msg logger.error.assert_called_once_with( f"{logger_error_msg}: ImportError {import_error_msg}.") with open(incorrect_logging_conf, "r") as f: contents = f.read() assert "This file doesn't have the correct extension" in contents
max_workers: str | None = None, total_workers: str | None = None, workers_per_core: str = "1", ) -> int: """Calculate the number of Gunicorn worker processes.""" cores = multiprocessing.cpu_count() default = max(int(float(workers_per_core) * cores), 2) use_max = m if max_workers and (m := int(max_workers)) > 0 else False use_total = t if total_workers and (t := int(total_workers)) > 0 else False use_least = min(use_max, use_total) if use_max and use_total else False use_default = min(use_max, default) if use_max else default return use_least or use_total or use_default # Gunicorn settings bind = os.getenv( "BIND") or f'{os.getenv("HOST", "0.0.0.0")}:{os.getenv("PORT", "80")}' accesslog = os.getenv("ACCESS_LOG", "-") errorlog = os.getenv("ERROR_LOG", "-") graceful_timeout = int(os.getenv("GRACEFUL_TIMEOUT", "120")) keepalive = int(os.getenv("KEEP_ALIVE", "5")) logconfig_dict = configure_logging() loglevel = os.getenv("LOG_LEVEL", "info") timeout = int(os.getenv("TIMEOUT", "120")) worker_tmp_dir = "/dev/shm" workers = calculate_workers( os.getenv("MAX_WORKERS"), os.getenv("WEB_CONCURRENCY"), workers_per_core=os.getenv("WORKERS_PER_CORE", "1"), )
"""Start the Uvicorn or Gunicorn server.""" try: if process_manager == "gunicorn": logger.debug("Running Uvicorn with Gunicorn.") gunicorn_options: list = set_gunicorn_options(app_module) subprocess.run(gunicorn_options) elif process_manager == "uvicorn": logger.debug("Running Uvicorn without Gunicorn.") uvicorn_options: dict = set_uvicorn_options( log_config=logging_conf_dict) uvicorn.run(app_module, **uvicorn_options) else: raise NameError( "Process manager needs to be either uvicorn or gunicorn") except Exception as e: logger.error( f"Error when starting server: {e.__class__.__name__} {e}.") raise if __name__ == "__main__": # pragma: no cover logger = logging.getLogger() logging_conf_dict = configure_logging(logger=logger) run_pre_start_script(logger=logger) start_server( str(os.getenv("PROCESS_MANAGER", "gunicorn")), app_module=set_app_module(logger=logger), logger=logger, logging_conf_dict=logging_conf_dict, )