Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Start using Sentry in our general stack #143

Merged
merged 6 commits into from Jun 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions documentation/changelog.rst
Expand Up @@ -14,6 +14,8 @@ Bugfixes
Infrastructure / Support
----------------------

* Add possibility to send errors to Sentry [see `PR #143 <http://www.github.com/SeitaBV/flexmeasures/pull/143>`_]


v0.5.0 | June 7, 2021
===========================
Expand Down
48 changes: 38 additions & 10 deletions documentation/configuration.rst
Expand Up @@ -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``

Expand All @@ -152,13 +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``


SQLAlchemy
----------
Expand Down Expand Up @@ -329,6 +322,41 @@ Password of mail system user.
Default: ``None``


.. _monitoring

Monitoring
-----------

Monitoring potential problems in FlexMeasure's operations.


SENTRY_DSN
^^^^^^^^^^^^

Set tokenized URL, so errors will be sent to Sentry when ``app.env`` is not in `debug` or `testing` mode.
E.g.: ``https://<examplePublicKey>@o<something>.ingest.sentry.io/<project-Id>``

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 <https://docs.sentry.io/platforms/python/configuration/options/>_` 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
Expand Down
15 changes: 9 additions & 6 deletions flexmeasures/app.py
Expand Up @@ -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
Expand All @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions flexmeasures/data/scripts/cli_tasks/data_add.py
Expand Up @@ -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
Expand Down
40 changes: 38 additions & 2 deletions flexmeasures/utils/app_utils.py
Expand Up @@ -4,17 +4,52 @@

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}",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be useful to also add which plugins are loaded (incl. their version). I'm not sure whether this is the right place to put such info, but this is where the thought came up for me.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this is already handled in line 146.

send_default_pii=True, # user data (current user id, email address, username) is attached to the event.
environment=app.env,
**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"))


def set_secret_key(app, filename="secret_key"):
"""Set the SECRET_KEY or exit.

Expand Down Expand Up @@ -104,3 +139,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", {}))
6 changes: 6 additions & 0 deletions flexmeasures/utils/config_defaults.py
Expand Up @@ -75,6 +75,12 @@ class Config(object):
3000 # Web interface poll period for updates in ms
)

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 = ""
FLEXMEASURES_TIMEZONE: str = "Asia/Seoul"
Expand Down
15 changes: 8 additions & 7 deletions flexmeasures/utils/config_utils.py
Expand Up @@ -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 (
Expand All @@ -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):
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion flexmeasures/utils/error_utils.py
Expand Up @@ -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
)
Expand Down
1 change: 1 addition & 0 deletions requirements/app.in
Expand Up @@ -48,6 +48,7 @@ Flask-Security-Too>=4.0
Flask-Classful
Flask-Marshmallow
Flask-Cors
sentry-sdk[flask]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you comment on this notation?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the common syntax for libraries that also install to load certain extensions. In this case, Flask is the extension. As it is rather standard, I'd like to not add a comment.

marshmallow-sqlalchemy>=0.23.1
webargs
# flask should be after all the flask plugins, because setup might find they ARE flask
Expand Down
9 changes: 8 additions & 1 deletion requirements/app.txt
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down