django-structlog attaches request and user identity to every Django log line
Structured Logging for Django
At a glance
- What is it?
- django-structlog is a Django integration for structlog that binds a request id, user id and client IP to each log record through middleware, so a log line can be traced back to the request that caused it. It supports Django REST framework and django-ninja out of the box, needs Celery configuration for task logging, and requires Python 3.10 and Django 4.2 or newer.
- Who is it for?
- Adopt django-structlog if you run a Django service and need to correlate a log line with the request and user behind it, since the middleware binds request id, user id and IP to every record. Do not adopt it if you are on Django below 4.2 or Python below 3.10, or if your log pipeline already handles correlation another way.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 11 days ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What it adds to a Django log line
django-structlog is a structured logging integration for a Django project using structlog. The README states the benefit in one line: logging will then produce additional cohesive metadata on each log that makes it easier to track events or incidents.
The comparison in the README makes the difference concrete. With standard logging:
>>> import logging
>>> logger = logging.get_logger(__name__)
>>> logger.info("An error occurred")which produces a line with no structure at all:
An error occurredThe README's own verdict on that is Well... ok. With django-structlog and the flat_line renderer, the same call becomes:
>>> import structlog
>>> logger = structlog.get_logger(__name__)
>>> logger.info("an_error_occurred", bar="Buz")and the output line carries the whole context:
timestamp='2019-04-13T19:39:31.089925Z' level='info' event='an_error_occurred' logger='my_awesome_project.my_awesome_module' request_id='3a8f801c-072b-4805-8f38-e1337f363ed4' user_id=1 ip='0.0.0.0' bar='Buz'So the value of the library is not the structured logging itself, which structlog already does. It is the four fields the middleware adds: request_id, user_id and ip alongside the event name, plus the standard timestamp, level and logger name. The event name has also changed from a prose sentence to a machine-searchable token, an_error_occurred, which is what makes grouping possible.
And the payoff is the search. The README shows it directly:
$ cat logs/flat_line.log | grep request_id='3a8f801c-072b-4805-8f38-e1337f363ed4'One request id and you have every line that request produced. With the json renderer the same query goes through jq instead:
$ cat logs/json.log | jq '.[] | select(.request_id="3a8f801c-072b-4805-8f38-e1337f363ed4")' -sThat is the whole argument for the library: correlation identifiers attached automatically, so an investigator does not have to reconstruct it from timestamps and paths.
Installation is one package, one app and one middleware
The Getting Started section is short, which is a good sign for a middleware integration.
The install:
pip install django-structlogThen the app has to be registered, which is the standard Django pattern:
INSTALLED_APP = [
# ...
"django_structlog",
# ...
]And the middleware:
MIDDLEWARE = [
# ...
"django_structlog.middlewares.RequestMiddleware",
]Those two entries are the whole integration on the Django side. The middleware is what populates the request id and the user id, and because it is middleware rather than a decorator or a logging filter, it applies to every request without touching the view code.
Then there is the part that is not optional and not short, which is the structlog configuration in settings.py. The README says to add appropriate structlog configuration to your settings.py, and the configuration it shows defines a LOGGING dict with version 1, disable_existing_loggers set to False, and a formatters section.
That formatters section is where the choice of renderer is made. The json_formatter entry uses structlog.stdlib.ProcessorFormatter with a processor of structlog.processors.JSONRenderer(), the plain_console entry uses the same ProcessorFormatter with structlog.dev.ConsoleRenderer(), and a third entry, key_value, also uses structlog.stdlib.ProcessorFormatter with a processor beginning structlog.stdlib.Proc, which is the key-value renderer that produces the flat_line output shown in the comparison above.
Three renderers in one configuration is the useful detail. The same application can emit JSON to a file for machine consumption, a formatted console output for a developer running the server locally, and key-value lines for a log aggregator that prefers them. The README's comparison demonstrates the first two, and the configuration exposes the third.
Because ProcessorFormatter is used throughout, the standard library loggers can be routed through the same processors as structlog, which is what stops the Django request logs and the application logs from ending up in two incompatible formats.
The DRF caveat is the detail to read before you adopt
The README has an Additional Popular Integrations section, and the Django REST framework entry contains the single most practically important caveat in the document.
Django REST framework is supported by default. But when using it with rest_framework.authentication.TokenAuthentication, or other DRF authentications, user_id will only be in request_finished and request_failed instead of each log.
The README points to issue 37 for details. The reason is straightforward once you understand the ordering. The middleware runs before the view, and at that point in the request cycle the authentication has not happened yet, so there is no authenticated user on the request for the middleware to read. By the time the response is finished and the middleware records request_finished or request_failed, the user is known.
That has a direct consequence for how you use the logs. If your API is token-authenticated, the per-request log lines will have a request id but no user id, and the user id appears only on the two lifecycle events. Any dashboard or alert that assumes every line has a user id will be wrong for the majority of lines, and correlation from a user back to their request activity requires the request id on the lifecycle lines to join on.
It is worth checking which authentication classes are actually enabled before deciding the library fits, because with session authentication the user is resolved by the session middleware before your middleware runs, and the problem does not arise in the same way.
The django-ninja entry is shorter and free of caveats: django-ninja is supported by default. That is a nice piece of coverage, since django-ninja is a common alternative to DRF and it is a Pydantic-based framework whose request handling differs enough that support is not automatic.
The third entry is Celery, and it is the one with extra work rather than extra support. Celery's task logging requires additional configurations, and the README points to the celery documentation page for the details. A Celery task does not go through Django's request middleware, so the request id has to be propagated into the task context by other means, which is why the setup is not just the two Django entries.
What the project ships alongside the library
The repository is more than the package, and the extra parts tell you how seriously the project treats the integration.
There is a django_structlog_demo_project/ directory, which is a working Django application using the library rather than a toy snippet, plus a test_app/ directory for the test suite. There are docs/ and a compose/ directory holding the Dockerfiles for the local development environment.
The docker-compose.yml at the root describes that environment in detail, and it is a useful picture of what a project using this library tends to need. The django service builds from ./compose/local/django/Dockerfile with PYTHON_VERSION 3.14 as a build argument, mounts the source at /app:cached, and loads its environment from three files in ./.envs/.local/ for django, redis and postgres. The port mapping is 8000:8000, and there is a comment on the tty setting that explains why it is there: tty is needed for colors to show in console logs.
That comment is a small but revealing detail. It means the structured output is colour-aware, which matters when you are reading a console log locally and does not matter when the output is being piped.
The compose file also defines three Django variants against the same image: django on 8000 with debug on, django_wsgi on 8001 and django_asgi on 8002 with DJANGO_DEBUG set to False. That is a deliberate coverage of the deployment modes that behave differently under middleware, and it is the kind of thing a library that claims to work in production would test.
Alongside those there are the celeryworker and celerybeat services, both depending on redis and postgres, both running the same local Django image with the /start-celeryworker command, and both carrying the same tty comment. And a redis service on 6379 and a postgres service with named volumes for local_postgres_data and local_postgres_data_backups.
The top level entries also include requirements.txt with a single line, -r requirements/local.txt, a requirements/ directory, a .pre-commit-config.yaml, an .editorconfig, a MANIFEST.in, a LICENSE.rst and a .readthedocs.yaml. The licence is MIT, recorded in the pyproject as license = { text = "MIT" }, with the classifiers including License :: OSI Approved :: MIT License and Development Status :: 5 - Production/Stable.
Where django-structlog is the wrong choice
The first limit is the version floor. The pyproject requires Python 3.10 or newer and django>=4.2, and the classifiers list Django 4.2 through 5.2 and Python 3.10 through 3.14. If you are on an older Django, the library is not an option, and given how long Django 4.2 has been the LTS line that is a narrowing constraint for anyone on a long-lived deployment.
The second limit is that the library does the correlation, not the logging. It is a middleware plus a structlog configuration, and it requires you to already be using structlog or to adopt it. If your logs are plain Django logging with a text format, adopting this library means adopting structlog as well, and that is the larger change.
The third limit is what happens outside the request cycle. The Celery entry says task logging requires additional configurations, and the missing request id on a Celery task is not a bug in the middleware but a consequence of there being no request. Background jobs, management commands, cron tasks and the Django shell all fall outside what the middleware can see, so a log line from a Celery worker has no request id and no user id unless you propagate them yourself.
The fourth is the DRF caveat again, seen from the other side. If you rely on user id being present on every line for a per-user audit trail, token-authenticated APIs will leave gaps in that trail, and the gaps are exactly the individual request lines.
The alternative to weigh is doing it with structlog and Django's own facilities, without this package. Django's request logging already produces messages that include the path and the status code, and structlog's contextvars support can carry a binding per request through a middleware you write yourself. That is roughly a page of code. The difference in approach is ownership: django-structlog is a maintained integration with a known set of behaviours, DRF and django-ninja support, tests and documentation, and it saves you from making the same choices about which fields to bind and where to bind them. Writing it yourself is fewer dependencies and exactly the behaviour you want; adopting this is less code and fewer decisions.
Maintenance, dependencies and the upgrade picture
On maintenance, the last push was on 2026-10-12 and the repository is not archived, so the code is being worked on. The repository has no GitHub releases, so the version to track is the one on PyPI, and the project is published on Django Packages as well as PyPI, with wheels available for the supported Python versions.
The dependency list in pyproject is short and tells you what the library is doing. There is django>=4.2 and structlog>=21.4.0, plus asgiref>=3.6.0 and django-ipware>=6.0.2.
Those last two are the interesting ones. django-ipware is there to resolve the client IP address, and it does so by consulting the HTTP_X_FORWARDED_FOR header and the set of addresses that are considered proxies. That is the mechanism behind the ip field in the example log line, and it also means the value is only as trustworthy as your proxy configuration. If the application is not behind a proxy that strips or rewrites X-Forwarded-For, a client can set that header and put a false address into every log line. Whether django-ipware is configured to trust those headers is worth checking before you treat ip as evidence.
asgiref is there for async support, and its presence along with the Django 5.2 classifier tells you the middleware works under ASGI as well as WSGI, which matches the three compose services in the demo environment.
The optional extras are small. The celery extra pulls celery>=5.1, and the commands extra pulls django-extensions>=1.4.9, which is for the management commands the library provides.
The version is dynamic, read from django_structlog.__version__ by setuptools, and the changelog is hosted on the documentation site rather than as a release history in the repository, which is consistent with a project that publishes to PyPI and Django Packages rather than to GitHub releases.
The licence is MIT with a LICENSE.rst at the root. That is a permissive grant with no copyleft obligation on a project that vendors this into a commercial application, and it is a licensing note rather than legal advice.
Editorial conclusion
Adopt django-structlog if you run a Django service and need to correlate a log line with the request and user behind it, since the middleware binds request id, user id and IP to every record. Do not adopt it if you are on Django below 4.2 or Python below 3.10, or if your log pipeline already handles correlation another way. Verify first that the DRF authentication method in use populates the user on the request, because the README notes that with TokenAuthentication the user id appears only in request_finished and request_failed, and that Celery task logging needs separate configuration.
Frequently asked questions
How do I install django-structlog and add it to a Django project?
Run pip install django-structlog, add django_structlog to INSTALLED_APP, and add django_structlog.middlewares.RequestMiddleware to MIDDLEWARE. Then add structlog configuration to settings.py, defining the formatters with structlog.stdlib.ProcessorFormatter.
What does django-structlog add to each log record?
The middleware binds a request_id, a user_id and the client ip to each record alongside the standard fields, and the example flat_line output shows timestamp, level, event, logger, request_id, user_id, ip and any extra key-value pairs you passed to the logger.
Does django-structlog work with Django REST framework and django-ninja?
Both are supported by default. django-ninja is supported out of the box, and DRF is supported by default, but the README notes that with rest_framework.authentication.TokenAuthentication or other DRF authentications, user_id will only be in request_finished and request_failed instead of each log.
How do I get django-structlog to log Celery tasks?
Celery's task logging requires additional configurations beyond the Django middleware setup, and the README points to the celery page of the documentation for the details. A Celery task has no incoming HTTP request, so the request context has to be propagated into the task separately.
What versions of Python and Django does django-structlog support?
The pyproject requires Python 3.10 or newer and django>=4.2, and the classifiers list Django 4.2 through 5.2 and Python 3.10 through 3.14. The runtime dependencies are structlog, asgiref and django-ipware, and the licence is MIT.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/jrobichaud-django-structlog)Was this useful?