Skip to content

Commit 54f028c

Browse files
Merge pull request #1639 from IFRCGo/feature/sentry-integration
Feature/sentry integration
2 parents e6314f4 + 36cff6c commit 54f028c

File tree

6 files changed

+844
-143
lines changed

6 files changed

+844
-143
lines changed

main/celery.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
import os
22
import celery
33

4+
from django.conf import settings
5+
from main import sentry
6+
47

58
class CustomCeleryApp(celery.Celery):
6-
pass
9+
def on_configure(self):
10+
if settings.SENTRY_DSN:
11+
sentry.init_sentry(
12+
app_type='WORKER',
13+
**settings.SENTRY_CONFIG,
14+
)
715

816

917
# set the default Django settings module for the 'celery' program.

main/exception_handler.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import sentry_sdk
2+
3+
from rest_framework.views import exception_handler
4+
from rest_framework.response import Response
5+
from rest_framework import status
6+
7+
8+
standard_error_string = (
9+
'Something unexpected has occured. '
10+
'Please contact an admin to fix this issue.'
11+
)
12+
13+
14+
def custom_exception_handler(exc, context):
15+
# Default exception handler
16+
response = exception_handler(exc, context)
17+
18+
# For 500 errors, we create new response and add extra attributes to sentry
19+
if not response:
20+
request = context.get('request')
21+
if request and request.user and request.user.id:
22+
with sentry_sdk.configure_scope() as scope:
23+
scope.user = {
24+
'id': request.user.id,
25+
'email': request.user.email,
26+
}
27+
scope.set_extra('is_superuser', request.user.is_superuser)
28+
sentry_sdk.capture_exception()
29+
response_data = {
30+
'errors': {
31+
'non_field_errors': [standard_error_string]
32+
},
33+
}
34+
response = Response(response_data, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
35+
36+
return response

main/sentry.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import os
2+
3+
import sentry_sdk
4+
from django.core.exceptions import PermissionDenied
5+
from celery.exceptions import Retry as CeleryRetry
6+
from sentry_sdk.integrations.logging import ignore_logger
7+
from sentry_sdk.integrations.celery import CeleryIntegration
8+
from sentry_sdk.integrations.django import DjangoIntegration
9+
from sentry_sdk.integrations.redis import RedisIntegration
10+
11+
# Celery Terminated Exception: The worker processing a job has been terminated by user request.
12+
from billiard.exceptions import Terminated
13+
14+
IGNORED_ERRORS = [
15+
Terminated,
16+
PermissionDenied,
17+
CeleryRetry,
18+
]
19+
IGNORED_LOGGERS = [
20+
'django.core.exceptions.ObjectDoesNotExist',
21+
]
22+
23+
for _logger in IGNORED_LOGGERS:
24+
ignore_logger(_logger)
25+
26+
27+
class InvalidGitRepository(Exception):
28+
pass
29+
30+
31+
def fetch_git_sha(path, head=None):
32+
"""
33+
Source: https://github.com/getsentry/raven-python/blob/03559bb05fd963e2be96372ae89fb0bce751d26d/raven/versioning.py
34+
>>> fetch_git_sha(os.path.dirname(__file__))
35+
"""
36+
if not head:
37+
head_path = os.path.join(path, '.git', 'HEAD')
38+
if not os.path.exists(head_path):
39+
raise InvalidGitRepository(
40+
'Cannot identify HEAD for git repository at %s' % (path,))
41+
42+
with open(head_path, 'r') as fp:
43+
head = str(fp.read()).strip()
44+
45+
if head.startswith('ref: '):
46+
head = head[5:]
47+
revision_file = os.path.join(
48+
path, '.git', *head.split('/')
49+
)
50+
else:
51+
return head
52+
else:
53+
revision_file = os.path.join(path, '.git', 'refs', 'heads', head)
54+
55+
if not os.path.exists(revision_file):
56+
if not os.path.exists(os.path.join(path, '.git')):
57+
raise InvalidGitRepository(
58+
'%s does not seem to be the root of a git repository' % (path,))
59+
60+
# Check for our .git/packed-refs' file since a `git gc` may have run
61+
# https://git-scm.com/book/en/v2/Git-Internals-Maintenance-and-Data-Recovery
62+
packed_file = os.path.join(path, '.git', 'packed-refs')
63+
if os.path.exists(packed_file):
64+
with open(packed_file) as fh:
65+
for line in fh:
66+
line = line.rstrip()
67+
if line and line[:1] not in ('#', '^'):
68+
try:
69+
revision, ref = line.split(' ', 1)
70+
except ValueError:
71+
continue
72+
if ref == head:
73+
return str(revision)
74+
75+
raise InvalidGitRepository(
76+
'Unable to find ref to head "%s" in repository' % (head,))
77+
78+
with open(revision_file) as fh:
79+
return str(fh.read()).strip()
80+
81+
82+
def init_sentry(app_type, tags={}, **config):
83+
integrations = [
84+
CeleryIntegration(),
85+
DjangoIntegration(),
86+
RedisIntegration(),
87+
]
88+
sentry_sdk.init(
89+
**config,
90+
ignore_errors=IGNORED_ERRORS,
91+
integrations=integrations,
92+
)
93+
with sentry_sdk.configure_scope() as scope:
94+
scope.set_tag('app_type', app_type)
95+
for tag, value in tags.items():
96+
scope.set_tag(tag, value)

main/settings.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
from django.utils.translation import gettext_lazy as _
88
# from celery.schedules import crontab
99
from urllib3.util.retry import Retry
10+
from corsheaders.defaults import default_headers
11+
12+
from main import sentry
1013

1114
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
1215
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
@@ -72,6 +75,9 @@
7275
# Appeal Server Credentials (https://go-api.ifrc.org/api/)
7376
APPEALS_USER=(str, None),
7477
APPEALS_PASS=(str, None),
78+
# Sentry
79+
SENTRY_DSN=(str, None),
80+
SENTRY_SAMPLE_RATE=(float, 0.2),
7581
)
7682

