From 75012ea2aa1269de24aef6f384cc55ca9d57222e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgar=20Ram=C3=ADrez-Mondrag=C3=B3n?= Date: Thu, 9 May 2024 20:22:18 -0600 Subject: [PATCH 1/8] feat: Developers can now customize the default logging configuration for their taps/targets by overriding the `get_default_logging_config` method Related: * Closes https://github.com/meltano/sdk/issues/1373 --- singer_sdk/logger.py | 52 +++++++++++++++++++++++++++++++++++++++ singer_sdk/metrics.py | 47 ----------------------------------- singer_sdk/plugin_base.py | 13 +++++++++- 3 files changed, 64 insertions(+), 48 deletions(-) create mode 100644 singer_sdk/logger.py diff --git a/singer_sdk/logger.py b/singer_sdk/logger.py new file mode 100644 index 000000000..31a733f5c --- /dev/null +++ b/singer_sdk/logger.py @@ -0,0 +1,52 @@ +"""Logging configuration for the Singer SDK.""" + +from __future__ import annotations + +import logging +import os +import typing as t +from pathlib import Path + +import yaml + +from singer_sdk.metrics import METRICS_LOG_LEVEL_SETTING, METRICS_LOGGER_NAME + +if t.TYPE_CHECKING: + from singer_sdk.helpers._compat import Traversable + +__all__ = ["setup_logging"] + + +def load_yaml_logging_config(path: Traversable | Path) -> t.Any: # noqa: ANN401 + """Load the logging config from the YAML file. + + Args: + path: A path to the YAML file. + + Returns: + The logging config. + """ + with path.open() as f: + return yaml.safe_load(f) + + +def setup_logging( + config: t.Mapping[str, t.Any], + default_logging_config: dict[str, t.Any], +) -> None: + """Setup logging. + + Args: + default_logging_config: A default + :py:std:label:`Python logging configuration dictionary`. + config: A plugin configuration dictionary. + """ + logging.config.dictConfig(default_logging_config) + + config = config or {} + metrics_log_level = config.get(METRICS_LOG_LEVEL_SETTING, "INFO").upper() + logging.getLogger(METRICS_LOGGER_NAME).setLevel(metrics_log_level) + + if "SINGER_SDK_LOG_CONFIG" in os.environ: # pragma: no cover + log_config_path = Path(os.environ["SINGER_SDK_LOG_CONFIG"]) + logging.config.dictConfig(load_yaml_logging_config(log_config_path)) diff --git a/singer_sdk/metrics.py b/singer_sdk/metrics.py index 8a7efe51e..826035b60 100644 --- a/singer_sdk/metrics.py +++ b/singer_sdk/metrics.py @@ -7,21 +7,14 @@ import json import logging import logging.config -import os import typing as t from dataclasses import dataclass, field -from pathlib import Path from time import time -import yaml - -from singer_sdk.helpers._resources import get_package_files - if t.TYPE_CHECKING: from types import TracebackType from singer_sdk.helpers import types - from singer_sdk.helpers._compat import Traversable DEFAULT_LOG_INTERVAL = 60.0 @@ -379,43 +372,3 @@ def sync_timer(stream: str, **tags: t.Any) -> Timer: """ tags[Tag.STREAM] = stream return Timer(Metric.SYNC_DURATION, tags) - - -def _load_yaml_logging_config(path: Traversable | Path) -> t.Any: # noqa: ANN401 - """Load the logging config from the YAML file. - - Args: - path: A path to the YAML file. - - Returns: - The logging config. - """ - with path.open() as f: - return yaml.safe_load(f) - - -def _get_default_config() -> t.Any: # noqa: ANN401 - """Get a logging configuration. - - Returns: - A logging configuration. - """ - log_config_path = get_package_files("singer_sdk").joinpath("default_logging.yml") - return _load_yaml_logging_config(log_config_path) - - -def _setup_logging(config: t.Mapping[str, t.Any]) -> None: - """Setup logging. - - Args: - config: A plugin configuration dictionary. - """ - logging.config.dictConfig(_get_default_config()) - - config = config or {} - metrics_log_level = config.get(METRICS_LOG_LEVEL_SETTING, "INFO").upper() - logging.getLogger(METRICS_LOGGER_NAME).setLevel(metrics_log_level) - - if "SINGER_SDK_LOG_CONFIG" in os.environ: - log_config_path = Path(os.environ["SINGER_SDK_LOG_CONFIG"]) - logging.config.dictConfig(_load_yaml_logging_config(log_config_path)) diff --git a/singer_sdk/plugin_base.py b/singer_sdk/plugin_base.py index 1564558cf..39add257b 100644 --- a/singer_sdk/plugin_base.py +++ b/singer_sdk/plugin_base.py @@ -15,6 +15,7 @@ import click from jsonschema import Draft7Validator +import singer_sdk.logger as singer_logger from singer_sdk import about, metrics from singer_sdk.cli import plugin_cli from singer_sdk.configuration._dict_config import ( @@ -23,6 +24,7 @@ ) from singer_sdk.exceptions import ConfigValidationError from singer_sdk.helpers._classproperty import classproperty +from singer_sdk.helpers._resources import get_package_files from singer_sdk.helpers._secrets import SecretString, is_common_secret_key from singer_sdk.helpers._util import read_json_file from singer_sdk.helpers.capabilities import ( @@ -162,7 +164,7 @@ def __init__( if self._is_secret_config(k): config_dict[k] = SecretString(v) self._config = config_dict - metrics._setup_logging(self.config) # noqa: SLF001 + singer_logger.setup_logging(self.config, self.get_default_logging_config()) self.metrics_logger = metrics.get_metrics_logger() self._validate_config(raise_errors=validate_config) @@ -178,6 +180,15 @@ def setup_mapper(self) -> None: logger=self.logger, ) + def get_default_logging_config(self) -> t.Any: # noqa: ANN401, PLR6301 + """Get a default logging configuration for the plugin. + + Returns: + A logging configuration. + """ + log_config_path = get_package_files("singer_sdk") / "default_logging.yml" + return singer_logger.load_yaml_logging_config(log_config_path) + @property def mapper(self) -> PluginMapper: """Plugin mapper for this tap. From 4a6f55bcb9d708a3cd04bc5790772003efee1501 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgar=20Ram=C3=ADrez-Mondrag=C3=B3n?= Date: Wed, 17 Jul 2024 18:09:27 -0600 Subject: [PATCH 2/8] Use a standard location for the default logging config --- docs/implementation/logging.md | 18 ++++++++++++ singer_sdk/logger.py | 52 ---------------------------------- singer_sdk/metrics.py | 52 ++++++++++++++++++++++++++++++++++ singer_sdk/plugin_base.py | 13 +-------- 4 files changed, 71 insertions(+), 64 deletions(-) delete mode 100644 singer_sdk/logger.py diff --git a/docs/implementation/logging.md b/docs/implementation/logging.md index da20a5780..530e886f8 100644 --- a/docs/implementation/logging.md +++ b/docs/implementation/logging.md @@ -77,3 +77,21 @@ This will send metrics to a `metrics.log`: 2022-09-29 00:48:53,302 INFO METRIC: {"metric_type": "timer", "metric": "sync_duration", "value": 0.5258760452270508, "tags": {"stream": "countries", "context": {}, "status": "succeeded"}} 2022-09-29 00:48:53,303 INFO METRIC: {"metric_type": "counter", "metric": "record_count", "value": 250, "tags": {"stream": "countries", "context": {}}} ``` + +## For package developers + +If you're developing a tap or target package, you can put a `default_loggging.yml` file in the package root to set the default logging configuration for your package. This file will be used if the `SINGER_SDK_LOG_CONFIG` environment variable is not set: + +``` +. +├── README.md +├── poetry.lock +├── pyproject.toml +└── tap_example +    ├── __init__.py +    ├── __main__.py +    ├── default_logging.yml # <-- This file will be used if SINGER_SDK_LOG_CONFIG is not set +    ├── client.py +    ├── streams.py +    └── tap.py +``` diff --git a/singer_sdk/logger.py b/singer_sdk/logger.py deleted file mode 100644 index 31a733f5c..000000000 --- a/singer_sdk/logger.py +++ /dev/null @@ -1,52 +0,0 @@ -"""Logging configuration for the Singer SDK.""" - -from __future__ import annotations - -import logging -import os -import typing as t -from pathlib import Path - -import yaml - -from singer_sdk.metrics import METRICS_LOG_LEVEL_SETTING, METRICS_LOGGER_NAME - -if t.TYPE_CHECKING: - from singer_sdk.helpers._compat import Traversable - -__all__ = ["setup_logging"] - - -def load_yaml_logging_config(path: Traversable | Path) -> t.Any: # noqa: ANN401 - """Load the logging config from the YAML file. - - Args: - path: A path to the YAML file. - - Returns: - The logging config. - """ - with path.open() as f: - return yaml.safe_load(f) - - -def setup_logging( - config: t.Mapping[str, t.Any], - default_logging_config: dict[str, t.Any], -) -> None: - """Setup logging. - - Args: - default_logging_config: A default - :py:std:label:`Python logging configuration dictionary`. - config: A plugin configuration dictionary. - """ - logging.config.dictConfig(default_logging_config) - - config = config or {} - metrics_log_level = config.get(METRICS_LOG_LEVEL_SETTING, "INFO").upper() - logging.getLogger(METRICS_LOGGER_NAME).setLevel(metrics_log_level) - - if "SINGER_SDK_LOG_CONFIG" in os.environ: # pragma: no cover - log_config_path = Path(os.environ["SINGER_SDK_LOG_CONFIG"]) - logging.config.dictConfig(load_yaml_logging_config(log_config_path)) diff --git a/singer_sdk/metrics.py b/singer_sdk/metrics.py index 826035b60..3c4b09c6c 100644 --- a/singer_sdk/metrics.py +++ b/singer_sdk/metrics.py @@ -7,14 +7,21 @@ import json import logging import logging.config +import os import typing as t from dataclasses import dataclass, field +from pathlib import Path from time import time +import yaml + +from singer_sdk.helpers._resources import get_package_files + if t.TYPE_CHECKING: from types import TracebackType from singer_sdk.helpers import types + from singer_sdk.helpers._compat import Traversable DEFAULT_LOG_INTERVAL = 60.0 @@ -372,3 +379,48 @@ def sync_timer(stream: str, **tags: t.Any) -> Timer: """ tags[Tag.STREAM] = stream return Timer(Metric.SYNC_DURATION, tags) + + +def _load_yaml_logging_config(path: Traversable | Path) -> t.Any: # noqa: ANN401 + """Load the logging config from the YAML file. + + Args: + path: A path to the YAML file. + + Returns: + The logging config. + """ + with path.open() as f: + return yaml.safe_load(f) + + +def _get_default_config() -> t.Any: # noqa: ANN401 + """Get a logging configuration. + + Returns: + A logging configuration. + """ + filename = "default_logging.yml" + path = get_package_files(__package__) / filename + if path.is_file(): + return _load_yaml_logging_config(path) + + path = get_package_files("singer_sdk").joinpath("default_logging.yml") + return _load_yaml_logging_config(path) + + +def _setup_logging(config: t.Mapping[str, t.Any]) -> None: + """Setup logging. + + Args: + config: A plugin configuration dictionary. + """ + logging.config.dictConfig(_get_default_config()) + + config = config or {} + metrics_log_level = config.get(METRICS_LOG_LEVEL_SETTING, "INFO").upper() + logging.getLogger(METRICS_LOGGER_NAME).setLevel(metrics_log_level) + + if "SINGER_SDK_LOG_CONFIG" in os.environ: + log_config_path = Path(os.environ["SINGER_SDK_LOG_CONFIG"]) + logging.config.dictConfig(_load_yaml_logging_config(log_config_path)) diff --git a/singer_sdk/plugin_base.py b/singer_sdk/plugin_base.py index 39add257b..1564558cf 100644 --- a/singer_sdk/plugin_base.py +++ b/singer_sdk/plugin_base.py @@ -15,7 +15,6 @@ import click from jsonschema import Draft7Validator -import singer_sdk.logger as singer_logger from singer_sdk import about, metrics from singer_sdk.cli import plugin_cli from singer_sdk.configuration._dict_config import ( @@ -24,7 +23,6 @@ ) from singer_sdk.exceptions import ConfigValidationError from singer_sdk.helpers._classproperty import classproperty -from singer_sdk.helpers._resources import get_package_files from singer_sdk.helpers._secrets import SecretString, is_common_secret_key from singer_sdk.helpers._util import read_json_file from singer_sdk.helpers.capabilities import ( @@ -164,7 +162,7 @@ def __init__( if self._is_secret_config(k): config_dict[k] = SecretString(v) self._config = config_dict - singer_logger.setup_logging(self.config, self.get_default_logging_config()) + metrics._setup_logging(self.config) # noqa: SLF001 self.metrics_logger = metrics.get_metrics_logger() self._validate_config(raise_errors=validate_config) @@ -180,15 +178,6 @@ def setup_mapper(self) -> None: logger=self.logger, ) - def get_default_logging_config(self) -> t.Any: # noqa: ANN401, PLR6301 - """Get a default logging configuration for the plugin. - - Returns: - A logging configuration. - """ - log_config_path = get_package_files("singer_sdk") / "default_logging.yml" - return singer_logger.load_yaml_logging_config(log_config_path) - @property def mapper(self) -> PluginMapper: """Plugin mapper for this tap. From 0c7f9d06a632f793d7528ce9b5036b97ce53bd66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgar=20Ram=C3=ADrez-Mondrag=C3=B3n?= Date: Wed, 17 Jul 2024 18:18:44 -0600 Subject: [PATCH 3/8] Small refactor --- singer_sdk/metrics.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/singer_sdk/metrics.py b/singer_sdk/metrics.py index 3c4b09c6c..2f38c6f04 100644 --- a/singer_sdk/metrics.py +++ b/singer_sdk/metrics.py @@ -394,7 +394,7 @@ def _load_yaml_logging_config(path: Traversable | Path) -> t.Any: # noqa: ANN40 return yaml.safe_load(f) -def _get_default_config() -> t.Any: # noqa: ANN401 +def _get_default_config_path() -> Traversable: """Get a logging configuration. Returns: @@ -403,10 +403,11 @@ def _get_default_config() -> t.Any: # noqa: ANN401 filename = "default_logging.yml" path = get_package_files(__package__) / filename if path.is_file(): - return _load_yaml_logging_config(path) + return path - path = get_package_files("singer_sdk").joinpath("default_logging.yml") - return _load_yaml_logging_config(path) + return get_package_files("singer_sdk").joinpath( # pragma: no cover + "default_logging.yml" + ) def _setup_logging(config: t.Mapping[str, t.Any]) -> None: @@ -415,7 +416,7 @@ def _setup_logging(config: t.Mapping[str, t.Any]) -> None: Args: config: A plugin configuration dictionary. """ - logging.config.dictConfig(_get_default_config()) + logging.config.dictConfig(_load_yaml_logging_config(_get_default_config_path())) config = config or {} metrics_log_level = config.get(METRICS_LOG_LEVEL_SETTING, "INFO").upper() From 9d0942a628bdf12941308a84b8c016ca1e845769 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgar=20Ram=C3=ADrez-Mondrag=C3=B3n?= Date: Wed, 17 Jul 2024 18:20:55 -0600 Subject: [PATCH 4/8] Tweak docs language --- docs/implementation/logging.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/implementation/logging.md b/docs/implementation/logging.md index 530e886f8..adf6f2182 100644 --- a/docs/implementation/logging.md +++ b/docs/implementation/logging.md @@ -80,7 +80,7 @@ This will send metrics to a `metrics.log`: ## For package developers -If you're developing a tap or target package, you can put a `default_loggging.yml` file in the package root to set the default logging configuration for your package. This file will be used if the `SINGER_SDK_LOG_CONFIG` environment variable is not set: +If you're developing a tap or target package and would like to customize its logging configuration, you can put a `default_loggging.yml` file in the package root to set the default logging configuration for your package. This file will be used if the `SINGER_SDK_LOG_CONFIG` environment variable is not set: ``` . From 4a7c2237efc95e5800e7fa1f7063c2d00880887c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgar=20Ram=C3=ADrez-Mondrag=C3=B3n?= Date: Wed, 17 Jul 2024 18:24:53 -0600 Subject: [PATCH 5/8] Small refactor --- singer_sdk/metrics.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/singer_sdk/metrics.py b/singer_sdk/metrics.py index 2f38c6f04..460aa3c51 100644 --- a/singer_sdk/metrics.py +++ b/singer_sdk/metrics.py @@ -402,12 +402,7 @@ def _get_default_config_path() -> Traversable: """ filename = "default_logging.yml" path = get_package_files(__package__) / filename - if path.is_file(): - return path - - return get_package_files("singer_sdk").joinpath( # pragma: no cover - "default_logging.yml" - ) + return path if path.is_file() else get_package_files("singer_sdk") / filename def _setup_logging(config: t.Mapping[str, t.Any]) -> None: From a2f6fb37a21cc2c251f188d73100c1541a72ac16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgar=20Ram=C3=ADrez-Mondrag=C3=B3n?= Date: Wed, 17 Jul 2024 18:41:29 -0600 Subject: [PATCH 6/8] Pass package name to get the logging config --- singer_sdk/metrics.py | 13 +++++++++---- singer_sdk/plugin_base.py | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/singer_sdk/metrics.py b/singer_sdk/metrics.py index 460aa3c51..23e86fd6b 100644 --- a/singer_sdk/metrics.py +++ b/singer_sdk/metrics.py @@ -394,24 +394,29 @@ def _load_yaml_logging_config(path: Traversable | Path) -> t.Any: # noqa: ANN40 return yaml.safe_load(f) -def _get_default_config_path() -> Traversable: +def _get_default_config_path(package_name: str) -> Traversable: """Get a logging configuration. + Args: + package_name: The package name to get the logging configuration for. + Returns: A logging configuration. """ filename = "default_logging.yml" - path = get_package_files(__package__) / filename + path = get_package_files(package_name) / filename return path if path.is_file() else get_package_files("singer_sdk") / filename -def _setup_logging(config: t.Mapping[str, t.Any]) -> None: +def _setup_logging(package: str, config: t.Mapping[str, t.Any]) -> None: """Setup logging. Args: + package: The package name to get the logging configuration for. config: A plugin configuration dictionary. """ - logging.config.dictConfig(_load_yaml_logging_config(_get_default_config_path())) + path = _get_default_config_path(package) + logging.config.dictConfig(_load_yaml_logging_config(path)) config = config or {} metrics_log_level = config.get(METRICS_LOG_LEVEL_SETTING, "INFO").upper() diff --git a/singer_sdk/plugin_base.py b/singer_sdk/plugin_base.py index 1564558cf..68780a4ed 100644 --- a/singer_sdk/plugin_base.py +++ b/singer_sdk/plugin_base.py @@ -162,7 +162,7 @@ def __init__( if self._is_secret_config(k): config_dict[k] = SecretString(v) self._config = config_dict - metrics._setup_logging(self.config) # noqa: SLF001 + metrics._setup_logging(__package__, self.config) # noqa: SLF001 self.metrics_logger = metrics.get_metrics_logger() self._validate_config(raise_errors=validate_config) From a7d9cd3d66fb54e8d9cfc9a314fb0f52cedc618f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgar=20Ram=C3=ADrez-Mondrag=C3=B3n?= Date: Wed, 17 Jul 2024 19:44:55 -0600 Subject: [PATCH 7/8] Get module name programmatically --- singer_sdk/metrics.py | 12 ++++++++---- singer_sdk/plugin_base.py | 5 ++++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/singer_sdk/metrics.py b/singer_sdk/metrics.py index 23e86fd6b..1b4612023 100644 --- a/singer_sdk/metrics.py +++ b/singer_sdk/metrics.py @@ -394,21 +394,25 @@ def _load_yaml_logging_config(path: Traversable | Path) -> t.Any: # noqa: ANN40 return yaml.safe_load(f) -def _get_default_config_path(package_name: str) -> Traversable: +def _get_default_config_path(package: str) -> Traversable: """Get a logging configuration. Args: - package_name: The package name to get the logging configuration for. + package: The package name to get the logging configuration for. Returns: A logging configuration. """ filename = "default_logging.yml" - path = get_package_files(package_name) / filename + path = get_package_files(package) / filename return path if path.is_file() else get_package_files("singer_sdk") / filename -def _setup_logging(package: str, config: t.Mapping[str, t.Any]) -> None: +def _setup_logging( + config: t.Mapping[str, t.Any], + *, + package: str | None = None, +) -> None: """Setup logging. Args: diff --git a/singer_sdk/plugin_base.py b/singer_sdk/plugin_base.py index 68780a4ed..b88559088 100644 --- a/singer_sdk/plugin_base.py +++ b/singer_sdk/plugin_base.py @@ -162,7 +162,10 @@ def __init__( if self._is_secret_config(k): config_dict[k] = SecretString(v) self._config = config_dict - metrics._setup_logging(__package__, self.config) # noqa: SLF001 + metrics._setup_logging( # noqa: SLF001 + self.config, + package=self.__module__.split(".", maxsplit=1)[0], + ) self.metrics_logger = metrics.get_metrics_logger() self._validate_config(raise_errors=validate_config) From b3aaa8434feba97f243393b750d73a33f692c1d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgar=20Ram=C3=ADrez-Mondrag=C3=B3n?= Date: Wed, 17 Jul 2024 20:10:00 -0600 Subject: [PATCH 8/8] Address broken `importlib.resources` in Python 3.9 --- poetry.lock | 2 +- pyproject.toml | 2 +- singer_sdk/helpers/_resources.py | 2 +- singer_sdk/metrics.py | 6 +----- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/poetry.lock b/poetry.lock index 92ea330cd..73a4db60d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2620,4 +2620,4 @@ testing = ["pytest", "pytest-durations"] [metadata] lock-version = "2.0" python-versions = ">=3.8" -content-hash = "91c9f418561bf500a204fa69295a5bb8b66da5b01ea1b77c397d38cb3b749ac4" +content-hash = "6a6acd1298eca878a1a7e0dc47decdb156fdc8eb6622dbba1e6b5422e190cecd" diff --git a/pyproject.toml b/pyproject.toml index dcf67b2e8..eae7ecd64 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ backports-datetime-fromisoformat = { version = ">=2.0.1", python = "<3.11" } click = "~=8.0" fs = ">=2.4.16" importlib-metadata = {version = "<9.0.0", python = "<3.12"} -importlib-resources = {version = ">=5.12.0,!=6.2.0,!=6.3.0,!=6.3.1", python = "<3.9"} +importlib-resources = {version = ">=5.12.0,!=6.2.0,!=6.3.0,!=6.3.1", python = "<3.10"} inflection = ">=0.5.1" joblib = ">=1.3.0" jsonpath-ng = ">=1.5.3" diff --git a/singer_sdk/helpers/_resources.py b/singer_sdk/helpers/_resources.py index 89b3ed269..02f7b30ff 100644 --- a/singer_sdk/helpers/_resources.py +++ b/singer_sdk/helpers/_resources.py @@ -8,7 +8,7 @@ from singer_sdk.helpers._compat import Traversable -if sys.version_info < (3, 9): +if sys.version_info < (3, 10): import importlib_resources else: import importlib.resources as importlib_resources diff --git a/singer_sdk/metrics.py b/singer_sdk/metrics.py index 1b4612023..50d7d3926 100644 --- a/singer_sdk/metrics.py +++ b/singer_sdk/metrics.py @@ -408,11 +408,7 @@ def _get_default_config_path(package: str) -> Traversable: return path if path.is_file() else get_package_files("singer_sdk") / filename -def _setup_logging( - config: t.Mapping[str, t.Any], - *, - package: str | None = None, -) -> None: +def _setup_logging(config: t.Mapping[str, t.Any], *, package: str) -> None: """Setup logging. Args: