Repository: litestar-org/litestar

Issue:
Enhancement: Support dictConfig in picologging
`picologging` will implement a `dictConfig` with the following PR: https://github.com/microsoft/picologging/issues/53

We should enhance our integration to call this method once it's officially released.



Discussion:
The picologging [PR](https://github.com/microsoft/picologging/pull/61) is closed. 

Resolving patch:
diff --git a/starlite/logging/__init__.py b/starlite/logging/__init__.py
--- a/starlite/logging/__init__.py
+++ b/starlite/logging/__init__.py
@@ -1,11 +1,16 @@
 from logging import config
-from typing import Any, Dict, List, Optional, Union
+from typing import Any, Dict, Generator, Iterable, List, Optional, Union
 
 from pydantic import BaseModel
 from typing_extensions import Literal
 
 from starlite.logging.standard import QueueListenerHandler
 
+try:
+    from picologging import config as picologging_config
+except ImportError:
+    picologging_config = None
+
 __all__ = ["LoggingConfig", "QueueListenerHandler"]
 
 
@@ -49,5 +54,33 @@ class LoggingConfig(BaseModel):
     except that the propagate setting will not be applicable."""
 
     def configure(self) -> None:
-        """Configured logger with the given configuration."""
+        """Configured logger with the given configuration.
+
+        If the logger class contains the word `picologging`, we try to
+        import and set the dictConfig
+        """
+        for logging_class in find_keys(self.handlers, "class"):
+            if "picologging" in logging_class and picologging_config:
+                picologging_config.dictConfig(self.dict(exclude_none=True))
+                break
         config.dictConfig(self.dict(exclude_none=True))
+
+
+def find_keys(node: Union[List, Dict], key: str) -> Generator[Iterable, None, None]:
+    """Find Nested Keys with name
+    Search a dictionary for the presence of key
+    Args:
+        node (Union[List, Dict]): a dictionary to search
+        key (str): the dictionary key to find
+
+    Yields:
+        Generator[Iterable, None, None]: Value of dictionary key
+    """
+    if isinstance(node, list):
+        for list_entry in node:
+            yield from find_keys(list_entry, key)
+    elif isinstance(node, dict):
+        if key in node:
+            yield node[key]
+        for dict_entry in node.values():
+            yield from find_keys(dict_entry, key)
diff --git a/starlite/logging/picologging.py b/starlite/logging/picologging.py
--- a/starlite/logging/picologging.py
+++ b/starlite/logging/picologging.py
@@ -18,7 +18,7 @@ def __init__(self, handlers: List[Any], respect_handler_level: bool = False, que
 
         Args:
             handlers (list): list of handler names.
-            respect_handler_level (bool): A handler’s level is respected (compared with the level for the message) when
+            respect_handler_level (bool): A handler's level is respected (compared with the level for the message) when
                 deciding whether to pass messages to that handler.
         """
         super().__init__(queue)
diff --git a/starlite/logging/standard.py b/starlite/logging/standard.py
--- a/starlite/logging/standard.py
+++ b/starlite/logging/standard.py
@@ -13,7 +13,7 @@ def __init__(self, handlers: List[Any], respect_handler_level: bool = False, que
 
         Args:
             handlers (list): list of handler names.
-            respect_handler_level (bool): A handler’s level is respected (compared with the level for the message) when
+            respect_handler_level (bool): A handler's level is respected (compared with the level for the message) when
                 deciding whether to pass messages to that handler.
         """
         super().__init__(queue)


Test patch:
diff --git a/tests/logging_config/test_picologging.py b/tests/logging_config/test_picologging.py
--- a/tests/logging_config/test_picologging.py
+++ b/tests/logging_config/test_picologging.py
@@ -32,6 +32,26 @@ def test_logging_debug(dict_config_mock: Mock) -> None:
     dict_config_mock.reset_mock()
 
 
+@patch("picologging.config.dictConfig")
+def test_picologging_dictconfig_debug(dict_config_mock: Mock) -> None:
+    log_config = LoggingConfig(
+        handlers={
+            "console": {
+                "class": "picologging.StreamHandler",
+                "level": "DEBUG",
+                "formatter": "standard",
+            },
+            "queue_listener": {
+                "class": "starlite.logging.picologging.QueueListenerHandler",
+                "handlers": ["cfg://handlers.console"],
+            },
+        }
+    )
+    log_config.configure()
+    assert dict_config_mock.mock_calls[0][1][0]["loggers"]["starlite"]["level"] == "INFO"
+    dict_config_mock.reset_mock()
+
+
 @patch("logging.config.dictConfig")
 def test_logging_startup(dict_config_mock: Mock) -> None:
     test_logger = LoggingConfig(
@@ -52,7 +72,37 @@ def test_logging_startup(dict_config_mock: Mock) -> None:
         assert dict_config_mock.called
 
 
+@patch("picologging.config.dictConfig")
+def test_picologging_dictconfig_startup(dict_config_mock: Mock) -> None:
+    test_logger = LoggingConfig(
+        handlers={
+            "console": {
+                "class": "picologging.StreamHandler",
+                "level": "DEBUG",
+                "formatter": "standard",
+            },
+            "queue_listener": {
+                "class": "starlite.logging.picologging.QueueListenerHandler",
+                "handlers": ["cfg://handlers.console"],
+            },
+        },
+        loggers={"app": {"level": "INFO", "handlers": ["console"]}},
+    )
+    with create_test_client([], on_startup=[test_logger.configure]):
+        assert dict_config_mock.called
+
+
+@patch("picologging.config.dictConfig")
+def test_picologging_dictconfig_when_disabled(dict_config_mock: Mock) -> None:
+    test_logger = LoggingConfig(
+        loggers={"app": {"level": "INFO", "handlers": ["console"]}},
+    )
+    with create_test_client([], on_startup=[test_logger.configure]):
+        assert not dict_config_mock.called
+
+
 logger = logging.getLogger()
+
 config = LoggingConfig(
     handlers={
         "console": {
@@ -67,6 +117,7 @@ def test_logging_startup(dict_config_mock: Mock) -> None:
     }
 )
 config.configure()
+picologger = picologging.getLogger()
 
 
 def test_queue_logger(caplog: "LogCaptureFixture") -> None:
@@ -101,4 +152,8 @@ def test_logger_startup(caplog: "LogCaptureFixture") -> None:
         client.options("/")
         test_logger = logging.getLogger()
         handlers = test_logger.handlers
+
+        test_picologging_logger = picologging.getLogger()
+        test_picologging_handlers = test_picologging_logger.handlers
         assert isinstance(handlers[0].handlers[0], picologging.StreamHandler)  # type: ignore
+        assert isinstance(test_picologging_handlers[0].handlers[0], picologging.StreamHandler)