Repository: modin-project/modin
Pull request: REFACTOR-#4534: Replace logging meta class with class decorator
<!--
Thank you for your contribution!
Please review the contributing docs: https://modin.readthedocs.io/en/latest/development/contributing.html
if you have questions about contributing.
-->

## What do these changes do?
Change the implementation of auto-adding the logging to class decorator and a base class using `__init_subclass__` to automatically add logging to its children.

This (hopefully) makes inheriting things easier, as a custom metaclass complicates multiple inheritance a lot.
<!-- Please give a short brief about these changes. -->

- [x] commit message follows format outlined [here](https://modin.readthedocs.io/en/latest/development/contributing.html#commit-message-formatting)
- [x] passes `flake8 modin/ asv_bench/benchmarks scripts/doc_checker.py`
- [x] passes `black --check modin/ asv_bench/benchmarks scripts/doc_checker.py`
- [x] signed commit with `git commit -s` <!-- you can amend your commit with a signature via `git commit -amend -s` -->
- [x] Resolves #4534 ? <!-- issue must be created for each patch -->
- [x] tests added and passing
- [x] module layout described at `docs/development/architecture.rst` is up-to-date <!-- if you have added, renamed or removed files or directories please update the documentation accordingly -->
- [x] added (Issue Number: PR title (PR Number)) and github username to release notes for next major release <!-- e.g. DOCS-#4077: Add release notes template to docs folder (#4078) -->


Problem statement:
Simplify logging design - remove metaclass
**Is your feature request related to a problem? Please describe.**
In some cases, having a custom `metaclass` would harden inheritance.

Plus I feel that it complicates the design a little bit, one of example of that would be the introduction of `metaclass_resolver` which aim was to solve arising conflicts.

Hints from issue discussion:

Commit message: Address review comment, move release notes

Signed-off-by: Vasily Litvinov <fam1ly.n4me@yandex.ru>

Patch to review:
diff --git a/docs/release_notes/release_notes-0.15.0.rst b/docs/release_notes/release_notes-0.15.0.rst
index 9135a41de5d..1b3ef1b85d6 100644
--- a/docs/release_notes/release_notes-0.15.0.rst
+++ b/docs/release_notes/release_notes-0.15.0.rst
@@ -90,4 +90,4 @@ Contributors
 @jrsacher
 @orcahmlee
 @naren-ponder
-@RehanSD
+@RehanSD
\ No newline at end of file
diff --git a/docs/release_notes/release_notes-0.16.0.rst b/docs/release_notes/release_notes-0.16.0.rst
index 6fc6827ae09..66d5e313c1f 100644
--- a/docs/release_notes/release_notes-0.16.0.rst
+++ b/docs/release_notes/release_notes-0.16.0.rst
@@ -12,7 +12,7 @@ Key Features and Updates
 * Benchmarking enhancements
   *
 * Refactor Codebase
-  *
+  * REFACTOR-#4534: Remove logging metaclass (#4535)
 * Pandas API implementations and improvements
   *
 * OmniSci enhancements
@@ -31,4 +31,5 @@ Key Features and Updates
 
 Contributors
 ------------
-@mvashishtha
\ No newline at end of file
+@mvashishtha
+@vnlitvinov
\ No newline at end of file
diff --git a/modin/core/dataframe/pandas/dataframe/dataframe.py b/modin/core/dataframe/pandas/dataframe/dataframe.py
index bcfc194a6b7..859224adb59 100644
--- a/modin/core/dataframe/pandas/dataframe/dataframe.py
+++ b/modin/core/dataframe/pandas/dataframe/dataframe.py
@@ -38,7 +38,7 @@
 )
 from modin.pandas.indexing import is_range_like
 from modin.pandas.utils import is_full_grab_slice, check_both_not_none
-from modin.logging import LoggerMetaClass
+from modin.logging import LoggerBase
 
 
 def lazy_metadata_decorator(apply_axis=None, axis_arg=-1, transpose=False):
@@ -133,7 +133,7 @@ def run_f_on_minimally_updated_metadata(self, *args, **kwargs):
     return decorator
 
 
-class PandasDataframe(object, metaclass=LoggerMetaClass):
+class PandasDataframe(LoggerBase):
     """
     An abstract class that represents the parent class for any pandas storage format dataframe class.
 
diff --git a/modin/core/io/file_dispatcher.py b/modin/core/io/file_dispatcher.py
index 7756e3a57ae..55f8b6c3cea 100644
--- a/modin/core/io/file_dispatcher.py
+++ b/modin/core/io/file_dispatcher.py
@@ -23,7 +23,7 @@
 import re
 from modin.config import StorageFormat
 from modin.utils import import_optional_dependency
-from modin.logging import logger_decorator
+from modin.logging import LoggerBase
 import numpy as np
 
 S3_ADDRESS_REGEX = re.compile("[sS]3://(.*?)/(.*)")
@@ -111,7 +111,7 @@ def __exit__(self, *args):
         self.file.close()
 
 
-class FileDispatcher:
+class FileDispatcher(LoggerBase):
     """
     Class handles util functions for reading data from different kinds of files.
 
@@ -127,7 +127,6 @@ class FileDispatcher:
     query_compiler_cls = None
 
     @classmethod
-    @logger_decorator("PANDAS-API", "FileDispatcher.read", "INFO")
     def read(cls, *args, **kwargs):
         """
         Read data according passed `args` and `kwargs`.
@@ -190,7 +189,6 @@ def _read(cls, *args, **kwargs):
         raise NotImplementedError(NOT_IMPLEMENTED_MESSAGE)
 
     @classmethod
-    @logger_decorator("PANDAS-API", "FileDispatcher.get_path", "INFO")
     def get_path(cls, file_path):
         """
         Process `file_path` in accordance to it's type.
@@ -217,7 +215,6 @@ def get_path(cls, file_path):
             return os.path.abspath(file_path)
 
     @classmethod
-    @logger_decorator("PANDAS-API", "FileDispatcher.file_size", "INFO")
     def file_size(cls, f):
         """
         Get the size of file associated with file handle `f`.
@@ -239,7 +236,6 @@ def file_size(cls, f):
         return size
 
     @classmethod
-    @logger_decorator("PANDAS-API", "FileDispatcher.file_exists", "INFO")
     def file_exists(cls, file_path):
         """
         Check if `file_path` exists.
diff --git a/modin/core/io/text/text_file_dispatcher.py b/modin/core/io/text/text_file_dispatcher.py
index 26a8c904180..3f8f002012d 100644
--- a/modin/core/io/text/text_file_dispatcher.py
+++ b/modin/core/io/text/text_file_dispatcher.py
@@ -35,7 +35,6 @@
 from modin.core.io.text.utils import CustomNewlineIterator
 from modin.config import NPartitions
 from modin.error_message import ErrorMessage
-from modin.logging import logger_decorator
 
 ColumnNamesTypes = Tuple[Union[pandas.Index, pandas.MultiIndex]]
 IndexColType = Union[int, str, bool, Sequence[int], Sequence[str], None]
@@ -49,7 +48,6 @@ class TextFileDispatcher(FileDispatcher):
     read_callback = None
 
     @classmethod
-    @logger_decorator("PANDAS-API", "TextFileDispatcher.get_path_or_buffer", "INFO")
     def get_path_or_buffer(cls, filepath_or_buffer):
         """
         Extract path from `filepath_or_buffer`.
@@ -82,7 +80,6 @@ def get_path_or_buffer(cls, filepath_or_buffer):
         return filepath_or_buffer
 
     @classmethod
-    @logger_decorator("PANDAS-API", "TextFileDispatcher.build_partition", "INFO")
     def build_partition(cls, partition_ids, row_lengths, column_widths):
         """
         Build array with partitions of `cls.frame_partition_cls` class.
@@ -117,7 +114,6 @@ def build_partition(cls, partition_ids, row_lengths, column_widths):
         )
 
     @classmethod
-    @logger_decorator("PANDAS-API", "TextFileDispatcher.pathlib_or_pypath", "INFO")
     def pathlib_or_pypath(cls, filepath_or_buffer):
         """
         Check if `filepath_or_buffer` is instance of `py.path.local` or `pathlib.Path`.
@@ -150,7 +146,6 @@ def pathlib_or_pypath(cls, filepath_or_buffer):
         return False
 
     @classmethod
-    @logger_decorator("PANDAS-API", "TextFileDispatcher.offset", "INFO")
     def offset(
         cls,
         f,
@@ -208,7 +203,6 @@ def offset(
         return outside_quotes
 
     @classmethod
-    @logger_decorator("PANDAS-API", "TextFileDispatcher.partitioned_file", "INFO")
     def partitioned_file(
         cls,
         f,
@@ -337,7 +331,6 @@ def partitioned_file(
         return result
 
     @classmethod
-    @logger_decorator("PANDAS-API", "TextFileDispatcher._read_rows", "INFO")
     def _read_rows(
         cls,
         f,
@@ -409,7 +402,6 @@ def _read_rows(
         return outside_quotes, rows_read
 
     @classmethod
-    @logger_decorator("PANDAS-API", "TextFileDispatcher.compute_newline", "INFO")
     def compute_newline(cls, file_like, encoding, quotechar):
         """
         Compute byte or sequence of bytes indicating line endings.
@@ -465,7 +457,6 @@ def compute_newline(cls, file_like, encoding, quotechar):
 
     # _read helper functions
     @classmethod
-    @logger_decorator("PANDAS-API", "TextFileDispatcher.rows_skipper_builder", "INFO")
     def rows_skipper_builder(
         cls, f, quotechar, is_quoting, encoding=None, newline=None
     ):
@@ -507,7 +498,6 @@ def skipper(n):
         return skipper
 
     @classmethod
