From e33728d6fb4f44bb672265f2fda4092943ea1c16 Mon Sep 17 00:00:00 2001 From: nhoening Date: Fri, 11 Jun 2021 20:21:15 +0000 Subject: [PATCH 1/5] Create draft PR for #119 From 03fb769a936574f3c66ecb276592636550867ba0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20H=C3=B6ning?= Date: Tue, 15 Jun 2021 14:43:12 +0200 Subject: [PATCH 2/5] fix type mismatch when queueing forecasting jobs from CLI --- flexmeasures/data/scripts/cli_tasks/data_add.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flexmeasures/data/scripts/cli_tasks/data_add.py b/flexmeasures/data/scripts/cli_tasks/data_add.py index 99d340972..96f0221b6 100644 --- a/flexmeasures/data/scripts/cli_tasks/data_add.py +++ b/flexmeasures/data/scripts/cli_tasks/data_add.py @@ -481,8 +481,8 @@ def create_forecasts( asset_id=asset_id, timed_value_type=value_type, horizons=[horizon], - start_of_roll=from_date - timedelta(hours=horizon), - end_of_roll=to_date - timedelta(hours=horizon), + start_of_roll=from_date - horizon, + end_of_roll=to_date - horizon, ) else: from flexmeasures.data.scripts.data_gen import populate_time_series_forecasts From 2394c1d757620f02bd7214afba646456adcec090 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20H=C3=B6ning?= Date: Tue, 15 Jun 2021 14:55:05 +0200 Subject: [PATCH 3/5] add sentr SDK as dependency and use it if SENTRY_DSN is set --- documentation/changelog.rst | 2 ++ documentation/configuration.rst | 9 ++++++ flexmeasures/app.py | 15 +++++---- flexmeasures/utils/app_utils.py | 44 +++++++++++++++++++++++++-- flexmeasures/utils/config_defaults.py | 2 ++ flexmeasures/utils/config_utils.py | 15 ++++----- flexmeasures/utils/error_utils.py | 2 +- requirements/app.in | 1 + requirements/app.txt | 9 +++++- 9 files changed, 82 insertions(+), 17 deletions(-) diff --git a/documentation/changelog.rst b/documentation/changelog.rst index e39578afe..e64a74a3e 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -14,6 +14,8 @@ Bugfixes Infrastructure / Support ---------------------- +* Add possibility to send errors to Sentry [see `PR #143 `_] + v0.5.0 | June 7, 2021 =========================== diff --git a/documentation/configuration.rst b/documentation/configuration.rst index 68b3c4759..361c2e00b 100644 --- a/documentation/configuration.rst +++ b/documentation/configuration.rst @@ -160,6 +160,15 @@ Token which external services can use to check on the status of recurring tasks Default: ``None`` +SENTRY_SDN +^^^^^^^^^^^^ + +Set tokenized URL, so errors will be sent to Sentry when ``app.env`` is not in `debug` or `testing` mode. +E.g.: ``https://@o.ingest.sentry.io/`` + +Default: ``None`` + + SQLAlchemy ---------- diff --git a/flexmeasures/app.py b/flexmeasures/app.py index 9918b6d9e..205f8d472 100644 --- a/flexmeasures/app.py +++ b/flexmeasures/app.py @@ -26,7 +26,7 @@ def create(env: Optional[str] = None, path_to_config: Optional[str] = None) -> F from flexmeasures.utils import config_defaults from flexmeasures.utils.config_utils import read_config, configure_logging - from flexmeasures.utils.app_utils import set_secret_key + from flexmeasures.utils.app_utils import set_secret_key, init_sentry from flexmeasures.utils.error_utils import add_basic_error_handlers # Create app @@ -36,17 +36,20 @@ def create(env: Optional[str] = None, path_to_config: Optional[str] = None) -> F # as we need to know the ENV now (for it to be recognised by Flask()). load_dotenv() app = Flask("flexmeasures") + if env is not None: # overwrite app.env = env - if env == "testing": - app.testing = True - if env == "development": - app.debug = config_defaults.DevelopmentConfig.DEBUG + if app.env == "testing": + app.testing = True + if app.env == "development": + app.debug = config_defaults.DevelopmentConfig.DEBUG # App configuration - read_config(app, path_to_config=path_to_config) + read_config(app, custom_path_to_config=path_to_config) add_basic_error_handlers(app) + if not app.env == "development" and not app.testing: + init_sentry(app) app.mail = Mail(app) FlaskJSON(app) diff --git a/flexmeasures/utils/app_utils.py b/flexmeasures/utils/app_utils.py index b54b752f7..945607e86 100644 --- a/flexmeasures/utils/app_utils.py +++ b/flexmeasures/utils/app_utils.py @@ -4,17 +4,56 @@ import click from flask import Flask -from flask.cli import FlaskGroup +from flask.cli import FlaskGroup, with_appcontext +import sentry_sdk +from sentry_sdk.integrations.flask import FlaskIntegration +from sentry_sdk.integrations.rq import RqIntegration +from pkg_resources import get_distribution from flexmeasures.app import create as create_app @click.group(cls=FlaskGroup, create_app=create_app) +@with_appcontext def flexmeasures_cli(): - """Management scripts for the FlexMeasures platform.""" + """ + Management scripts for the FlexMeasures platform. + We use @app_context here so things from the app setup are initialised + only once. This is crucial for Sentry, for example. + """ pass +def init_sentry(app: Flask): + """ + Configure Sentry. + We need the app to read the Sentry DSN from configuration, and also + to send some additional meta information. + """ + sentry_dsn = app.config.get("SENTRY_DSN") + if not sentry_dsn: + app.logger.info( + "[FLEXMEASURES] No SENTRY_DSN setting found, so initialising Sentry cannot happen ..." + ) + return + app.logger.info("[FLEXMEASURES] Initialising Sentry ...") + sentry_sdk.init( + dsn=sentry_dsn, + integrations=[FlaskIntegration(), RqIntegration()], + debug=app.debug, + release=f"flexmeasures@{get_distribution('flexmeasures').version}", + send_default_pii=True, # user data (current user id, email address, username) is attached to the event. + environment=app.env, + # Set traces_sample_rate to 1.0 to capture 100% + # of transactions for performance monitoring. + # We recommend adjusting this value in production. + # TODO: Decide if we need this and if to configure it. + traces_sample_rate=0.33, + ) + sentry_sdk.set_tag("mode", app.config.get("FLEXMEASURES_MODE")) + sentry_sdk.set_tag("platform-name", app.config.get("FLEXMEASURES_PLATFORM_NAME")) + + def set_secret_key(app, filename="secret_key"): """Set the SECRET_KEY or exit. @@ -104,3 +143,4 @@ def register_plugins(app: Flask): plugin_version = getattr(plugin_blueprint, "__version__", "0.1") app.config["LOADED_PLUGINS"][plugin_name] = plugin_version app.logger.info(f"Loaded plugins: {app.config['LOADED_PLUGINS']}") + sentry_sdk.set_context("plugins", app.config.get("LOADED_PLUGINS", {})) diff --git a/flexmeasures/utils/config_defaults.py b/flexmeasures/utils/config_defaults.py index 4c787a8c1..10bdb9695 100644 --- a/flexmeasures/utils/config_defaults.py +++ b/flexmeasures/utils/config_defaults.py @@ -75,6 +75,8 @@ class Config(object): 3000 # Web interface poll period for updates in ms ) + SENTRY_DSN: Optional[str] = None + FLEXMEASURES_PLATFORM_NAME: str = "FlexMeasures" FLEXMEASURES_MODE: str = "" FLEXMEASURES_TIMEZONE: str = "Asia/Seoul" diff --git a/flexmeasures/utils/config_utils.py b/flexmeasures/utils/config_utils.py index eea227e08..aa9b80d45 100644 --- a/flexmeasures/utils/config_utils.py +++ b/flexmeasures/utils/config_utils.py @@ -50,7 +50,7 @@ def configure_logging(): loggingDictConfig(flexmeasures_logging_config) -def read_config(app: Flask, path_to_config: Optional[str]): +def read_config(app: Flask, custom_path_to_config: Optional[str]): """Read configuration from various expected sources, complain if not setup correctly. """ if app.env not in ( @@ -65,21 +65,22 @@ def read_config(app: Flask, path_to_config: Optional[str]): ) sys.exit(2) - # Load default config settings + # First, load default config settings app.config.from_object( "flexmeasures.utils.config_defaults.%sConfig" % camelize(app.env) ) - # Now read user config, if possible. If no explicit path is given, try home dir first, then instance dir + # Now, potentially overwrite those from config file + # These two locations are possible (besides the custom path) path_to_config_home = str(Path.home().joinpath(".flexmeasures.cfg")) path_to_config_instance = os.path.join(app.instance_path, "flexmeasures.cfg") - if not app.testing: + if not app.testing: # testing runs completely on defaults + # If no custom path is given, this will try home dir first, then instance dir used_path_to_config = read_custom_config( - app, path_to_config, path_to_config_home, path_to_config_instance + app, custom_path_to_config, path_to_config_home, path_to_config_instance ) # Check for missing values. - # Testing might affect only specific functionality (-> dev's responsibility) # Documentation runs fine without them. if not app.testing and app.env != "documentation": if not are_required_settings_complete(app): @@ -126,7 +127,7 @@ def read_custom_config( app.config.from_pyfile(path_to_config) except FileNotFoundError: pass - # Finally, all required varaiables can be set as env var: + # Finally, all required variables can be set as env var: for req_var in required: app.config[req_var] = os.getenv(req_var, app.config.get(req_var, None)) return path_to_config diff --git a/flexmeasures/utils/error_utils.py b/flexmeasures/utils/error_utils.py index 93a581c0a..ead6a48d0 100644 --- a/flexmeasures/utils/error_utils.py +++ b/flexmeasures/utils/error_utils.py @@ -23,7 +23,7 @@ def log_error(exc: Exception, error_msg: str): extra = dict(url=request.path, **get_err_source_info(last_traceback)) msg = ( - '{error_name}:"{message}" [occured at {src_module}({src_func}):{src_linenr},' + '{error_name}:"{message}" [occurred at {src_module}({src_func}):{src_linenr},' "URL was: {url}]".format( error_name=exc.__class__.__name__, message=error_msg, **extra ) diff --git a/requirements/app.in b/requirements/app.in index 2264d838c..efa53d299 100644 --- a/requirements/app.in +++ b/requirements/app.in @@ -48,6 +48,7 @@ Flask-Security-Too>=4.0 Flask-Classful Flask-Marshmallow Flask-Cors +sentry-sdk[flask] marshmallow-sqlalchemy>=0.23.1 webargs # flask should be after all the flask plugins, because setup might find they ARE flask diff --git a/requirements/app.txt b/requirements/app.txt index 05c12e7b2..d1a1443b7 100644 --- a/requirements/app.txt +++ b/requirements/app.txt @@ -22,12 +22,15 @@ blinker==1.4 # via # flask-mail # flask-principal + # sentry-sdk bokeh==1.0.4 # via # -r requirements/app.in # pandas-bokeh certifi==2020.12.5 - # via requests + # via + # requests + # sentry-sdk cffi==1.14.5 # via bcrypt cftime==1.4.1 @@ -99,6 +102,7 @@ flask==1.1.2 # flask-sslify # flask-wtf # rq-dashboard + # sentry-sdk greenlet==1.0.0 # via sqlalchemy humanize==3.3.0 @@ -280,6 +284,8 @@ scipy==1.6.2 # timetomodel selenium==3.141.0 # via timely-beliefs +sentry-sdk[flask]==1.1.0 + # via -r requirements/app.in siphon==0.9 # via -r requirements/app.in six==1.15.0 @@ -331,6 +337,7 @@ urllib3==1.26.4 # via # requests # selenium + # sentry-sdk webargs==7.0.1 # via -r requirements/app.in werkzeug==1.0.1 From 9312c7f83e131464f09306b13bb2515c26fa11aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20H=C3=B6ning?= Date: Sat, 19 Jun 2021 20:29:57 +0200 Subject: [PATCH 4/5] add FLEXMEASURES_SENTRY_CONFIG setting --- documentation/configuration.rst | 57 ++++++++++++++++++--------- flexmeasures/utils/app_utils.py | 6 +-- flexmeasures/utils/config_defaults.py | 4 ++ 3 files changed, 43 insertions(+), 24 deletions(-) diff --git a/documentation/configuration.rst b/documentation/configuration.rst index 361c2e00b..0771d3c47 100644 --- a/documentation/configuration.rst +++ b/documentation/configuration.rst @@ -133,13 +133,13 @@ The horizon to use when making schedules. Default: ``timedelta(hours=2 * 24)`` -Tokens ------- +Access Tokens +--------------- OPENWEATHERMAP_API_KEY ^^^^^^^^^^^^^^^^ -Token for accessing the OPenWeatherMap weather forecasting service. +Token for accessing the OpenWeatherMap weather forecasting service. Default: ``None`` @@ -152,22 +152,6 @@ Token for accessing the MapBox API (for displaying maps on the dashboard and ass Default: ``None`` -FLEXMEASURES_TASK_CHECK_AUTH_TOKEN -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Token which external services can use to check on the status of recurring tasks within FlexMeasures. - -Default: ``None`` - - -SENTRY_SDN -^^^^^^^^^^^^ - -Set tokenized URL, so errors will be sent to Sentry when ``app.env`` is not in `debug` or `testing` mode. -E.g.: ``https://@o.ingest.sentry.io/`` - -Default: ``None`` - SQLAlchemy ---------- @@ -338,6 +322,41 @@ Password of mail system user. Default: ``None`` +.. _monitoring + +Monitoring +----------- + +Monitoring potential problems in FlexMeasure's operations. + + +SENTRY_SDN +^^^^^^^^^^^^ + +Set tokenized URL, so errors will be sent to Sentry when ``app.env`` is not in `debug` or `testing` mode. +E.g.: ``https://@o.ingest.sentry.io/`` + +Default: ``None`` + + +FLEXMEASURES_SENTRY_CONFIG +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +A dictionary with values to configure reporting to Sentry. Some options are taken care of by FlexMeasures (e.g. environment and release), but not all. +See `here _` for a complete list. + +Default: ``{}`` + + +FLEXMEASURES_TASK_CHECK_AUTH_TOKEN +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Token which external services can use to check on the status of recurring tasks within FlexMeasures. + +Default: ``None`` + + + .. _redis-config: Redis diff --git a/flexmeasures/utils/app_utils.py b/flexmeasures/utils/app_utils.py index 945607e86..093a7cbda 100644 --- a/flexmeasures/utils/app_utils.py +++ b/flexmeasures/utils/app_utils.py @@ -44,11 +44,7 @@ def init_sentry(app: Flask): release=f"flexmeasures@{get_distribution('flexmeasures').version}", send_default_pii=True, # user data (current user id, email address, username) is attached to the event. environment=app.env, - # Set traces_sample_rate to 1.0 to capture 100% - # of transactions for performance monitoring. - # We recommend adjusting this value in production. - # TODO: Decide if we need this and if to configure it. - traces_sample_rate=0.33, + **app.config["FLEXMEASURES_SENTRY_CONFIG"], ) sentry_sdk.set_tag("mode", app.config.get("FLEXMEASURES_MODE")) sentry_sdk.set_tag("platform-name", app.config.get("FLEXMEASURES_PLATFORM_NAME")) diff --git a/flexmeasures/utils/config_defaults.py b/flexmeasures/utils/config_defaults.py index 10bdb9695..70de7332a 100644 --- a/flexmeasures/utils/config_defaults.py +++ b/flexmeasures/utils/config_defaults.py @@ -76,6 +76,10 @@ class Config(object): ) SENTRY_DSN: Optional[str] = None + # Place additional Sentry config here. + # traces_sample_rate is for performance monitoring across all transactions, + # you probably want to adjust this. + FLEXMEASURES_SENTRY_CONFIG: dict = dict(traces_sample_rate=0.33) FLEXMEASURES_PLATFORM_NAME: str = "FlexMeasures" FLEXMEASURES_MODE: str = "" From c10825a20d50acc3d4ffbd2a1733c03df9846a2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20H=C3=B6ning?= Date: Sat, 19 Jun 2021 20:32:48 +0200 Subject: [PATCH 5/5] fix typo --- documentation/configuration.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/configuration.rst b/documentation/configuration.rst index 0771d3c47..f000e8549 100644 --- a/documentation/configuration.rst +++ b/documentation/configuration.rst @@ -330,7 +330,7 @@ Monitoring Monitoring potential problems in FlexMeasure's operations. -SENTRY_SDN +SENTRY_DSN ^^^^^^^^^^^^ Set tokenized URL, so errors will be sent to Sentry when ``app.env`` is not in `debug` or `testing` mode.