7783

@@ -172,6 +178,7 @@
172178
'rest_framework.authentication.BasicAuthentication',
173179
'rest_framework.authentication.SessionAuthentication',
174180
),
181+
'EXCEPTION_HANDLER': 'main.exception_handler.custom_exception_handler',
175182
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
176183
'PAGE_SIZE': 50,
177184
'DEFAULT_FILTER_BACKENDS': (
@@ -214,6 +221,9 @@
214221
)
215222

216223
CORS_ORIGIN_ALLOW_ALL = True
224+
CORS_ALLOW_HEADERS = list(default_headers) + [
225+
'sentry-trace',
226+
]
217227

218228
ROOT_URLCONF = 'main.urls'
219229

@@ -479,3 +489,24 @@
479489
# Appeal Server Credentials
480490
APPEALS_USER = env('APPEALS_USER')
481491
APPEALS_PASS = env('APPEALS_PASS')
492+
493+
# Sentry Config
494+
SENTRY_DSN = env('SENTRY_DSN')
495+
SENTRY_SAMPLE_RATE = env('SENTRY_SAMPLE_RATE')
496+
497+
SENTRY_CONFIG = {
498+
'dsn': SENTRY_DSN,
499+
'send_default_pii': True,
500+
'traces_sample_rate': SENTRY_SAMPLE_RATE,
501+
'release': sentry.fetch_git_sha(BASE_DIR),
502+
'environment': GO_ENVIRONMENT,
503+
'debug': DEBUG,
504+
'tags': {
505+
'site': GO_API_FQDN,
506+
},
507+
}
508+
if SENTRY_DSN:
509+
sentry.init_sentry(
510+
app_type='API',
511+
**SENTRY_CONFIG,
512+
)

0 commit comments

Comments
 (0)