From b3313ebc8c760d514f9bdf7680ae3e5848cb14d8 Mon Sep 17 00:00:00 2001
From: "Antoine R. Dumont (@ardumont)" <antoine.romain.dumont@gmail.com>
Date: Fri, 20 Jan 2023 12:10:02 +0100
Subject: [PATCH] Add new module to allow common logging configuration from
 swh.core

This module only depends on core python library.

That will allow to simplify deployment logging configuration per infrastructure.

Refs. swh/infra/sysadm-environment#4524
---
 swh/core/cli/__init__.py                | 29 ++----------
 swh/core/logging.py                     | 55 +++++++++++++++++++++++
 swh/core/tests/data/logging-config.yaml | 35 +++++++++++++++
 swh/core/tests/test_logging.py          | 59 +++++++++++++++++++++++++
 4 files changed, 153 insertions(+), 25 deletions(-)
 create mode 100644 swh/core/logging.py
 create mode 100644 swh/core/tests/data/logging-config.yaml
 create mode 100644 swh/core/tests/test_logging.py

diff --git a/swh/core/cli/__init__.py b/swh/core/cli/__init__.py
index c354f4d6..d9cf5e86 100644
--- a/swh/core/cli/__init__.py
+++ b/swh/core/cli/__init__.py
@@ -1,16 +1,16 @@
-# Copyright (C) 2019  The Software Heritage developers
+# Copyright (C) 2019-2023  The Software Heritage developers
 # See the AUTHORS file at the top-level directory of this distribution
 # License: GNU General Public License version 3, or any later version
 # See top-level LICENSE file for more information
 
 import logging
-import logging.config
-from typing import Optional
 import warnings
 
 import click
 import pkg_resources
 
