-
-
Notifications
You must be signed in to change notification settings - Fork 222
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
exc_info is not resolved properly when calling BoundLogger.exception #571
Comments
Got a small code snippet to demo this behavior? |
Sure, I've put together a gist with a minimal example: https://gist.github.com/bcdickinson/b5ec383a518995addc0df629f6c7a83f |
Gotcha, I see now. Will look at the codebase to see if there is a bug/better way, but here is a Also, as stated on the
This was missing (part of the update to import logging.config
import structlog.dev
import structlog.processors
import structlog.stdlib
def fixup_event(
logger: logging.Logger, method_name: str, event_dict: structlog.typing.EventDict
) -> structlog.typing.EventDict:
if method_name != "error":
return event_dict
if event_dict.get('exc_info') == True:
event_dict['exc_info'] = structlog.processors._figure_out_exc_info(event_dict['exc_info'])
return event_dict
structlog.configure(
processors=[fixup_event, structlog.stdlib.ProcessorFormatter.wrap_for_formatter],
logger_factory=structlog.stdlib.LoggerFactory(),
wrapper_class=structlog.stdlib.BoundLogger,
cache_logger_on_first_use=True,
)
class CustomHandler(logging.Handler):
"""
The idea here is still that this understands structlog-y records that have
been through ProcessorFormatter.wrap_for_formatter, but it doesn't then use
ProcessorFormatter to format them.
"""
def emit(self, record: logging.LogRecord) -> None:
message, exc_info = record.getMessage(), record.exc_info
if isinstance(record.msg, dict):
message, exc_info = record.msg['event'], record.msg.get('exc_info', None)
print(
f"level: {record.levelname}; "
f"msg: {message}; "
f"exc_info: {exc_info}"
)
if __name__ == "__main__":
logging.config.dictConfig(
{
"version": 1,
"formatters": {
"console": {
"()": structlog.stdlib.ProcessorFormatter,
"processors": [
structlog.stdlib.ProcessorFormatter.remove_processors_meta,
structlog.processors.JSONRenderer(),
],
}
},
"handlers": {
"normal_handler": {
"class": "logging.StreamHandler",
"formatter": "console",
"stream": "ext://sys.stdout",
},
"custom_handler": {
"class": "__main__.CustomHandler",
},
},
"loggers": {
"structlog_logger": {
"handlers": [
"normal_handler",
"custom_handler",
],
"level": "DEBUG",
"propagate": False,
},
"non_structlog_logger": {
"handlers": [
"normal_handler",
"custom_handler",
],
"level": "DEBUG",
"propagate": False,
},
},
}
)
structlog_logger = structlog.get_logger("structlog_logger")
non_structlog_logger = logging.getLogger("non_structlog_logger")
structlog_logger.info("structlog info log")
print()
non_structlog_logger.info("non-structlog info log")
print()
try:
raise Exception("Bang!")
except Exception:
structlog_logger.exception("structlog exception log")
print()
non_structlog_logger.exception("non-structlog exception log") Now produces: {"event": "structlog info log"}
level: INFO; msg: structlog info log; exc_info: None
{"event": "non-structlog info log"}
level: INFO; msg: non-structlog info log; exc_info: None
{"exc_info": ["<class 'Exception'>", "Exception('Bang!')", "<traceback object at 0x103bfe840>"], "event": "structlog exception log"}
level: ERROR; msg: structlog exception log; exc_info: (<class 'Exception'>, Exception('Bang!'), <traceback object at 0x103bfe840>)
{"event": "non-structlog exception log", "exc_info": ["<class 'Exception'>", "Exception('Bang!')", "<traceback object at 0x103bfe840>"]}
level: ERROR; msg: non-structlog exception log; exc_info: (<class 'Exception'>, Exception('Bang!'), <traceback object at 0x103bfe840>) |
Thanks for looking into this! I tried just monkey patching Monkey patch: def monkey_patched_boundlogger_exception(
self: structlog.stdlib.BoundLogger,
event: str | None = None,
*args: Any,
**kw: Any,
) -> Any:
kw.setdefault("exc_info", True)
return self._proxy_to_logger("exception", event, *args, **kw)
structlog.stdlib.BoundLogger.exception = monkey_patched_boundlogger_exception structlog exception log output:
EDIT 2 - I've realised the above is wrong. The issue seems to be that |
Hi @pahrohfit, I've put together a PR (#572) that fixes my issue and keeps all the tests green and I'd really appreciate your feedback on whether this needs more documentation or tests or anything. |
Structlog now correctly uses stdlib's `logging.exception` handler when we call `structlog_logger.exception`. Issue: hynek/structlog#571 Fix: hynek/structlog#572
I have a slightly unusual setup that I should explain first. I'm using the maximalist approach of routing all of my structlog and stdlib logging through
ProcessorFormatter
as per the docs, but we're also sending logs atERROR
and above to Rollbar usingpyrollbar
and its RollbarHandler.Unfortunately,
RollbarHandler
on its own doesn't play nicely with that approach because it doesn't callself.format()
anywhere meaning we don't have anywhere to plug inProcessorFormatter
. So, we have a custom subclass (calledStructlogRollbarHandler
) that does similar things toProcessorFormatter.format()
in itsemit()
method before calling the superclassemit()
method to send the payload to Rollbar. A notable exception to that is using the rawrecord.msg
rather thanrecord.getMessage()
as per #520.That means that it's really important for us that all our
LogRecord
objects, whether fromstructlog
or stdlib loggers, look the same when they get to theHandler.emit(record)
stage of the pipeline, because we can't rely onProcessorFormatter
or any structlog renderers patching things up for us. This is mostly working fine, apart from calls toBoundLogger.exception
.When we call
BoundLogger.exception
, theLogRecord
that gets to ourStructlogRollbarHandler.emit(record)
method has an event dict for itsrecord.msg
attribute, as you'd expect, and that dict has"exc_info"
in it, but therecord.exc_info
attribute itself isNone
! Obviously this isn't the case for stdlib-originated records.Having spent a while debugging, this seems to be because
BoundLogger.exception
callsself.error
rather than doing something likeself._proxy_to_logger("exception", event, *args, **{"exc_info": True, **kw})
. That means that the wrappedlogging.Logger
thinks this was alogger.error
call and not alogger.exception
call, which changes the behaviour in a really subtle way.All of our
LogRecord
objects fromstructlog
go throughProcessorFormatter.wrap_for_formatter
, which putsexc_info
in therecord.msg
event dict and strips it out of the kwargs. Then, because of the aforementioned change of the effective log method from.exception
to.error
, the stdlib logger doesn't set this back toTrue
for us and thenLogger._log
doesn't callsys.exc_info()
for us.I'm honestly not sure whether this is a bug in my code or
structlog
. I accept that usingProcessorFormatter
alongside other formatters isn't really documented/supported, but it's really close to working perfectly apart from this!Sorry for writing such a long issue, thank you for an amazing library! ❤️
The text was updated successfully, but these errors were encountered: