We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Azure State backend - silence logs
### Feature scope

Configuration (settings parsing, validation, etc.)

### Description

Using Azure as a backend fills the meltano log with noise, multiple log lines for each http request, basically making the log unusable.

So far I have managed to silence these logs with an addition to `meltano/core/state_store/azure.py`

```
import logging

logger=logging.getLogger('azure.core.pipeline.policies.http_logging_policy')
logger.setLevel(logging.WARNING)
```

Without the suppression above, the logs are full of multiline messages like this, logs become virtually useless:

```
2023-05-31T08:29:59.892347Z [info     ] Request URL: 'https://***.blob.core.windows.net/meltano-state/dev%3Atap-freshdesk-to-target-postgres/lock'
Request method: 'HEAD'
Request headers:
    'x-ms-version': 'REDACTED'
    'Accept': 'application/xml'
    'User-Agent': 'azsdk-python-storage-blob/12.16.0 Python/3.9.16 (Linux-5.10.102.2-microsoft-standard-x86_64-with-glibc2.31)'
    'x-ms-date': 'REDACTED'
    'x-ms-client-request-id': '54f13080-ff8d-11ed-8616-00155da6d400'
    'Authorization': 'REDACTED'
No body was attached to the request
2023-05-31T08:29:59.901579Z [info     ] Response status: 404
Response headers:
    'Transfer-Encoding': 'chunked'
    'Server': 'Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0'
    'x-ms-request-id': 'fe209118-f01e-0044-229a-935122000000'
    'x-ms-client-request-id': '54f13080-ff8d-11ed-8616-00155da6d400'
    'x-ms-version': 'REDACTED'
    'x-ms-error-code': 'BlobNotFound'
    'Date': 'Wed, 31 May 2023 08:29:59 GMT'
2023-05-31T08:29:59.913150Z [info     ] Request URL: 'https://***.blob.core.windows.net/meltano-state/dev%3Atap-freshdesk-to-target-postgres/lock?comp=REDACTED&blockid=REDACTED'
Request method: 'PUT'
Request headers:
    'Content-Length': '17'
    'x-ms-version': 'REDACTED'
    'Content-Type': 'application/octet-stream'
    'Accept': 'application/xml'
    'User-Agent': 'azsdk-python-storage-blob/12.16.0 Python/3.9.16 (Linux-5.10.102.2-microsoft-standard-x86_64-with-glibc2.31)'
    'x-ms-date': 'REDACTED'
    'x-ms-client-request-id': '54f45b48-ff8d-11ed-8616-00155da6d400'
    'Authorization': 'REDACTED'
A body is sent with the request
```