+from swh.core.logging import logging_configure
+
 LOG_LEVEL_NAMES = ["NOTSET", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
 
 CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
@@ -125,8 +125,6 @@ def swh(ctx, log_levels, log_config, sentry_dsn, sentry_debug):
     """Command line interface for Software Heritage."""
     import signal
 
-    import yaml
-
     from ..sentry import init_sentry
 
     signal.signal(signal.SIGTERM, clean_exit_on_signal)
@@ -134,26 +132,7 @@ def swh(ctx, log_levels, log_config, sentry_dsn, sentry_debug):
 
     init_sentry(sentry_dsn=sentry_dsn, debug=sentry_debug)
 
-    set_default_loglevel: Optional[str] = None
-
-    if log_config:
-        logging.config.dictConfig(yaml.safe_load(log_config.read()))
-        set_default_loglevel = logging.root.getEffectiveLevel()
-
-    if not log_levels:
-        log_levels = []
-
-    for module, log_level in log_levels:
-        logger = logging.getLogger(module)
-        log_level = logging.getLevelName(log_level)
-        logger.setLevel(log_level)
-
-        if module is None:
-            set_default_loglevel = log_level
-
-    if not set_default_loglevel:
-        logging.root.setLevel("INFO")
-        set_default_loglevel = "INFO"
+    set_default_loglevel = logging_configure(log_levels, log_config)
 
     ctx.ensure_object(dict)
     ctx.obj["log_level"] = set_default_loglevel
diff --git a/swh/core/logging.py b/swh/core/logging.py
new file mode 100644
index 00000000..334d958f
--- /dev/null
+++ b/swh/core/logging.py
@@ -0,0 +1,55 @@
+# Copyright (C) 2023  The Software Heritage developers
+# See the AUTHORS file at the top-level directory of this distribution
+# License: GNU General Public License version 3, or any later version
+# See top-level LICENSE file for more information
+
+"""Logging module providing common swh logging configuration. This only depends on
+python core library.
+
+"""
+
+import logging
+from logging.config import dictConfig
+from typing import Any, List, Optional, Tuple
+
+from yaml import safe_load
+
+
+def logging_configure(
+    log_levels: List[Tuple[str, str]] = [], log_config: Optional[Any] = None
+) -> str:
+    """A default configuration function to unify swh module logger configuration.
+
+    The log_config YAML file must conform to the logging.config.dictConfig schema
+    documented at https://docs.python.org/3/library/logging.config.html.
+
+    Returns:
+        The actual root logger log level name defined.
+
+    """
+    set_default_loglevel: Optional[str] = None
+
+    if log_config:
+        with open(log_config, "r") as f:
+            config_dict = safe_load(f.read())
+        # Configure logging using a dictionary config
+        dictConfig(config_dict)
+        effective_level = logging.root.getEffectiveLevel()
+        set_default_loglevel = logging.getLevelName(effective_level)
+
+    if not log_levels:
+        log_levels = []
+
+    for module, log_level in log_levels:
+        logger = logging.getLogger(module)
+        log_level = logging.getLevelName(log_level)
+        logger.setLevel(log_level)
+
+        if module is None:
+            set_default_loglevel = log_level
+
+    if not set_default_loglevel:
+        logging.root.setLevel("INFO")
+        set_default_loglevel = "INFO"
+
+    return set_default_loglevel
diff --git a/swh/core/tests/data/logging-config.yaml b/swh/core/tests/data/logging-config.yaml
new file mode 100644
index 00000000..20d2a308
--- /dev/null
+++ b/swh/core/tests/data/logging-config.yaml
@@ -0,0 +1,35 @@
+---
+version: 1
+
+handlers:
+  console:
+    class: logging.StreamHandler
+    formatter: task
+    stream: ext://sys.stdout
+  systemd:
+    class: swh.core.logger.JournalHandler
+    formatter: task
+
+formatters:
+  task:
+    (): celery.app.log.TaskFormatter
+    fmt: "[%(asctime)s: %(levelname)s/%(processName)s] %(task_name)s[%(task_id)s]: %(message)s"
+    use_color: false
+
+loggers:
+  celery:
+    level: INFO
+  amqp:
+    level: WARNING
+  urllib3:
+    level: WARNING
+  azure.core.pipeline.policies.http_logging_policy:
+    level: WARNING
+  swh: {}
+  celery.task: {}
+
+root:
+  level: DEBUG
+  handlers:
+  - console
+  - systemd
diff --git a/swh/core/tests/test_logging.py b/swh/core/tests/test_logging.py
new file mode 100644
index 00000000..4fcfc854
--- /dev/null
+++ b/swh/core/tests/test_logging.py
@@ -0,0 +1,59 @@
+# Copyright (C) 2023  The Software Heritage developers
+# See the AUTHORS file at the top-level directory of this distribution
+# License: GNU General Public License version 3, or any later version
+# See top-level LICENSE file for more information
+
+import logging
+import os
+
+from yaml import safe_load
+
+from swh.core.logging import logging_configure
+
+
+def test_logging_configure_default():
+    """Logging should be configured to INFO by default"""
+    root_log_level = logging_configure()
+
+    assert root_log_level == "INFO"
+
+
+def test_logging_configure_with_override():
+    """Logging module should be configured according to log_levels provided."""
+    log_levels = [
+        ("swh.core.tests", "DEBUG"),
+        ("swh.core", "CRITICAL"),
+        ("swh.loader.core.tests", "ERROR"),
+        ("swh.loader.core", "WARNING"),
+    ]
+
+    for module, log_level in log_levels:
+        logger = logging.getLogger(module)
+        assert logger.getEffectiveLevel() != logging.getLevelName(log_level)
+
+    # Set it up
+    root_log_level = logging_configure(log_levels)
+    assert root_log_level == "INFO"
+
+    for module, log_level in log_levels:
+        logger = logging.getLogger(module)
+        assert logger.getEffectiveLevel() == logging.getLevelName(log_level)
+
+
+def test_logging_configure_from_yaml(datadir):
+    """Logging should be configurable from yaml configuration file."""
+    logging_config = os.path.join(datadir, "logging-config.yaml")
+    root_log_level = logging_configure([], logging_config)
+
+    with open(logging_config, "r") as f:
+        config = safe_load(f.read())
+
+    for module, logger_config in config["loggers"].items():
+        if not logger_config:
+            continue
+        log_level = logger_config["level"]
+
+        logger = logging.getLogger(module)
+        assert logger.getEffectiveLevel() == logging.getLevelName(log_level)
+
+    assert root_log_level == config["root"]["level"]
-- 
GitLab