Skip to content

Commit

Permalink
Feat: Sentry error monitoring (#253)
Browse files Browse the repository at this point in the history
  • Loading branch information
thekaveman authored Nov 8, 2023
2 parents 98173ba + 4879308 commit 0a2dccd
Show file tree
Hide file tree
Showing 8 changed files with 156 additions and 2 deletions.
5 changes: 5 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
FLASK_APP=eligibility_server/app.py

# this sample uses an odd relative path because the value is
# used by code under the eligibility_server directory
# and thus the config folder is one level up

ELIGIBILITY_SERVER_SETTINGS=../config/sample.py
38 changes: 38 additions & 0 deletions docs/configuration/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ The sections below outline in more detail the settings that you either must set

## App settings

### `AGENCY_NAME`

The name of the agency that this server is deployed for

### `APP_NAME`

The name set on the Flask app
Expand Down Expand Up @@ -133,3 +137,37 @@ These are the possible values for the `CSV_QUOTING` variable:
- `csv.QUOTE_ALL`: 1 - To be used when all the values in the CSV file are present inside quotation marks
- `csv.QUOTE_NONNUMERIC`: 2 - To be used when the CSV file uses quotes around non-numeric entries
- `csv.QUOTE_NONE`: 3 - To be used when the CSV file does not use quotes around entries

## Sentry

### `SENTRY_DSN`

Cal-ITP's Sentry instance collects both [errors ("Issues")](https://sentry.calitp.org/organizations/sentry/issues/?project=4) and app [performance info](https://sentry.calitp.org/organizations/sentry/performance/?project=4).

[Alerts are sent to #benefits-notify in Slack.](https://sentry.calitp.org/organizations/sentry/alerts/rules/eligibility-server/10/details/) [Others can be configured.](https://sentry.calitp.org/organizations/sentry/alerts/rules/)

You can troubleshoot Sentry itself by [turning on debug mode](#debug_mode) and visiting `/error/`.

!!! tldr "Sentry docs"

[Data Source Name (DSN)](https://docs.sentry.io/product/sentry-basics/dsn-explainer/)

Enables sending events to Sentry.

### `SENTRY_ENVIRONMENT`

!!! tldr "Sentry docs"

[`environment` config value](https://docs.sentry.io/platforms/python/configuration/options/#environment)

Segments errors by which deployment they occur in. This defaults to `local`, and can be set to match one of the environment names.

### `SENTRY_TRACES_SAMPLE_RATE`

!!! tldr "Sentry docs"

[`traces_sample_rate` config value](https://docs.sentry.io/platforms/python/configuration/options/#traces-sample-rate)

Control the volume of transactions sent to Sentry. Value must be a float in the range `[0.0, 1.0]`.

The default is `0.0` (i.e. no transactions are tracked).
11 changes: 10 additions & 1 deletion eligibility_server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from flask_restful import Api
from flask.logging import default_handler

from eligibility_server import db
from eligibility_server import db, sentry
from eligibility_server.keypair import get_server_public_key
from eligibility_server.settings import Configuration
from eligibility_server.verify import Verify
Expand All @@ -23,6 +23,7 @@

# use an app context for access to config settings
with app.app_context():
sentry.configure(config)
# configure root logger first, to prevent duplicate log entries from Flask's logger
logging.basicConfig(level=config.log_level, format=format_string)
# configure Flask's logger
Expand All @@ -37,6 +38,14 @@ def TextResponse(content):
return response


with app.app_context():
if config.debug_mode:

@app.route("/error")
def trigger_error():
raise ValueError("testing Sentry for eligibility-server")


@app.route("/healthcheck")
def healthcheck():
app.logger.info("Request healthcheck")
Expand Down
66 changes: 66 additions & 0 deletions eligibility_server/sentry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import logging
import os
import subprocess

import sentry_sdk
from sentry_sdk.integrations.flask import FlaskIntegration
from sentry_sdk.scrubber import EventScrubber, DEFAULT_DENYLIST

from eligibility_server.settings import Configuration

logger = logging.getLogger(__name__)


SENTRY_ENVIRONMENT = os.environ.get("SENTRY_ENVIRONMENT", "local")


# https://stackoverflow.com/a/21901260/358804
def get_git_revision_hash():
return subprocess.check_output(["git", "rev-parse", "HEAD"]).decode("ascii").strip()


def get_release() -> str:
return get_git_revision_hash()


def get_denylist():
# custom denylist
denylist = DEFAULT_DENYLIST + ["sub", "name"]
return denylist


def get_traces_sample_rate(config: Configuration):
rate = config.sentry_traces_sample_rate
if rate < 0.0 or rate > 1.0:
logger.warning("SENTRY_TRACES_SAMPLE_RATE was not in the range [0.0, 1.0], defaulting to 0.0")
rate = 0.0
else:
logger.info(f"SENTRY_TRACES_SAMPLE_RATE set to: {rate}")

return rate


def configure(config: Configuration):
SENTRY_DSN = config.sentry_dsn
if SENTRY_DSN:
release = get_release()
logger.info(f"Enabling Sentry for environment '{SENTRY_ENVIRONMENT}', release '{release}'...")

sentry_sdk.init(
dsn=SENTRY_DSN,
integrations=[
FlaskIntegration(),
],
traces_sample_rate=get_traces_sample_rate(config),
environment=SENTRY_ENVIRONMENT,
release=release,
in_app_include=["eligibility_server"],
# send_default_pii must be False (the default) for a custom EventScrubber/denylist
# https://docs.sentry.io/platforms/python/data-management/sensitive-data/#event_scrubber
send_default_pii=False,
event_scrubber=EventScrubber(denylist=get_denylist()),
)

sentry_sdk.set_tag("agency_name", config.agency_name)
else:
logger.warning("SENTRY_DSN not set, so won't send events")
21 changes: 21 additions & 0 deletions eligibility_server/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

# App settings

AGENCY_NAME = "[unset]"
APP_NAME = "eligibility_server.app"
DEBUG_MODE = True
HOST = "0.0.0.0" # nosec
Expand Down Expand Up @@ -43,10 +44,19 @@
CSV_QUOTING = 3
CSV_QUOTECHAR = '"'

# Sentry

SENTRY_DSN = None
SENTRY_TRACES_SAMPLE_RATE = 0.0


class Configuration:
# App settings

@property
def agency_name(self):
return str(current_app.config["AGENCY_NAME"])

@property
def app_name(self):
return str(current_app.config["APP_NAME"])
Expand Down Expand Up @@ -134,3 +144,14 @@ def csv_quoting(self):
@property
def csv_quotechar(self):
return str(current_app.config["CSV_QUOTECHAR"])

# Sentry

@property
def sentry_dsn(self):
sentry_dsn = current_app.config["SENTRY_DSN"]
return str(sentry_dsn) if sentry_dsn else None

@property
def sentry_traces_sample_rate(self):
return float(current_app.config["SENTRY_TRACES_SAMPLE_RATE"])
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ dependencies = [
"Flask==2.3.3",
"Flask-RESTful==0.3.10",
"Flask-SQLAlchemy==3.1.1",
"requests==2.31.0"
"requests==2.31.0",
"sentry-sdk[flask]==1.19.1"
]

[project.optional-dependencies]
Expand Down
3 changes: 3 additions & 0 deletions terraform/app_service.tf
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ resource "azurerm_linux_web_app" "main" {
"WEBSITES_ENABLE_APP_SERVICE_STORAGE" = "false"
"WEBSITES_PORT" = "8000"
"WEBSITES_CONTAINER_START_TIME_LIMIT" = "1800"

# Sentry
"SENTRY_ENVIRONMENT" = "${local.env_name}"
}

identity {
Expand Down
11 changes: 11 additions & 0 deletions tests/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,3 +225,14 @@ def test_configuration_csv_quotechar(mocker, configuration: Configuration):
mocker.patch.dict("eligibility_server.settings.current_app.config", mocked_config)

assert configuration.csv_quotechar == new_value


@pytest.mark.usefixtures("flask")
def test_configuration_sentry_dsn(mocker, configuration: Configuration):
assert configuration.sentry_dsn is None

new_value = "https://sentry.example.com"
mocked_config = {"SENTRY_DSN": new_value}
mocker.patch.dict("eligibility_server.settings.current_app.config", mocked_config)

assert configuration.sentry_dsn == new_value

0 comments on commit 0a2dccd

Please sign in to comment.