Would you consider a PR to suppress the INFO logs from Azure Blob Client?


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `src/meltano/core/logging/utils.py`
Content:
```
1 """Various utilities for configuring logging in a meltano project."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 import logging
7 import os
8 import sys
9 from logging import config as logging_config
10 
11 import structlog
12 import yaml
13 
14 from meltano.core.logging.formatters import (
15     LEVELED_TIMESTAMPED_PRE_CHAIN,
16     TIMESTAMPER,
17     rich_exception_formatter_factory,
18 )
19 from meltano.core.project import Project
20 from meltano.core.utils import get_no_color_flag
21 
22 if sys.version_info >= (3, 8):
23     from typing import Protocol
24 else:
25     from typing_extensions import Protocol
26 
27 
28 LEVELS = {  # noqa: WPS407
29     "debug": logging.DEBUG,
30     "info": logging.INFO,
31     "warning": logging.WARNING,
32     "error": logging.ERROR,
33     "critical": logging.CRITICAL,
34 }
35 DEFAULT_LEVEL = "info"
36 FORMAT = "[%(asctime)s] [%(process)d|%(threadName)10s|%(name)s] [%(levelname)s] %(message)s"  # noqa: WPS323, E501
37 
38 
39 def parse_log_level(log_level: dict[str, int]) -> int:
40     """Parse a level descriptor into an logging level.
41 
42     Args:
43         log_level: level descriptor.
44 
45     Returns:
46         int: actual logging level.
47     """
48     return LEVELS.get(log_level, LEVELS[DEFAULT_LEVEL])
49 
50 
51 def read_config(config_file: str | None = None) -> dict:
52     """Read a logging config yaml from disk.
53 
54     Args:
55         config_file: path to the config file to read.
56 
57     Returns:
58         dict: parsed yaml config
59     """
60     if config_file and os.path.exists(config_file):
61         with open(config_file) as cf:
62             return yaml.safe_load(cf.read())
63     else:
64         return None
65 
66 
67 def default_config(log_level: str) -> dict:
68     """Generate a default logging config.
69 
70     Args:
71         log_level: set log levels to provided level.
72 
73     Returns:
74          A logging config suitable for use with `logging.config.dictConfig`.
75     """
76     no_color = get_no_color_flag()
77 
78     if no_color:
79         formatter = rich_exception_formatter_factory(no_color=True)
80     else:
81         formatter = rich_exception_formatter_factory(color_system="truecolor")
82 
83     return {
84         "version": 1,
85         "disable_existing_loggers": False,
86         "formatters": {
87             "colored": {
88                 "()": structlog.stdlib.ProcessorFormatter,
89                 "processor": structlog.dev.ConsoleRenderer(
90                     colors=not no_color,
91                     exception_formatter=formatter,
92                 ),
93                 "foreign_pre_chain": LEVELED_TIMESTAMPED_PRE_CHAIN,
94             },
95         },
96         "handlers": {
97             "console": {
98                 "class": "logging.StreamHandler",
99                 "level": log_level.upper(),
100                 "formatter": "colored",
101                 "stream": "ext://sys.stderr",
102             },
103         },
104         "loggers": {
105             "": {
106                 "handlers": ["console"],
107                 "level": log_level.upper(),
108                 "propagate": True,
109             },
110             "snowplow_tracker.emitters": {
111                 "handlers": ["console"],
112                 "level": logging.ERROR,
113             },
114             "urllib3": {
115                 "handlers": ["console"],
116                 "level": logging.INFO,
117                 "propagate": False,
118             },
119             "asyncio": {
120                 "handlers": ["console"],
121                 "level": logging.INFO,
122                 "propagate": False,
123             },
124         },
125     }
126 
127 
128 def setup_logging(  # noqa: WPS210
129     project: Project | None = None,
130     log_level: str = DEFAULT_LEVEL,
131     log_config: dict | None = None,
132 ) -> None:
133     """Configure logging for a meltano project.
134 
135     Args:
136         project: the meltano project
137         log_level: set log levels to provided level.
138         log_config: a logging config suitable for use with `logging.config.dictConfig`.
139     """
140     # Mimick Python 3.8's `force=True` kwarg to override any
141     # existing logger handlers
142     # See https://github.com/python/cpython/commit/cf67d6a934b51b1f97e72945b596477b271f70b8
143     root = logging.getLogger()
144     for handler in root.handlers[:]:
145         root.removeHandler(handler)
146         handler.close()
147 
148     log_level = log_level.upper()
149 
150     if project:
151         log_config = log_config or project.settings.get("cli.log_config")
152         log_level = project.settings.get("cli.log_level")
153 
154     config = read_config(log_config) or default_config(log_level)
155     logging_config.dictConfig(config)
156     structlog.configure(
157         processors=[
158             structlog.stdlib.add_log_level,
159             structlog.stdlib.PositionalArgumentsFormatter(),
160             TIMESTAMPER,
161             structlog.processors.StackInfoRenderer(),
162             structlog.processors.format_exc_info,
163             structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
164         ],
165         logger_factory=structlog.stdlib.LoggerFactory(),
166         wrapper_class=structlog.stdlib.BoundLogger,
167         cache_logger_on_first_use=True,
168     )
169 
170 
171 def change_console_log_level(log_level: int = logging.DEBUG) -> None:
172     """Change the log level for the root logger, but only on the 'console' handler.
173 
174     Most useful when you want change the log level on the fly for console
175     output, but want to respect other aspects of any potential `logging.yaml`
176     sourced configs. Note that if a `logging.yaml` config without a 'console'
177     handler is used, this will not override the log level.
178 
179     Args:
180         log_level: set log levels to provided level.
181     """
182     root_logger = logging.getLogger()
183     root_logger.setLevel(log_level)
184     for handler in root_logger.handlers:
185         if handler.name == "console":
186             handler.setLevel(log_level)
187 
188 
189 class SubprocessOutputWriter(Protocol):
190     """A basic interface suitable for use with `capture_subprocess_output`."""
191 
192     def writelines(self, lines: str):
193         """Write the provided lines to an output.
194 
195         Args:
196             lines: String to write
197         """
198 
199 
200 async def _write_line_writer(writer, line):
201     # StreamWriters like a subprocess's stdin need special consideration
202     if isinstance(writer, asyncio.StreamWriter):
203         try:
204             writer.write(line)
205             await writer.drain()
206         except (BrokenPipeError, ConnectionResetError):
207             await writer.wait_closed()
208             return False
209     else:
210         writer.writeline(line.decode())
211 
212     return True
213 
214 
215 async def capture_subprocess_output(
216     reader: asyncio.StreamReader | None,
217     *line_writers: SubprocessOutputWriter,
218 ) -> None:
219     """Capture in real time the output stream of a suprocess that is run async.
220 
221     The stream has been set to asyncio.subprocess.PIPE and is provided using
222     reader to this function.
223 
224     As new lines are captured for reader, they are written to output_stream.
225     This async function should be run with await asyncio.wait() while waiting
226     for the subprocess to end.
227 
228     Args:
229         reader: `asyncio.StreamReader` object that is the output stream of the
230             subprocess.
231         line_writers: A `StreamWriter`, or object has a compatible writelines method.
232     """
233     while not reader.at_eof():
234         line = await reader.readline()
235         if not line:
236             continue
237 
238         for writer in line_writers:
239             if not await _write_line_writer(writer, line):
240                 # If the destination stream is closed, we can stop capturing output.
241                 return
242
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/src/meltano/core/logging/utils.py b/src/meltano/core/logging/utils.py
--- a/src/meltano/core/logging/utils.py
+++ b/src/meltano/core/logging/utils.py
@@ -108,18 +108,18 @@
                 "propagate": True,
             },
             "snowplow_tracker.emitters": {
-                "handlers": ["console"],
                 "level": logging.ERROR,
             },
             "urllib3": {
-                "handlers": ["console"],
                 "level": logging.INFO,
-                "propagate": False,
             },
             "asyncio": {
-                "handlers": ["console"],
                 "level": logging.INFO,
-                "propagate": False,
+            },
+            # Azure HTTP logs at info level are too noisy; see
+            # https://github.com/meltano/meltano/issues/7723
+            "azure.core.pipeline.policies.http_logging_policy": {
+                "level": logging.WARNING,
             },
         },
     }

{"golden_diff": "diff --git a/src/meltano/core/logging/utils.py b/src/meltano/core/logging/utils.py\n--- a/src/meltano/core/logging/utils.py\n+++ b/src/meltano/core/logging/utils.py\n@@ -108,18 +108,18 @@\n                 \"propagate\": True,\n             },\n             \"snowplow_tracker.emitters\": {\n-                \"handlers\": [\"console\"],\n                 \"level\": logging.ERROR,\n             },\n             \"urllib3\": {\n-                \"handlers\": [\"console\"],\n                 \"level\": logging.INFO,\n-                \"propagate\": False,\n             },\n             \"asyncio\": {\n-                \"handlers\": [\"console\"],\n                 \"level\": logging.INFO,\n-                \"propagate\": False,\n+            },\n+            # Azure HTTP logs at info level are too noisy; see\n+            # https://github.com/meltano/meltano/issues/7723\n+            \"azure.core.pipeline.policies.http_logging_policy\": {\n+                \"level\": logging.WARNING,\n             },\n         },\n     }\n", "issue": "Azure State backend - silence logs\n### Feature scope\n\nConfiguration (settings parsing, validation, etc.)\n\n### Description\n\nUsing Azure as a backend fills the meltano log with noise, multiple log lines for each http request, basically making the log unusable.\r\n\r\nSo far I have managed to silence these logs with an addition to `meltano/core/state_store/azure.py`\r\n\r\n```\r\nimport logging\r\n\r\nlogger=logging.getLogger('azure.core.pipeline.policies.http_logging_policy')\r\nlogger.setLevel(logging.WARNING)\r\n```\r\n\r\nWithout the suppression above, the logs are full of multiline messages like this, logs become virtually useless:\r\n\r\n```\r\n2023-05-31T08:29:59.892347Z [info     ] Request URL: 'https://***.blob.core.windows.net/meltano-state/dev%3Atap-freshdesk-to-target-postgres/lock'\r\nRequest method: 'HEAD'\r\nRequest headers:\r\n    'x-ms-version': 'REDACTED'\r\n    'Accept': 'application/xml'\r\n    'User-Agent': 'azsdk-python-storage-blob/12.16.0 Python/3.9.16 (Linux-5.10.102.2-microsoft-standard-x86_64-with-glibc2.31)'\r\n    'x-ms-date': 'REDACTED'\r\n    'x-ms-client-request-id': '54f13080-ff8d-11ed-8616-00155da6d400'\r\n    'Authorization': 'REDACTED'\r\nNo body was attached to the request\r\n2023-05-31T08:29:59.901579Z [info     ] Response status: 404\r\nResponse headers:\r\n    'Transfer-Encoding': 'chunked'\r\n    'Server': 'Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0'\r\n    'x-ms-request-id': 'fe209118-f01e-0044-229a-935122000000'\r\n    'x-ms-client-request-id': '54f13080-ff8d-11ed-8616-00155da6d400'\r\n    'x-ms-version': 'REDACTED'\r\n    'x-ms-error-code': 'BlobNotFound'\r\n    'Date': 'Wed, 31 May 2023 08:29:59 GMT'\r\n2023-05-31T08:29:59.913150Z [info     ] Request URL: 'https://***.blob.core.windows.net/meltano-state/dev%3Atap-freshdesk-to-target-postgres/lock?comp=REDACTED&blockid=REDACTED'\r\nRequest method: 'PUT'\r\nRequest headers:\r\n    'Content-Length': '17'\r\n    'x-ms-version': 'REDACTED'\r\n    'Content-Type': 'application/octet-stream'\r\n    'Accept': 'application/xml'\r\n    'User-Agent': 'azsdk-python-storage-blob/12.16.0 Python/3.9.16 (Linux-5.10.102.2-microsoft-standard-x86_64-with-glibc2.31)'\r\n    'x-ms-date': 'REDACTED'\r\n    'x-ms-client-request-id': '54f45b48-ff8d-11ed-8616-00155da6d400'\r\n    'Authorization': 'REDACTED'\r\nA body is sent with the request\r\n```\r\n\r\nWould you consider a PR to suppress the INFO logs from Azure Blob Client?\r\n\n", "before_files": [{"content": "\"\"\"Various utilities for configuring logging in a meltano project.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport logging\nimport os\nimport sys\nfrom logging import config as logging_config\n\nimport structlog\nimport yaml\n\nfrom meltano.core.logging.formatters import (\n    LEVELED_TIMESTAMPED_PRE_CHAIN,\n    TIMESTAMPER,\n    rich_exception_formatter_factory,\n)\nfrom meltano.core.project import Project\nfrom meltano.core.utils import get_no_color_flag\n\nif sys.version_info >= (3, 8):\n    from typing import Protocol\nelse:\n    from typing_extensions import Protocol\n\n\nLEVELS = {  # noqa: WPS407\n    \"debug\": logging.DEBUG,\n    \"info\": logging.INFO,\n    \"warning\": logging.WARNING,\n    \"error\": logging.ERROR,\n    \"critical\": logging.CRITICAL,\n}\nDEFAULT_LEVEL = \"info\"\nFORMAT = \"[%(asctime)s] [%(process)d|%(threadName)10s|%(name)s] [%(levelname)s] %(message)s\"  # noqa: WPS323, E501\n\n\ndef parse_log_level(log_level: dict[str, int]) -> int:\n    \"\"\"Parse a level descriptor into an logging level.\n\n    Args:\n        log_level: level descriptor.\n\n    Returns:\n        int: actual logging level.\n    \"\"\"\n    return LEVELS.get(log_level, LEVELS[DEFAULT_LEVEL])\n\n\ndef read_config(config_file: str | None = None) -> dict:\n    \"\"\"Read a logging config yaml from disk.\n\n    Args:\n        config_file: path to the config file to read.\n\n    Returns:\n        dict: parsed yaml config\n    \"\"\"\n    if config_file and os.path.exists(config_file):\n        with open(config_file) as cf:\n            return yaml.safe_load(cf.read())\n    else:\n        return None\n\n\ndef default_config(log_level: str) -> dict:\n    \"\"\"Generate a default logging config.\n\n    Args:\n        log_level: set log levels to provided level.\n\n    Returns:\n         A logging config suitable for use with `logging.config.dictConfig`.\n    \"\"\"\n    no_color = get_no_color_flag()\n\n    if no_color:\n        formatter = rich_exception_formatter_factory(no_color=True)\n    else:\n        formatter = rich_exception_formatter_factory(color_system=\"truecolor\")\n\n    return {\n        \"version\": 1,\n        \"disable_existing_loggers\": False,\n        \"formatters\": {\n            \"colored\": {\n                \"()\": structlog.stdlib.ProcessorFormatter,\n                \"processor\": structlog.dev.ConsoleRenderer(\n                    colors=not no_color,\n                    exception_formatter=formatter,\n                ),\n                \"foreign_pre_chain\": LEVELED_TIMESTAMPED_PRE_CHAIN,\n            },\n        },\n        \"handlers\": {\n            \"console\": {\n                \"class\": \"logging.StreamHandler\",\n                \"level\": log_level.upper(),\n                \"formatter\": \"colored\",\n                \"stream\": \"ext://sys.stderr\",\n            },\n        },\n        \"loggers\": {\n            \"\": {\n                \"handlers\": [\"console\"],\n                \"level\": log_level.upper(),\n                \"propagate\": True,\n            },\n            \"snowplow_tracker.emitters\": {\n                \"handlers\": [\"console\"],\n                \"level\": logging.ERROR,\n            },\n            \"urllib3\": {\n                \"handlers\": [\"console\"],\n                \"level\": logging.INFO,\n                \"propagate\": False,\n            },\n            \"asyncio\": {\n                \"handlers\": [\"console\"],\n                \"level\": logging.INFO,\n                \"propagate\": False,\n            },\n        },\n    }\n\n\ndef setup_logging(  # noqa: WPS210\n    project: Project | None = None,\n    log_level: str = DEFAULT_LEVEL,\n    log_config: dict | None = None,\n) -> None:\n    \"\"\"Configure logging for a meltano project.\n\n    Args:\n        project: the meltano project\n        log_level: set log levels to provided level.\n        log_config: a logging config suitable for use with `logging.config.dictConfig`.\n    \"\"\"\n    # Mimick Python 3.8's `force=True` kwarg to override any\n    # existing logger handlers\n    # See https://github.com/python/cpython/commit/cf67d6a934b51b1f97e72945b596477b271f70b8\n    root = logging.getLogger()\n    for handler in root.handlers[:]:\n        root.removeHandler(handler)\n        handler.close()\n\n    log_level = log_level.upper()\n\n    if project:\n        log_config = log_config or project.settings.get(\"cli.log_config\")\n        log_level = project.settings.get(\"cli.log_level\")\n\n    config = read_config(log_config) or default_config(log_level)\n    logging_config.dictConfig(config)\n    structlog.configure(\n        processors=[\n            structlog.stdlib.add_log_level,\n            structlog.stdlib.PositionalArgumentsFormatter(),\n            TIMESTAMPER,\n            structlog.processors.StackInfoRenderer(),\n            structlog.processors.format_exc_info,\n            structlog.stdlib.ProcessorFormatter.wrap_for_formatter,\n        ],\n        logger_factory=structlog.stdlib.LoggerFactory(),\n        wrapper_class=structlog.stdlib.BoundLogger,\n        cache_logger_on_first_use=True,\n    )\n\n\ndef change_console_log_level(log_level: int = logging.DEBUG) -> None:\n    \"\"\"Change the log level for the root logger, but only on the 'console' handler.\n\n    Most useful when you want change the log level on the fly for console\n    output, but want to respect other aspects of any potential `logging.yaml`\n    sourced configs. Note that if a `logging.yaml` config without a 'console'\n    handler is used, this will not override the log level.\n\n    Args:\n        log_level: set log levels to provided level.\n    \"\"\"\n    root_logger = logging.getLogger()\n    root_logger.setLevel(log_level)\n    for handler in root_logger.handlers:\n        if handler.name == \"console\":\n            handler.setLevel(log_level)\n\n\nclass SubprocessOutputWriter(Protocol):\n    \"\"\"A basic interface suitable for use with `capture_subprocess_output`.\"\"\"\n\n    def writelines(self, lines: str):\n        \"\"\"Write the provided lines to an output.\n\n        Args:\n            lines: String to write\n        \"\"\"\n\n\nasync def _write_line_writer(writer, line):\n    # StreamWriters like a subprocess's stdin need special consideration\n    if isinstance(writer, asyncio.StreamWriter):\n        try:\n            writer.write(line)\n            await writer.drain()\n        except (BrokenPipeError, ConnectionResetError):\n            await writer.wait_closed()\n            return False\n    else:\n        writer.writeline(line.decode())\n\n    return True\n\n\nasync def capture_subprocess_output(\n    reader: asyncio.StreamReader | None,\n    *line_writers: SubprocessOutputWriter,\n) -> None:\n    \"\"\"Capture in real time the output stream of a suprocess that is run async.\n\n    The stream has been set to asyncio.subprocess.PIPE and is provided using\n    reader to this function.\n\n    As new lines are captured for reader, they are written to output_stream.\n    This async function should be run with await asyncio.wait() while waiting\n    for the subprocess to end.\n\n    Args:\n        reader: `asyncio.StreamReader` object that is the output stream of the\n            subprocess.\n        line_writers: A `StreamWriter`, or object has a compatible writelines method.\n    \"\"\"\n    while not reader.at_eof():\n        line = await reader.readline()\n        if not line:\n            continue\n\n        for writer in line_writers:\n            if not await _write_line_writer(writer, line):\n                # If the destination stream is closed, we can stop capturing output.\n                return\n", "path": "src/meltano/core/logging/utils.py"}], "after_files": [{"content": "\"\"\"Various utilities for configuring logging in a meltano project.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport logging\nimport os\nimport sys\nfrom logging import config as logging_config\n\nimport structlog\nimport yaml\n\nfrom meltano.core.logging.formatters import (\n    LEVELED_TIMESTAMPED_PRE_CHAIN,\n    TIMESTAMPER,\n    rich_exception_formatter_factory,\n)\nfrom meltano.core.project import Project\nfrom meltano.core.utils import get_no_color_flag\n\nif sys.version_info >= (3, 8):\n    from typing import Protocol\nelse:\n    from typing_extensions import Protocol\n\n\nLEVELS = {  # noqa: WPS407\n    \"debug\": logging.DEBUG,\n    \"info\": logging.INFO,\n    \"warning\": logging.WARNING,\n    \"error\": logging.ERROR,\n    \"critical\": logging.CRITICAL,\n}\nDEFAULT_LEVEL = \"info\"\nFORMAT = \"[%(asctime)s] [%(process)d|%(threadName)10s|%(name)s] [%(levelname)s] %(message)s\"  # noqa: WPS323, E501\n\n\ndef parse_log_level(log_level: dict[str, int]) -> int:\n    \"\"\"Parse a level descriptor into an logging level.\n\n    Args:\n        log_level: level descriptor.\n\n    Returns:\n        int: actual logging level.\n    \"\"\"\n    return LEVELS.get(log_level, LEVELS[DEFAULT_LEVEL])\n\n\ndef read_config(config_file: str | None = None) -> dict:\n    \"\"\"Read a logging config yaml from disk.\n\n    Args:\n        config_file: path to the config file to read.\n\n    Returns:\n        dict: parsed yaml config\n    \"\"\"\n    if config_file and os.path.exists(config_file):\n        with open(config_file) as cf:\n            return yaml.safe_load(cf.read())\n    else:\n        return None\n\n\ndef default_config(log_level: str) -> dict:\n    \"\"\"Generate a default logging config.\n\n    Args:\n        log_level: set log levels to provided level.\n\n    Returns:\n         A logging config suitable for use with `logging.config.dictConfig`.\n    \"\"\"\n    no_color = get_no_color_flag()\n\n    if no_color:\n        formatter = rich_exception_formatter_factory(no_color=True)\n    else:\n        formatter = rich_exception_formatter_factory(color_system=\"truecolor\")\n\n    return {\n        \"version\": 1,\n        \"disable_existing_loggers\": False,\n        \"formatters\": {\n            \"colored\": {\n                \"()\": structlog.stdlib.ProcessorFormatter,\n                \"processor\": structlog.dev.ConsoleRenderer(\n                    colors=not no_color,\n                    exception_formatter=formatter,\n                ),\n                \"foreign_pre_chain\": LEVELED_TIMESTAMPED_PRE_CHAIN,\n            },\n        },\n        \"handlers\": {\n            \"console\": {\n                \"class\": \"logging.StreamHandler\",\n                \"level\": log_level.upper(),\n                \"formatter\": \"colored\",\n                \"stream\": \"ext://sys.stderr\",\n            },\n        },\n        \"loggers\": {\n            \"\": {\n                \"handlers\": [\"console\"],\n                \"level\": log_level.upper(),\n                \"propagate\": True,\n            },\n            \"snowplow_tracker.emitters\": {\n                \"level\": logging.ERROR,\n            },\n            \"urllib3\": {\n                \"level\": logging.INFO,\n            },\n            \"asyncio\": {\n                \"level\": logging.INFO,\n            },\n            # Azure HTTP logs at info level are too noisy; see\n            # https://github.com/meltano/meltano/issues/7723\n            \"azure.core.pipeline.policies.http_logging_policy\": {\n                \"level\": logging.WARNING,\n            },\n        },\n    }\n\n\ndef setup_logging(  # noqa: WPS210\n    project: Project | None = None,\n    log_level: str = DEFAULT_LEVEL,\n    log_config: dict | None = None,\n) -> None:\n    \"\"\"Configure logging for a meltano project.\n\n    Args:\n        project: the meltano project\n        log_level: set log levels to provided level.\n        log_config: a logging config suitable for use with `logging.config.dictConfig`.\n    \"\"\"\n    # Mimick Python 3.8's `force=True` kwarg to override any\n    # existing logger handlers\n    # See https://github.com/python/cpython/commit/cf67d6a934b51b1f97e72945b596477b271f70b8\n    root = logging.getLogger()\n    for handler in root.handlers[:]:\n        root.removeHandler(handler)\n        handler.close()\n\n    log_level = log_level.upper()\n\n    if project:\n        log_config = log_config or project.settings.get(\"cli.log_config\")\n        log_level = project.settings.get(\"cli.log_level\")\n\n    config = read_config(log_config) or default_config(log_level)\n    logging_config.dictConfig(config)\n    structlog.configure(\n        processors=[\n            structlog.stdlib.add_log_level,\n            structlog.stdlib.PositionalArgumentsFormatter(),\n            TIMESTAMPER,\n            structlog.processors.StackInfoRenderer(),\n            structlog.processors.format_exc_info,\n            structlog.stdlib.ProcessorFormatter.wrap_for_formatter,\n        ],\n        logger_factory=structlog.stdlib.LoggerFactory(),\n        wrapper_class=structlog.stdlib.BoundLogger,\n        cache_logger_on_first_use=True,\n    )\n\n\ndef change_console_log_level(log_level: int = logging.DEBUG) -> None:\n    \"\"\"Change the log level for the root logger, but only on the 'console' handler.\n\n    Most useful when you want change the log level on the fly for console\n    output, but want to respect other aspects of any potential `logging.yaml`\n    sourced configs. Note that if a `logging.yaml` config without a 'console'\n    handler is used, this will not override the log level.\n\n    Args:\n        log_level: set log levels to provided level.\n    \"\"\"\n    root_logger = logging.getLogger()\n    root_logger.setLevel(log_level)\n    for handler in root_logger.handlers:\n        if handler.name == \"console\":\n            handler.setLevel(log_level)\n\n\nclass SubprocessOutputWriter(Protocol):\n    \"\"\"A basic interface suitable for use with `capture_subprocess_output`.\"\"\"\n\n    def writelines(self, lines: str):\n        \"\"\"Write the provided lines to an output.\n\n        Args:\n            lines: String to write\n        \"\"\"\n\n\nasync def _write_line_writer(writer, line):\n    # StreamWriters like a subprocess's stdin need special consideration\n    if isinstance(writer, asyncio.StreamWriter):\n        try:\n            writer.write(line)\n            await writer.drain()\n        except (BrokenPipeError, ConnectionResetError):\n            await writer.wait_closed()\n            return False\n    else:\n        writer.writeline(line.decode())\n\n    return True\n\n\nasync def capture_subprocess_output(\n    reader: asyncio.StreamReader | None,\n    *line_writers: SubprocessOutputWriter,\n) -> None:\n    \"\"\"Capture in real time the output stream of a suprocess that is run async.\n\n    The stream has been set to asyncio.subprocess.PIPE and is provided using\n    reader to this function.\n\n    As new lines are captured for reader, they are written to output_stream.\n    This async function should be run with await asyncio.wait() while waiting\n    for the subprocess to end.\n\n    Args:\n        reader: `asyncio.StreamReader` object that is the output stream of the\n            subprocess.\n        line_writers: A `StreamWriter`, or object has a compatible writelines method.\n    \"\"\"\n    while not reader.at_eof():\n        line = await reader.readline()\n        if not line:\n            continue\n\n        for writer in line_writers:\n            if not await _write_line_writer(writer, line):\n                # If the destination stream is closed, we can stop capturing output.\n                return\n", "path": "src/meltano/core/logging/utils.py"}]}