From ebf77ab1cc726f3d8383a8d3c5a84d93be7acdd4 Mon Sep 17 00:00:00 2001 From: "Ochui, Princewill Patrick" <21917688+ochui@users.noreply.github.com> Date: Sun, 5 Apr 2026 22:55:38 +0100 Subject: [PATCH 1/4] Add SUPPRESS_ACCESS_LOGS setting and related logging configuration tests - Introduced `SUPPRESS_ACCESS_LOGS` setting to control Uvicorn access logs. - Updated `configure_logging` function to accept the new setting. - Enhanced `QueueServerSettings` to include the new configuration option. - Added unit tests for logging configuration behavior based on the new setting. --- .codex | 0 README.md | 1 + asgi.py | 11 +++++++++-- docs/configuration.rst | 5 +++++ fq_server/settings.py | 11 ++++++++++- tests/test_config_settings.py | 10 +++++++++- tests/test_logging_config.py | 25 +++++++++++++++++++++++++ 7 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 .codex create mode 100644 tests/test_logging_config.py diff --git a/.codex b/.codex new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md index 88fc37b..dff981b 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ These application settings are validated at startup by `QueueServerSettings` wit | `DEFAULT_JOB_REQUEUE_LIMIT` | `-1` | Default retry limit. `-1` retries forever. | | `ENABLE_REQUEUE_SCRIPT` | `true` | Enables the background requeue loop. | | `LOG_LEVEL` | `INFO` | Application log level. | +| `SUPPRESS_ACCESS_LOGS` | `true` | Suppresses all Uvicorn access logs. | | `REDIS_DB` | `0` | Redis database number. | | `REDIS_KEY_PREFIX` | `fq_server` | Prefix used for Redis keys. | | `REDIS_CONN_TYPE` | `tcp_sock` | Redis connection type: `tcp_sock` or `unix_sock`. | diff --git a/asgi.py b/asgi.py index 17252d0..82c80d2 100644 --- a/asgi.py +++ b/asgi.py @@ -6,7 +6,10 @@ from fq_server import QueueServerSettings, setup_server -def configure_logging(log_level: str) -> None: +def configure_logging( + log_level: str, + suppress_access_logs: bool = True, +) -> None: level = getattr(logging, log_level) root_logger = logging.getLogger() @@ -17,10 +20,14 @@ def configure_logging(log_level: str) -> None: ) logging.getLogger("fq_server").setLevel(level) + logging.getLogger("uvicorn.access").disabled = suppress_access_logs settings = QueueServerSettings.from_env() -configure_logging(settings.log_level) +configure_logging( + settings.log_level, + suppress_access_logs=settings.suppress_access_logs, +) server = setup_server(settings.to_fq_config()) # ASGI app exposed for Uvicorn/Hypercorn diff --git a/docs/configuration.rst b/docs/configuration.rst index 8036c7b..125984f 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -24,6 +24,9 @@ Queue settings Application log level. Supported values are ``DEBUG``, ``INFO``, ``WARNING``, ``ERROR``, and ``CRITICAL``. +``SUPPRESS_ACCESS_LOGS`` + Suppresses all Uvicorn access logs. + Redis settings -------------- @@ -69,6 +72,8 @@ Defaults - ``true`` * - ``LOG_LEVEL`` - ``INFO`` + * - ``SUPPRESS_ACCESS_LOGS`` + - ``true`` * - ``REDIS_DB`` - ``0`` * - ``REDIS_KEY_PREFIX`` diff --git a/fq_server/settings.py b/fq_server/settings.py index 5b82d9a..09a9319 100644 --- a/fq_server/settings.py +++ b/fq_server/settings.py @@ -50,6 +50,10 @@ class QueueServerSettings(BaseSettings): default=True, validation_alias="ENABLE_REQUEUE_SCRIPT" ) log_level: LogLevelName = Field(default="INFO", validation_alias="LOG_LEVEL") + suppress_access_logs: bool = Field( + default=True, + validation_alias="SUPPRESS_ACCESS_LOGS", + ) redis_db: int = Field(default=0, ge=0, validation_alias="REDIS_DB") redis_key_prefix: str = Field( @@ -74,7 +78,12 @@ class QueueServerSettings(BaseSettings): validation_alias="REDIS_UNIX_SOCKET_PATH", ) - @field_validator("enable_requeue_script", "redis_clustered", mode="before") + @field_validator( + "enable_requeue_script", + "suppress_access_logs", + "redis_clustered", + mode="before", + ) @classmethod def validate_boolean_env(cls, value: bool | str) -> bool: if isinstance(value, bool): diff --git a/tests/test_config_settings.py b/tests/test_config_settings.py index b7186c8..92d4341 100644 --- a/tests/test_config_settings.py +++ b/tests/test_config_settings.py @@ -66,6 +66,14 @@ def test_queue_server_settings_log_level_override(self): settings = QueueServerSettings.from_env({"LOG_LEVEL": "debug"}) self.assertEqual(settings.log_level, "DEBUG") + def test_queue_server_settings_suppress_access_logs_default(self): + settings = QueueServerSettings.from_env({}) + self.assertTrue(settings.suppress_access_logs) + + def test_queue_server_settings_suppress_access_logs_override(self): + settings = QueueServerSettings.from_env({"SUPPRESS_ACCESS_LOGS": "false"}) + self.assertFalse(settings.suppress_access_logs) + def test_queue_server_settings_rejects_invalid_log_level(self): with self.assertRaisesRegex(ValidationError, "LOG_LEVEL"): - QueueServerSettings.from_env({"LOG_LEVEL": "verbose"}) \ No newline at end of file + QueueServerSettings.from_env({"LOG_LEVEL": "verbose"}) diff --git a/tests/test_logging_config.py b/tests/test_logging_config.py new file mode 100644 index 0000000..b72ecc5 --- /dev/null +++ b/tests/test_logging_config.py @@ -0,0 +1,25 @@ +# Copyright (c) 2025 Flowdacity Development Team. See LICENSE.txt for details. + +import logging +import unittest + +from asgi import configure_logging + + +class TestLoggingConfig(unittest.TestCase): + def setUp(self): + self.access_logger = logging.getLogger("uvicorn.access") + self.original_disabled = self.access_logger.disabled + + def tearDown(self): + self.access_logger.disabled = self.original_disabled + + def test_configure_logging_disables_access_logger_when_suppressed(self): + self.access_logger.disabled = False + configure_logging("INFO", suppress_access_logs=True) + self.assertTrue(self.access_logger.disabled) + + def test_configure_logging_enables_access_logger_when_not_suppressed(self): + self.access_logger.disabled = True + configure_logging("INFO", suppress_access_logs=False) + self.assertFalse(self.access_logger.disabled) From 3c108c8b133de3ada5276452ed67b96ae378c35d Mon Sep 17 00:00:00 2001 From: "Ochui, Princewill Patrick" <21917688+ochui@users.noreply.github.com> Date: Sun, 5 Apr 2026 23:09:16 +0100 Subject: [PATCH 2/4] Add initial configuration files for Qlty and set up .gitignore --- .qlty/.gitignore | 7 +++ .qlty/configs/.hadolint.yaml | 2 + .qlty/qlty.toml | 95 ++++++++++++++++++++++++++++++++++++ 3 files changed, 104 insertions(+) create mode 100644 .qlty/.gitignore create mode 100644 .qlty/configs/.hadolint.yaml create mode 100644 .qlty/qlty.toml diff --git a/.qlty/.gitignore b/.qlty/.gitignore new file mode 100644 index 0000000..3036618 --- /dev/null +++ b/.qlty/.gitignore @@ -0,0 +1,7 @@ +* +!configs +!configs/** +!hooks +!hooks/** +!qlty.toml +!.gitignore diff --git a/.qlty/configs/.hadolint.yaml b/.qlty/configs/.hadolint.yaml new file mode 100644 index 0000000..8f7e23e --- /dev/null +++ b/.qlty/configs/.hadolint.yaml @@ -0,0 +1,2 @@ +ignored: + - DL3008 diff --git a/.qlty/qlty.toml b/.qlty/qlty.toml new file mode 100644 index 0000000..2657085 --- /dev/null +++ b/.qlty/qlty.toml @@ -0,0 +1,95 @@ +# This file was automatically generated by `qlty init`. +# You can modify it to suit your needs. +# We recommend you to commit this file to your repository. +# +# This configuration is used by both Qlty CLI and Qlty Cloud. +# +# Qlty CLI -- Code quality toolkit for developers +# Qlty Cloud -- Fully automated Code Health Platform +# +# Try Qlty Cloud: https://qlty.sh +# +# For a guide to configuration, visit https://qlty.sh/d/config +# Or for a full reference, visit https://qlty.sh/d/qlty-toml +config_version = "0" + +exclude_patterns = [ + "*_min.*", + "*-min.*", + "*.min.*", + "**/.yarn/**", + "**/*.d.ts", + "**/assets/**", + "**/bower_components/**", + "**/build/**", + "**/cache/**", + "**/config/**", + "**/db/**", + "**/deps/**", + "**/dist/**", + "**/extern/**", + "**/external/**", + "**/generated/**", + "**/Godeps/**", + "**/gradlew/**", + "**/mvnw/**", + "**/node_modules/**", + "**/protos/**", + "**/seed/**", + "**/target/**", + "**/templates/**", + "**/testdata/**", + "**/vendor/**", +] + +test_patterns = [ + "**/test/**", + "**/spec/**", + "**/*.test.*", + "**/*.spec.*", + "**/*_test.*", + "**/*_spec.*", + "**/test_*.*", + "**/spec_*.*", +] + +[smells] +mode = "comment" + +[[source]] +name = "default" +default = true + + +[[plugin]] +name = "actionlint" + +[[plugin]] +name = "bandit" + +[[plugin]] +name = "hadolint" + +[[plugin]] +name = "radarlint-iac" +mode = "monitor" + +[[plugin]] +name = "radarlint-python" +mode = "comment" + +[[plugin]] +name = "ripgrep" +mode = "comment" + +[[plugin]] +name = "ruff" +drivers = [ + "lint", +] + +[[plugin]] +name = "trufflehog" + +[[plugin]] +name = "zizmor" From 65798011f38ffb413643a68fab1d3f113a709b7f Mon Sep 17 00:00:00 2001 From: "Ochui, Princewill Patrick" <21917688+ochui@users.noreply.github.com> Date: Sun, 5 Apr 2026 23:16:15 +0100 Subject: [PATCH 3/4] Add .codex to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 41cb5a5..3221ce2 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,4 @@ test_fq.py .idea/* venv/* .vscode/ +.codex From fed1168214abe5f31a41d346c31a88365bc267ef Mon Sep 17 00:00:00 2001 From: "Ochui, Princewill Patrick" <21917688+ochui@users.noreply.github.com> Date: Sun, 5 Apr 2026 23:26:20 +0100 Subject: [PATCH 4/4] Refactor logging configuration into a separate module and update tests for improved clarity and maintainability --- asgi.py | 20 +------------------- fq_server/logging.py | 20 ++++++++++++++++++++ tests/test_logging_config.py | 19 ++++++++++++++++++- 3 files changed, 39 insertions(+), 20 deletions(-) create mode 100644 fq_server/logging.py diff --git a/asgi.py b/asgi.py index 82c80d2..b26cc23 100644 --- a/asgi.py +++ b/asgi.py @@ -1,26 +1,8 @@ # Copyright (c) 2025 Flowdacity Team. See LICENSE.txt for details. # ASGI application entrypoint for Flowdacity Queue (FQ) Server -import logging - from fq_server import QueueServerSettings, setup_server - - -def configure_logging( - log_level: str, - suppress_access_logs: bool = True, -) -> None: - level = getattr(logging, log_level) - root_logger = logging.getLogger() - - if not root_logger.handlers: - logging.basicConfig( - level=level, - format="%(asctime)s %(levelname)s [%(name)s] %(message)s", - ) - - logging.getLogger("fq_server").setLevel(level) - logging.getLogger("uvicorn.access").disabled = suppress_access_logs +from fq_server.logging import configure_logging settings = QueueServerSettings.from_env() diff --git a/fq_server/logging.py b/fq_server/logging.py new file mode 100644 index 0000000..e8cd952 --- /dev/null +++ b/fq_server/logging.py @@ -0,0 +1,20 @@ +# Copyright (c) 2025 Flowdacity Development Team. See LICENSE.txt for details. + +import logging + + +def configure_logging( + log_level: str, + suppress_access_logs: bool = True, +) -> None: + level = getattr(logging, log_level) + root_logger = logging.getLogger() + + if not root_logger.handlers: + logging.basicConfig( + level=level, + format="%(asctime)s %(levelname)s [%(name)s] %(message)s", + ) + + logging.getLogger("fq_server").setLevel(level) + logging.getLogger("uvicorn.access").disabled = suppress_access_logs diff --git a/tests/test_logging_config.py b/tests/test_logging_config.py index b72ecc5..c598d8f 100644 --- a/tests/test_logging_config.py +++ b/tests/test_logging_config.py @@ -3,15 +3,27 @@ import logging import unittest -from asgi import configure_logging +from fq_server.logging import configure_logging class TestLoggingConfig(unittest.TestCase): def setUp(self): + self.root_logger = logging.getLogger() + self.original_root_handlers = list(self.root_logger.handlers) + self.original_root_level = self.root_logger.level + self.fq_logger = logging.getLogger("fq_server") + self.original_fq_level = self.fq_logger.level self.access_logger = logging.getLogger("uvicorn.access") self.original_disabled = self.access_logger.disabled def tearDown(self): + for handler in list(self.root_logger.handlers): + if handler not in self.original_root_handlers: + self.root_logger.removeHandler(handler) + handler.close() + + self.root_logger.setLevel(self.original_root_level) + self.fq_logger.setLevel(self.original_fq_level) self.access_logger.disabled = self.original_disabled def test_configure_logging_disables_access_logger_when_suppressed(self): @@ -23,3 +35,8 @@ def test_configure_logging_enables_access_logger_when_not_suppressed(self): self.access_logger.disabled = True configure_logging("INFO", suppress_access_logs=False) self.assertFalse(self.access_logger.disabled) + + def test_configure_logging_sets_fq_logger_level(self): + self.fq_logger.setLevel(logging.NOTSET) + configure_logging("WARNING", suppress_access_logs=False) + self.assertEqual(self.fq_logger.level, logging.WARNING)