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

Added CallsiteNamespaceAdder processor #570

Closed
wants to merge 37 commits into from

Conversation

pahrohfit
Copy link
Contributor

@pahrohfit pahrohfit commented Nov 2, 2023

Summary

Added a namespace event entry processor as AddCallingNamespace -- replacing #568 with a recommendation from @macintacos to change the references from class_path to namespace

Referenced in #386
Referenced in #492

Takes an optional set or list to limit which logging levels to add this at, as lookups potentially can add overhead (though should run in N(1)).

Pull Request Check List

  • Do not open pull requests from your main branch – use a separate branch!
    • There's a ton of footguns waiting if you don't heed this warning. You can still go back to your project, create a branch from your main branch, push it, and open the pull request from the new branch.
    • This is not a pre-requisite for your your pull request to be accepted, but you have been warned.
  • Added tests for changed code.
    • The CI fails with less than 100% coverage.
  • New APIs are added to our typing tests in api.py.
  • Updated documentation for changed code.
    • New functions/classes have to be added to docs/api.rst by hand.
    • Changed/added classes/methods/functions have appropriate versionadded, versionchanged, or deprecated directives.
      • The next version is the second number in the current release + 1. The first number represents the current year. So if the current version on PyPI is 23.1.0, the next version is gonna be 23.2.0. If the next version is the first in the new year, it'll be 24.1.0.
  • Documentation in .rst and .md files is written using semantic newlines.
  • Changes (and possible deprecations) are documented in the changelog.
  • Consider granting push permissions to the PR branch, so maintainers can fix minor issues themselves without pestering you.

@pahrohfit
Copy link
Contributor Author

pahrohfit commented Nov 2, 2023

As mentioned in the original PR #568 , the use of collections.deque is solely to pass the PR, as the more direct for loop had a break call, which was showing a partially complete branch in coverage.

Sample code:

foo/sub_module.py

import structlog


logger = structlog.get_logger()

async def async_logging(l):
    await logger.ainfo(l)

class SubModuleClassBasedCall:
    def class_sync_logging(l):
        logger.info(l)

    async def class_async_logging(l):
        await logger.adebug(l)

demo.py

import asyncio
import structlog

from foo import sub_module
from foo.sub_module import SubModuleClassBasedCall

structlog.configure(
    processors=[
        structlog.processors.CallsiteNamespaceAdder(levels={}),
        structlog.processors.JSONRenderer(),
    ],
    context_class=dict,
    cache_logger_on_first_use=True,
)
logger = structlog.get_logger()


def sync_logging(l):
    logger.debug(l)


class ClassBasedCall:
    async def class_async_logging(l):
        await logger.adebug(l)


if __name__ == "__main__":
    logger.info("hello from __main__ direct")
    sync_logging("hello from __main__ via function")
    sub_module.SubModuleClassBasedCall.class_sync_logging(
        "hello from sub_module.class sync logging"
    )
    SubModuleClassBasedCall.class_sync_logging(
        "hello from sub_module imported class sync logging"
    )

    loop = asyncio.get_event_loop()
    loop.run_until_complete(
        ClassBasedCall.class_async_logging(
            "hello from __main__ class async logging"
        )
    )
    loop.run_until_complete(
        sub_module.async_logging(
            "hello from sub_module function async logging"
        )
    )
    loop.run_until_complete(
        SubModuleClassBasedCall.class_async_logging(
            "hello from sub_module imported class async logging"
        )
    )
    loop.close()
$ python demo.py
{"event": "hello from __main__ direct", "namespace": "__main__.main"}
{"event": "hello from __main__ via function", "namespace": "__main__.sync_logging"}
{"event": "hello from sub_module.class sync logging", "namespace": "foo.sub_module.SubModuleClassBasedCall.class_sync_logging"}
{"event": "hello from sub_module imported class sync logging", "namespace": "foo.sub_module.SubModuleClassBasedCall.class_sync_logging"}
{"event": "hello from __main__ class async logging", "namespace": "__main__.ClassBasedCall.class_async_logging"}
{"event": "hello from sub_module function async logging", "namespace": "foo.sub_module.async_logging"}
{"event": "hello from sub_module imported class async logging", "namespace": "foo.sub_module.SubModuleClassBasedCall.class_async_logging"}

@pahrohfit pahrohfit mentioned this pull request Nov 2, 2023
9 tasks
@pahrohfit
Copy link
Contributor Author

@hynek Thoughts?

@hynek hynek changed the title Added AddCallingNamespace processor Added CallsiteNamespaceAdder processor Nov 13, 2023
def __call__(
self, logger: WrappedLogger, name: str, event_dict: EventDict
) -> EventDict:
if self.levels and name not in self.levels:
Copy link
Owner

Choose a reason for hiding this comment

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

I think using levels as a container doesn't make a lot of sense given how it's used elsewhere. Unless you have a good reason for it, I think it should be an int that is compared against.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved to int

src/structlog/processors.py Outdated Show resolved Hide resolved
tests/test_processors.py Outdated Show resolved Hide resolved
tests/test_processors.py Outdated Show resolved Hide resolved
tests/test_processors.py Outdated Show resolved Hide resolved
f"{member.__module__}.{member.__qualname__}"
)
# we found our code match, can stop looking
namespace_found = True
Copy link
Owner

Choose a reason for hiding this comment

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

A simple break should do, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I had that, but it was failing coverage tests due to the early exit to the loop. Re-wrote it this way to hit the 100% mark.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I reverted back to break based logic

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Name                       Stmts   Miss Branch BrPart  Cover   Missing
----------------------------------------------------------------------
src/structlog/_frames.py      47      0     15      1    98%   104->115
----------------------------------------------------------------------
TOTAL                       1781      0    462      1    99%

18 files skipped due to complete coverage.
Coverage failure: total of 99 is less than fail-under=100
Error: Process completed with exit code 2.

There is the coverage fault on the break logic.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I got creative and worked around it without needing to nocover it.

Copy link
Owner

Choose a reason for hiding this comment

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

What this is telling you means that you're missing branch coverage. I suspect it means that the loop never loops twice (aka: the break is always executed)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Correct — which is why the “should operate at O(1)” comment… loop is to be thorough for edge cases.

I got it cleaned and passing

Copy link
Owner

Choose a reason for hiding this comment

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

OK, so this is actually great, because I honestly don't quite understand what/how the code is doing.

Could you please in plain words explain to me what it's doing? Finding a test case where it needs more than one iteration is very helpful for my understanding.

src/structlog/processors.py Outdated Show resolved Hide resolved
src/structlog/processors.py Outdated Show resolved Hide resolved
CHANGELOG.md Outdated Show resolved Hide resolved
docs/api.rst Outdated Show resolved Hide resolved
pahrohfit and others added 9 commits November 14, 2023 11:35
Co-authored-by: Hynek Schlawack <[email protected]>
Co-authored-by: Hynek Schlawack <[email protected]>
Co-authored-by: Hynek Schlawack <[email protected]>
Co-authored-by: Hynek Schlawack <[email protected]>
Co-authored-by: Hynek Schlawack <[email protected]>
Co-authored-by: Hynek Schlawack <[email protected]>
Co-authored-by: Hynek Schlawack <[email protected]>
@pahrohfit pahrohfit requested a review from hynek November 14, 2023 20:07
identified_namespace = f"{cls.__module__}.{frame.f_code.co_name}"
if inspect.isfunction(member) and member.__code__ == frame.f_code:
# we found our code match, can stop looking
"""identified_namespace = f"{member.__module__}.{member.__qualname__}" """
Copy link
Owner

Choose a reason for hiding this comment

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

What is this?

@hynek
Copy link
Owner

hynek commented Nov 23, 2023

No rush, but I'm probably gonna push a PyPI release soon-ish, but if you'd like this to be part of it, we should try to wrap things up here.

@hynek hynek closed this Apr 2, 2024
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.

2 participants