From a4c003367bce4e92c23867c250b07b54dfe1cca1 Mon Sep 17 00:00:00 2001 From: Paul Cruse III Date: Wed, 17 Jun 2026 01:34:30 -0500 Subject: [PATCH] logger: dedupe equal RedactionFilters on register_callback RedactionFilter gains value equality (__eq__/__hash__ on keys, pattern sources, redact_with) and CommonLogger.register_callback skips a callback that already compares equal. Registering the same filter from multiple handler entrypoints in one process is now idempotent (no stacked passes). Distinct callbacks still stack. --- acai_aws/common/logger/common_logger.py | 2 ++ acai_aws/common/logger/redaction.py | 15 ++++++++++++++- tests/acai_aws/common/test_logger.py | 10 ++++++++++ tests/acai_aws/common/test_redaction.py | 15 +++++++++++++++ 4 files changed, 41 insertions(+), 1 deletion(-) diff --git a/acai_aws/common/logger/common_logger.py b/acai_aws/common/logger/common_logger.py index 6e71e74..9e3cb4b 100644 --- a/acai_aws/common/logger/common_logger.py +++ b/acai_aws/common/logger/common_logger.py @@ -34,6 +34,8 @@ def __init__(self): @classmethod def register_callback(cls, callback): + if callback in cls._callbacks: + return cls._callbacks.append(callback) @classmethod diff --git a/acai_aws/common/logger/redaction.py b/acai_aws/common/logger/redaction.py index f7a57e1..201b233 100644 --- a/acai_aws/common/logger/redaction.py +++ b/acai_aws/common/logger/redaction.py @@ -7,9 +7,22 @@ class RedactionFilter: def __init__(self, **kwargs): self._keys = {str(key).lower() for key in kwargs.get('keys') or []} - self._patterns = [re.compile(pattern) for pattern in kwargs.get('patterns') or []] + self._pattern_sources = tuple(kwargs.get('patterns') or []) + self._patterns = [re.compile(pattern) for pattern in self._pattern_sources] self._redact_with = kwargs.get('redact_with', self.DEFAULT_REDACTION) + def __eq__(self, other): + if not isinstance(other, RedactionFilter): + return NotImplemented + return ( + self._keys == other._keys + and self._pattern_sources == other._pattern_sources + and self._redact_with == other._redact_with + ) + + def __hash__(self): + return hash((frozenset(self._keys), self._pattern_sources, self._redact_with)) + def __call__(self, record): if isinstance(record, dict) and 'log' in record: record['log'] = self._scrub(record['log']) diff --git a/tests/acai_aws/common/test_logger.py b/tests/acai_aws/common/test_logger.py index 4984cb8..2083c51 100644 --- a/tests/acai_aws/common/test_logger.py +++ b/tests/acai_aws/common/test_logger.py @@ -212,5 +212,15 @@ def test_reset_callbacks_clears_registry(self): CommonLogger.reset_callbacks() self.assertEqual(0, len(CommonLogger._callbacks)) + def test_register_callback_dedupes_equal_filters(self): + CommonLogger.register_callback(RedactionFilter(keys=['email'])) + CommonLogger.register_callback(RedactionFilter(keys=['email'])) + self.assertEqual(1, len(CommonLogger._callbacks)) + + def test_register_callback_keeps_distinct_filters(self): + CommonLogger.register_callback(RedactionFilter(keys=['email'])) + CommonLogger.register_callback(RedactionFilter(keys=['phone'])) + self.assertEqual(2, len(CommonLogger._callbacks)) + def test_common_logger_is_singleton(self): self.assertIs(CommonLogger(), CommonLogger()) diff --git a/tests/acai_aws/common/test_redaction.py b/tests/acai_aws/common/test_redaction.py index bda613a..122ed89 100644 --- a/tests/acai_aws/common/test_redaction.py +++ b/tests/acai_aws/common/test_redaction.py @@ -60,3 +60,18 @@ def test_keys_and_patterns_together(self): self.assertEqual('[REDACTED]', log['email']) self.assertNotIn('123-45-6789', log['message']) self.assertNotIn('ada@example.com', log['message']) + + def test_equal_filters_compare_equal_and_hash_equal(self): + one = RedactionFilter(keys=['email', 'ssn'], patterns=[r'\d{3}'], redact_with='*') + two = RedactionFilter(keys=['ssn', 'email'], patterns=[r'\d{3}'], redact_with='*') + self.assertEqual(one, two) + self.assertEqual(hash(one), hash(two)) + + def test_filters_with_different_config_are_not_equal(self): + base = RedactionFilter(keys=['email'], patterns=[r'\d{3}']) + self.assertNotEqual(base, RedactionFilter(keys=['email'], patterns=[r'\d{4}'])) + self.assertNotEqual(base, RedactionFilter(keys=['phone'], patterns=[r'\d{3}'])) + self.assertNotEqual(base, RedactionFilter(keys=['email'], patterns=[r'\d{3}'], redact_with='*')) + + def test_not_equal_to_non_filter(self): + self.assertNotEqual(RedactionFilter(keys=['email']), 'not-a-filter')