Make the `structlog` integration for Sentry logs
Problem Statement
structlog logs can be sent to Sentry via the existing LoggingIntegration; however, this does not preserve the structuring of the logs (e.g. if the logs have custom attributes set, these are included in the log message). See this example
Solution Brainstorm
Instead, create a dedicated structlog integration, which would add these logs to the log event as custom attributes.
Hi @sector119, thank you for raising this issue.
While we would be open to considering adding a structlog integration, it is already possible to get structlog to work by calling structlog.stdlib.recreate_defaults() near the beginning of your application's lifecycle, ideally right after the sentry_sdk.init() call.
Calling structlog.stdlib.recreate_defaults() causes all of your structlog logs to be routed through Python's default logging library. That way, the Sentry SDK's logging integration can pick them up.
Here is an example of how you can get your structlog logs to show up in Sentry.
import sentry_sdk
import structlog
def main():
sentry_sdk.init(
dsn="your dsn here",
_experiments={
"enable_logs": True,
},
)
structlog.stdlib.recreate_defaults() # Add this call
logger = structlog.get_logger()
logger.info("Hello, world!") # This gets logged in Sentry
if __name__ == "__main__":
main()
Does this solution work for you? If yes, please close the issue. If not, please explain why this does not work, and we will use that information to inform our decision on whether to create a dedicated structlog integration.
Does this solution work for you? If yes, please close the issue. If not, please explain why this does not work, and we will use that information to inform our decision on whether to create a dedicated
structlogintegration.
Thanks a lot, it works for me, but that is not what I exactly want to get.
for example I perform
logger.error("Account not found", person_id=person_id, street_id=street_id, building=building, apartment=apartment)
with structlog json formatter I get message like this, so I have message and arguments logged separately
{"person_id":2036273,"street_id":null,"building":null,"apartment":null,"event":"Account not found","level":"error","logger":"epsilon-xmlrpc","timestamp":"2025-05-27T00:05:19.820515","sentry_id":"f54adb5462254e0fb42111e3d3149452"}
I thought that with sentry logging I would get something similar, so I would be able to search by all that args
Right now I just have message with all args in the same line (
2025-05-27 00:01:20 [error ] Account not found [epsilon-xmlrpc] apartment=None building=None person_id=2036273 street_id=None
Ah okay, I understand now! Basically, you would like us to attach this information as attributes onto the log that we send to Sentry.
I believe this is a completely valid request and should be possible for us to implement, so I will add this to our backlog as a feature request.
Ah okay, I understand now! Basically, you would like us to attach this information as attributes onto the log that we send to Sentry.
Yes, as attributes, hints or something similar that we can search by
Thank you
The integration would work differently to integrations for logging or loguru because structlog is a framework for transforming structured logs before they are handled by a wrapped logger. The wrapped logger could be provided by logging, loguru, be one of structlog's own implementations or come from anywhere else.
Structlog calls the wrapper class a bound logger. It does not seem straightforward to hook Sentry up in the bound logger, because the available logging methods ultimately depend on the wrapped logger. See https://www.structlog.org/en/stable/typing.html.
Logs can be transformed by structlog by a chain of processors. The logging severity is only indirectly available in the processors via the method name invoked on the wrapped logger. See https://www.structlog.org/en/stable/processors.html
The last processor is referred to as a renderer in the structlog docs. The structlog.processors.JSONRenderer returns a string. The attributes are therefore lost before reaching the wrapped logger.
One option is providing a SentryProcessor users can inject at a sensible place in their processing chain. The best place in the chain is likely the processor immediately before the structlog's event dictionary is turned into a string by one of the user's processors. The problem with the approach is that the severity is not available in the processing chain, and Sentry logs require a severity field.
If it helps, I ended up writing my own integration for Sentry logging for Structlog. I initially tried processors but that ended up having issues, I think it was also reading the logs in from stdout and duplicating them in Sentry. I ended up using a handler and that seems to be working pretty well for me.
import logging
from sentry_sdk.logger import _capture_log
SEVERITY_NUMBER = {
"trace": 1,
"debug": 5,
"info": 9,
"warning": 13,
"error": 17,
"fatal": 21,
}
class SentryHandler(logging.Handler):
def emit(self, record: logging.LogRecord) -> None:
if not isinstance(record.msg, dict):
return
kwargs = dict(record.msg)
level = kwargs.pop("level")
# _capture_log will try to .format the string so escape any curly braces
event = str(kwargs.pop("event")).replace("{", "{{").replace("}", "}}")
_capture_log(level, SEVERITY_NUMBER[level], event, **kwargs)
This was in Django so I then configured my logs something like this
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"filters": {
"filter_uptime": {
"()": "my_project.logging.ExcludeRequestPathFilter",
"exclude_request_path": "/uptime",
},
},
"formatters": {
"json_formatter": {
"()": structlog.stdlib.ProcessorFormatter,
"processor": structlog.processors.JSONRenderer(),
"foreign_pre_chain": [
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.StackInfoRenderer(),
structlog.processors.format_exc_info, # Capture full traceback
],
},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "json_formatter",
"filters": ["filter_uptime"]
},
"sentry": {
"class": "my_project.sentry_logging.SentryHandler",
"filters": ["filter_uptime"]
},
},
"loggers": {
"django_structlog": {
"handlers": ["console", "sentry"],
"level": "INFO",
"filters": ["filter_uptime"]
},
"django": {
"handlers": ["console", "sentry"],
"level": "INFO",
"filters": ["filter_uptime"]
},
...
},
}
This had the added benefit of working with any filters we applied to the logging too, so we could filter out logs from things like our uptime probe or Prometheus integration