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

feat: add event_logger to test_connection and create_database commands #13468

Merged
merged 48 commits into from
Mar 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
9e2f527
leverage enter and exit magic functions for with statements
hughhhh Mar 3, 2021
6730835
Merge branch 'master' of https://github.com/apache/incubator-superset…
hughhhh Mar 3, 2021
80cd5bd
setup new logger pattern
hughhhh Mar 3, 2021
f4c8a5f
Update superset/utils/log.py
hughhhh Mar 4, 2021
e2ab146
update test to incorporate user_id
hughhhh Mar 4, 2021
440a9d6
Merge branch 'hugh/event-logger-refactor' of https://github.com/apach…
hughhhh Mar 4, 2021
1266d12
update test
hughhhh Mar 4, 2021
d7e1247
fix mypy
hughhhh Mar 4, 2021
b130962
Apply suggestions from code review
hughhhh Mar 4, 2021
9718486
update test
hughhhh Mar 4, 2021
2d05a37
add logs to database/create
hughhhh Mar 4, 2021
f9d5f3f
add logs to test_connection
hughhhh Mar 4, 2021
58fcd28
Merge branch 'master' of https://github.com/apache/incubator-superset…
hughhhh Mar 4, 2021
47fe377
fix pylint
hughhhh Mar 4, 2021
2fda45e
Merge branch 'hugh/event-logger-refactor' of https://github.com/apach…
hughhhh Mar 4, 2021
35763ef
Merge branch 'master' of https://github.com/apache/incubator-superset…
hughhhh Mar 4, 2021
31abdc8
pylint
hughhhh Mar 5, 2021
12e7731
Merge branch 'hugh/event-logger-refactor' of https://github.com/apach…
hughhhh Mar 5, 2021
e0871f0
Merge branch 'master' of https://github.com/apache/incubator-superset…
hughhhh Mar 5, 2021
bcf85f8
fix celery test
hughhhh Mar 5, 2021
f42efe2
fix celery test
hughhhh Mar 5, 2021
82612ea
fix all cta test
hughhhh Mar 5, 2021
9d7fc0e
add exception block for no user
hughhhh Mar 5, 2021
bfa5477
fixed test
hughhhh Mar 5, 2021
68f25d0
reup celery test
hughhhh Mar 5, 2021
f483764
pylint
hughhhh Mar 5, 2021
538d9dd
Merge branch 'hugh/event-logger-refactor' of https://github.com/apach…
hughhhh Mar 5, 2021
ea81349
event logger
hughhhh Mar 5, 2021
46ce47f
add pyline
hughhhh Mar 5, 2021
f4eaac1
Merge branch 'master' of https://github.com/apache/incubator-superset…
hughhhh Mar 5, 2021
8f6d379
Merge branch 'master' of https://github.com/apache/incubator-superset…
hughhhh Mar 6, 2021
cfbab0c
fix test
hughhhh Mar 7, 2021
55601ee
added 1 more test
hughhhh Mar 7, 2021
cffe48e
add more test
hughhhh Mar 7, 2021
c4039f9
add name
hughhhh Mar 7, 2021
b2c120c
address comment
hughhhh Mar 8, 2021
2f86e2a
fix docstring
hughhhh Mar 8, 2021
3856999
updated test
hughhhh Mar 8, 2021
33e4180
update security test
hughhhh Mar 8, 2021
67d42b4
update test
hughhhh Mar 8, 2021
f11ab8a
fix annotation on DatabaseDAO.build_db_for_connection_test
hughhhh Mar 8, 2021
7c28363
fix
hughhhh Mar 9, 2021
ce0240a
make the names consistent
hughhhh Mar 9, 2021
e20f6dd
remove unused packages
hughhhh Mar 9, 2021
864173d
fix test
hughhhh Mar 9, 2021
a904cd8
fix test
hughhhh Mar 9, 2021
46095ca
fix connection test
hughhhh Mar 9, 2021
c48ecd7
fix connection test
hughhhh Mar 9, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions superset/databases/commands/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
)
from superset.databases.commands.test_connection import TestConnectionDatabaseCommand
from superset.databases.dao import DatabaseDAO
from superset.extensions import db, security_manager
from superset.extensions import db, event_logger, security_manager

logger = logging.getLogger(__name__)

Expand All @@ -50,8 +50,12 @@ def run(self) -> Model:

try:
TestConnectionDatabaseCommand(self._actor, self._properties).run()
except Exception:
except Exception as ex: # pylint: disable=broad-except
db.session.rollback()
event_logger.log_with_context(
action=f"db_creation_failed.{ex.__class__.__name__}",
engine=database.db_engine_spec.__name__,
)
raise DatabaseConnectionFailedError()

