zulip/zproject/sentry.py

115 lines
4.7 KiB
Python
Raw Normal View History

import os
from typing import TYPE_CHECKING, Any, Optional
import sentry_sdk
from django.utils.translation import override as override_language
from sentry_sdk.integrations.django import DjangoIntegration
sentry: Ignore all SuspiciousOperation loggers. django.security.DisallowedHost is only one of a set of exceptions that are "SuspiciousOperation" exceptions; all return a 400 to the user when they bubble up[1]; all of them are uninteresting to Sentry. While they may, in bulk, show a mis-configuration of some sort of the application, such a failure should be detected via the increase in 400's, not via these, which are uninteresting individually. While all of these are subclasses of SuspiciousOperation, we enumerate them explicitly for a number of reasons: - There is no one logger we can ignore that captures all of them. Each of the errors uses its own logger, and django does not supply a `django.security` logger that all of them feed into. - Nor can we catch this by examining the exception object. The SuspiciousOperation exception is raised too early in the stack for us to catch the exception by way of middleware and check `isinstance`. But at the Sentry level, in `add_context`, it is no longer an exception but a log entry, and as such we have no `isinstance` that can be applied; we only know the logger name. - Finally, there is the semantic argument that while we have decided to ignore this set of security warnings, we _may_ wish to log new ones that may be added at some point in the future. It is better to opt into those ignores than to blanket ignore all messages from the security logger. This moves the DisallowedHost `ignore_logger` to be adjacent to its kin, and not on the middleware that may trigger it. Consistency is more important than locality in this case. Of these, the DisallowedHost logger if left as the only one that is explicitly ignored in the LOGGING configuration in `computed_settings.py`; it is by far the most frequent, and the least likely to be malicious or impactful (unlike, say, RequestDataTooBig). [1] https://docs.djangoproject.com/en/3.0/ref/exceptions/#suspiciousoperation
2020-08-12 04:06:04 +02:00
from sentry_sdk.integrations.logging import ignore_logger
from sentry_sdk.integrations.redis import RedisIntegration
from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration
from sentry_sdk.utils import capture_internal_exceptions
from version import ZULIP_VERSION
from zproject.config import DEPLOY_ROOT
if TYPE_CHECKING:
from sentry_sdk._types import Event, Hint
def add_context(event: "Event", hint: "Hint") -> Optional["Event"]:
if "exc_info" in hint:
_, exc_value, _ = hint["exc_info"]
# Ignore GeneratorExit, KeyboardInterrupt, and SystemExit exceptions
if not isinstance(exc_value, Exception):
return None
from django.conf import settings
from zerver.models.users import get_user_profile_by_id
with capture_internal_exceptions():
# event.user is the user context, from Sentry, which is
# pre-populated with some keys via its Django integration:
# https://docs.sentry.io/platforms/python/guides/django/enriching-error-data/additional-data/identify-user/
event.setdefault("tags", {})
user_info = event.get("user", {})
user_id = user_info.get("id")
if isinstance(user_id, str):
user_profile = get_user_profile_by_id(int(user_id))
event["tags"]["realm"] = user_info["realm"] = user_profile.realm.string_id or "root"
with override_language(settings.LANGUAGE_CODE):
# str() to force the lazy-translation to apply now,
# since it won't serialize into json for Sentry otherwise
user_info["role"] = str(user_profile.get_role_name())
# These are PII, and should be scrubbed
if "username" in user_info:
del user_info["username"]
if "email" in user_info:
del user_info["email"]
return event
def traces_sampler(sampling_context: dict[str, Any]) -> float | bool:
from django.conf import settings
queue = sampling_context.get("queue")
if queue is not None and isinstance(queue, str):
if isinstance(settings.SENTRY_TRACE_WORKER_RATE, dict):
return settings.SENTRY_TRACE_WORKER_RATE.get(queue, 0.0)
else:
return settings.SENTRY_TRACE_WORKER_RATE
else:
return settings.SENTRY_TRACE_RATE
def setup_sentry(dsn: str | None, environment: str) -> None:
from django.conf import settings
if not dsn:
return
sentry_release = ZULIP_VERSION
if os.path.exists(os.path.join(DEPLOY_ROOT, "sentry-release")):
with open(os.path.join(DEPLOY_ROOT, "sentry-release")) as sentry_release_file:
sentry_release = sentry_release_file.readline().strip()
sentry_sdk.init(
dsn=dsn,
environment=environment,
release=sentry_release,
integrations=[
DjangoIntegration(),
RedisIntegration(),
SqlalchemyIntegration(),
],
before_send=add_context,
# Increase possible max wait to send exceptions during
# shutdown, from 2 to 10; potentially-large exceptions are of
# value to catch during shutdown.
shutdown_timeout=10,
# Because we strip the email/username from the Sentry data
# above, the effect of this flag is that the requests/users
# involved in exceptions will be identified in Sentry only by
# their IP address, user ID, realm, and role. We consider
# this an appropriate balance between avoiding Sentry getting
# PII while having the identifiers needed to determine that an
# exception only affects a small subset of users or realms.
send_default_pii=True,
traces_sampler=traces_sampler,
profiles_sample_rate=settings.SENTRY_PROFILE_RATE,
)
sentry: Ignore all SuspiciousOperation loggers. django.security.DisallowedHost is only one of a set of exceptions that are "SuspiciousOperation" exceptions; all return a 400 to the user when they bubble up[1]; all of them are uninteresting to Sentry. While they may, in bulk, show a mis-configuration of some sort of the application, such a failure should be detected via the increase in 400's, not via these, which are uninteresting individually. While all of these are subclasses of SuspiciousOperation, we enumerate them explicitly for a number of reasons: - There is no one logger we can ignore that captures all of them. Each of the errors uses its own logger, and django does not supply a `django.security` logger that all of them feed into. - Nor can we catch this by examining the exception object. The SuspiciousOperation exception is raised too early in the stack for us to catch the exception by way of middleware and check `isinstance`. But at the Sentry level, in `add_context`, it is no longer an exception but a log entry, and as such we have no `isinstance` that can be applied; we only know the logger name. - Finally, there is the semantic argument that while we have decided to ignore this set of security warnings, we _may_ wish to log new ones that may be added at some point in the future. It is better to opt into those ignores than to blanket ignore all messages from the security logger. This moves the DisallowedHost `ignore_logger` to be adjacent to its kin, and not on the middleware that may trigger it. Consistency is more important than locality in this case. Of these, the DisallowedHost logger if left as the only one that is explicitly ignored in the LOGGING configuration in `computed_settings.py`; it is by far the most frequent, and the least likely to be malicious or impactful (unlike, say, RequestDataTooBig). [1] https://docs.djangoproject.com/en/3.0/ref/exceptions/#suspiciousoperation
2020-08-12 04:06:04 +02:00
# Ignore all of the loggers from django.security that are for user
# errors; see https://docs.djangoproject.com/en/5.0/ref/exceptions/#suspiciousoperation
ignore_logger("django.security.SuspiciousOperation")
sentry: Ignore all SuspiciousOperation loggers. django.security.DisallowedHost is only one of a set of exceptions that are "SuspiciousOperation" exceptions; all return a 400 to the user when they bubble up[1]; all of them are uninteresting to Sentry. While they may, in bulk, show a mis-configuration of some sort of the application, such a failure should be detected via the increase in 400's, not via these, which are uninteresting individually. While all of these are subclasses of SuspiciousOperation, we enumerate them explicitly for a number of reasons: - There is no one logger we can ignore that captures all of them. Each of the errors uses its own logger, and django does not supply a `django.security` logger that all of them feed into. - Nor can we catch this by examining the exception object. The SuspiciousOperation exception is raised too early in the stack for us to catch the exception by way of middleware and check `isinstance`. But at the Sentry level, in `add_context`, it is no longer an exception but a log entry, and as such we have no `isinstance` that can be applied; we only know the logger name. - Finally, there is the semantic argument that while we have decided to ignore this set of security warnings, we _may_ wish to log new ones that may be added at some point in the future. It is better to opt into those ignores than to blanket ignore all messages from the security logger. This moves the DisallowedHost `ignore_logger` to be adjacent to its kin, and not on the middleware that may trigger it. Consistency is more important than locality in this case. Of these, the DisallowedHost logger if left as the only one that is explicitly ignored in the LOGGING configuration in `computed_settings.py`; it is by far the most frequent, and the least likely to be malicious or impactful (unlike, say, RequestDataTooBig). [1] https://docs.djangoproject.com/en/3.0/ref/exceptions/#suspiciousoperation
2020-08-12 04:06:04 +02:00
ignore_logger("django.security.DisallowedHost")
ignore_logger("django.security.DisallowedModelAdminLookup")
ignore_logger("django.security.DisallowedModelAdminToField")
ignore_logger("django.security.DisallowedRedirect")
ignore_logger("django.security.InvalidSessionKey")
ignore_logger("django.security.RequestDataTooBig")
ignore_logger("django.security.SuspiciousFileOperation")
ignore_logger("django.security.SuspiciousMultipartForm")
ignore_logger("django.security.SuspiciousSession")
ignore_logger("django.security.TooManyFieldsSent")