0

I wanted to override the structlog logger for the whole application, by doing this:

import enum
from collections.abc import Iterable

import structlog
from structlog.typing import Processor
from typing_extensions import override


class WarningCode(enum.Enum):
    ...


class WrappedLogger(structlog.stdlib.BoundLogger):
    @override
    def warning(self, event, code: WarningCode, **kw):  # type: ignore[override]
        # i.e. make code required here, for tracking in DataDog
        return self._proxy_to_logger('warning', event, code=code, **kw)

    def trace(self, event, *args, **kw):
        # to be replaced in future by debug() call
        return self._proxy_to_logger('info', event, *args, **kw)

    def debug_sensitive(self, event, **kw):
        """
        Log sensitive debug information (such as containing PII).
        """

        # will implement some mechanism for not logging sensitive information certain environments in the future
        return self._proxy_to_logger('debug', event, **kw)


def configure_structlog(processors: Iterable[Processor]) -> None:
    return structlog.configure(
        processors=processors,
        logger_factory=structlog.stdlib.LoggerFactory(),
        cache_logger_on_first_use=True,
        wrapper_class=WrappedLogger,
    )

Then I use it in my Django application config like this:

_STRUCTLOG_PROCESSORS: list[Processor] = [
    *_SHARED_PROCESSORS,
    dev_formatter,
    structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
]

configure_structlog(
    processors=_STRUCTLOG_PROCESSORS,  # type: ignore
)

In application code I instantiate the logger by doing get_logger():

from structlog import get_logger

logger = get_logger(__name__)

However, logger here is of type Any. And if someone forgets that code parameter is required, or that trace function is called trace and not trace_log or something, the application will fail in runtime, which I want to avoid.

How can I redefine get_logger return type so that it returns my WrappedLogger instead?

I've tried doing mypy stubs like this:

mypy.ini

[mypy]
mypy_path = stubs

and in stubs do this structure:

stubs/
  structlog/
    __init__.py # re-define ONLY the get_logger() function here.

But this results in a lot of mypy errors, which tell me that:

admin_project/settings_dev.py:22: error: Module has no attribute "ProcessorFormatter"  [attr-defined]
admin_project/settings_dev.py:35: error: Module has no attribute "ProcessorFormatter"  [attr-defined]
admin_project/settings_dev.py:37: error: Module has no attribute "ProcessorFormatter"  [attr-defined]
admin_project/settings_dev.py:38: error: Module has no attribute "dev"  [attr-defined]

How can I redefine only the get_logger part of the structlog exports? Or how can I "extend" the existing stubs, without copying all of them into my codebase?

Is there a better practice of adding new methods / requirements to logger calls in structlog?

2
  • 4
    IMO you're making this wayyyy too complicated. structlog.get_logger is annotated with a return type of Any probably to specifically allow you to explicitly annotate the assigned type. Remove the whole stubs idea and just do logger: WrappedLogger = get_logger(__name__). Commented Oct 1 at 3:52
  • probably, the easiest way is to instantiate whatever logger you want in a separate common file and import/use instantiated logger wherever you need Commented Oct 1 at 14:45

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.