Skip to content

🔊(back) log as JSON with django-structlog - #289

Open
kernicPanel wants to merge 7 commits into
mainfrom
structlog
Open

kernicPanel wants to merge 7 commits into
mainfrom
structlog

Conversation

@kernicPanel

@kernicPanel kernicPanel commented Oct 27, 2026 •

Copy link
Copy Markdown
Collaborator

Purpose

A failed migration has to be matched with the Drive logs of the same
request, and the logs of concurrent migrations can't be told apart.

Proposal

Write every log as a JSON line with its context, using django-structlog.

  • JSON logs (event, level, logger, timestamp), extra fields included
  • request_id taken from the X-Request-ID header (generated otherwise),
    with user_id, on every log of a request
  • request_started, request_finished and request_failed replace
    request.summary and django.request, with duration_ms and the API
    error_name
  • Celery tasks keep the context of the request that sent them, without
    their arguments
  • workspace_id on every log of a migration
  • Every outgoing call sends an X-Request-ID, kept by its retries, and
    each attempt is logged with it, its status and its duration
  • Request id appended to the gunicorn access log
  • Readable key-value lines in development instead of JSON

Existing log calls are unchanged: their message goes to the event field.
Log queries and dashboards parsing the text format need to be updated.

Comment on lines -820 to -836
LOGGING = values.DictValue(
{
"version": 1,
"disable_existing_loggers": False,
"handlers": {
"console": {
"class": "logging.StreamHandler",
},
},
"loggers": {
"main": {
"handlers": ["console"],
"level": "DEBUG",
},
},
}
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nice catch

"django-parler==2.3",
"django-redis==5.4.0",
"django-storages[s3]==1.14.2",
"django-structlog==10.1.0",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

why not using v11?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

v11 requires Celery 5.5+ (we're on 5.3.6) and stops trusting X-Request-ID by default, which this PR relies on. We'll upgrade separately.

def test_logging_request_user(console_logs):
"""A request should be logged with the id of its user."""
user = factories.UserFactory()
client = APIClient()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think you can have a client in test parameters with pytest-django

https://pytest-django.readthedocs.io/en/stable/helpers.html#client-django-test-client

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done, thanks.

Comment thread src/backend/core/retrying_session.py Outdated
_QUERY_IN_TEXT_RE = re.compile(r"\?\S*")

# Logs the calls of every service, whose retries go to the logger of the service
request_logger = structlog.get_logger(__name__)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

seems logger are not specifically named across the project codebase, logger = structlog.get_logger(__name__) is more consistent

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done. The module logger is now logger; the session's parameter became retry_logger to avoid shadowing it.

started_at = time.monotonic()
try:
response = super().request(method, url, *args, **kwargs)
except Exception as error:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

too broad exception, not a good practice, try to narrow (ie requests.exceptions.HTTPError, requests.exceptions.Timeout)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This only logs the attempt and re-raises right after, so nothing is swallowed. Keeping it broad means every failed attempt gets its log line.

from sentry_sdk.integrations.django import DjangoIntegration
from sentry_sdk.integrations.logging import ignore_logger

# pylint: disable=too-many-lines

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

maybe we should split the setting file into chunks

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Agreed it's getting long, but it's unrelated to logging. Better in its own PR.

A failed migration has to be matched with the Drive logs of the same
request. Each log becomes a JSON object with the request id, taken from
the X-Request-ID header, the user, the logger and the level. The Celery
tasks keep the context of the request that sent them, but not their
arguments, which can hold user data.

The request logs of django-structlog replace request.summary and
django.request, with the duration and the API error name. The access
log gets the request id too. The tests now use the deployed logging.
Read the console lines as production writes them, to pin the fields a
request or a task is traced by: request id, user, duration, API error
name, traceback, and the request id carried over to the Celery tasks.
Most logs of a migration did not tell its workspace, so the logs of the
migrations running at the same time could not be told apart. The
workspace id is now bound to the context of the migration task.
A failed call to Drive or Resana could not be found in their logs. Every
call now sends an X-Request-ID header, which Drive takes as the request
id of its logs, and each attempt is logged with it, its status or error,
and its duration. The attempts of a call keep the same id, including the
retry loops outside the session: item creation, upload-ended and the
Resana download. The S3 upload is logged without its path, which holds
the file name.
The JSON lines of the deployed logs are hard to read in a terminal.
The development configuration renders them as colored key-value lines,
with the tracebacks in clear.
kernicPanel and others added 2 commits November 5, 2026 15:29
The client fixture of pytest-django already does what these tests need.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LzBgHnj8LE8FhuX6gT72hz
Name the module logger like the other modules, and the service one apart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LzBgHnj8LE8FhuX6gT72hz
@kernicPanel
kernicPanel requested a review from martigan November 5, 2026 15:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants