diff --git a/.env.example b/.env.example index 51cac1ae4..0ffe2a1e8 100644 --- a/.env.example +++ b/.env.example @@ -4,3 +4,6 @@ FLASK_SECRET=abc #API_KEY_REQUIRED_STARTING_AT=2021-07-30 API_KEY_ADMIN_PASSWORD=abc API_KEY_REGISTER_WEBHOOK_TOKEN=abc + +# Sentry +# If setting a Sentry DSN, note that the URL should NOT be quoted! diff --git a/dev/local/Makefile b/dev/local/Makefile index 84c308257..d0854a064 100644 --- a/dev/local/Makefile +++ b/dev/local/Makefile @@ -77,6 +77,7 @@ LOG_REDIS:=delphi_redis_instance_$(NOW).log WEB_CONTAINER_ID:=$(shell docker ps -q --filter 'name=delphi_web_epidata') DATABASE_CONTAINER_ID:=$(shell docker ps -q --filter 'name=delphi_database_epidata') REDIS_CONTAINER_ID:=$(shell docker ps -q --filter 'name=delphi_redis') +ENV_FILE:=repos/delphi/delphi-epidata/.env M1= ifeq ($(shell uname -smp), Darwin arm64 arm) @@ -104,8 +105,10 @@ web: @# Run the web server @# MODULE_NAME specifies the location of the `app` variable, the actual WSGI application object to run. @# see https://github.com/tiangolo/meinheld-gunicorn-docker#module_name + @touch $(ENV_FILE) @docker run --rm -p 127.0.0.1:10080:80 \ $(M1) \ + --env-file $(ENV_FILE) \ --env "MODULE_NAME=delphi.epidata.server.main" \ --env "SQLALCHEMY_DATABASE_URI=$(sqlalchemy_uri)" \ --env "FLASK_SECRET=abc" --env "FLASK_PREFIX=/epidata" --env "LOG_DEBUG" \ diff --git a/devops/Dockerfile b/devops/Dockerfile index 97dc0e2c8..3c5ff2672 100644 --- a/devops/Dockerfile +++ b/devops/Dockerfile @@ -7,7 +7,6 @@ FROM tiangolo/meinheld-gunicorn:python3.8 LABEL org.opencontainers.image.source=https://github.com/cmu-delphi/delphi-epidata COPY ./devops/gunicorn_conf.py /app -COPY ./devops/start_wrapper.sh / RUN mkdir -p /app/delphi/epidata COPY ./src/server /app/delphi/epidata/server COPY ./src/common /app/delphi/epidata/common @@ -18,7 +17,6 @@ COPY requirements.api.txt /app/requirements_also.txt RUN ln -s -f /usr/share/zoneinfo/America/New_York /etc/localtime \ && rm -rf /app/delphi/epidata/__pycache__ \ && chmod -R o+r /app/delphi/epidata \ - && chmod 755 /start_wrapper.sh \ && pip install --no-cache-dir -r /tmp/requirements.txt -r requirements_also.txt # the file /tmp/requirements.txt is created in the parent docker definition. (see: # https://github.com/tiangolo/meinheld-gunicorn-docker/blob/master/docker-images/python3.8.dockerfile#L5 ) @@ -28,4 +26,4 @@ RUN ln -s -f /usr/share/zoneinfo/America/New_York /etc/localtime \ ENV PYTHONUNBUFFERED 1 ENTRYPOINT [ "/entrypoint.sh" ] -CMD [ "/start_wrapper.sh" ] +CMD [ "/start.sh" ] diff --git a/devops/start_wrapper.sh b/devops/start_wrapper.sh deleted file mode 100644 index a5192d6e3..000000000 --- a/devops/start_wrapper.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env sh -set -e - -# If a New Relic license key is found then we start with custom New Relic -# commands, otherwise we start via start.sh. -if [ -z "${NEW_RELIC_LICENSE_KEY}" ]; then - sh /start.sh -else - newrelic-admin run-program /start.sh -fi diff --git a/docs/epidata_development.md b/docs/epidata_development.md index 29a0fdde7..c8c35e11f 100644 --- a/docs/epidata_development.md +++ b/docs/epidata_development.md @@ -388,3 +388,13 @@ The command above maps two local directories into the container: - `/repos/delphi/delphi-epidata/src`: Just the source code, which forms the container's `delphi.epidata` python package. +## instrumentation with Sentry + +Delphi uses [Sentry](https://sentry.io/welcome/) in production for debugging, APM, and other observability purposes. You can instrument your local environment if you want to take advantage of Sentry's features during the development process. In most cases this option is available to internal Delphi team members only. + +The bare minimum to set up instrumentation is to supply the DSN for the [epidata-api](https://cmu-delphi.sentry.io/projects/epidata-api/?project=4506123377442816) Sentry project to the application environment. + +- You can get the DSN from the Sentry [project's keys config](https://cmu-delphi.sentry.io/settings/projects/epidata-api/keys/), or by asking someone in the prodsys, DevOps, or sysadmin space. +- Once you have the DSN, add it to your local `.env` file and rebuild your containers to start sending telemetry to Sentry. + +Additional internal documentation for Sentry can be found [here](https://bookstack.delphi.cmu.edu/books/systems-handbook/page/sentry). diff --git a/requirements.api.txt b/requirements.api.txt index d2c47f64b..b00eb3e06 100644 --- a/requirements.api.txt +++ b/requirements.api.txt @@ -5,7 +5,6 @@ Flask-Limiter==3.3.0 jinja2==3.0.3 more_itertools==8.4.0 mysqlclient==2.1.1 -newrelic orjson==3.4.7 pandas==1.2.3 python-dotenv==0.15.0 @@ -13,6 +12,7 @@ pyyaml redis==3.5.3 requests==2.31.0 scipy==1.10.0 +sentry-sdk[flask] SQLAlchemy==1.4.40 structlog==22.1.0 tenacity==7.0.0 diff --git a/src/server/main.py b/src/server/main.py index a91a91ee2..2ec07e5a5 100644 --- a/src/server/main.py +++ b/src/server/main.py @@ -1,5 +1,7 @@ +import os import pathlib import logging +import sentry_sdk from typing import Dict, Callable from flask import request, send_file, Response, send_from_directory, jsonify @@ -13,6 +15,18 @@ from .endpoints.admin import bp as admin_bp, enable_admin from ._limiter import limiter, apply_limit +SENTRY_DSN = os.environ.get('SENTRY_DSN') +if SENTRY_DSN: + sentry_sdk.init( + dsn = SENTRY_DSN, + environment = os.environ.get('SENTRY_ENVIRONMENT', 'development'), + profiles_sample_rate = float(os.environ.get('SENTRY_PROFILES_SAMPLE_RATE', 1.0)), + traces_sample_rate = float(os.environ.get('SENTRY_TRACES_SAMPLE_RATE', 1.0)), + attach_stacktrace = os.environ.get('SENTRY_ATTACH_STACKTRACE', 'False').lower() in ('true', '1', 't'), + debug = os.environ.get('SENTRY_DEBUG', 'False').lower() in ('true', '1', 't') + ) + + __all__ = ["app"] logger = get_structured_logger("webapp_main")