-    @logger_decorator("PANDAS-API", "TextFileDispatcher._define_header_size", "INFO")
     def _define_header_size(
         cls,
         header: Union[int, Sequence[int], str, None] = "infer",
@@ -539,7 +529,6 @@ def _define_header_size(
         return header_size
 
     @classmethod
-    @logger_decorator("PANDAS-API", "TextFileDispatcher._define_metadata", "INFO")
     def _define_metadata(
         cls,
         df: pandas.DataFrame,
@@ -587,7 +576,6 @@ def _define_metadata(
         return column_widths, num_splits
 
     @classmethod
-    @logger_decorator("PANDAS-API", "TextFileDispatcher._launch_tasks", "INFO")
     def _launch_tasks(cls, splits: list, **partition_kwargs) -> Tuple[list, list, list]:
         """
         Launch tasks to read partitions.
@@ -622,9 +610,6 @@ def _launch_tasks(cls, splits: list, **partition_kwargs) -> Tuple[list, list, li
         return partition_ids, index_ids, dtypes_ids
 
     @classmethod
-    @logger_decorator(
-        "PANDAS-API", "TextFileDispatcher.check_parameters_support", "INFO"
-    )
     def check_parameters_support(
         cls,
         filepath_or_buffer,
@@ -683,7 +668,6 @@ def check_parameters_support(
 
     @classmethod
     @_inherit_docstrings(pandas.io.parsers.base_parser.ParserBase._validate_usecols_arg)
-    @logger_decorator("PANDAS-API", "TextFileDispatcher._validate_usecols_arg", "INFO")
     def _validate_usecols_arg(cls, usecols):
         msg = (
             "'usecols' must either be list-like of all strings, all unicode, "
@@ -707,9 +691,6 @@ def _validate_usecols_arg(cls, usecols):
         return usecols, None
 
     @classmethod
-    @logger_decorator(
-        "PANDAS-API", "TextFileDispatcher._manage_skiprows_parameter", "INFO"
-    )
     def _manage_skiprows_parameter(
         cls,
         skiprows: Union[int, Sequence[int], Callable, None] = None,
@@ -813,7 +794,6 @@ def skiprows_func(x):
         return skiprows_md, pre_reading, skiprows_partitioning
 
     @classmethod
-    @logger_decorator("PANDAS-API", "TextFileDispatcher._define_index", "INFO")
     def _define_index(
         cls,
         index_ids: list,
@@ -849,7 +829,6 @@ def _define_index(
         return new_index, row_lengths
 
     @classmethod
-    @logger_decorator("PANDAS-API", "TextFileDispatcher._get_new_qc", "INFO")
     def _get_new_qc(
         cls,
         partition_ids: list,
@@ -956,7 +935,6 @@ def _get_new_qc(
         return new_query_compiler
 
     @classmethod
-    @logger_decorator("PANDAS-API", "TextFileDispatcher._read", "INFO")
     def _read(cls, filepath_or_buffer, **kwargs):
         """
         Read data from `filepath_or_buffer` according to `kwargs` parameters.
@@ -1086,7 +1064,6 @@ def _read(cls, filepath_or_buffer, **kwargs):
         return new_query_compiler
 
     @classmethod
-    @logger_decorator("PANDAS-API", "TextFileDispatcher._get_skip_mask", "INFO")
     def _get_skip_mask(cls, rows_index: pandas.Index, skiprows: Callable):
         """
         Get mask of skipped by callable `skiprows` rows.
diff --git a/modin/core/storage_formats/base/query_compiler.py b/modin/core/storage_formats/base/query_compiler.py
index 4b4a026eb20..f76731939e4 100644
--- a/modin/core/storage_formats/base/query_compiler.py
+++ b/modin/core/storage_formats/base/query_compiler.py
@@ -32,7 +32,7 @@
 )
 from modin.error_message import ErrorMessage
 import modin.core.storage_formats.base.doc_utils as doc_utils
-from modin.logging import LoggerMetaClass
+from modin.logging import LoggerBase
 
 from pandas.core.dtypes.common import is_scalar
 import pandas.core.resample
@@ -90,9 +90,7 @@ def axis_setter(self, labels):
 # Currently actual arguments are placed in the methods docstrings, but since they're
 # not presented in the function's signature it makes linter to raise `PR02: unknown parameters`
 # warning. For now, they're silenced by using `noqa` (Modin issue #3108).
-class BaseQueryCompiler(
-    abc.ABC, metaclass=type("", (abc.ABCMeta, LoggerMetaClass), {})
-):
+class BaseQueryCompiler(LoggerBase, abc.ABC):
     """
     Abstract class that handles the queries to Modin dataframes.
 
diff --git a/modin/core/storage_formats/pandas/parsers.py b/modin/core/storage_formats/pandas/parsers.py
index 63ffda39454..7cc1aa4fa9c 100644
--- a/modin/core/storage_formats/pandas/parsers.py
+++ b/modin/core/storage_formats/pandas/parsers.py
@@ -53,7 +53,7 @@
 from modin.db_conn import ModinDatabaseConnection
 from modin.core.storage_formats.pandas.utils import split_result_of_axis_func_pandas
 from modin.error_message import ErrorMessage
-from modin.logging import LoggerMetaClass
+from modin.logging import LoggerBase
 
 _doc_pandas_parser_class = """
 Class for handling {data_type} on the workers using pandas storage format.
@@ -137,7 +137,7 @@ def find_common_type_cat(types):
         return find_common_type(list(types))
 
 
-class PandasParser(object, metaclass=LoggerMetaClass):
+class PandasParser(LoggerBase):
     """Base class for parser classes with pandas storage format."""
 
     @staticmethod
diff --git a/modin/experimental/cloud/rpyc_proxy.py b/modin/experimental/cloud/rpyc_proxy.py
index db48f0749f2..d77c419ba8a 100644
--- a/modin/experimental/cloud/rpyc_proxy.py
+++ b/modin/experimental/cloud/rpyc_proxy.py
@@ -23,7 +23,6 @@
 from . import get_connection
 from .meta_magic import _LOCAL_ATTRS, RemoteMeta, _KNOWN_DUALS
 from modin.config import DoTraceRpyc
-from modin.logging import LoggerMetaClass
 
 from .rpyc_patches import apply_pathes
 
@@ -418,9 +417,7 @@ def method(_self, *_args, __method_name__=name, **_kw):
                         namespace[name] = method
             return namespace
 
-    class Wrapper(
-        override, origin_cls, metaclass=type("", (ProxyMeta, LoggerMetaClass), {})
-    ):
+    class Wrapper(override, origin_cls, metaclass=ProxyMeta):
         """
         Subclass origin_cls replacing attributes with what is defined in override while
         relaying requests for all other attributes to remote_cls.
diff --git a/modin/logging/__init__.py b/modin/logging/__init__.py
index 87ed216a09f..ffb28dffdcf 100644
--- a/modin/logging/__init__.py
+++ b/modin/logging/__init__.py
@@ -11,18 +11,13 @@
 # ANY KIND, either express or implied. See the License for the specific language
 # governing permissions and limitations under the License.
 
-from .logger_metaclass import (  # noqa: F401
-    LoggerMetaClass,
-    logger_class_wrapper,
-    metaclass_resolver,
-)
+from .logger_base_class import LoggerBase  # noqa: F401
 from .config import get_logger  # noqa: F401
-from .logger_function import logger_decorator  # noqa: F401
+from .logger_function import logger_decorator, disable_logging  # noqa: F401
 
 __all__ = [
-    "LoggerMetaClass",
-    "logger_class_wrapper",
-    "metaclass_resolver",
+    "LoggerBase",
     "get_logger",
     "logger_decorator",
+    "disable_logging",
 ]
diff --git a/modin/logging/config.py b/modin/logging/config.py
index 9cad15eb9be..0a35e4098fe 100644
--- a/modin/logging/config.py
+++ b/modin/logging/config.py
@@ -27,6 +27,8 @@
 import pkg_resources
 import threading
 import time
+
+import modin
 from modin.config import LogMemoryInterval, LogFileSize, LogMode
 
 __LOGGER_CONFIGURED__: bool = False
@@ -85,7 +87,7 @@ def bytes_int_to_str(num_bytes, suffix="B"):
         if num_bytes < factor:
             return f"{num_bytes:.2f}{unit}{suffix}"
         num_bytes /= factor
-    return f"{num_bytes:.2f}{1000+P}{suffix}"
+    return f"{num_bytes * 1000:.2f}P{suffix}"
 
 
 def configure_logging():
@@ -112,15 +114,15 @@ def configure_logging():
 
     if LogMode.get() == "enable_api_only":
         logger.setLevel(logging.INFO)
-    logger.setLevel(logging.DEBUG)
+    else:
+        logger.setLevel(logging.DEBUG)
 
     logger.info(f"OS Version: {platform.platform()}")
     logger.info(f"Python Version: {platform.python_version()}")
-    modin_version = pkg_resources.get_distribution("modin").version
     pandas_version = pkg_resources.get_distribution("pandas").version
     num_physical_cores = str(psutil.cpu_count(logical=False))
     num_total_cores = str(psutil.cpu_count(logical=True))
-    logger.info(f"Modin Version: {modin_version}")
+    logger.info(f"Modin Version: {modin.__version__}")
     logger.info(f"Pandas Version: {pandas_version}")
     logger.info(f"Physical Cores: {num_physical_cores}")
     logger.info(f"Total Cores: {num_total_cores}")
diff --git a/modin/logging/logger_base_class.py b/modin/logging/logger_base_class.py
new file mode 100644
index 00000000000..1520f6e6617
--- /dev/null
+++ b/modin/logging/logger_base_class.py
@@ -0,0 +1,56 @@
+# Licensed to Modin Development Team under one or more contributor license agreements.
+# See the NOTICE file distributed with this work for additional information regarding
+# copyright ownership.  The Modin Development Team licenses this file to you under the
+# Apache License, Version 2.0 (the "License"); you may not use this file except in
+# compliance with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under
+# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
+# ANY KIND, either express or implied. See the License for the specific language
+# governing permissions and limitations under the License.
+
+"""
+Module contains ``LoggerBase`` class.
+
+``LoggerBase`` is used for adding logging to Modin classes and their subclasses.
+"""
+from typing import Optional
+
+from .logger_function import logger_decorator
+
+
+class LoggerBase:
+    """
+    Ensure all subclasses of the class being inherited are logged, too.
+
+    Notes
+    -----
+    This mixin must go first in class bases declaration to have the desired effect.
+    """
+
+    @classmethod
+    def __init_subclass__(
+        cls,
+        modin_layer: str = "PANDAS-API",
+        class_name: Optional[str] = None,
+        log_level: Optional[str] = "info",
+        **kwargs,
+    ) -> None:
+        """
+        Apply logging decorator to all children of LoggerBase.
+
+        Parameters
+        ----------
+        modin_layer : str, default: "PANDAS-API"
+            Specified by the logger (e.g. PANDAS-API).
+        class_name : str, optional
+            The name of the class the decorator is being applied to.
+            Composed from the decorated class name if not specified.
+        log_level : str, default: "info"
+            The log level (INFO, DEBUG, WARNING, etc.).
+        **kwargs : dict
+        """
+        super().__init_subclass__(**kwargs)
+        logger_decorator(modin_layer, class_name, log_level)(cls)
diff --git a/modin/logging/logger_function.py b/modin/logging/logger_function.py
index ceda9bc3ac3..45638f25b8e 100644
--- a/modin/logging/logger_function.py
+++ b/modin/logging/logger_function.py
@@ -17,35 +17,92 @@
 ``logger_decorator`` is used for decorating individual Modin functions.
 """
 
+from typing import Optional, Callable, Union, Type
+from types import FunctionType, MethodType
 from functools import wraps
+from logging import Logger
 
 from modin.config import LogMode
 from .config import get_logger
 
+_MODIN_LOGGER_NOWRAP = "__modin_logging_nowrap__"
 
-def logger_decorator(modin_layer: str, function_name: str, log_level: str):
+
+def disable_logging(func):
     """
-    Log Decorator used on specific internal Modin functions.
+    Disable logging of one particular function. Useful for decorated classes.
 
     Parameters
     ----------
-    modin_layer : str
-        Specified by the logger (PANDAS-API).
-    function_name : str
-        The name of the function the decorator is being applied to.
-    log_level : str
-        The log level (logging.INFO, logging.DEBUG, logging.WARNING, etc.).
+    func : callable
+        A method in a logger-decorated class for which logging should be disabled.
 
     Returns
     -------
     func
-        A decorator function.
+        A function with logging disabled.
     """
+    setattr(func, _MODIN_LOGGER_NOWRAP, True)
+    return func
 
-    def decorator(f):
-        """Decorate function to add logs to Modin API function."""
 
-        @wraps(f)
+def logger_decorator(
+    modin_layer: Union[str, Callable, Type] = "PANDAS-API",
+    name: Optional[str] = None,
+    log_level: Optional[str] = "info",
+):
+    """
+    Log Decorator used on specific internal Modin functions or classes.
+
+    Parameters
+    ----------
+    modin_layer : str or object to decorate, default: "PANDAS-API"
+        Specified by the logger (e.g. PANDAS-API).
+        If it's an object to decorate, call logger_decorator() on it with default arguments.
+    name : str, optional
+        The name of the object the decorator is being applied to.
+        Composed from the decorated object name if not specified.
+    log_level : str, default: "info"
+        The log level (INFO, DEBUG, WARNING, etc.).
+
+    Returns
+    -------
+    func
+        A decorator function.
+    """
+    if not isinstance(modin_layer, str):
+        # assume the decorator is used in a form without parenthesis like:
+        # @logger_decorator
+        # def func()
+        return logger_decorator()(modin_layer)
+
+    log_level = log_level.lower()
+    assert hasattr(Logger, log_level.lower()), f"Invalid log level: {log_level}"
+
+    def decorator(obj):
+        """Decorate function or class to add logs to Modin API function(s)."""
+        if isinstance(obj, type):
+            seen = {}
+            for attr_name, attr_value in vars(obj).items():
+                if isinstance(attr_value, (FunctionType, MethodType)) and not hasattr(
+                    attr_value, _MODIN_LOGGER_NOWRAP
+                ):
+                    try:
+                        wrapped = seen[attr_value]
+                    except KeyError:
+                        wrapped = seen[attr_value] = logger_decorator(
+                            modin_layer,
+                            f"{name or obj.__name__}.{attr_name}",
+                            log_level,
+                        )(attr_value)
+
+                    setattr(obj, attr_name, wrapped)
+            return obj
+
+        start_line = f"START::{modin_layer.upper()}::{name or obj.__name__}"
+        stop_line = f"STOP::{modin_layer.upper()}::{name or obj.__name__}"
+
+        @wraps(obj)
         def run_and_log(*args, **kwargs):
             """
             Compute function with logging if Modin logging is enabled.
@@ -62,19 +119,20 @@ def run_and_log(*args, **kwargs):
             Any
             """
             if LogMode.get() == "disable":
-                return f(*args, **kwargs)
+                return obj(*args, **kwargs)
 
             logger = get_logger()
+            logger_level = getattr(logger, log_level)
+            logger_level(start_line)
             try:
-                logger_level = getattr(logger, log_level.lower())
-            except AttributeError:
-                raise AttributeError(f"Invalid log_level: {log_level}")
-
-            logger_level(f"START::{modin_layer.upper()}::{function_name}")
-            result = f(*args, **kwargs)
-            logger_level(f"STOP::{modin_layer.upper()}::{function_name}")
+                result = obj(*args, **kwargs)
+            except BaseException:
+                logger.exception(stop_line)
+                raise
+            logger_level(stop_line)
             return result
 
-        return run_and_log
+        # make sure we won't decorate multiple times
+        return disable_logging(run_and_log)
 
     return decorator
diff --git a/modin/logging/logger_metaclass.py b/modin/logging/logger_metaclass.py
deleted file mode 100644
index 7db323c5433..00000000000
--- a/modin/logging/logger_metaclass.py
+++ /dev/null
@@ -1,133 +0,0 @@
-# Licensed to Modin Development Team under one or more contributor license agreements.
-# See the NOTICE file distributed with this work for additional information regarding
-# copyright ownership.  The Modin Development Team licenses this file to you under the
-# Apache License, Version 2.0 (the "License"); you may not use this file except in
-# compliance with the License.  You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software distributed under
-# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
-# ANY KIND, either express or implied. See the License for the specific language
-# governing permissions and limitations under the License.
-
-"""
-Module contains ``LoggerMetaClass`` class.
-
-``LoggerMetaClass`` is used for adding logging to Modin classes.
-"""
-
-from functools import wraps
-from types import FunctionType, MethodType
-
-from .config import get_logger
-from modin.config import LogMode
-
-
-def logger_class_wrapper(class_name, method_name, method):
-    """
-    Execute Modin functions with logging if enabled.
-
-    Parameters
-    ----------
-    class_name : str
-        The name of the class the LoggerMetaClass is being applied to.
-    method_name : str
-        The name of the Modin function within the class.
-    method : callable
-        The function to apply on the arguments.
-
-    Returns
-    -------
-    func
-        A decorator function.
-    """
-
-    @wraps(method)
-    def log_wrap(*args, **kwargs):
-        """
-        Compute function with logging if Modin logging is enabled.
-
-        Parameters
-        ----------
-        *args : tuple
-            The function arguments.
-        **kwargs : dict
-            The function keyword arguments.
-
-        Returns
-        -------
-        Any
-        """
-        if LogMode.get() != "disable":
-            logger = get_logger()
-            logger.info(f"START::PANDAS-API::{class_name}.{method_name}")
-            result = method(*args, **kwargs)
-            logger.info(f"END::PANDAS-API::{class_name}.{method_name}")
-            return result
-        return method(*args, **kwargs)
-
-    return log_wrap
-
-
-def metaclass_resolver(*classes):
-    """
-    Resolve metaclass typing issues resulting from class inheritance.
-
-    Parameters
-    ----------
-    *classes : dict
-        Dictionary of parent classes to resolve metaclass conflicts for.
-
-    Returns
-    -------
-    str
-        The correct Metaclass that resolves inheritance conflicts.
-    """
-    metaclass = tuple(set(type(cls) for cls in classes))
-    metaclass = (
-        metaclass[0]
-        if len(metaclass) == 1
-        else type("_".join(mcls.__name__ for mcls in metaclass), metaclass, {})
-    )
-    return metaclass("_".join(cls.__name__ for cls in classes), classes, {})
-
-
-class LoggerMetaClass(type):  # noqa: PR01
-    """Log Metaclass to attach to class definitions."""
-
-    def __new__(mcls, class_name, bases, class_dict):
-        """
-        Complete class instance creation with metaclass.
-
-        Parameters
-        ----------
-        mcls : class
-            The class to create a new instance of.
-        class_name : str
-            Name of the future class.
-        bases : dict
-            The parent classes.
-        class_dict : dict
-            Dictionary of attributes for the class.
-
-        Returns
-        -------
-        class
-            The new class instance to be created.
-        """
-        new_class_dict = {}
-        seen_attributes = {}
-        exclude_attributes = {"__getattribute__"}
-        for attribute_name, attribute in class_dict.items():
-            if (
-                isinstance(attribute, (FunctionType, MethodType))
-                and attribute_name not in exclude_attributes
-            ):
-                if attribute not in seen_attributes:
-                    seen_attributes[attribute] = logger_class_wrapper(
-                        class_name, attribute_name, attribute
-                    )
-                attribute = seen_attributes[attribute]
-            new_class_dict[attribute_name] = attribute
-        return type.__new__(mcls, class_name, bases, new_class_dict)
diff --git a/modin/pandas/accessor.py b/modin/pandas/accessor.py
index 58770845aeb..760d5ec62f3 100644
--- a/modin/pandas/accessor.py
+++ b/modin/pandas/accessor.py
@@ -27,10 +27,10 @@
 import modin.pandas as pd
 from modin.error_message import ErrorMessage
 from modin.utils import _inherit_docstrings
-from modin.logging import LoggerMetaClass, metaclass_resolver
+from modin.logging import LoggerBase
 
 
-class BaseSparseAccessor(object, metaclass=LoggerMetaClass):
+class BaseSparseAccessor(LoggerBase):
     """
     Base class for various sparse DataFrame accessor classes.
 
@@ -86,7 +86,7 @@ def _default_to_pandas(self, op, *args, **kwargs):
 
 
 @_inherit_docstrings(pandas.core.arrays.sparse.accessor.SparseFrameAccessor)
-class SparseFrameAccessor(metaclass_resolver(BaseSparseAccessor)):
+class SparseFrameAccessor(BaseSparseAccessor):
     def _validate(self, data):
         """
         Verify that `data` dtypes are compatible with `pandas.core.arrays.sparse.dtype.SparseDtype`.
@@ -124,7 +124,7 @@ def to_coo(self):
 
 
 @_inherit_docstrings(pandas.core.arrays.sparse.accessor.SparseAccessor)
-class SparseAccessor(metaclass_resolver(BaseSparseAccessor)):
+class SparseAccessor(BaseSparseAccessor):
     def _validate(self, data):
         """
         Verify that `data` dtype is compatible with `pandas.core.arrays.sparse.dtype.SparseDtype`.
@@ -177,7 +177,7 @@ def to_dense(self):
 
 
 @_inherit_docstrings(pandas.core.accessor.CachedAccessor)
-class CachedAccessor(object, metaclass=LoggerMetaClass):
+class CachedAccessor(LoggerBase):
     def __init__(self, name: str, accessor) -> None:
         self._name = name
         self._accessor = accessor
diff --git a/modin/pandas/base.py b/modin/pandas/base.py
index 30f43189eac..dc611e8cf93 100644
--- a/modin/pandas/base.py
+++ b/modin/pandas/base.py
@@ -55,7 +55,7 @@
 import modin.pandas as pd
 from modin.pandas.utils import is_scalar
 from modin.config import IsExperimental
-from modin.logging import LoggerMetaClass
+from modin.logging import LoggerBase, disable_logging
 
 # Similar to pandas, sentinel value to use as kwarg in place of None when None has
 # special meaning and needs to be distinguished from a user explicitly passing None.
@@ -101,7 +101,7 @@
 
 
 @_inherit_docstrings(pandas.DataFrame, apilink=["pandas.DataFrame", "pandas.Series"])
-class BasePandasDataset(object, metaclass=LoggerMetaClass):
+class BasePandasDataset(LoggerBase):
     """
     Implement most of the common code that exists in DataFrame/Series.
 
@@ -3862,6 +3862,7 @@ def values(self):  # noqa: RT01, D200
         """
         return self.to_numpy()
 
+    @disable_logging
     def __getattribute__(self, item):
         """
         Return item from the `BasePandasDataset`.
diff --git a/modin/pandas/dataframe.py b/modin/pandas/dataframe.py
index 6d3cc6812b4..6abeda7d50b 100644
--- a/modin/pandas/dataframe.py
+++ b/modin/pandas/dataframe.py
@@ -34,7 +34,6 @@
 from typing import IO, Optional, Union, Iterator
 import warnings
 
-from modin.logging import metaclass_resolver
 from modin.pandas import Categorical
 from modin.error_message import ErrorMessage
 from modin.utils import _inherit_docstrings, to_pandas, hashable
@@ -54,7 +53,7 @@
 @_inherit_docstrings(
     pandas.DataFrame, excluded=[pandas.DataFrame.__init__], apilink="pandas.DataFrame"
 )
-class DataFrame(metaclass_resolver(BasePandasDataset)):
+class DataFrame(BasePandasDataset):
     """
     Modin distributed representation of ``pandas.DataFrame``.
 
diff --git a/modin/pandas/general.py b/modin/pandas/general.py
index 1d29ae63594..d0da424d151 100644
--- a/modin/pandas/general.py
+++ b/modin/pandas/general.py
@@ -30,7 +30,7 @@
 
 
 @_inherit_docstrings(pandas.isna, apilink="pandas.isna")
-@logger_decorator("PANDAS-API", "isna", "info")
+@logger_decorator
 def isna(obj):  # noqa: PR01, RT01, D200
     """
     Detect missing values for an array-like object.
@@ -45,7 +45,7 @@ def isna(obj):  # noqa: PR01, RT01, D200
 
 
 @_inherit_docstrings(pandas.notna, apilink="pandas.notna")
-@logger_decorator("PANDAS-API", "notna", "info")
+@logger_decorator
 def notna(obj):  # noqa: PR01, RT01, D200
     """
     Detect non-missing values for an array-like object.
@@ -60,7 +60,7 @@ def notna(obj):  # noqa: PR01, RT01, D200
 
 
 @_inherit_docstrings(pandas.merge, apilink="pandas.merge")
-@logger_decorator("PANDAS-API", "merge", "info")
+@logger_decorator
 def merge(
     left,
     right,
@@ -107,7 +107,7 @@ def merge(
 
 
 @_inherit_docstrings(pandas.merge_ordered, apilink="pandas.merge_ordered")
-@logger_decorator("PANDAS-API", "merge_ordered", "info")
+@logger_decorator
 def merge_ordered(
     left,
     right,
@@ -147,7 +147,7 @@ def merge_ordered(
 
 
 @_inherit_docstrings(pandas.merge_asof, apilink="pandas.merge_asof")
-@logger_decorator("PANDAS-API", "merge_asof", "info")
+@logger_decorator
 def merge_asof(
     left,
     right,
@@ -319,7 +319,7 @@ def merge_asof(
 
 
 @_inherit_docstrings(pandas.pivot_table, apilink="pandas.pivot_table")
-@logger_decorator("PANDAS-API", "pivot_table", "info")
+@logger_decorator
 def pivot_table(
     data,
     values=None,
@@ -355,7 +355,7 @@ def pivot_table(
 
 
 @_inherit_docstrings(pandas.pivot, apilink="pandas.pivot")
-@logger_decorator("PANDAS-API", "pivot", "info")
+@logger_decorator
 def pivot(data, index=None, columns=None, values=None):  # noqa: PR01, RT01, D200
     """
     Return reshaped DataFrame organized by given index / column values.
@@ -366,7 +366,7 @@ def pivot(data, index=None, columns=None, values=None):  # noqa: PR01, RT01, D20
 
 
 @_inherit_docstrings(pandas.to_numeric, apilink="pandas.to_numeric")
-@logger_decorator("PANDAS-API", "to_numeric", "info")
+@logger_decorator
 def to_numeric(arg, errors="raise", downcast=None):  # noqa: PR01, RT01, D200
     """
     Convert argument to a numeric type.
@@ -377,7 +377,7 @@ def to_numeric(arg, errors="raise", downcast=None):  # noqa: PR01, RT01, D200
 
 
 @_inherit_docstrings(pandas.unique, apilink="pandas.unique")
-@logger_decorator("PANDAS-API", "unique", "info")
+@logger_decorator
 def unique(values):  # noqa: PR01, RT01, D200
     """
     Return unique values based on a hash table.
@@ -386,7 +386,7 @@ def unique(values):  # noqa: PR01, RT01, D200
 
 
 # Adding docstring since pandas docs don't have web section for this function.
-@logger_decorator("PANDAS-API", "value_counts", "info")
+@logger_decorator
 def value_counts(
     values, sort=True, ascending=False, normalize=False, bins=None, dropna=True
 ):
@@ -423,7 +423,7 @@ def value_counts(
 
 
 @_inherit_docstrings(pandas.concat, apilink="pandas.concat")
-@logger_decorator("PANDAS-API", "concat", "info")
+@logger_decorator
 def concat(
     objs: "Iterable[DataFrame | Series] | Mapping[Hashable, DataFrame | Series]",
     axis=0,
@@ -563,7 +563,7 @@ def concat(
 
 
 @_inherit_docstrings(pandas.to_datetime, apilink="pandas.to_datetime")
-@logger_decorator("PANDAS-API", "to_datetime", "info")
+@logger_decorator
 def to_datetime(
     arg,
     errors="raise",
@@ -609,7 +609,7 @@ def to_datetime(
 
 
 @_inherit_docstrings(pandas.get_dummies, apilink="pandas.get_dummies")
-@logger_decorator("PANDAS-API", "get_dummies", "info")
+@logger_decorator
 def get_dummies(
     data,
     prefix=None,
@@ -658,7 +658,7 @@ def get_dummies(
 
 
 @_inherit_docstrings(pandas.melt, apilink="pandas.melt")
-@logger_decorator("PANDAS-API", "melt", "info")
+@logger_decorator
 def melt(
     frame,
     id_vars=None,
@@ -682,7 +682,7 @@ def melt(
 
 
 @_inherit_docstrings(pandas.crosstab, apilink="pandas.crosstab")
-@logger_decorator("PANDAS-API", "crosstab", "info")
+@logger_decorator
 def crosstab(
     index,
     columns,
@@ -715,7 +715,7 @@ def crosstab(
 
 
 # Adding docstring since pandas docs don't have web section for this function.
-@logger_decorator("PANDAS-API", "lreshape", "info")
+@logger_decorator
 def lreshape(data: DataFrame, groups, dropna=True, label=None):
     """
     Reshape wide-format data to long. Generalized inverse of ``DataFrame.pivot``.
@@ -749,7 +749,7 @@ def lreshape(data: DataFrame, groups, dropna=True, label=None):
 
 
 @_inherit_docstrings(pandas.wide_to_long, apilink="pandas.wide_to_long")
-@logger_decorator("PANDAS-API", "wide_to_long", "info")
+@logger_decorator
 def wide_to_long(
     df: DataFrame, stubnames, i, j, sep: str = "", suffix: str = r"\d+"
 ) -> DataFrame:  # noqa: PR01, RT01, D200
diff --git a/modin/pandas/groupby.py b/modin/pandas/groupby.py
index e9dff233b7b..47f4afb269c 100644
--- a/modin/pandas/groupby.py
+++ b/modin/pandas/groupby.py
@@ -34,13 +34,13 @@
 from modin.core.storage_formats.base.query_compiler import BaseQueryCompiler
 from modin.core.dataframe.algebra.default2pandas.groupby import GroupBy
 from modin.config import IsExperimental
-from modin.logging import LoggerMetaClass, metaclass_resolver
+from modin.logging import LoggerBase
 from .series import Series
 from .utils import is_label
 
 
 @_inherit_docstrings(pandas.core.groupby.DataFrameGroupBy)
-class DataFrameGroupBy(object, metaclass=LoggerMetaClass):
+class DataFrameGroupBy(LoggerBase):
     def __init__(
         self,
         df,
@@ -1180,7 +1180,7 @@ def groupby_on_multiple_columns(df, *args, **kwargs):
 
 
 @_inherit_docstrings(pandas.core.groupby.SeriesGroupBy)
-class SeriesGroupBy(metaclass_resolver(DataFrameGroupBy)):
+class SeriesGroupBy(DataFrameGroupBy):
     @property
     def ndim(self):
         """
diff --git a/modin/pandas/indexing.py b/modin/pandas/indexing.py
index d7424178d85..f1c62da0039 100644
--- a/modin/pandas/indexing.py
+++ b/modin/pandas/indexing.py
@@ -36,7 +36,7 @@
 from pandas.core.dtypes.common import is_integer, is_bool_dtype, is_integer_dtype
 from pandas.core.indexing import IndexingError
 from modin.error_message import ErrorMessage
-from modin.logging import LoggerMetaClass, metaclass_resolver
+from modin.logging import LoggerBase
 
 from .dataframe import DataFrame
 from .series import Series
@@ -268,7 +268,7 @@ def _compute_ndim(row_loc, col_loc):
     return ndim
 
 
-class _LocationIndexerBase(object, metaclass=LoggerMetaClass):
+class _LocationIndexerBase(LoggerBase):
     """
     Base class for location indexer like loc and iloc.
 
@@ -593,7 +593,7 @@ def _handle_boolean_masking(self, row_loc, col_loc):
         return type(self)(masked_df)[(slice(None), col_loc)]
 
 
-class _LocIndexer(metaclass_resolver(_LocationIndexerBase)):
+class _LocIndexer(_LocationIndexerBase):
     """
     An indexer for modin_df.loc[] functionality.
 
@@ -846,7 +846,7 @@ def _compute_lookup(self, row_loc, col_loc):
         return lookups
 
 
-class _iLocIndexer(metaclass_resolver(_LocationIndexerBase)):
+class _iLocIndexer(_LocationIndexerBase):
     """
     An indexer for modin_df.iloc[] functionality.
 
diff --git a/modin/pandas/io.py b/modin/pandas/io.py
index 4c01ae00b89..786ddee2a1d 100644
--- a/modin/pandas/io.py
+++ b/modin/pandas/io.py
@@ -30,7 +30,7 @@
 from typing import Union, IO, AnyStr, Sequence, Dict, List, Optional, Any
 
 from modin.error_message import ErrorMessage
-from modin.logging import LoggerMetaClass, logger_decorator
+from modin.logging import LoggerBase, logger_decorator
 from .dataframe import DataFrame
 from modin.utils import _inherit_docstrings, Engine
 from . import _update_engine
@@ -73,7 +73,7 @@ def _read(**kwargs):
 
 
 @_inherit_docstrings(pandas.read_csv, apilink="pandas.read_csv")
-@logger_decorator("PANDAS-API", "read_csv", "info")
+@logger_decorator
 def read_csv(
     filepath_or_buffer: "FilePath | ReadCsvBuffer[bytes] | ReadCsvBuffer[str]",
     sep=lib.no_default,
@@ -141,7 +141,7 @@ def read_csv(
 
 
 @_inherit_docstrings(pandas.read_table, apilink="pandas.read_table")
-@logger_decorator("PANDAS-API", "read_table", "info")
+@logger_decorator
 def read_table(
     filepath_or_buffer: "FilePath | ReadCsvBuffer[bytes] | ReadCsvBuffer[str]",
     sep=lib.no_default,
@@ -211,7 +211,7 @@ def read_table(
 
 
 @_inherit_docstrings(pandas.read_parquet, apilink="pandas.read_parquet")
-@logger_decorator("PANDAS-API", "read_parquet", "info")
+@logger_decorator
 def read_parquet(
     path,
     engine: str = "auto",
@@ -239,7 +239,7 @@ def read_parquet(
 
 
 @_inherit_docstrings(pandas.read_json, apilink="pandas.read_json")
-@logger_decorator("PANDAS-API", "read_json", "info")
+@logger_decorator
 def read_json(
     path_or_buf=None,
     orient=None,
@@ -271,7 +271,7 @@ def read_json(
 
 
 @_inherit_docstrings(pandas.read_gbq, apilink="pandas.read_gbq")
-@logger_decorator("PANDAS-API", "read_gbq", "info")
+@logger_decorator
 def read_gbq(
     query: str,
     project_id: Optional[str] = None,
@@ -300,7 +300,7 @@ def read_gbq(
 
 
 @_inherit_docstrings(pandas.read_html, apilink="pandas.read_html")
-@logger_decorator("PANDAS-API", "read_html", "info")
+@logger_decorator
 def read_html(
     io,
     match=".+",
@@ -330,7 +330,7 @@ def read_html(
 
 
 @_inherit_docstrings(pandas.read_clipboard, apilink="pandas.read_clipboard")
-@logger_decorator("PANDAS-API", "read_clipboard", "info")
+@logger_decorator
 def read_clipboard(sep=r"\s+", **kwargs):  # pragma: no cover  # noqa: PR01, RT01, D200
     """
     Read text from clipboard and pass to read_csv.
@@ -345,7 +345,7 @@ def read_clipboard(sep=r"\s+", **kwargs):  # pragma: no cover  # noqa: PR01, RT0
 
 
 @_inherit_docstrings(pandas.read_excel, apilink="pandas.read_excel")
-@logger_decorator("PANDAS-API", "read_excel", "info")
+@logger_decorator
 def read_excel(
     io,
     sheet_name: "str | int | list[IntStrT] | None" = 0,
@@ -394,7 +394,7 @@ def read_excel(
 
 
 @_inherit_docstrings(pandas.read_hdf, apilink="pandas.read_hdf")
-@logger_decorator("PANDAS-API", "read_hdf", "info")
+@logger_decorator
 def read_hdf(
     path_or_buf,
     key=None,
@@ -421,7 +421,7 @@ def read_hdf(
 
 
 @_inherit_docstrings(pandas.read_feather, apilink="pandas.read_feather")
-@logger_decorator("PANDAS-API", "read_feather", "info")
+@logger_decorator
 def read_feather(
     path,
     columns=None,
@@ -440,7 +440,7 @@ def read_feather(
 
 
 @_inherit_docstrings(pandas.read_stata, apilink="pandas.read_stata")
-@logger_decorator("PANDAS-API", "read_stata", "info")
+@logger_decorator
 def read_stata(
     filepath_or_buffer,
     convert_dates=True,
@@ -467,7 +467,7 @@ def read_stata(
 
 
 @_inherit_docstrings(pandas.read_sas, apilink="pandas.read_sas")
-@logger_decorator("PANDAS-API", "read_sas", "info")
+@logger_decorator
 def read_sas(
     filepath_or_buffer,
     format=None,
@@ -488,7 +488,7 @@ def read_sas(
 
 
 @_inherit_docstrings(pandas.read_pickle, apilink="pandas.read_pickle")
-@logger_decorator("PANDAS-API", "read_pickle", "info")
+@logger_decorator
 def read_pickle(
     filepath_or_buffer,
     compression: Optional[str] = "infer",
@@ -506,7 +506,7 @@ def read_pickle(
 
 
 @_inherit_docstrings(pandas.read_sql, apilink="pandas.read_sql")
-@logger_decorator("PANDAS-API", "read_sql", "info")
+@logger_decorator
 def read_sql(
     sql,
     con,
@@ -535,7 +535,7 @@ def read_sql(
 
 
 @_inherit_docstrings(pandas.read_fwf, apilink="pandas.read_fwf")
-@logger_decorator("PANDAS-API", "read_fwf", "info")
+@logger_decorator
 def read_fwf(
     filepath_or_buffer: Union[str, pathlib.Path, IO[AnyStr]],
     colspecs="infer",
@@ -566,7 +566,7 @@ def read_fwf(
 
 
 @_inherit_docstrings(pandas.read_sql_table, apilink="pandas.read_sql_table")
-@logger_decorator("PANDAS-API", "read_sql_table", "info")
+@logger_decorator
 def read_sql_table(
     table_name,
     con,
@@ -589,7 +589,7 @@ def read_sql_table(
 
 
 @_inherit_docstrings(pandas.read_sql_query, apilink="pandas.read_sql_query")
-@logger_decorator("PANDAS-API", "read_sql_query", "info")
+@logger_decorator
 def read_sql_query(
     sql,
     con,
@@ -612,7 +612,7 @@ def read_sql_query(
 
 
 @_inherit_docstrings(pandas.read_spss, apilink="pandas.read_spss")
-@logger_decorator("PANDAS-API", "read_spss", "info")
+@logger_decorator
 def read_spss(
     path: Union[str, pathlib.Path],
     usecols: Union[Sequence[str], type(None)] = None,
@@ -630,7 +630,7 @@ def read_spss(
 
 
 # Adding docstring since pandas docs don't have web section for this function.
-@logger_decorator("PANDAS-API", "to_pickle", "info")
+@logger_decorator
 def to_pickle(
     obj: Any,
     filepath_or_buffer,
@@ -688,7 +688,7 @@ def to_pickle(
 
 
 @_inherit_docstrings(pandas.json_normalize, apilink="pandas.json_normalize")
-@logger_decorator("PANDAS-API", "json_normalize", "info")
+@logger_decorator
 def json_normalize(
     data: Union[Dict, List[Dict]],
     record_path: Optional[Union[str, List]] = None,
@@ -712,7 +712,7 @@ def json_normalize(
 
 
 @_inherit_docstrings(pandas.read_orc, apilink="pandas.read_orc")
-@logger_decorator("PANDAS-API", "read_orc", "info")
+@logger_decorator
 def read_orc(
     path, columns: Optional[List[str]] = None, **kwargs
 ) -> DataFrame:  # noqa: PR01, RT01, D200
@@ -725,7 +725,7 @@ def read_orc(
 
 
 @_inherit_docstrings(pandas.read_xml, apilink="pandas.read_xml")
-@logger_decorator("PANDAS-API", "read_xml", "info")
+@logger_decorator
 def read_xml(
     path_or_buffer,
     xpath="./*",
@@ -762,7 +762,7 @@ def read_xml(
 
 
 @_inherit_docstrings(pandas.HDFStore)
-class HDFStore(pandas.HDFStore, metaclass=LoggerMetaClass):  # noqa: PR01, D200
+class HDFStore(LoggerBase, pandas.HDFStore):  # noqa: PR01, D200
     """
     Dict-like IO interface for storing pandas objects in PyTables.
     """
@@ -819,7 +819,7 @@ def return_handler(*args, **kwargs):
 
 
 @_inherit_docstrings(pandas.ExcelFile)
-class ExcelFile(pandas.ExcelFile, metaclass=LoggerMetaClass):  # noqa: PR01, D200
+class ExcelFile(LoggerBase, pandas.ExcelFile):  # noqa: PR01, D200
     """
     Class for parsing tabular excel sheets into DataFrame objects.
     """
diff --git a/modin/pandas/plotting.py b/modin/pandas/plotting.py
index 14da0107b54..fc341148478 100644
--- a/modin/pandas/plotting.py
+++ b/modin/pandas/plotting.py
@@ -16,12 +16,12 @@
 from pandas import plotting as pdplot
 
 from modin.utils import instancer, to_pandas
-from modin.logging import LoggerMetaClass
+from modin.logging import LoggerBase
 from .dataframe import DataFrame
 
 
 @instancer
-class Plotting(object, metaclass=LoggerMetaClass):
+class Plotting(LoggerBase):
     """Wrapper of pandas plotting module."""
 
     def __dir__(self):
diff --git a/modin/pandas/resample.py b/modin/pandas/resample.py
index ddf096bbe10..9cd1bd91694 100644
--- a/modin/pandas/resample.py
+++ b/modin/pandas/resample.py
@@ -23,11 +23,11 @@
 from pandas.core.dtypes.common import is_list_like
 from typing import Optional, Union
 from modin.utils import _inherit_docstrings
-from modin.logging import LoggerMetaClass
+from modin.logging import LoggerBase
 
 
 @_inherit_docstrings(pandas.core.resample.Resampler)
-class Resampler(object, metaclass=LoggerMetaClass):
+class Resampler(LoggerBase):
     def __init__(
         self,
         dataframe,
diff --git a/modin/pandas/series.py b/modin/pandas/series.py
index c5fdfe6eb5b..74098a4172e 100644
--- a/modin/pandas/series.py
+++ b/modin/pandas/series.py
@@ -30,7 +30,6 @@
 
 from modin.utils import _inherit_docstrings, to_pandas, Engine
 from modin.config import IsExperimental, PersistentPickle
-from modin.logging import metaclass_resolver
 from .base import BasePandasDataset, _ATTRS_NO_LOOKUP
 from .iterator import PartitionIterator
 from .utils import from_pandas, is_scalar, _doc_binary_op
@@ -41,7 +40,7 @@
 @_inherit_docstrings(
     pandas.Series, excluded=[pandas.Series.__init__], apilink="pandas.Series"
 )
-class Series(metaclass_resolver(BasePandasDataset)):
+class Series(BasePandasDataset):
     """
     Modin distributed representation of `pandas.Series`.
 
diff --git a/modin/pandas/series_utils.py b/modin/pandas/series_utils.py
index 381f05c244c..ee61d1ead8c 100644
--- a/modin/pandas/series_utils.py
+++ b/modin/pandas/series_utils.py
@@ -20,7 +20,7 @@
 import sys
 import numpy as np
 import pandas
-from modin.logging import LoggerMetaClass
+from modin.logging import LoggerBase
 from modin.utils import _inherit_docstrings
 from .series import Series
 
@@ -33,7 +33,7 @@
 
 
 @_inherit_docstrings(pandas.core.arrays.categorical.CategoricalAccessor)
-class CategoryMethods(object, metaclass=LoggerMetaClass):
+class CategoryMethods(LoggerBase):
     def __init__(self, series):
         self._series = series
         self._query_compiler = series._query_compiler
@@ -124,7 +124,7 @@ def _default_to_pandas(self, op, *args, **kwargs):
 
 
 @_inherit_docstrings(pandas.core.strings.StringMethods)
-class StringMethods(object, metaclass=LoggerMetaClass):
+class StringMethods(LoggerBase):
     def __init__(self, series):
         # Check if dtypes is objects
 
@@ -455,7 +455,7 @@ def _default_to_pandas(self, op, *args, **kwargs):
 
 
 @_inherit_docstrings(pandas.core.indexes.accessors.CombinedDatetimelikeProperties)
-class DatetimeProperties(object, metaclass=LoggerMetaClass):
+class DatetimeProperties(LoggerBase):
     def __init__(self, series):
         self._series = series
         self._query_compiler = series._query_compiler
diff --git a/modin/pandas/window.py b/modin/pandas/window.py
index ba1fa54b996..82b4167301e 100644
--- a/modin/pandas/window.py
+++ b/modin/pandas/window.py
@@ -16,13 +16,13 @@
 from typing import Optional
 import pandas.core.window.rolling
 from modin.utils import _inherit_docstrings
-from modin.logging import LoggerMetaClass
+from modin.logging import LoggerBase
 
 from pandas.core.dtypes.common import is_list_like
 
 
 @_inherit_docstrings(pandas.core.window.rolling.Window)
-class Window(object, metaclass=LoggerMetaClass):
+class Window(LoggerBase):
     def __init__(
         self,
         dataframe,
@@ -82,7 +82,7 @@ def std(self, ddof=1, *args, **kwargs):
     pandas.core.window.rolling.Rolling,
     excluded=[pandas.core.window.rolling.Rolling.__init__],
 )
-class Rolling(object, metaclass=LoggerMetaClass):
+class Rolling(LoggerBase):
     def __init__(
         self,
         dataframe,


Code review comments:
diff_hunk: @@ -0,0 +1,56 @@
+# Licensed to Modin Development Team under one or more contributor license agreements.
+# See the NOTICE file distributed with this work for additional information regarding
+# copyright ownership.  The Modin Development Team licenses this file to you under the
+# Apache License, Version 2.0 (the "License"); you may not use this file except in
+# compliance with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under
+# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
+# ANY KIND, either express or implied. See the License for the specific language
+# governing permissions and limitations under the License.
+
+"""
+Module contains ``LoggerBase`` class.
+
+``LoggerBase`` is used for adding logging to Modin classes and their subclasses.
+"""
+from typing import Optional
+
+from .logger_function import logger_decorator
+
+
+class LoggerBase:
path: modin/logging/logger_base_class.py
text: @user1:
Why base? Do we expect any child logger of this class? If so, I think we should rename it to `BaseLogger` and `logger_base_class.py` to `base_logger.py`. If this name only stands for a base class to classes to be decorated, `LoggerBase` -> `Logger` and `logger_base_class.py` -> `logger.py`.

@author:
I had originally named it `LoggerMixin` to highlight it was really a mixin, but then decided against that...
I wonder if a better name would be something like `LoggedMethods`

@user1:
`Logger` is finer to me 😄 

@user2:
We have `logger_decorator.py` that decorates at the function level, so would prefer this name to not be just `logger.py`, but rather a bit more descriptive. What about `ClassLogger`? 

@author:
Maybe it's better be `LoggerClass` then? I'm no native speaker, but `ClassLogger` sounds like it's logging some classes...

That said, `LoggerClass` also feels a bit wrong, like it's doing the logging itself... 🤨 

@user1:
Don't both `class ClassLogger` and `class LoggerClass` sound like a bit duplicated? I still tend to stick to just `Logger`.

@user1:
cc @author, @user3-ponder 

@user2:
Outside of the definition, we would be doing `from modin.logging import ClassLogger` and then attaching it to different classes so that naming convention makes sense to me. If you have a strong objection, I am okay changing to `Logger`.

@user1:
I don't like using `Class` word in class names or `function` word in function names because it looks like a repetition. I just want to be sure that we won't want to rename it in future. If @author is okay to keep just `Logger` too, let's use this name.

@author:
It's not exactly a repetition, and I feel that just `Logger` would introduce a confusion as it would overlap with `logging.Logger` (from the standard library) _but_ it has completely different functionality.

I would rather leave it be a `ClassLogger` or invent a better name describing its functionality (like maybe `LogEnabler`?..), but I'm certainly against `Logger` here.

@user1:
Let's leave as is.

diff_hunk: @@ -90,4 +90,4 @@ Contributors
 @jrsacher
 @orcahmlee
 @naren-ponder
-@RehanSD
+@RehanSD
path: docs/release_notes/release_notes-0.15.0.rst
text: @user1:
Can we remove this change from this PR?

@user2:
@author, please remove this change.

@author:
TBH I have no idea why it isn't rolled back, I have tried it several times to revert to no avail for now.
Is this a blocker?..

@author:
I think I had finally managed to beat it!