Repository: getsentry/sentry-python

Issue:
Consider logging hierarchy for ignore_logger
Ref: https://docs.sentry.io/platforms/python/logging/#ignoring-a-logger

I'm trying to ignore a spammy module (in this case "ddtrace.writer"). While setting up my Sentry integration, I decided that I don't care about any error log from the entire module and so I called `ignore_logger("ddtrace")` instead of "ddtrace.writer".

This doesn't work (and looking at the implementation, it's clear why https://github.com/getsentry/sentry-python/blob/master/sentry_sdk/integrations/logging.py#L91-L93).

Since Python stdlib logging configuration works at a hierarchical level (eg. I can configure a logger with `qualname = ddtrace` and it'll apply to all its submodules), can the Sentry SDK be made to behave in a similar way?

Alternatives are either call ignore_logger for every single possible spammy source explicitly for all my dependencies, add sections to silence each module in my logging config, or write a custom before_send hook to filter. While these are all feasible, I'd still prefer to just call ignore_logger once per module as it's ultimately the simplest (IMO).

Discussion:

Patch:
diff --git a/linter-requirements.txt b/linter-requirements.txt
index 3accdd5e..08b47958 100644
--- a/linter-requirements.txt
+++ b/linter-requirements.txt
@@ -1,6 +1,6 @@
 black==20.8b1
-flake8==3.8.4
+flake8==3.9.0
 flake8-import-order==0.18.1
 mypy==0.782
-flake8-bugbear==21.3.1
+flake8-bugbear==21.3.2
 pep8-naming==0.11.1
diff --git a/scripts/build_awslambda_layer.py b/scripts/build_awslambda_layer.py
index ae0ee185..1fda06e7 100644
--- a/scripts/build_awslambda_layer.py
+++ b/scripts/build_awslambda_layer.py
@@ -51,12 +51,14 @@ class PackageBuilder:
         Method that creates the init_serverless_sdk pkg in the
         sentry-python-serverless zip
         """
-        serverless_sdk_path = f'{self.packages_dir}/sentry_sdk/' \
-                              f'integrations/init_serverless_sdk'
+        serverless_sdk_path = (
+            f"{self.packages_dir}/sentry_sdk/" f"integrations/init_serverless_sdk"
+        )
         if not os.path.exists(serverless_sdk_path):
             os.makedirs(serverless_sdk_path)
-        shutil.copy('scripts/init_serverless_sdk.py',
-                    f'{serverless_sdk_path}/__init__.py')
+        shutil.copy(
+            "scripts/init_serverless_sdk.py", f"{serverless_sdk_path}/__init__.py"
+        )
 
     def zip(
         self, filename  # type: str
diff --git a/scripts/init_serverless_sdk.py b/scripts/init_serverless_sdk.py
index 42107e4c..07b453ea 100644
--- a/scripts/init_serverless_sdk.py
+++ b/scripts/init_serverless_sdk.py
@@ -19,7 +19,7 @@ if MYPY:
 sentry_sdk.init(
     dsn=os.environ["SENTRY_DSN"],
     integrations=[AwsLambdaIntegration(timeout_warning=True)],
-    traces_sample_rate=float(os.environ["SENTRY_TRACES_SAMPLE_RATE"])
+    traces_sample_rate=float(os.environ["SENTRY_TRACES_SAMPLE_RATE"]),
 )
 
 
diff --git a/sentry_sdk/integrations/django/templates.py b/sentry_sdk/integrations/django/templates.py
index 3f805f36..2ff9d1b1 100644
--- a/sentry_sdk/integrations/django/templates.py
+++ b/sentry_sdk/integrations/django/templates.py
@@ -42,6 +42,15 @@ def get_template_frame_from_exception(exc_value):
     return None
 
 
+def _get_template_name_description(template_name):
+    # type: (str) -> str
+    if isinstance(template_name, (list, tuple)):
+        if template_name:
+            return "[{}, ...]".format(template_name[0])
+    else:
+        return template_name
+
+
 def patch_templates():
     # type: () -> None
     from django.template.response import SimpleTemplateResponse
@@ -57,7 +66,8 @@ def patch_templates():
             return real_rendered_content.fget(self)
 
         with hub.start_span(
-            op="django.template.render", description=self.template_name
+            op="django.template.render",
+            description=_get_template_name_description(self.template_name),
         ) as span:
             span.set_data("context", self.context_data)
             return real_rendered_content.fget(self)
@@ -78,7 +88,8 @@ def patch_templates():
             return real_render(request, template_name, context, *args, **kwargs)
 
         with hub.start_span(
-            op="django.template.render", description=template_name
+            op="django.template.render",
+            description=_get_template_name_description(template_name),
         ) as span:
             span.set_data("context", context)
             return real_render(request, template_name, context, *args, **kwargs)
diff --git a/sentry_sdk/integrations/logging.py b/sentry_sdk/integrations/logging.py
index 138a8531..80524dba 100644
--- a/sentry_sdk/integrations/logging.py
+++ b/sentry_sdk/integrations/logging.py
@@ -2,6 +2,7 @@ from __future__ import absolute_import
 
 import logging
 import datetime
+from fnmatch import fnmatch
 
 from sentry_sdk.hub import Hub
 from sentry_sdk.utils import (
@@ -98,7 +99,11 @@ class LoggingIntegration(Integration):
 
 def _can_record(record):
     # type: (LogRecord) -> bool
-    return record.name not in _IGNORED_LOGGERS
+    """Prevents ignored loggers from recording"""
+    for logger in _IGNORED_LOGGERS:
+        if fnmatch(record.name, logger):
+            return False
+    return True
 
 
 def _breadcrumb_from_record(record):


Test patch:
diff --git a/tests/integrations/django/myapp/views.py b/tests/integrations/django/myapp/views.py
index 4bd05f8b..57d8fb98 100644
--- a/tests/integrations/django/myapp/views.py
+++ b/tests/integrations/django/myapp/views.py
@@ -122,7 +122,9 @@ def template_test(request, *args, **kwargs):
 
 @csrf_exempt
 def template_test2(request, *args, **kwargs):
-    return TemplateResponse(request, "user_name.html", {"user_age": 25})
+    return TemplateResponse(
+        request, ("user_name.html", "another_template.html"), {"user_age": 25}
+    )
 
 
 @csrf_exempt
diff --git a/tests/integrations/django/test_basic.py b/tests/integrations/django/test_basic.py
index 186a7d3f..9341dc23 100644
--- a/tests/integrations/django/test_basic.py
+++ b/tests/integrations/django/test_basic.py
@@ -563,18 +563,25 @@ def test_render_spans(sentry_init, client, capture_events, render_span_tree):
         integrations=[DjangoIntegration()],
         traces_sample_rate=1.0,
     )
-    views_urls = [reverse("template_test2")]
+    views_tests = [
+        (
+            reverse("template_test2"),
+            '- op="django.template.render": description="[user_name.html, ...]"',
+        ),
+    ]
     if DJANGO_VERSION >= (1, 7):
-        views_urls.append(reverse("template_test"))
+        views_tests.append(
+            (
+                reverse("template_test"),
+                '- op="django.template.render": description="user_name.html"',
+            ),
+        )
 
-    for url in views_urls:
+    for url, expected_line in views_tests:
         events = capture_events()
         _content, status, _headers = client.get(url)
         transaction = events[0]
-        assert (
-            '- op="django.template.render": description="user_name.html"'
-            in render_span_tree(transaction)
-        )
+        assert expected_line in render_span_tree(transaction)
 
 
 def test_middleware_spans(sentry_init, client, capture_events, render_span_tree):
diff --git a/tests/integrations/logging/test_logging.py b/tests/integrations/logging/test_logging.py
index e9940279..22ea14f8 100644
--- a/tests/integrations/logging/test_logging.py
+++ b/tests/integrations/logging/test_logging.py
@@ -3,7 +3,7 @@ import sys
 import pytest
 import logging
 
-from sentry_sdk.integrations.logging import LoggingIntegration
+from sentry_sdk.integrations.logging import LoggingIntegration, ignore_logger
 
 other_logger = logging.getLogger("testfoo")
 logger = logging.getLogger(__name__)
@@ -134,3 +134,30 @@ def test_logging_filters(sentry_init, capture_events):
 
     (event,) = events
     assert event["logentry"]["message"] == "hi"
+
+
+def test_ignore_logger(sentry_init, capture_events):
+    sentry_init(integrations=[LoggingIntegration()], default_integrations=False)
+    events = capture_events()
+
+    ignore_logger("testfoo")
+
+    other_logger.error("hi")
+
+    assert not events
+
+
+def test_ignore_logger_wildcard(sentry_init, capture_events):
+    sentry_init(integrations=[LoggingIntegration()], default_integrations=False)
+    events = capture_events()
+
+    ignore_logger("testfoo.*")
+
+    nested_logger = logging.getLogger("testfoo.submodule")
+
+    logger.error("hi")
+
+    nested_logger.error("bye")
+
+    (event,) = events
+    assert event["logentry"]["message"] == "hi"