# adding a new database we always want to force refresh schema list
Expand All @@ -63,7 +67,10 @@ def run(self) -> Model:
security_manager.add_permission_view_menu("database_access", database.perm)
db.session.commit()
except DAOCreateFailedError as ex:
logger.exception(ex.exception)
event_logger.log_with_context(
action=f"db_creation_failed.{ex.__class__.__name__}",
engine=database.db_engine_spec.__name__,
Copy link
Member

Choose a reason for hiding this comment

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

@hughhhh what did you want to do about making the engine string consistent around each of these logs so that we can group by the same engine later when observing the logs?

Copy link
Member Author

@hughhhh hughhhh Mar 9, 2021

Choose a reason for hiding this comment

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

I ended up changing all the engine references in the event_logger.log_with_context to pull data from the database.db_engine_spec.__name__ I say we go with this for now and see the data then see if we need to create some mapping either in the service or downstream

)
raise DatabaseCreateFailedError()
return database

Expand All @@ -84,4 +91,7 @@ def validate(self) -> None:
if exceptions:
exception = DatabaseInvalidError()
exception.add_list(exceptions)
event_logger.log_with_context(
action=f"db_connection_failed.{exception.__class__.__name__}"
)
raise exception
40 changes: 29 additions & 11 deletions superset/databases/commands/test_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@

from flask_appbuilder.security.sqla.models import User
from flask_babel import gettext as _
from sqlalchemy.engine.url import make_url
from sqlalchemy.exc import DBAPIError, NoSuchModuleError

from superset.commands.base import BaseCommand
Expand All @@ -32,6 +31,7 @@
)
from superset.databases.dao import DatabaseDAO
from superset.exceptions import SupersetSecurityException
from superset.extensions import event_logger
from superset.models.core import Database

logger = logging.getLogger(__name__)
Expand All @@ -55,24 +55,42 @@ def run(self) -> None:
impersonate_user=self._properties.get("impersonate_user", False),
encrypted_extra=self._properties.get("encrypted_extra", "{}"),
)
if database is not None:
database.set_sqlalchemy_uri(uri)
database.db_engine_spec.mutate_db_for_connection_test(database)
username = self._actor.username if self._actor is not None else None
engine = database.get_sqla_engine(user_name=username)

database.set_sqlalchemy_uri(uri)
database.db_engine_spec.mutate_db_for_connection_test(database)
username = self._actor.username if self._actor is not None else None
engine = database.get_sqla_engine(user_name=username)
with closing(engine.raw_connection()) as conn:
if not engine.dialect.do_ping(conn):
raise DBAPIError(None, None, None)
except (NoSuchModuleError, ModuleNotFoundError):
driver_name = make_url(uri).drivername

except (NoSuchModuleError, ModuleNotFoundError) as ex:
event_logger.log_with_context(
action=f"test_connection_error.{ex.__class__.__name__}",
engine=database.db_engine_spec.__name__,
)
raise DatabaseTestConnectionDriverError(
message=_("Could not load database driver: {}").format(driver_name),
message=_("Could not load database driver: {}").format(
database.db_engine_spec.__name__
),
)
except DBAPIError as ex:
event_logger.log_with_context(
action=f"test_connection_error.{ex.__class__.__name__}",
engine=database.db_engine_spec.__name__,
)
except DBAPIError:
raise DatabaseTestConnectionFailedError()
except SupersetSecurityException as ex:
event_logger.log_with_context(
action=f"test_connection_error.{ex.__class__.__name__}",
engine=database.db_engine_spec.__name__,
)
raise DatabaseSecurityUnsafeError(message=str(ex))
except Exception:
except Exception as ex: # pylint: disable=broad-except
event_logger.log_with_context(
action=f"test_connection_error.{ex.__class__.__name__}",
engine=database.db_engine_spec.__name__,
)
raise DatabaseTestConnectionUnexpectedError()

