Skip to content

✨(backend) Audit logging [WIP]#1415

Draft
cameledev wants to merge 2 commits into
mainfrom
audit-logging
Draft

✨(backend) Audit logging [WIP]#1415
cameledev wants to merge 2 commits into
mainfrom
audit-logging

Conversation

@cameledev

@cameledev cameledev commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator

WIP: Open for feedback

Purpose

Description and CSIRT recommendations

Proposal

  • create src/backend/core/audit.py meant to be ported to django-lasuite when approved
  • src/backend/core/audit.py from which you can import a logger (getLogger) and a json formatter (AuditJsonFormatter)
  • The audit logger requires a mandatory request variable (may be None). When passed, multiple information are derived and added to logs (source_ip, source_port etc.)
  • additional kwargs can be passed
  • getLogger may be overriden with project specific serializers (see src/backend/core/audit_meet.py). For example, we can pass logger.info(..., original_room=room), and room.slug, room.name are automatically added to the log (prefixed with the variable name, e.g. original_room.slug)

TODO:

  • complete audit logging coverage, when architecture is approved

Comment thread src/backend/core/audit.py Fixed
Comment thread src/backend/core/tests/test_audit_log.py Fixed
Comment thread src/backend/core/tests/test_audit_log.py Fixed
@sonarqubecloud

Copy link
Copy Markdown

@cameledev cameledev changed the title Add audit logging ✨(backend) Audit logging [WIP] Jun 10, 2026
@cameledev cameledev requested a review from qbey June 18, 2026 09:59
Comment thread src/backend/core/audit.py


def getLogger(
name: str | None = None, custom_serializers: list[tuple | None] = None

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.

?

Suggested change
name: str | None = None, custom_serializers: list[tuple | None] = None
name: str | None = None, custom_serializers: list[tuple] | None = None

Comment thread src/backend/core/audit.py
self,
level,
event_type,
request: HttpRequest | None, # Intentionnaly mandatory

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.

The comment is contradicting the typing, why?
*intentionaly

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.

There is no = None so it's valid from my pov

Comment thread src/backend/core/audit.py
Comment on lines +153 to +170
# One public method per standard logging level, all sharing ``_emit``.
# The ``TYPE_CHECKING`` declarations expose the real signature to editors;
# the ``else`` branch is what runs, binding the level via ``partialmethod``.
if TYPE_CHECKING:
debug: _AuditEmit
info: _AuditEmit
warning: _AuditEmit
error: _AuditEmit
critical: _AuditEmit
# ``exception`` mirrors stdlib: ERROR level with the active traceback.
exception: _AuditEmit
else:
debug = partialmethod(_emit, logging.DEBUG)
info = partialmethod(_emit, logging.INFO)
warning = partialmethod(_emit, logging.WARNING)
error = partialmethod(_emit, logging.ERROR)
critical = partialmethod(_emit, logging.CRITICAL)
exception = partialmethod(_emit, logging.ERROR, exc_info=True)

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.

What would you think about just spelling these out as plain methods, like CPython does for Logger.debug/info/warning/..., thin wrappers that each delegate to self._log(LEVEL, ...)?

My worry is that the partialmethod + Protocol + TYPE_CHECKING approach doesn't really remove the duplication, just moves it into the typing layer where it's harder to follow, the method names still appear twice, and only the body is actually deduplicated (which a plain delegating method does too, since _emit holds the real work).

Since this is headed for django-lasuite, I'd lean toward keeping it stupid-simple: plain defs give honest signatures and tracebacks for free (the thing the Protocol is there to recover) and sidestep stub/runtime drift. The levels are fixed by stdlib, so it's about as stable as duplication gets. Curious what drove the current choice though, is there a constraint I'm missing?

application.client_id,
email,
audit_logger.warning(
"Application denied delegation",

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.

Looks like we're mixing up event message and event type here. Bit confused tbh, is passing just the event type (snake_case) what's expected? And should we list allowed event types in a data structure somewhere, or is that overkill?

@FloChehab FloChehab left a comment

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.

Few things by reading this :)

At first I was like, why not just configure specific handle for audit loggers whose name start with audit but I do understand the usefullness of a dedicated class for this.

Comment thread src/backend/core/audit.py
def __init__(
self,
logger_name=AUDIT_LOGGER_NAME,
custom_serializers: list[tuple] | None = None,

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.

Could this be typed better or documented ? list[tuple] ?

Comment thread src/backend/core/audit.py
exception = partialmethod(_emit, logging.ERROR, exc_info=True)


def getLogger(

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'd rename this to get_audit_logger, and maybe make the AuditLogger class private ?

Comment thread src/backend/core/audit.py
return AuditLogger(AUDIT_LOGGER_NAME, custom_serializers=custom_serializers)
return AuditLogger(
f"{AUDIT_LOGGER_NAME}.{name}", custom_serializers=custom_serializers
)

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.

You should add an __ALL__ = ... to this module

Comment thread src/backend/core/audit.py
self,
level,
event_type,
request: HttpRequest | None, # Intentionnaly mandatory

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.

There is no = None so it's valid from my pov

Comment thread src/backend/core/audit.py
Comment on lines +107 to +110
self._logger = logging.getLogger(logger_name)
self._serializers = []
if custom_serializers is not None:
self._serializers = custom_serializers

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.

You should build a mapping of class -> serializer method, so that later you can make direct lookups in 0(1) based on the type of the item. (and fallback to 0(n) if it's a subclass of a class you have handling for).

It might also be possible to type this in a generic way, so that if we try to pass something that is not supported we get typehint feedback.

You should crash early if you have multiple sirializers for the same kind of object.

Comment thread src/backend/core/audit.py
Comment on lines +131 to +141
new_extra = {}
for key, value in extra.items():
for cls, serializer in self._serializers:
if isinstance(value, cls):
new_extra = new_extra | {
f"{key}.{ser_key}": ser_field
for ser_key, ser_field in serializer(value).items()
}
break
else:
new_extra[key] = value

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.

This is not very optimal perf wise, see my comment above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants