From d15e67e2e5ac83452097e82a1af0f0c5769974a3 Mon Sep 17 00:00:00 2001 From: Josema Camacho Date: Wed, 25 Mar 2026 09:55:12 +0100 Subject: [PATCH] fix(api): filter neo4j.io defunct connection logs in Sentry before_send (#10452) --- api/CHANGELOG.md | 1 + api/src/backend/api/tests/test_sentry.py | 70 +++++++++++++++++++---- api/src/backend/config/settings/sentry.py | 17 +++++- 3 files changed, 74 insertions(+), 14 deletions(-) diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md index 72e5ddd146..e95cf27e79 100644 --- a/api/CHANGELOG.md +++ b/api/CHANGELOG.md @@ -8,6 +8,7 @@ All notable changes to the **Prowler API** are documented in this file. - Finding groups list/latest now apply computed status/severity filters and finding-level prefilters (delta, region, service, category, resource group, scan, resource type), plus `check_title` support for sort/filter consistency [(#10428)](https://github.com/prowler-cloud/prowler/pull/10428) - Populate compliance data inside `check_metadata` for findings, which was always returned as `null` [(#10449)](https://github.com/prowler-cloud/prowler/pull/10449) +- Filter transient Neo4j defunct connection logs in Sentry `before_send` to suppress false-positive alerts handled by `RetryableSession` retries [(#10452)](https://github.com/prowler-cloud/prowler/pull/10452) ## [1.23.0] (Prowler v5.22.0) diff --git a/api/src/backend/api/tests/test_sentry.py b/api/src/backend/api/tests/test_sentry.py index cf71593469..fb7abaffb4 100644 --- a/api/src/backend/api/tests/test_sentry.py +++ b/api/src/backend/api/tests/test_sentry.py @@ -4,14 +4,25 @@ from unittest.mock import MagicMock from config.settings.sentry import before_send +def _make_log_record(msg, level=logging.ERROR, name="test", args=None): + """Build a real LogRecord so getMessage() works like in production.""" + record = logging.LogRecord( + name=name, + level=level, + pathname="", + lineno=0, + msg=msg, + args=args, + exc_info=None, + ) + return record + + def test_before_send_ignores_log_with_ignored_exception(): """Test that before_send ignores logs containing ignored exceptions.""" - log_record = MagicMock() - log_record.msg = "Provider kubernetes is not connected" - log_record.levelno = logging.ERROR # 40 + log_record = _make_log_record("Provider kubernetes is not connected") hint = {"log_record": log_record} - event = MagicMock() result = before_send(event, hint) @@ -36,12 +47,9 @@ def test_before_send_ignores_exception_with_ignored_exception(): def test_before_send_passes_through_non_ignored_log(): """Test that before_send passes through logs that don't contain ignored exceptions.""" - log_record = MagicMock() - log_record.msg = "Some other error message" - log_record.levelno = logging.ERROR # 40 + log_record = _make_log_record("Some other error message") hint = {"log_record": log_record} - event = MagicMock() result = before_send(event, hint) @@ -66,15 +74,53 @@ def test_before_send_passes_through_non_ignored_exception(): def test_before_send_handles_warning_level(): """Test that before_send handles warning level logs.""" - log_record = MagicMock() - log_record.msg = "Provider kubernetes is not connected" - log_record.levelno = logging.WARNING # 30 + log_record = _make_log_record( + "Provider kubernetes is not connected", level=logging.WARNING + ) hint = {"log_record": log_record} - event = MagicMock() result = before_send(event, hint) # Assert that the event was dropped (None returned) assert result is None + + +def test_before_send_ignores_neo4j_defunct_connection(): + """Test that before_send drops neo4j.io defunct connection logs. + + The Neo4j driver logs transient connection errors at ERROR level + before RetryableSession retries them. These are noise. + + The driver uses %s formatting, so "defunct" is in the args, not + in the template. This test mirrors the real LogRecord structure. + """ + log_record = _make_log_record( + msg="[#%04X] _: error: %s: %r", + name="neo4j.io", + args=( + 0xE5CC, + "Failed to read from defunct connection " + "IPv4Address(('cloud-neo4j.prowler.com', 7687))", + ConnectionResetError(104, "Connection reset by peer"), + ), + ) + + hint = {"log_record": log_record} + event = MagicMock() + + assert before_send(event, hint) is None + + +def test_before_send_passes_non_defunct_neo4j_log(): + """Test that before_send passes through neo4j.io logs that are not about defunct connections.""" + log_record = _make_log_record( + msg="Some other neo4j transport error", + name="neo4j.io", + ) + + hint = {"log_record": log_record} + event = MagicMock() + + assert before_send(event, hint) == event diff --git a/api/src/backend/config/settings/sentry.py b/api/src/backend/config/settings/sentry.py index 6b8c554258..65c6277817 100644 --- a/api/src/backend/config/settings/sentry.py +++ b/api/src/backend/config/settings/sentry.py @@ -1,4 +1,5 @@ import sentry_sdk + from config.env import env IGNORED_EXCEPTIONS = [ @@ -85,8 +86,20 @@ def before_send(event, hint): # Ignore logs with the ignored_exceptions # https://docs.python.org/3/library/logging.html#logrecord-objects if "log_record" in hint: - log_msg = hint["log_record"].msg - log_lvl = hint["log_record"].levelno + log_record = hint["log_record"] + log_msg = log_record.getMessage() + log_lvl = log_record.levelno + + # The Neo4j driver logs transient connection errors (defunct + # connections, resets) at ERROR level via the `neo4j.io` logger. + # `RetryableSession` handles these with retries. If all retries + # are exhausted, the exception propagates and Sentry captures + # it as a normal exception event. + if ( + getattr(log_record, "name", "").startswith("neo4j.io") + and "defunct" in log_msg + ): + return None # Handle Error and Critical events and discard the rest if log_lvl <= 40 and any(ignored in log_msg for ignored in IGNORED_EXCEPTIONS):