def validate(self) -> None:
Expand Down
2 changes: 1 addition & 1 deletion superset/databases/dao.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def get_database_by_name(database_name: str) -> Optional[Database]:
@staticmethod
def build_db_for_connection_test(
server_cert: str, extra: str, impersonate_user: bool, encrypted_extra: str
) -> Optional[Database]:
) -> Database:
return Database(
server_cert=server_cert,
extra=extra,
Expand Down
7 changes: 5 additions & 2 deletions superset/utils/log.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,14 +104,17 @@ def log( # pylint: disable=too-many-arguments
def log_with_context( # pylint: disable=too-many-locals
self,
action: str,
duration: timedelta,
duration: Optional[timedelta] = None,
object_ref: Optional[str] = None,
log_to_statsd: bool = True,
**payload_override: Optional[Dict[str, Any]],
) -> None:
from superset.views.core import get_form_data

referrer = request.referrer[:1000] if request.referrer else None

duration_ms = int(duration.total_seconds() * 1000) if duration else None

try:
user_id = g.user.get_id()
except Exception as ex: # pylint: disable=broad-except
Expand Down Expand Up @@ -158,7 +161,7 @@ def log_with_context( # pylint: disable=too-many-locals
records=records,
dashboard_id=dashboard_id,
slice_id=slice_id,
duration_ms=int(duration.total_seconds() * 1000),
duration_ms=duration_ms,
referrer=referrer,
)

Expand Down
4 changes: 2 additions & 2 deletions tests/databases/api_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -819,7 +819,7 @@ def test_test_connection_failed(self):
self.assertEqual(rv.headers["Content-Type"], "application/json; charset=utf-8")
response = json.loads(rv.data.decode("utf-8"))
expected_response = {
"message": "Could not load database driver: broken",
"message": "Could not load database driver: BaseEngineSpec",
}
self.assertEqual(response, expected_response)

Expand All @@ -834,7 +834,7 @@ def test_test_connection_failed(self):
self.assertEqual(rv.headers["Content-Type"], "application/json; charset=utf-8")
response = json.loads(rv.data.decode("utf-8"))
expected_response = {
"message": "Could not load database driver: mssql+pymssql",
"message": "Could not load database driver: MssqlEngineSpec",
}
self.assertEqual(response, expected_response)

Expand Down
88 changes: 86 additions & 2 deletions tests/databases/commands_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,30 @@
# specific language governing permissions and limitations
# under the License.
# pylint: disable=no-self-use, invalid-name
from unittest import mock
from unittest.mock import patch

import pytest
import yaml
from sqlalchemy.exc import DBAPIError

from superset import db, security_manager
from superset import db, event_logger, security_manager
from superset.commands.exceptions import CommandInvalidError
from superset.commands.importers.exceptions import IncorrectVersionError
from superset.connectors.sqla.models import SqlaTable
from superset.databases.commands.exceptions import DatabaseNotFoundError
from superset.databases.commands.exceptions import (
DatabaseNotFoundError,
DatabaseSecurityUnsafeError,
DatabaseTestConnectionDriverError,
DatabaseTestConnectionFailedError,
DatabaseTestConnectionUnexpectedError,
)
from superset.databases.commands.export import ExportDatabasesCommand
from superset.databases.commands.importers.v1 import ImportDatabasesCommand
from superset.databases.commands.test_connection import TestConnectionDatabaseCommand
from superset.databases.schemas import DatabaseTestConnectionSchema
from superset.errors import SupersetError
from superset.exceptions import SupersetSecurityException
from superset.models.core import Database
from superset.utils.core import backend, get_example_database
from tests.base_tests import SupersetTestCase
Expand Down Expand Up @@ -508,3 +520,75 @@ def test_import_v1_rollback(self, mock_import_dataset):
# verify that the database was not added
new_num_databases = db.session.query(Database).count()
assert new_num_databases == num_databases


class TestTestConnectionDatabaseCommand(SupersetTestCase):
@mock.patch("superset.databases.dao.Database.get_sqla_engine")
@mock.patch(
"superset.databases.commands.test_connection.event_logger.log_with_context"
)
def test_connection_db_exception(self, mock_event_logger, mock_get_sqla_engine):
"""Test to make sure event_logger is called when an exception is raised"""
database = get_example_database()
mock_get_sqla_engine.side_effect = Exception("An error has occurred!")
db_uri = database.sqlalchemy_uri_decrypted
json_payload = {"sqlalchemy_uri": db_uri}
command_without_db_name = TestConnectionDatabaseCommand(
security_manager.find_user("admin"), json_payload
)

with pytest.raises(DatabaseTestConnectionUnexpectedError) as excinfo:
command_without_db_name.run()
assert str(excinfo.value) == (
"Unexpected error occurred, please check your logs for details"
)
mock_event_logger.assert_called()

@mock.patch("superset.databases.dao.Database.get_sqla_engine")
@mock.patch(
"superset.databases.commands.test_connection.event_logger.log_with_context"
)
def test_connection_superset_security_connection(
self, mock_event_logger, mock_get_sqla_engine
):
"""Test to make sure event_logger is called when security
connection exc is raised"""
database = get_example_database()
mock_get_sqla_engine.side_effect = SupersetSecurityException(
SupersetError(error_type=500, message="test", level="info", extra={})
)
db_uri = database.sqlalchemy_uri_decrypted
json_payload = {"sqlalchemy_uri": db_uri}
command_without_db_name = TestConnectionDatabaseCommand(
security_manager.find_user("admin"), json_payload
)

with pytest.raises(DatabaseSecurityUnsafeError) as excinfo:
command_without_db_name.run()
assert str(excinfo.value) == ("Stopped an unsafe database connection")

mock_event_logger.assert_called()

@mock.patch("superset.databases.dao.Database.get_sqla_engine")
@mock.patch(
"superset.databases.commands.test_connection.event_logger.log_with_context"
)
def test_connection_db_api_exc(self, mock_event_logger, mock_get_sqla_engine):
"""Test to make sure event_logger is called when DBAPIError is raised"""
database = get_example_database()
mock_get_sqla_engine.side_effect = DBAPIError(
statement="error", params={}, orig={}
)
db_uri = database.sqlalchemy_uri_decrypted
json_payload = {"sqlalchemy_uri": db_uri}
command_without_db_name = TestConnectionDatabaseCommand(
security_manager.find_user("admin"), json_payload
)

with pytest.raises(DatabaseTestConnectionFailedError) as excinfo:
command_without_db_name.run()
assert str(excinfo.value) == (
"Connection failed, please check your connection settings"
)

mock_event_logger.assert_called()