Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add extended debug logging utils #158

Merged

Conversation

pipermerriam
Copy link
Member

Replaces: ethereum/py-evm#1750

What was wrong?

We have a few libraries that need to log with more granularity than the default logging module provides. We also regularly want loggers attached to our classes and we end up re-inventing mechanisms to make this DRY across different codebases.

How was it fixed?

Added the following utilities to eth-utils to make this easier.

  • HasLogger: inherit from this class and a logger is available on both the class and instances of the class at the logger property
  • HasExtendedDebugLogger: Same as HasLogger but exposes an ExtendedDebugLogger
  • ExtendedDebugLogger: Subclass of logging.Logger that has a debug2(...) logging method for logging at the DEBUG2 level
  • setup_DEBUG2_logging: Installs the DEBUG2 log level into the standard library logging module at the numeric level of 8 with the string name 'DEBUG2'.

This improves on existing mechanisms by:

  • using a metaclass so that the logger is available at the class level and shared across all instances as a class level attribute.
  • uses the __qualname__ for the logger name from PEP3155 which seems to be exactly what we want.
  • still allows you to specify a logger property manually and respects that.
  • HasExtendedDebugLogger no longer relies on the user having called setup_DEBUG2_logging to work. Calling the setup function is merely a nicety now that lets the logging module know the correct human readable name to use as well as making logging.DEBUG2 an importable property.

Cute Animal Picture

DSC05779

pass


class _BaseHasExtendedDebugLogger(HasLogger[ExtendedDebugLogger]): # type: ignore
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noting this API of doing HasLogger[LoggerClass] to create a new one that uses the new logger class is intentionally not documented so that we aren't committed to supporting this API.

# This class exists to a allow us to define the type of the logger. Once
# python3.5 is deprecated this can be removed in favor of a simple type
# annotation on the main class.
logger = ExtendedDebugLogger("") # type: ExtendedDebugLogger
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried a whole bunch of things to try and do this cleanly but this was the one way I could make it work without ugly side effects.

  • Tried programatically populating __annotations__
  • Tried using a typing.Generic
  • Looked into mypy plugins
  • Looked into writing stub files.

@carver
Copy link
Collaborator

carver commented May 13, 2019

Hah, @lithp was also working on getting the logger available at the class level.

Edit: oh, I see you already pinged him. Maybe you knew that.

Copy link
Collaborator

@carver carver left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, so two primary things:

  • Performance: no idea what the impact of adding a metaclass to the mix is. Maybe not good? Would we just not use this logger for py-evm and trinity?
  • Getting it working on the class level almost certainly requires some magic. Brian took a look, and had another option. So it would be worth comparing the two incantations side by side for readability.

... pass
...
>>> hasattr(MyClass, 'logger')
True
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you like, we could highlight the benefit here:

>>> MyClass.logger.debug('Can log from the class')

^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Same as the ``HasLogger`` class except the logger it exposes is an instance of
``ExtendedDebugLogger``
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that if you don't run setup then this all still works, right? I guess it would be nice to specifically address that.

class NoisyClass(HasExtendedDebugLogger):
  pass

NoisyClass.logger.debug2('What if I add a debug2 log without running setup_DEBUG2_logging?')

# replace it.
return super().__new__(mcls, name, bases, namespace)
if "__qualname__" not in namespace:
raise AttributeError("Missing __qualname__")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've never really dived into meta classes. It all seems pretty magical to me still. Brian did another implementation that was also magical. Maybe he still has it floating around. I'm not sure which one was better. Also performance matters on some use cases, like py-evm/trinity. I don't know this code well enough to understand the performance impact.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding is that there should be no performance hit and this should more performant than the @property based approach since metaclass code runs at class creation time which is module loading time. Assuming I am correct, there should be zero runtime overhead as everything should happen at module loading time.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just scrounged through my reflog to find my solution:

class classproperty(Generic[TResult]):
    def __init__(self, func: Callable[[Type[Any]], TResult]) -> None:
        self.func = func

    def __get__(self, instance: Any, owner: Type[Any]) -> TResult:
        return self.func(owner)

def get_logger(cls: Type['BasePlugin']) -> logging.Logger:
    return logging.getLogger(f'trinity.extensibility.plugin#{cls.__name__}')

class BasePlugin(ABC):
    logger = classproperty(get_logger)
   ....

Where get_logger was pulled out to make mypy happy but could also be in-line with a # type: ignore

Never thought I'd say this but I expect that the metaclass is not only faster but also less magical.

@@ -35,6 +35,6 @@ extras=lint
commands=
flake8 {toxinidir}/eth_utils setup.py
mypy --follow-imports=silent --ignore-missing-imports --disallow-incomplete-defs --warn-unused-ignore -p eth_utils
mypy --follow-imports=silent --ignore-missing-imports --disallow-incomplete-defs tests/mypy_typing_of_functional_utils.py
mypy --follow-imports=silent --ignore-missing-imports --disallow-incomplete-defs tests/type-checks/
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@pipermerriam
Copy link
Member Author

@carver

Performance: no idea what the impact of adding a metaclass to the mix is. Maybe not good? Would we just not use this logger for py-evm and trinity?

Mirrored from my other comment. This was actually written to address the performance part since metaclass code runs at module loading time, effectively front-loading the work so that it's already done by the time you actually work with any of the classes.

While metaclasses are magic, this I don't feel this is dark magic. I've tried to write reasonably exhaustive tests to ensure that it works as expected in all of the inheritance scenarios we may encounter, though now that I'm saying this, I realize I need to write a test that ensures this behaves nicely with classes that also inherit from typing.Generic.

@pipermerriam
Copy link
Member Author

Did not find a clean way to make this work nicely with typing.Generic so I added a .. warning to the docs indicating that if you need a typing.Generic you'll need to assign your logger manually.

@pipermerriam
Copy link
Member Author

@carver are you 👍 to merge this? @lithp do you have any feedback on it this approach?

@carver
Copy link
Collaborator

carver commented May 14, 2019

Hm, I think it causes problems if you run tox locally and expect it to pick up your changes.

@carver
Copy link
Collaborator

carver commented May 14, 2019

I think the right thing to do is to drop the pyproject.toml until we're actually using it to build the project.

@carver
Copy link
Collaborator

carver commented May 14, 2019

may pass --no-use-pep517 to opt out of pyproject.toml-style processing appears to be a runner-up solution

@pipermerriam
Copy link
Member Author

Dropped pyproject.toml and used comment mechanism to keep black from formatting eth_utils/units.py


def __new__(mcls, name, bases, namespace):
if "logger" in namespace:
# If a logger was explicitely declared we shouldn't do anything to
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/explicitely/explicitly/

logging.setLoggerClass(original_logger_cls)


class HasLoggerMeta(ABCMeta):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this derive from ABCMeta?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because..... I don't know. removing.

@lithp
Copy link

lithp commented May 15, 2019

Didn't look through this thoroughly but this approach seems reasonable! 👍

@pipermerriam pipermerriam merged commit f15d431 into ethereum:master May 16, 2019
@pipermerriam pipermerriam deleted the piper/extended-debug-logging branch May 16, 2019 03:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants