From 3b221f76785706f8414b609af061133b2718ad76 Mon Sep 17 00:00:00 2001 From: itsthatianguy Date: Thu, 18 Jun 2026 14:45:52 +0100 Subject: [PATCH 1/5] Fixing signals, updating timestamp, and logging both user ids for target --- common/logging.py | 10 ++++++++++ common/signals.py | 22 +++++++++++++++------- metrics/api/settings/default.py | 1 + 3 files changed, 26 insertions(+), 7 deletions(-) create mode 100644 common/logging.py diff --git a/common/logging.py b/common/logging.py new file mode 100644 index 000000000..64491a506 --- /dev/null +++ b/common/logging.py @@ -0,0 +1,10 @@ +import logging +from datetime import datetime, timezone + + +class AuditFormatter(logging.Formatter): + def formatTime(self, record, datefmt=None): + return ( + datetime.fromtimestamp(record.created, tz=timezone.utc) + .strftime("%Y-%m-%dT%H:%M:%S.") + f"{record.msecs:03.0f}Z" + ) diff --git a/common/signals.py b/common/signals.py index a99f59f51..1118409f3 100644 --- a/common/signals.py +++ b/common/signals.py @@ -7,6 +7,10 @@ audit_logger = logging.getLogger("audit") +AUDIT_EXCLUDED_FIELDS: dict[str, set[str]] = { + "User": {"last_login", "password"}, + "PermissionSet": set(), +} AUDITABLE_MODELS = ["PermissionSet", "User"] AUDITABLE_RELATIONSHIPS = ["User_permission_sets"] @@ -25,6 +29,8 @@ def track_concrete_field_changes(sender, instance, update_fields=None, **kwargs) if sender.__name__ not in AUDITABLE_MODELS: return + excluded = AUDIT_EXCLUDED_FIELDS.get(sender.__name__, set()) + if not instance.pk: instance.audit_fields_changed = True return @@ -35,7 +41,8 @@ def track_concrete_field_changes(sender, instance, update_fields=None, **kwargs) for f in instance._meta.get_fields() # noqa: E261 SLF001 if f.many_to_many } - instance.audit_fields_changed = bool(set(update_fields) - m2m_names) + auditable_fields = set(update_fields) - m2m_names - excluded + instance.audit_fields_changed = bool(auditable_fields) return try: @@ -43,6 +50,7 @@ def track_concrete_field_changes(sender, instance, update_fields=None, **kwargs) instance.audit_fields_changed = any( getattr(instance, f) != getattr(stored, f) for f in _concrete_field_names(instance) + if f not in excluded ) except sender.DoesNotExist: instance.audit_fields_changed = True @@ -65,7 +73,7 @@ def audit_m2m_relationships_log(sender, instance, action, pk_set, **kwargs): extra={ "user": user_id, "action": f"ADD {sender.__name__} {pk_set}", - "target": instance.pk, + "target": f"pk={instance.pk}, id={instance.user_id}", }, ) elif action == "post_remove": @@ -74,7 +82,7 @@ def audit_m2m_relationships_log(sender, instance, action, pk_set, **kwargs): extra={ "user": user_id, "action": f"REMOVE {sender.__name__} {pk_set}", - "target": instance.pk, + "target": f"pk={instance.pk}, id={instance.user_id}", }, ) elif action == "post_clear": @@ -83,7 +91,7 @@ def audit_m2m_relationships_log(sender, instance, action, pk_set, **kwargs): extra={ "user": user_id, "action": f"CLEAR {sender.__name__}", - "target": instance.pk, + "target": f"pk={instance.pk}, user_id={instance.user_id}", }, ) @@ -105,7 +113,7 @@ def audit_save_log(sender, instance, created, **kwargs): extra={ "user": user_id, "action": f"{action} {sender.__name__}", - "target": f"id={instance.pk}", + "target": f"pk={instance.pk}, id={instance.user_id}", }, ) @@ -122,7 +130,7 @@ def audit_delete_log(sender, instance, **kwargs): "Model deleted", extra={ "user": user_id, - "action": f"DELETE {sender.__name__}", - "target": f"id={instance.pk}", + "action": f"pk={instance.pk}, DELETE {sender.__name__}", + "target": f"pk={instance.pk}, id={instance.user_id}", }, ) diff --git a/metrics/api/settings/default.py b/metrics/api/settings/default.py index 6f8ea4709..9351bcf9f 100644 --- a/metrics/api/settings/default.py +++ b/metrics/api/settings/default.py @@ -197,6 +197,7 @@ "format": f"%(asctime)s [%(levelname)s] [ENVIRONMENT:{config.APIENV}] [%(name)s - %(funcName)s] %(message)s" }, "audit": { + "()": "common.logging.AuditFormatter", "format": "[AUDIT_EVENT] %(asctime)s [User:%(user)s - Action:%(action)s - Target:%(target)s]" }, }, From 6eb6ea845e975c72f9fcfeeda70fb030f32bc764 Mon Sep 17 00:00:00 2001 From: itsthatianguy Date: Fri, 19 Jun 2026 10:24:02 +0100 Subject: [PATCH 2/5] Updating excluded fields --- common/signals.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/signals.py b/common/signals.py index 1118409f3..918ff149c 100644 --- a/common/signals.py +++ b/common/signals.py @@ -8,7 +8,7 @@ audit_logger = logging.getLogger("audit") AUDIT_EXCLUDED_FIELDS: dict[str, set[str]] = { - "User": {"last_login", "password"}, + "User": {"last_login"}, "PermissionSet": set(), } AUDITABLE_MODELS = ["PermissionSet", "User"] From 0551d071d93e4e4ccada4777b9eb11409df775a3 Mon Sep 17 00:00:00 2001 From: itsthatianguy Date: Fri, 19 Jun 2026 10:31:31 +0100 Subject: [PATCH 3/5] Lint fixes --- common/logging.py | 10 +++++++--- metrics/api/settings/default.py | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/common/logging.py b/common/logging.py index 64491a506..0e2fcd5fe 100644 --- a/common/logging.py +++ b/common/logging.py @@ -1,10 +1,14 @@ import logging -from datetime import datetime, timezone +import typing +from datetime import UTC, datetime class AuditFormatter(logging.Formatter): + @typing.override def formatTime(self, record, datefmt=None): return ( - datetime.fromtimestamp(record.created, tz=timezone.utc) - .strftime("%Y-%m-%dT%H:%M:%S.") + f"{record.msecs:03.0f}Z" + datetime.fromtimestamp(record.created, tz=UTC).strftime( + "%Y-%m-%dT%H:%M:%S." + ) + + f"{record.msecs:03.0f}Z" ) diff --git a/metrics/api/settings/default.py b/metrics/api/settings/default.py index 9351bcf9f..fdd0a68a9 100644 --- a/metrics/api/settings/default.py +++ b/metrics/api/settings/default.py @@ -198,7 +198,7 @@ }, "audit": { "()": "common.logging.AuditFormatter", - "format": "[AUDIT_EVENT] %(asctime)s [User:%(user)s - Action:%(action)s - Target:%(target)s]" + "format": "[AUDIT_EVENT] %(asctime)s [User:%(user)s - Action:%(action)s - Target:%(target)s]", }, }, "handlers": { From d70099237c0fd7a29dc11a737c9b00bd8eeca758 Mon Sep 17 00:00:00 2001 From: itsthatianguy Date: Fri, 19 Jun 2026 10:52:25 +0100 Subject: [PATCH 4/5] checking user_id exists --- common/signals.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/common/signals.py b/common/signals.py index 918ff149c..87e5023ce 100644 --- a/common/signals.py +++ b/common/signals.py @@ -66,6 +66,11 @@ def audit_m2m_relationships_log(sender, instance, action, pk_set, **kwargs): user = get_current_user() user_id = user.id if user and user.is_authenticated else "anonymous" + target_string = ( + f"pk={instance.pk}, id={instance.user_id}" + if hasattr(instance, "user_id") + else f"pk={instance.pk}" + ) if action == "post_add": audit_logger.info( @@ -73,7 +78,7 @@ def audit_m2m_relationships_log(sender, instance, action, pk_set, **kwargs): extra={ "user": user_id, "action": f"ADD {sender.__name__} {pk_set}", - "target": f"pk={instance.pk}, id={instance.user_id}", + "target": target_string, }, ) elif action == "post_remove": @@ -82,7 +87,7 @@ def audit_m2m_relationships_log(sender, instance, action, pk_set, **kwargs): extra={ "user": user_id, "action": f"REMOVE {sender.__name__} {pk_set}", - "target": f"pk={instance.pk}, id={instance.user_id}", + "target": target_string, }, ) elif action == "post_clear": @@ -91,7 +96,7 @@ def audit_m2m_relationships_log(sender, instance, action, pk_set, **kwargs): extra={ "user": user_id, "action": f"CLEAR {sender.__name__}", - "target": f"pk={instance.pk}, user_id={instance.user_id}", + "target": target_string, }, ) @@ -107,13 +112,18 @@ def audit_save_log(sender, instance, created, **kwargs): user = get_current_user() user_id = user.id if user else "anonymous" action = "CREATED" if created else "UPDATED" + target_string = ( + f"pk={instance.pk}, id={instance.user_id}" + if hasattr(instance, "user_id") + else f"pk={instance.pk}" + ) audit_logger.info( "Model saved", extra={ "user": user_id, "action": f"{action} {sender.__name__}", - "target": f"pk={instance.pk}, id={instance.user_id}", + "target": target_string, }, ) @@ -125,12 +135,17 @@ def audit_delete_log(sender, instance, **kwargs): user = get_current_user() user_id = user.id if user else "anonymous" + target_string = ( + f"pk={instance.pk}, id={instance.user_id}" + if hasattr(instance, "user_id") + else f"pk={instance.pk}" + ) audit_logger.info( "Model deleted", extra={ "user": user_id, "action": f"pk={instance.pk}, DELETE {sender.__name__}", - "target": f"pk={instance.pk}, id={instance.user_id}", + "target": target_string, }, ) From 4197395b2bb7707e4d8f9c75b625358bfec6bdb4 Mon Sep 17 00:00:00 2001 From: itsthatianguy Date: Fri, 26 Jun 2026 10:29:47 +0100 Subject: [PATCH 5/5] Ignore config log level for audit logs --- metrics/api/settings/default.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/metrics/api/settings/default.py b/metrics/api/settings/default.py index fdd0a68a9..55bcbf15a 100644 --- a/metrics/api/settings/default.py +++ b/metrics/api/settings/default.py @@ -208,7 +208,7 @@ "formatter": "standard", }, "audit_console": { - "level": config.LOG_LEVEL, + "level": "INFO", "class": "logging.StreamHandler", "formatter": "audit", }, @@ -226,7 +226,7 @@ }, "audit": { "handlers": ["audit_console"], - "level": config.LOG_LEVEL, + "level": "INFO", "propagate": False, }, },