Repository navigation
🔊(back) log as JSON with django-structlog - #289
kernicPanel wants to merge 7 commits into
Conversation
fa20348 to
b16cb21
Compare
| LOGGING = values.DictValue( | ||
| { | ||
| "version": 1, | ||
| "disable_existing_loggers": False, | ||
| "handlers": { | ||
| "console": { | ||
| "class": "logging.StreamHandler", | ||
| }, | ||
| }, | ||
| "loggers": { | ||
| "main": { | ||
| "handlers": ["console"], | ||
| "level": "DEBUG", | ||
| }, | ||
| }, | ||
| } | ||
| ) |
| "django-parler==2.3", | ||
| "django-redis==5.4.0", | ||
| "django-storages[s3]==1.14.2", | ||
| "django-structlog==10.1.0", |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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
| _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__) |
There was a problem hiding this comment.
seems logger are not specifically named across the project codebase, logger = structlog.get_logger(__name__) is more consistent
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
too broad exception, not a good practice, try to narrow (ie requests.exceptions.HTTPError, requests.exceptions.Timeout)
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
maybe we should split the setting file into chunks
There was a problem hiding this comment.
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.
726c0e7 to
2c4d5c0
Compare
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
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.
event,level,logger,timestamp), extra fields includedrequest_idtaken from theX-Request-IDheader (generated otherwise),with
user_id, on every log of a requestrequest_started,request_finishedandrequest_failedreplacerequest.summaryanddjango.request, withduration_msand the APIerror_nametheir arguments
workspace_idon every log of a migrationX-Request-ID, kept by its retries, andeach attempt is logged with it, its status and its duration
Existing log calls are unchanged: their message goes to the
eventfield.Log queries and dashboards parsing the text format need to be updated.