Introduce register_log_level/reset_log_levels - #835
Conversation
These two methods, housed in a dedicated module, call out to register custom log levels across various other components. Registering a log level does the following: - confirm the level and name are good - add the level and name to mappings - add a relevant named method to structlog-provided loggers - add the log level to level-based filtering bound loggers The reset method inverts these steps, removing from these same registered locations in reverse order. Names which are already used by `structlog` are forbidden, so no special checking or "undo rules" are needed for the reset.
sirosen
left a comment
There was a problem hiding this comment.
I've done a self-review to note my high-level questions and concerns.
|
|
||
| while CUSTOM_LOG_LEVELS: | ||
| level = CUSTOM_LOG_LEVELS.pop() | ||
| del LEVEL_TO_NAME[level] |
There was a problem hiding this comment.
Is a new module the right place for all of this? stdlib didn't feel right to me.
| # NOTSET is a special 0-valued level, but not a method name | ||
| "notset", | ||
| ) | ||
| ) |
There was a problem hiding this comment.
If these names are to be forbidden, I think statically listing them is more legible than trying to be fancy/dynamic. But there's a risk of drift...
Plus, forbidding the names means that adding a new method to this list could be a breaking change! Yuck!
This is one of the things I winced at.
| meths: dict[str, Callable[..., Any]] = {"log": log, "alog": alog} | ||
| for lvl, name in LEVEL_TO_NAME.items(): | ||
| meths[name], meths[f"a{name}"] = make_method(lvl) | ||
| meths[name], meths[f"a{name}"] = _make_method(min_level, lvl) |
There was a problem hiding this comment.
The diff is illegible, but all I did is pop out make_method, add min_level to its args, and dedent it. I needed that for the usage below.
| for bound_logger in LEVEL_TO_FILTERING_LOGGER.values(): | ||
| delattr(bound_logger, level_name) | ||
| delattr(bound_logger, f"a{level_name}") | ||
| del LEVEL_TO_FILTERING_LOGGER[level] |
There was a problem hiding this comment.
Alternatively, these methods could fully rebuild the LEVEL_TO_FILTERING_LOGGER table each time? But that's a lot of repeat work each time a log level gets registered.
| def remove_custom_level_method(level_name: str, /) -> None: | ||
| """Remove all registered log level methods.""" | ||
| for logger_class in (BytesLogger, PrintLogger, WriteLogger): | ||
| delattr(logger_class, level_name) |
There was a problem hiding this comment.
I thought it's slightly more manageable if some of the responsibility here is passed to the component modules. You can see a similar bit of logic added to _native, but this is more clearly isolated.
As above though, we have to patch with setattr/delattr and there's a real feeling here that it would be very easy to make a mistake. e.g., Add a new logger class but not add it here.
Summary
This is open as a draft for initial feedback. I want to confirm whether or not the overall approach is acceptable before working on fleshing out documentation and refining the work further.
This PR is a potential solution for #47.
These two methods, housed in a dedicated module, call out to register custom log levels across various other components. Registering a log level does the following:
The reset method inverts these steps, removing from these same registered locations in reverse order.
Names which are already used by
structlogare forbidden, so no special checking or "undo rules" are needed for the reset.Pull Request Checklist
mainbranch.api.py.docs/api.rstby hand.versionadded,versionchanged, ordeprecateddirectives..rstand.mdfiles is written using semantic newlines.