Skip to content

Commit

Permalink
Setup sentry tracing and profiling (#1492)
Browse files Browse the repository at this point in the history
  • Loading branch information
rhysyngsun authored Sep 3, 2024
1 parent 7e395dc commit 4567001
Show file tree
Hide file tree
Showing 15 changed files with 620 additions and 630 deletions.
8 changes: 4 additions & 4 deletions Procfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
release: bash scripts/heroku-release-phase.sh
web: bin/start-nginx bin/start-pgbouncer newrelic-admin run-program uwsgi uwsgi.ini
worker: bin/start-pgbouncer newrelic-admin run-program celery -A main.celery:app worker -E -Q default --concurrency=2 -B -l $MITOL_LOG_LEVEL
extra_worker_2x: bin/start-pgbouncer newrelic-admin run-program celery -A main.celery:app worker -E -Q edx_content,default --concurrency=2 -l $MITOL_LOG_LEVEL
extra_worker_performance: bin/start-pgbouncer newrelic-admin run-program celery -A main.celery:app worker -E -Q edx_content,default -l $MITOL_LOG_LEVEL
web: bin/start-nginx bin/start-pgbouncer uwsgi uwsgi.ini
worker: bin/start-pgbouncer celery -A main.celery:app worker -E -Q default --concurrency=2 -B -l $MITOL_LOG_LEVEL
extra_worker_2x: bin/start-pgbouncer celery -A main.celery:app worker -E -Q edx_content,default --concurrency=2 -l $MITOL_LOG_LEVEL
extra_worker_performance: bin/start-pgbouncer celery -A main.celery:app worker -E -Q edx_content,default -l $MITOL_LOG_LEVEL
2 changes: 1 addition & 1 deletion app.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"addons": ["heroku-postgresql:hobby-dev", "newrelic:wayne", "rediscloud:30"],
"addons": ["heroku-postgresql:hobby-dev", "rediscloud:30"],
"buildpacks": [
{
"url": "https://github.com/heroku/heroku-buildpack-apt"
Expand Down
12 changes: 0 additions & 12 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,3 @@ def prevent_requests(mocker, request): # noqa: PT004
autospec=True,
side_effect=DoNotUseRequestException,
)


@pytest.fixture(autouse=True)
def _disable_silky(settings):
"""Disable django-silky during tests"""
settings.SILKY_INTERCEPT_PERCENT = 0

settings.MIDDLEWARE = tuple(
middleware
for middleware in settings.MIDDLEWARE
if middleware != "silk.middleware.SilkyMiddleware"
)
1 change: 1 addition & 0 deletions env/frontend.env
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
NODE_ENV=development
PORT=8062
MITOL_AXIOS_WITH_CREDENTIALS=true
SENTRY_ENV=dev
7 changes: 7 additions & 0 deletions frontends/mit-learn/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ Sentry.init({
dsn: APP_SETTINGS.SENTRY_DSN,
release: APP_SETTINGS.VERSION,
environment: APP_SETTINGS.SENTRY_ENV,
integrations: [
Sentry.browserTracingIntegration(),
Sentry.browserProfilingIntegration(),
],
profilesSampleRate: APP_SETTINGS.SENTRY_PROFILES_SAMPLE_RATE,
tracesSampleRate: APP_SETTINGS.SENTRY_TRACES_SAMPLE_RATE,
tracePropagationTargets: [APP_SETTINGS.MITOL_API_BASE_URL],
})

const container = document.getElementById("app-container")
Expand Down
14 changes: 13 additions & 1 deletion frontends/mit-learn/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin")
const HtmlWebpackPlugin = require("html-webpack-plugin")
const CopyPlugin = require("copy-webpack-plugin")
const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin")
const { cleanEnv, str, bool, port } = require("envalid")
const { cleanEnv, str, bool, port, num } = require("envalid")

const {
NODE_ENV,
Expand All @@ -40,6 +40,8 @@ const {
CKEDITOR_UPLOAD_URL,
SENTRY_DSN,
SENTRY_ENV,
SENTRY_TRACES_SAMPLE_RATE,
SENTRY_PROFILES_SAMPLE_RATE,
CSRF_COOKIE_NAME,
APPZI_URL,
} = cleanEnv(process.env, {
Expand Down Expand Up @@ -100,6 +102,14 @@ const {
desc: "Sentry Data Source Name",
default: "",
}),
SENTRY_TRACES_SAMPLE_RATE: num({
desc: "Sentry tracing sample rate",
default: 0.0,
}),
SENTRY_PROFILES_SAMPLE_RATE: num({
desc: "Sentry profiling sample rate",
default: 0.0,
}),
CSRF_COOKIE_NAME: str({
desc: "Name of the CSRF cookie",
default: "csrftoken",
Expand Down Expand Up @@ -242,6 +252,8 @@ module.exports = (env, argv) => {
VERSION: JSON.stringify(VERSION),
SENTRY_DSN: JSON.stringify(SENTRY_DSN),
SENTRY_ENV: JSON.stringify(SENTRY_ENV),
SENTRY_PROFILES_SAMPLE_RATE,
SENTRY_TRACES_SAMPLE_RATE,
POSTHOG: getPostHogSettings(),
SITE_NAME: JSON.stringify(SITE_NAME),
MITOL_SUPPORT_EMAIL: JSON.stringify(MITOL_SUPPORT_EMAIL),
Expand Down
4 changes: 3 additions & 1 deletion frontends/ol-utilities/src/types/settings.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ export declare global {
EMBEDLY_KEY: string
CKEDITOR_UPLOAD_URL?: string
SENTRY_DSN?: string
VERSION?: string
SENTRY_ENV?: string
SENTRY_PROFILES_SAMPLE_RATE: number
SENTRY_TRACES_SAMPLE_RATE: number
VERSION?: string
POSTHOG?: PostHogSettings
SITE_NAME: string
MITOL_SUPPORT_EMAIL: string
Expand Down
26 changes: 15 additions & 11 deletions main/envs.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,25 +76,29 @@ def get_int(name, default):
return parsed_value


def get_any(name, default):
def get_float(name, default):
"""
Get an environment variable as a bool, int, or a string.
Get an environment variable as an int.
Args:
name (str): An environment variable name
default (any): The default value to use if the environment variable doesn't exist.
default (float): The default value to use if the environment variable doesn't exist.
Returns:
any:
The environment variable value parsed as a bool, int, or a string
float:
The environment variable value parsed as an float
""" # noqa: E501
value = os.environ.get(name)
if value is None:
return default

try:
return get_bool(name, default)
except EnvironmentVariableParseException:
try:
return get_int(name, default)
except EnvironmentVariableParseException:
return get_string(name, default)
parsed_value = float(value)
except ValueError as ex:
msg = f"Expected value in {name}={value} to be a float"
raise EnvironmentVariableParseException(msg) from ex

return parsed_value


def get_list_of_str(name, default):
Expand Down
61 changes: 35 additions & 26 deletions main/envs_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@

from main.envs import (
EnvironmentVariableParseException,
get_any,
get_bool,
get_float,
get_int,
get_list_of_str,
get_string,
Expand All @@ -19,7 +19,9 @@
"positive": "123",
"negative": "-456",
"zero": "0",
"float": "1.1",
"float-positive": "1.1",
"float-negative": "-1.1",
"float-zero": "0.0",
"expression": "123-456",
"none": "None",
"string": "a b c d e f g",
Expand All @@ -28,29 +30,6 @@
}


def test_get_any():
"""
get_any should parse an environment variable into a bool, int, or a string
"""
expected = {
"true": True,
"false": False,
"positive": 123,
"negative": -456,
"zero": 0,
"float": "1.1",
"expression": "123-456",
"none": "None",
"string": "a b c d e f g",
"list_of_int": "[3,4,5]",
"list_of_str": '["x", "y", \'z\']',
}
with patch("main.envs.os", environ=FAKE_ENVIRONS):
for key, value in expected.items():
assert get_any(key, "default") == value
assert get_any("missing", "default") == "default"


def test_get_string():
"""
get_string should get the string from the environment variable
Expand Down Expand Up @@ -82,6 +61,36 @@ def test_get_int():
assert get_int("missing", "default") == "default"


def test_get_float():
"""
get_float should get the float from the environment variable, or raise an exception if it's not parseable as an float
"""
with patch("main.envs.os", environ=FAKE_ENVIRONS):
assert get_float("positive", 1234) == 123
assert get_float("negative", 1234) == -456
assert get_float("zero", 1234) == 0
assert get_float("float-positive", 1234) == 1.1
assert get_float("float-negative", 1234) == -1.1
assert get_float("float-zero", 1234) == 0.0

for key, value in FAKE_ENVIRONS.items():
if key not in (
"positive",
"negative",
"zero",
"float-zero",
"float-positive",
"float-negative",
):
with pytest.raises(EnvironmentVariableParseException) as ex:
get_float(key, 1234)
assert (
ex.value.args[0] == f"Expected value in {key}={value} to be a float"
)

assert get_float("missing", "default") == "default"


def test_get_bool():
"""
get_bool should get the bool from the environment variable, or raise an exception if it's not parseable as a bool
Expand All @@ -99,7 +108,7 @@ def test_get_bool():
== f"Expected value in {key}={value} to be a boolean"
)

assert get_int("missing", "default") == "default"
assert get_bool("missing", "default") == "default"


def test_get_list_of_str():
Expand Down
31 changes: 30 additions & 1 deletion main/sentry.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Sentry setup and configuration"""

import logging

import sentry_sdk
from celery.exceptions import WorkerLostError
from sentry_sdk.integrations.celery import CeleryIntegration
Expand All @@ -10,6 +12,9 @@
SHUTDOWN_ERRORS = (WorkerLostError, SystemExit)


log = logging.getLogger()


def before_send(event, hint):
"""
Filter or transform events before they're sent to Sentry
Expand All @@ -29,7 +34,15 @@ def before_send(event, hint):
return event


def init_sentry(*, dsn, environment, version, log_level):
def init_sentry( # noqa: PLR0913
*,
dsn,
environment,
version,
log_level,
traces_sample_rate,
profiles_sample_rate,
):
"""
Initializes sentry
Expand All @@ -38,12 +51,28 @@ def init_sentry(*, dsn, environment, version, log_level):
environment (str): the application environment
version (str): the version of the application
log_level (str): the sentry log level
traces_sample_rate (int): int between 0 and 100 for the sample rate
profiles_sample_rate (int): int between 0 and 100 for the sample rate
""" # noqa: D401
if not 0 <= traces_sample_rate <= 1:
log.error(
"SENTRY_TRACES_SAMPLE_RATE should be between 0 <= x <= 1, defaulting to 0"
)
traces_sample_rate = 0

if not 0 <= profiles_sample_rate <= 1:
log.error(
"SENTRY_PROFILES_SAMPLE_RATE should be between 0 <= x <= 1, defaulting to 0"
)
profiles_sample_rate = 0

sentry_sdk.init( # pylint:disable=abstract-class-instantiated
dsn=dsn,
environment=environment,
release=version,
before_send=before_send,
traces_sample_rate=traces_sample_rate,
profiles_sample_rate=profiles_sample_rate,
integrations=[
DjangoIntegration(),
CeleryIntegration(),
Expand Down
38 changes: 23 additions & 15 deletions main/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
from django.core.exceptions import ImproperlyConfigured

from main.envs import (
get_any,
get_bool,
get_float,
get_int,
get_list_of_str,
get_string,
Expand All @@ -43,8 +43,15 @@
# initialize Sentry before doing anything else so we capture any config errors
SENTRY_DSN = get_string("SENTRY_DSN", "")
SENTRY_LOG_LEVEL = get_string("SENTRY_LOG_LEVEL", "ERROR")
SENTRY_TRACES_SAMPLE_RATE = get_float("SENTRY_TRACES_SAMPLE_RATE", 0)
SENTRY_PROFILES_SAMPLE_RATE = get_float("SENTRY_PROFILES_SAMPLE_RATE", 0)
init_sentry(
dsn=SENTRY_DSN, environment=ENVIRONMENT, version=VERSION, log_level=SENTRY_LOG_LEVEL
dsn=SENTRY_DSN,
environment=ENVIRONMENT,
version=VERSION,
log_level=SENTRY_LOG_LEVEL,
traces_sample_rate=SENTRY_TRACES_SAMPLE_RATE,
profiles_sample_rate=SENTRY_PROFILES_SAMPLE_RATE,
)

BASE_DIR = os.path.dirname( # noqa: PTH120
Expand Down Expand Up @@ -110,7 +117,6 @@
"news_events",
"testimonials",
"data_fixtures",
"silk",
)

if not get_bool("RUN_DATA_MIGRATIONS", default=False):
Expand Down Expand Up @@ -151,14 +157,26 @@
"hijack.middleware.HijackUserMiddleware",
"oauth2_provider.middleware.OAuth2TokenMiddleware",
"django_scim.middleware.SCIMAuthCheckMiddleware",
"silk.middleware.SilkyMiddleware",
)

# CORS
CORS_ALLOWED_ORIGINS = get_list_of_str("CORS_ALLOWED_ORIGINS", [])
CORS_ALLOWED_ORIGIN_REGEXES = get_list_of_str("CORS_ALLOWED_ORIGIN_REGEXES", [])

CORS_ALLOW_CREDENTIALS = get_bool("CORS_ALLOW_CREDENTIALS", True) # noqa: FBT003
CORS_ALLOW_HEADERS = (
# defaults
"accept",
"authorization",
"content-type",
"user-agent",
"x-csrftoken",
"x-requested-with",
# sentry tracing
"baggage",
"sentry-trace",
)

SECURE_CROSS_ORIGIN_OPENER_POLICY = get_string(
"SECURE_CROSS_ORIGIN_OPENER_POLICY",
"same-origin",
Expand Down Expand Up @@ -633,7 +651,7 @@ def get_all_config_keys():
MITOL_FEATURES_PREFIX = get_string("MITOL_FEATURES_PREFIX", "FEATURE_")
MITOL_FEATURES_DEFAULT = get_bool("MITOL_FEATURES_DEFAULT", False) # noqa: FBT003
FEATURES = {
key[len(MITOL_FEATURES_PREFIX) :]: get_any(key, None)
key[len(MITOL_FEATURES_PREFIX) :]: get_bool(key, False) # noqa: FBT003
for key in get_all_config_keys()
if key.startswith(MITOL_FEATURES_PREFIX)
}
Expand Down Expand Up @@ -745,13 +763,3 @@ def get_all_config_keys():
name="POSTHOG_PROJECT_ID",
default=None,
)

SILKY_INTERCEPT_PERCENT = get_int(name="SILKY_INTERCEPT_PERCENT", default=0)
SILKY_MAX_RECORDED_REQUESTS = get_int(name="SILKY_MAX_RECORDED_REQUESTS", default=10**3)
SILKY_MAX_RECORDED_REQUESTS_CHECK_PERCENT = get_int(
name="SILKY_MAX_RECORDED_REQUESTS_CHECK_PERCENT", default=10
)
SILKY_PYTHON_PROFILER = get_bool("SILKY_PYTHON_PROFILER", default=False)
SILKY_AUTHENTICATION = True # User must login
SILKY_AUTHORISATION = True
SILKY_PERMISSIONS = lambda user: user.is_superuser # noqa: E731
Loading

0 comments on commit 4567001

Please sign in to comment.