diff --git a/UPDATING.md b/UPDATING.md index b4c686338804a..eaa40ccbba90f 100644 --- a/UPDATING.md +++ b/UPDATING.md @@ -25,7 +25,10 @@ assists people when migrating to a new version. ## Next - [11704](https://github.com/apache/incubator-superset/pull/11704) Breaking change: Jinja templating for SQL queries has been updated, removing default modules such as `datetime` and `random` and enforcing static template values. To restore or extend functionality, use `JINJA_CONTEXT_ADDONS` and `CUSTOM_TEMPLATE_PROCESSORS` in `superset_config.py`. - +- [11714](https://github.com/apache/incubator-superset/pull/11714): Logs + significantly more analytics events (roughly double?), and when + using DBEventLogger (default) could result in stressing the metadata + database more. - [11509](https://github.com/apache/incubator-superset/pull/11509): Config value `TABLE_NAMES_CACHE_CONFIG` has been renamed to `DATA_CACHE_CONFIG`, which will now also hold query results cache from connected datasources (previously held in `CACHE_CONFIG`), in addition to the table names. If you will set `DATA_CACHE_CONFIG` to a new cache backend different than your previous `CACHE_CONFIG`, plan for additional cache warmup to avoid degrading charting performance for the end users. - [11575](https://github.com/apache/incubator-superset/pull/11575) The Row Level Security (RLS) config flag has been moved to a feature flag. To migrate, add `ROW_LEVEL_SECURITY: True` to the `FEATURE_FLAGS` dict in `superset_config.py`. diff --git a/superset/annotation_layers/api.py b/superset/annotation_layers/api.py index 31fa7a0d7b153..c608e30157d56 100644 --- a/superset/annotation_layers/api.py +++ b/superset/annotation_layers/api.py @@ -47,6 +47,7 @@ openapi_spec_methods_override, ) from superset.constants import RouteMethod +from superset.extensions import event_logger from superset.models.annotations import AnnotationLayer from superset.views.base_api import BaseSupersetModelRestApi, statsd_metrics @@ -110,6 +111,7 @@ class AnnotationLayerRestApi(BaseSupersetModelRestApi): @safe @statsd_metrics @permission_name("delete") + @event_logger.log_this_with_context(log_to_statsd=False) def delete(self, pk: int) -> Response: """Delete an annotation layer --- @@ -159,6 +161,7 @@ def delete(self, pk: int) -> Response: @safe @statsd_metrics @permission_name("post") + @event_logger.log_this_with_context(log_to_statsd=False) def post(self) -> Response: """Creates a new Annotation Layer --- @@ -218,6 +221,7 @@ def post(self) -> Response: @safe @statsd_metrics @permission_name("put") + @event_logger.log_this_with_context(log_to_statsd=False) def put(self, pk: int) -> Response: """Updates an Annotation Layer --- @@ -284,6 +288,7 @@ def put(self, pk: int) -> Response: @safe @statsd_metrics @rison(get_delete_ids_schema) + @event_logger.log_this_with_context(log_to_statsd=False) def bulk_delete(self, **kwargs: Any) -> Response: """Delete bulk Annotation layers --- diff --git a/superset/cachekeys/api.py b/superset/cachekeys/api.py index 94a8c4bf72558..8548898b4c455 100644 --- a/superset/cachekeys/api.py +++ b/superset/cachekeys/api.py @@ -45,10 +45,10 @@ class CacheRestApi(BaseSupersetModelRestApi): openapi_spec_component_schemas = (CacheInvalidationRequestSchema,) @expose("/invalidate", methods=["POST"]) - @event_logger.log_this @protect() @safe @statsd_metrics + @event_logger.log_this_with_context(log_to_statsd=False) def invalidate(self) -> Response: """ Takes a list of datasources, finds the associated cache records and diff --git a/superset/charts/api.py b/superset/charts/api.py index 8e8789da6a261..cb6a33dffb4a6 100644 --- a/superset/charts/api.py +++ b/superset/charts/api.py @@ -214,6 +214,7 @@ def __init__(self) -> None: @protect() @safe @statsd_metrics + @event_logger.log_this_with_context(log_to_statsd=False) def post(self) -> Response: """Creates a new Chart --- @@ -270,6 +271,7 @@ def post(self) -> Response: @protect() @safe @statsd_metrics + @event_logger.log_this_with_context(log_to_statsd=False) def put(self, pk: int) -> Response: """Changes a Chart --- @@ -343,6 +345,7 @@ def put(self, pk: int) -> Response: @protect() @safe @statsd_metrics + @event_logger.log_this_with_context(log_to_statsd=False) def delete(self, pk: int) -> Response: """Deletes a Chart --- @@ -393,6 +396,7 @@ def delete(self, pk: int) -> Response: @safe @statsd_metrics @rison(get_delete_ids_schema) + @event_logger.log_this_with_context(log_to_statsd=False) def bulk_delete(self, **kwargs: Any) -> Response: """Delete bulk Charts --- @@ -444,10 +448,10 @@ def bulk_delete(self, **kwargs: Any) -> Response: return self.response_422(message=str(ex)) @expose("/data", methods=["POST"]) - @event_logger.log_this @protect() @safe @statsd_metrics + @event_logger.log_this_with_context(log_to_statsd=False) def data(self) -> Response: """ Takes a query context constructed in the client and returns payload @@ -532,6 +536,7 @@ def data(self) -> Response: @rison(screenshot_query_schema) @safe @statsd_metrics + @event_logger.log_this_with_context(log_to_statsd=False) def cache_screenshot(self, pk: int, **kwargs: Dict[str, bool]) -> WerkzeugResponse: """ --- @@ -604,6 +609,7 @@ def trigger_celery() -> WerkzeugResponse: @rison(screenshot_query_schema) @safe @statsd_metrics + @event_logger.log_this_with_context(log_to_statsd=False) def screenshot(self, pk: int, digest: str) -> WerkzeugResponse: """Get Chart screenshot --- @@ -657,6 +663,7 @@ def screenshot(self, pk: int, digest: str) -> WerkzeugResponse: @rison(thumbnail_query_schema) @safe @statsd_metrics + @event_logger.log_this_with_context(log_to_statsd=False) def thumbnail( self, pk: int, digest: str, **kwargs: Dict[str, bool] ) -> WerkzeugResponse: @@ -730,6 +737,7 @@ def thumbnail( @safe @statsd_metrics @rison(get_export_ids_schema) + @event_logger.log_this_with_context(log_to_statsd=False) def export(self, **kwargs: Any) -> Response: """Export charts --- @@ -787,6 +795,7 @@ def export(self, **kwargs: Any) -> Response: @safe @statsd_metrics @rison(get_fav_star_ids_schema) + @event_logger.log_this_with_context(log_to_statsd=False) def favorite_status(self, **kwargs: Any) -> Response: """Favorite stars for Charts --- diff --git a/superset/css_templates/api.py b/superset/css_templates/api.py index e59fd5184f4b9..8e6f07ab3b5d4 100644 --- a/superset/css_templates/api.py +++ b/superset/css_templates/api.py @@ -33,6 +33,7 @@ get_delete_ids_schema, openapi_spec_methods_override, ) +from superset.extensions import event_logger from superset.models.core import CssTemplate from superset.views.base_api import BaseSupersetModelRestApi, statsd_metrics @@ -87,6 +88,7 @@ class CssTemplateRestApi(BaseSupersetModelRestApi): @safe @statsd_metrics @rison(get_delete_ids_schema) + @event_logger.log_this_with_context(log_to_statsd=False) def bulk_delete(self, **kwargs: Any) -> Response: """Delete bulk CSS Templates --- diff --git a/superset/dashboards/api.py b/superset/dashboards/api.py index 815bfd19fcd69..c4c874b666e3c 100644 --- a/superset/dashboards/api.py +++ b/superset/dashboards/api.py @@ -60,6 +60,7 @@ openapi_spec_methods_override, thumbnail_query_schema, ) +from superset.extensions import event_logger from superset.models.dashboard import Dashboard from superset.tasks.thumbnails import cache_dashboard_thumbnail from superset.utils.screenshots import DashboardScreenshot @@ -205,6 +206,7 @@ def __init__(self) -> None: @protect() @safe @statsd_metrics + @event_logger.log_this_with_context(log_to_statsd=False) def post(self) -> Response: """Creates a new Dashboard --- @@ -263,6 +265,7 @@ def post(self) -> Response: @protect() @safe @statsd_metrics + @event_logger.log_this_with_context(log_to_statsd=False) def put(self, pk: int) -> Response: """Changes a Dashboard --- @@ -333,6 +336,7 @@ def put(self, pk: int) -> Response: @protect() @safe @statsd_metrics + @event_logger.log_this_with_context(log_to_statsd=False) def delete(self, pk: int) -> Response: """Deletes a Dashboard --- @@ -383,6 +387,7 @@ def delete(self, pk: int) -> Response: @safe @statsd_metrics @rison(get_delete_ids_schema) + @event_logger.log_this_with_context(log_to_statsd=False) def bulk_delete(self, **kwargs: Any) -> Response: """Delete bulk Dashboards --- @@ -440,6 +445,7 @@ def bulk_delete(self, **kwargs: Any) -> Response: @safe @statsd_metrics @rison(get_export_ids_schema) + @event_logger.log_this_with_context(log_to_statsd=False) def export(self, **kwargs: Any) -> Response: """Export dashboards --- @@ -515,6 +521,7 @@ def export(self, **kwargs: Any) -> Response: @protect() @safe @rison(thumbnail_query_schema) + @event_logger.log_this_with_context(log_to_statsd=False) def thumbnail( self, pk: int, digest: str, **kwargs: Dict[str, bool] ) -> WerkzeugResponse: @@ -602,6 +609,7 @@ def thumbnail( @safe @statsd_metrics @rison(get_fav_star_ids_schema) + @event_logger.log_this_with_context(log_to_statsd=False) def favorite_status(self, **kwargs: Any) -> Response: """Favorite Stars for Dashboards --- diff --git a/superset/databases/api.py b/superset/databases/api.py index 4d8a0d4134793..326d4d8528b3a 100644 --- a/superset/databases/api.py +++ b/superset/databases/api.py @@ -184,6 +184,7 @@ class DatabaseRestApi(BaseSupersetModelRestApi): @protect() @safe @statsd_metrics + @event_logger.log_this_with_context(log_to_statsd=False) def post(self) -> Response: """Creates a new Database --- @@ -246,6 +247,7 @@ def post(self) -> Response: @protect() @safe @statsd_metrics + @event_logger.log_this_with_context(log_to_statsd=False) def put( # pylint: disable=too-many-return-statements, arguments-differ self, pk: int ) -> Response: @@ -319,6 +321,7 @@ def put( # pylint: disable=too-many-return-statements, arguments-differ @protect() @safe @statsd_metrics + @event_logger.log_this_with_context(log_to_statsd=False) def delete(self, pk: int) -> Response: # pylint: disable=arguments-differ """Deletes a Database --- @@ -369,6 +372,7 @@ def delete(self, pk: int) -> Response: # pylint: disable=arguments-differ @safe @rison(database_schemas_query_schema) @statsd_metrics + @event_logger.log_this_with_context(log_to_statsd=False) def schemas(self, pk: int, **kwargs: Any) -> FlaskResponse: """Get all schemas from a database --- @@ -422,8 +426,8 @@ def schemas(self, pk: int, **kwargs: Any) -> FlaskResponse: @protect() @check_datasource_access @safe - @event_logger.log_this @statsd_metrics + @event_logger.log_this_with_context(log_to_statsd=False) def table_metadata( self, database: Database, table_name: str, schema_name: str ) -> FlaskResponse: @@ -479,8 +483,8 @@ def table_metadata( @protect() @check_datasource_access @safe - @event_logger.log_this @statsd_metrics + @event_logger.log_this_with_context(log_to_statsd=False) def select_star( self, database: Database, table_name: str, schema_name: Optional[str] = None ) -> FlaskResponse: @@ -536,8 +540,8 @@ def select_star( @expose("/test_connection", methods=["POST"]) @protect() @safe - @event_logger.log_this @statsd_metrics + @event_logger.log_this_with_context(log_to_statsd=False) def test_connection( # pylint: disable=too-many-return-statements self, ) -> FlaskResponse: @@ -617,6 +621,7 @@ def test_connection( # pylint: disable=too-many-return-statements @protect() @safe @statsd_metrics + @event_logger.log_this_with_context(log_to_statsd=False) def related_objects(self, pk: int) -> Response: """Get charts and dashboards count associated to a database --- @@ -675,6 +680,7 @@ def related_objects(self, pk: int) -> Response: @safe @statsd_metrics @rison(get_export_ids_schema) + @event_logger.log_this_with_context(log_to_statsd=False) def export(self, **kwargs: Any) -> Response: """Export database(s) with associated datasets --- diff --git a/superset/datasets/api.py b/superset/datasets/api.py index 1decc707ac032..2f36f7ee68119 100644 --- a/superset/datasets/api.py +++ b/superset/datasets/api.py @@ -27,7 +27,7 @@ from flask_babel import ngettext from marshmallow import ValidationError -from superset import is_feature_enabled +from superset import event_logger, is_feature_enabled from superset.commands.exceptions import CommandInvalidError from superset.connectors.sqla.models import SqlaTable from superset.constants import RouteMethod @@ -181,6 +181,7 @@ class DatasetRestApi(BaseSupersetModelRestApi): @protect() @safe @statsd_metrics + @event_logger.log_this_with_context(log_to_statsd=False) def post(self) -> Response: """Creates a new Dataset --- @@ -237,6 +238,7 @@ def post(self) -> Response: @protect() @safe @statsd_metrics + @event_logger.log_this_with_context(log_to_statsd=False) def put(self, pk: int) -> Response: """Changes a Dataset --- @@ -307,6 +309,7 @@ def put(self, pk: int) -> Response: @protect() @safe @statsd_metrics + @event_logger.log_this_with_context(log_to_statsd=False) def delete(self, pk: int) -> Response: """Deletes a Dataset --- @@ -357,6 +360,7 @@ def delete(self, pk: int) -> Response: @safe @statsd_metrics @rison(get_export_ids_schema) + @event_logger.log_this_with_context(log_to_statsd=False) def export(self, **kwargs: Any) -> Response: """Export datasets --- @@ -432,6 +436,7 @@ def export(self, **kwargs: Any) -> Response: @protect() @safe @statsd_metrics + @event_logger.log_this_with_context(log_to_statsd=False) def refresh(self, pk: int) -> Response: """Refresh a Dataset --- @@ -481,6 +486,7 @@ def refresh(self, pk: int) -> Response: @protect() @safe @statsd_metrics + @event_logger.log_this_with_context(log_to_statsd=False) def related_objects(self, pk: int) -> Response: """Get charts and dashboards count associated to a dataset --- @@ -539,6 +545,7 @@ def related_objects(self, pk: int) -> Response: @safe @statsd_metrics @rison(get_delete_ids_schema) + @event_logger.log_this_with_context(log_to_statsd=False) def bulk_delete(self, **kwargs: Any) -> Response: """Delete bulk Datasets --- diff --git a/superset/migrations/versions/a8173232b786_add_path_to_logs.py b/superset/migrations/versions/a8173232b786_add_path_to_logs.py new file mode 100644 index 0000000000000..d88f3245d8ff9 --- /dev/null +++ b/superset/migrations/versions/a8173232b786_add_path_to_logs.py @@ -0,0 +1,45 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +"""Add path to logs + +Revision ID: a8173232b786 +Revises: 49b5a32daba5 +Create Date: 2020-11-15 16:08:24.580764 + +""" + +# revision identifiers, used by Alembic. +revision = "a8173232b786" +down_revision = "49b5a32daba5" + +import sqlalchemy as sa +from alembic import op +from sqlalchemy.dialects import mysql + + +def upgrade(): + op.add_column("logs", sa.Column("path", sa.String(length=256), nullable=True)) + op.add_column( + "logs", sa.Column("path_no_int", sa.String(length=256), nullable=True) + ) + op.add_column("logs", sa.Column("ref", sa.String(length=256), nullable=True)) + + +def downgrade(): + op.drop_column("logs", "path") + op.drop_column("logs", "path_no_int") + op.drop_column("logs", "ref") diff --git a/superset/models/core.py b/superset/models/core.py index c4a8369a474bb..2ceff5456c386 100755 --- a/superset/models/core.py +++ b/superset/models/core.py @@ -715,6 +715,9 @@ class Log(Model): # pylint: disable=too-few-public-methods dttm = Column(DateTime, default=datetime.utcnow) duration_ms = Column(Integer) referrer = Column(String(1024)) + path = Column(String(256)) + path_no_int = Column(String(256)) + ref = Column(String(256)) class FavStarClassName(str, Enum): diff --git a/superset/utils/log.py b/superset/utils/log.py index 4380006b2aaca..24c9161f6c33d 100644 --- a/superset/utils/log.py +++ b/superset/utils/log.py @@ -30,15 +30,35 @@ from superset.stats_logger import BaseStatsLogger +def strip_int_from_path(path: Optional[str]) -> str: + """Simple function to remove ints from '/' separated paths""" + if path: + return "/".join(["" if s.isdigit() else s for s in path.split("/")]) + return "" + + class AbstractEventLogger(ABC): @abstractmethod - def log( - self, user_id: Optional[int], action: str, *args: Any, **kwargs: Any + def log( # pylint: disable=too-many-arguments + self, + user_id: Optional[int], + action: str, + dashboard_id: Optional[int], + duration_ms: Optional[int], + slice_id: Optional[int], + path: Optional[str], + path_no_int: Optional[str], + ref: Optional[str], + referrer: Optional[str], + *args: Any, + **kwargs: Any, ) -> None: pass @contextmanager - def log_context(self, action: str) -> Iterator[Callable[..., None]]: + def log_context( + self, action: str, ref: Optional[str] = None, log_to_statsd: bool = True, + ) -> Iterator[Callable[..., None]]: """ Log an event while reading information from the request context. `kwargs` will be appended directly to the log payload. @@ -69,7 +89,8 @@ def log_context(self, action: str) -> Iterator[Callable[..., None]]: except (TypeError, ValueError): slice_id = 0 - self.stats_logger.incr(action) + if log_to_statsd: + self.stats_logger.incr(action) # bulk insert try: @@ -86,18 +107,38 @@ def log_context(self, action: str) -> Iterator[Callable[..., None]]: slice_id=slice_id, duration_ms=round((time.time() - start_time) * 1000), referrer=referrer, + path=request.path, + path_no_int=strip_int_from_path(request.path), + ref=ref, ) - def log_this(self, f: Callable[..., Any]) -> Callable[..., Any]: + def _wrapper( + self, f: Callable[..., Any], **wrapper_kwargs: Any + ) -> Callable[..., Any]: + action_str = wrapper_kwargs.get("action") or f.__name__ + ref = f.__qualname__ if hasattr(f, "__qualname__") else None + @functools.wraps(f) def wrapper(*args: Any, **kwargs: Any) -> Any: - with self.log_context(f.__name__) as log: + with self.log_context(action_str, ref, **wrapper_kwargs) as log: value = f(*args, **kwargs) log(**kwargs) return value return wrapper + def log_this(self, f: Callable[..., Any]) -> Callable[..., Any]: + """Decorator that uses the function name as the action""" + return self._wrapper(f) + + def log_this_with_context(self, **kwargs: Any) -> Callable[..., Any]: + """Decorator that can override kwargs of log_context""" + + def func(f: Callable[..., Any]) -> Callable[..., Any]: + return self._wrapper(f, **kwargs) + + return func + def log_manually(self, f: Callable[..., Any]) -> Callable[..., Any]: """Allow a function to manually update""" @@ -162,16 +203,23 @@ def get_event_logger_from_cfg_value(cfg_value: Any) -> AbstractEventLogger: class DBEventLogger(AbstractEventLogger): """Event logger that commits logs to Superset DB""" - def log( # pylint: disable=too-many-locals - self, user_id: Optional[int], action: str, *args: Any, **kwargs: Any + def log( # pylint: disable=too-many-arguments,too-many-locals + self, + user_id: Optional[int], + action: str, + dashboard_id: Optional[int], + duration_ms: Optional[int], + slice_id: Optional[int], + path: Optional[str], + path_no_int: Optional[str], + ref: Optional[str], + referrer: Optional[str], + *args: Any, + **kwargs: Any, ) -> None: from superset.models.core import Log records = kwargs.get("records", list()) - dashboard_id = kwargs.get("dashboard_id") - slice_id = kwargs.get("slice_id") - duration_ms = kwargs.get("duration_ms") - referrer = kwargs.get("referrer") logs = list() for record in records: @@ -188,6 +236,9 @@ def log( # pylint: disable=too-many-locals duration_ms=duration_ms, referrer=referrer, user_id=user_id, + path=path, + path_no_int=path_no_int, + ref=ref, ) logs.append(log) try: diff --git a/superset/views/base_api.py b/superset/views/base_api.py index 1495a79b17134..0f05f07ee1627 100644 --- a/superset/views/base_api.py +++ b/superset/views/base_api.py @@ -31,7 +31,7 @@ from sqlalchemy import and_, distinct, func from sqlalchemy.orm.query import Query -from superset.extensions import db, security_manager +from superset.extensions import db, event_logger, security_manager from superset.models.core import FavStar from superset.models.dashboard import Dashboard from superset.models.slice import Slice @@ -49,6 +49,7 @@ "filter": {"type": "string"}, }, } +log_context = event_logger.log_context class RelatedResultResponseSchema(Schema): @@ -312,49 +313,61 @@ def info_headless(self, **kwargs: Any) -> Response: """ Add statsd metrics to builtin FAB _info endpoint """ - duration, response = time_function(super().info_headless, **kwargs) - self.send_stats_metrics(response, self.info.__name__, duration) - return response + ref = f"{self.__class__.__name__}.info" + with log_context(ref, ref, log_to_statsd=False): + duration, response = time_function(super().info_headless, **kwargs) + self.send_stats_metrics(response, self.info.__name__, duration) + return response def get_headless(self, pk: int, **kwargs: Any) -> Response: """ Add statsd metrics to builtin FAB GET endpoint """ - duration, response = time_function(super().get_headless, pk, **kwargs) - self.send_stats_metrics(response, self.get.__name__, duration) - return response + ref = f"{self.__class__.__name__}.get" + with log_context(ref, ref, log_to_statsd=False): + duration, response = time_function(super().get_headless, pk, **kwargs) + self.send_stats_metrics(response, self.get.__name__, duration) + return response def get_list_headless(self, **kwargs: Any) -> Response: """ Add statsd metrics to builtin FAB GET list endpoint """ - duration, response = time_function(super().get_list_headless, **kwargs) - self.send_stats_metrics(response, self.get_list.__name__, duration) - return response + ref = f"{self.__class__.__name__}.get_list" + with log_context(ref, ref, log_to_statsd=False): + duration, response = time_function(super().get_list_headless, **kwargs) + self.send_stats_metrics(response, self.get_list.__name__, duration) + return response def post_headless(self) -> Response: """ Add statsd metrics to builtin FAB POST endpoint """ - duration, response = time_function(super().post_headless) - self.send_stats_metrics(response, self.post.__name__, duration) - return response + ref = f"{self.__class__.__name__}.post" + with log_context(ref, ref, log_to_statsd=False): + duration, response = time_function(super().post_headless) + self.send_stats_metrics(response, self.post.__name__, duration) + return response def put_headless(self, pk: int) -> Response: """ Add statsd metrics to builtin FAB PUT endpoint """ - duration, response = time_function(super().put_headless, pk) - self.send_stats_metrics(response, self.put.__name__, duration) - return response + ref = f"{self.__class__.__name__}.put" + with log_context(ref, ref, log_to_statsd=False): + duration, response = time_function(super().put_headless, pk) + self.send_stats_metrics(response, self.put.__name__, duration) + return response def delete_headless(self, pk: int) -> Response: """ Add statsd metrics to builtin FAB DELETE endpoint """ - duration, response = time_function(super().delete_headless, pk) - self.send_stats_metrics(response, self.delete.__name__, duration) - return response + ref = f"{self.__class__.__name__}.delete" + with log_context(ref, ref, log_to_statsd=False): + duration, response = time_function(super().delete_headless, pk) + self.send_stats_metrics(response, self.delete.__name__, duration) + return response @expose("/related/", methods=["GET"]) @protect() diff --git a/superset/views/core.py b/superset/views/core.py index e12e50c248cce..c8781c3b49c06 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -155,6 +155,7 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods logger = logging.getLogger(__name__) @has_access_api + @event_logger.log_this @expose("/datasources/") def datasources(self) -> FlaskResponse: return self.json_response( @@ -169,6 +170,7 @@ def datasources(self) -> FlaskResponse: ) @has_access_api + @event_logger.log_this @expose("/override_role_permissions/", methods=["POST"]) def override_role_permissions(self) -> FlaskResponse: """Updates the role with the give datasource permissions. @@ -220,8 +222,8 @@ def override_role_permissions(self) -> FlaskResponse: {"granted": granted_perms, "requested": list(db_ds_names)}, status=201 ) - @event_logger.log_this @has_access + @event_logger.log_this @expose("/request_access/") def request_access(self) -> FlaskResponse: datasources = set() @@ -263,8 +265,8 @@ def request_access(self) -> FlaskResponse: datasource_names=", ".join([o.name for o in datasources]), ) - @event_logger.log_this @has_access + @event_logger.log_this @expose("/approve") def approve(self) -> FlaskResponse: # pylint: disable=too-many-locals,no-self-use def clean_fulfilled_requests(session: Session) -> None: @@ -368,6 +370,7 @@ def clean_fulfilled_requests(session: Session) -> None: return redirect("/accessrequestsmodelview/list/") @has_access + @event_logger.log_this @expose("/slice//") def slice(self, slice_id: int) -> FlaskResponse: # pylint: disable=no-self-use _, slc = get_form_data(slice_id, use_slice_data=True) @@ -450,9 +453,9 @@ def slice_json(self, slice_id: int) -> FlaskResponse: except SupersetException as ex: return json_error_response(utils.error_msg_from_exception(ex)) - @event_logger.log_this @api @has_access_api + @event_logger.log_this @expose("/annotation_json/") def annotation_json( # pylint: disable=no-self-use self, layer_id: int @@ -484,10 +487,10 @@ def annotation_json( # pylint: disable=no-self-use if not is_feature_enabled("ENABLE_EXPLORE_JSON_CSRF_PROTECTION"): EXPLORE_JSON_METHODS.append("GET") - @event_logger.log_this @api @has_access_api @handle_api_exception + @event_logger.log_this @expose( "/explore_json///", methods=EXPLORE_JSON_METHODS, @@ -535,8 +538,8 @@ def explore_json( except SupersetException as ex: return json_error_response(utils.error_msg_from_exception(ex), 400) - @event_logger.log_this @has_access + @event_logger.log_this @expose("/import_dashboards", methods=["GET", "POST"]) def import_dashboards(self) -> FlaskResponse: """Overrides the dashboards using json instances from the file.""" @@ -578,8 +581,8 @@ def import_dashboards(self) -> FlaskResponse: "superset/import_dashboards.html", databases=databases ) - @event_logger.log_this @has_access + @event_logger.log_this @expose("/explore///", methods=["GET", "POST"]) @expose("/explore/", methods=["GET", "POST"]) def explore( # pylint: disable=too-many-locals,too-many-return-statements @@ -733,6 +736,7 @@ def explore( # pylint: disable=too-many-locals,too-many-return-statements @api @handle_api_exception @has_access_api + @event_logger.log_this @expose("/filter////") def filter( # pylint: disable=no-self-use self, datasource_type: str, datasource_id: int, column: str @@ -881,6 +885,7 @@ def save_or_overwrite_slice( # pylint: disable=too-many-arguments,too-many-loca @api @has_access_api + @event_logger.log_this @expose("/schemas//") @expose("/schemas///") def schemas( # pylint: disable=no-self-use @@ -905,6 +910,7 @@ def schemas( # pylint: disable=no-self-use @api @has_access_api + @event_logger.log_this @expose("/tables////") @expose("/tables/////") def tables( # pylint: disable=too-many-locals,no-self-use @@ -1009,6 +1015,7 @@ def get_datasource_label(ds_name: utils.DatasourceName) -> str: @api @has_access_api + @event_logger.log_this @expose("/copy_dash//", methods=["GET", "POST"]) def copy_dash( # pylint: disable=no-self-use self, dashboard_id: int @@ -1058,6 +1065,7 @@ def copy_dash( # pylint: disable=no-self-use @api @has_access_api + @event_logger.log_this @expose("/save_dash//", methods=["GET", "POST"]) def save_dash( # pylint: disable=no-self-use self, dashboard_id: int @@ -1096,6 +1104,7 @@ def save_dash( # pylint: disable=no-self-use @api @has_access_api + @event_logger.log_this @expose("/add_slices//", methods=["POST"]) def add_slices( # pylint: disable=no-self-use self, dashboard_id: int @@ -1114,6 +1123,7 @@ def add_slices( # pylint: disable=no-self-use @api @has_access_api + @event_logger.log_this @expose("/testconn", methods=["POST", "GET"]) def testconn( # pylint: disable=too-many-return-statements,no-self-use self, @@ -1194,6 +1204,7 @@ def testconn( # pylint: disable=too-many-return-statements,no-self-use @api @has_access_api + @event_logger.log_this @expose("/recent_activity//", methods=["GET"]) def recent_activity( # pylint: disable=no-self-use self, user_id: int @@ -1292,6 +1303,7 @@ def recent_activity( # pylint: disable=no-self-use @api @has_access_api + @event_logger.log_this @expose("/csrf_token/", methods=["GET"]) def csrf_token(self) -> FlaskResponse: return Response( @@ -1300,6 +1312,7 @@ def csrf_token(self) -> FlaskResponse: @api @has_access_api + @event_logger.log_this @expose("/available_domains/", methods=["GET"]) def available_domains(self) -> FlaskResponse: # pylint: disable=no-self-use """ @@ -1313,6 +1326,7 @@ def available_domains(self) -> FlaskResponse: # pylint: disable=no-self-use @api @has_access_api + @event_logger.log_this @expose("/fave_dashboards_by_username//", methods=["GET"]) def fave_dashboards_by_username(self, username: str) -> FlaskResponse: """This lets us use a user's username to pull favourite dashboards""" @@ -1321,6 +1335,7 @@ def fave_dashboards_by_username(self, username: str) -> FlaskResponse: @api @has_access_api + @event_logger.log_this @expose("/fave_dashboards//", methods=["GET"]) def fave_dashboards( # pylint: disable=no-self-use self, user_id: int @@ -1355,6 +1370,7 @@ def fave_dashboards( # pylint: disable=no-self-use @api @has_access_api + @event_logger.log_this @expose("/created_dashboards//", methods=["GET"]) def created_dashboards( # pylint: disable=no-self-use self, user_id: int @@ -1383,6 +1399,7 @@ def created_dashboards( # pylint: disable=no-self-use @api @has_access_api + @event_logger.log_this @expose("/user_slices", methods=["GET"]) @expose("/user_slices//", methods=["GET"]) def user_slices( # pylint: disable=no-self-use @@ -1434,6 +1451,7 @@ def user_slices( # pylint: disable=no-self-use @api @has_access_api + @event_logger.log_this @expose("/created_slices", methods=["GET"]) @expose("/created_slices//", methods=["GET"]) def created_slices( # pylint: disable=no-self-use @@ -1461,6 +1479,7 @@ def created_slices( # pylint: disable=no-self-use @api @has_access_api + @event_logger.log_this @expose("/fave_slices", methods=["GET"]) @expose("/fave_slices//", methods=["GET"]) def fave_slices( # pylint: disable=no-self-use @@ -1591,6 +1610,7 @@ def warm_up_cache( # pylint: disable=too-many-locals,no-self-use return json_success(json.dumps(result)) @has_access_api + @event_logger.log_this @expose("/favstar////") def favstar( # pylint: disable=no-self-use self, class_name: str, obj_id: int, action: str @@ -1624,6 +1644,7 @@ def favstar( # pylint: disable=no-self-use @api @has_access_api + @event_logger.log_this @expose("/dashboard//published/", methods=("GET", "POST")) def publish( # pylint: disable=no-self-use self, dashboard_id: int @@ -1770,7 +1791,6 @@ def dashboard( # pylint: disable=too-many-locals ) @api - @event_logger.log_this @has_access @expose("/log/", methods=["POST"]) def log(self) -> FlaskResponse: # pylint: disable=no-self-use @@ -2113,8 +2133,8 @@ def stop_query(self) -> FlaskResponse: return self.json_response("OK") @has_access_api - @expose("/validate_sql_json/", methods=["POST", "GET"]) @event_logger.log_this + @expose("/validate_sql_json/", methods=["POST", "GET"]) def validate_sql_json( # pylint: disable=too-many-locals,too-many-return-statements,no-self-use self, ) -> FlaskResponse: @@ -2299,8 +2319,8 @@ def _sql_json_sync( @has_access_api @handle_api_exception - @expose("/sql_json/", methods=["POST"]) @event_logger.log_this + @expose("/sql_json/", methods=["POST"]) def sql_json(self) -> FlaskResponse: log_params = { "user_agent": cast(Optional[str], request.headers.get("USER_AGENT")) @@ -2433,8 +2453,8 @@ def sql_json_exec( # pylint: disable=too-many-statements,too-many-locals ) @has_access - @expose("/csv/") @event_logger.log_this + @expose("/csv/") def csv(self, client_id: str) -> FlaskResponse: # pylint: disable=no-self-use """Download the query results as csv.""" logger.info("Exporting CSV file [%s]", client_id) @@ -2488,8 +2508,8 @@ def csv(self, client_id: str) -> FlaskResponse: # pylint: disable=no-self-use @api @handle_api_exception @has_access - @expose("/fetch_datasource_metadata") @event_logger.log_this + @expose("/fetch_datasource_metadata") def fetch_datasource_metadata(self) -> FlaskResponse: # pylint: disable=no-self-use """ Fetch the datasource metadata. @@ -2510,6 +2530,7 @@ def fetch_datasource_metadata(self) -> FlaskResponse: # pylint: disable=no-self return json_success(json.dumps(datasource.data)) @has_access_api + @event_logger.log_this @expose("/queries/") @expose("/queries/") def queries(self, last_updated_ms: Union[float, int]) -> FlaskResponse: @@ -2543,8 +2564,8 @@ def queries_exec(last_updated_ms: Union[float, int]) -> FlaskResponse: return json_success(json.dumps(dict_queries, default=utils.json_int_dttm_ser)) @has_access - @expose("/search_queries") @event_logger.log_this + @expose("/search_queries") def search_queries(self) -> FlaskResponse: # pylint: disable=no-self-use """ Search for previously run sqllab queries. Used for Sqllab Query Search @@ -2614,6 +2635,7 @@ def show_traceback(self) -> FlaskResponse: # pylint: disable=no-self-use 500, ) + @event_logger.log_this @expose("/welcome") def welcome(self) -> FlaskResponse: """Personalized welcome page""" @@ -2644,6 +2666,7 @@ def welcome(self) -> FlaskResponse: ) @has_access + @event_logger.log_this @expose("/profile//") def profile(self, username: str) -> FlaskResponse: """User profile page""" @@ -2714,6 +2737,7 @@ def _get_sqllab_tabs(user_id: int) -> Dict[str, Any]: } @has_access + @event_logger.log_this @expose("/sqllab", methods=["GET", "POST"]) def sqllab(self) -> FlaskResponse: """SQL Editor""" @@ -2740,7 +2764,9 @@ def sqllab(self) -> FlaskResponse: ) @has_access + @event_logger.log_this @expose("/sqllab/history/", methods=["GET"]) + @event_logger.log_this def sqllab_search(self) -> FlaskResponse: if not ( is_feature_enabled("ENABLE_REACT_CRUD_VIEWS") @@ -2752,6 +2778,7 @@ def sqllab_search(self) -> FlaskResponse: @api @has_access_api + @event_logger.log_this @expose("/schemas_access_for_csv_upload") def schemas_access_for_csv_upload(self) -> FlaskResponse: """