diff --git a/superset/advanced_data_type/api.py b/superset/advanced_data_type/api.py index 0fd3375ca0..8271a18ded 100644 --- a/superset/advanced_data_type/api.py +++ b/superset/advanced_data_type/api.py @@ -18,7 +18,7 @@ from typing import Any from flask import current_app as app from flask.wrappers import Response -from flask_appbuilder.api import BaseApi, expose, permission_name, protect, rison, safe +from flask_appbuilder.api import expose, permission_name, protect, rison, safe from flask_babel import lazy_gettext as _ from superset.advanced_data_type.schemas import ( @@ -27,12 +27,13 @@ from superset.advanced_data_type.schemas import ( ) from superset.advanced_data_type.types import AdvancedDataTypeResponse from superset.extensions import event_logger +from superset.views.base_api import BaseSupersetApi config = app.config ADVANCED_DATA_TYPES = config["ADVANCED_DATA_TYPES"] -class AdvancedDataTypeRestApi(BaseApi): +class AdvancedDataTypeRestApi(BaseSupersetApi): """ Advanced Data Type Rest API -Will return available AdvancedDataTypes when the /types endpoint is accessed @@ -41,7 +42,6 @@ class AdvancedDataTypeRestApi(BaseApi): """ allow_browser_login = True - include_route_methods = {"get", "get_types"} resource_name = "advanced_data_type" class_permission_name = "AdvancedDataType" diff --git a/superset/async_events/api.py b/superset/async_events/api.py index 61916162ee..d3f3bee64d 100644 --- a/superset/async_events/api.py +++ b/superset/async_events/api.py @@ -18,21 +18,19 @@ import logging from flask import request, Response from flask_appbuilder import expose -from flask_appbuilder.api import BaseApi, safe +from flask_appbuilder.api import safe from flask_appbuilder.security.decorators import permission_name, protect from superset.extensions import async_query_manager, event_logger from superset.utils.async_query_manager import AsyncQueryTokenException +from superset.views.base_api import BaseSupersetApi logger = logging.getLogger(__name__) -class AsyncEventsRestApi(BaseApi): +class AsyncEventsRestApi(BaseSupersetApi): resource_name = "async_event" allow_browser_login = True - include_route_methods = { - "events", - } @expose("/", methods=["GET"]) @event_logger.log_this diff --git a/superset/available_domains/api.py b/superset/available_domains/api.py index 533f240afb..b35f4c0702 100644 --- a/superset/available_domains/api.py +++ b/superset/available_domains/api.py @@ -17,21 +17,21 @@ import logging from flask import Response -from flask_appbuilder.api import BaseApi, expose, protect, safe +from flask_appbuilder.api import expose, protect, safe from superset import conf from superset.available_domains.schemas import AvailableDomainsSchema -from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod +from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP from superset.extensions import event_logger +from superset.views.base_api import BaseSupersetApi, statsd_metrics logger = logging.getLogger(__name__) -class AvailableDomainsRestApi(BaseApi): +class AvailableDomainsRestApi(BaseSupersetApi): available_domains_schema = AvailableDomainsSchema() method_permission_name = MODEL_API_RW_METHOD_PERMISSION_MAP - include_route_methods = {RouteMethod.GET} allow_browser_login = True class_permission_name = "AvailableDomains" resource_name = "available_domains" @@ -41,6 +41,7 @@ class AvailableDomainsRestApi(BaseApi): @expose("/", methods=["GET"]) @protect() @safe + @statsd_metrics @event_logger.log_this_with_context( action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get", log_to_statsd=True, diff --git a/superset/cachekeys/api.py b/superset/cachekeys/api.py index e686c0a6df..78e680d524 100644 --- a/superset/cachekeys/api.py +++ b/superset/cachekeys/api.py @@ -26,7 +26,7 @@ from sqlalchemy.exc import SQLAlchemyError from superset.cachekeys.schemas import CacheInvalidationRequestSchema from superset.connectors.sqla.models import SqlaTable -from superset.extensions import cache_manager, db, event_logger +from superset.extensions import cache_manager, db, event_logger, stats_logger_manager from superset.models.cache import CacheKey from superset.views.base_api import BaseSupersetModelRestApi, statsd_metrics @@ -117,7 +117,9 @@ class CacheRestApi(BaseSupersetModelRestApi): ) db.session.execute(delete_stmt) db.session.commit() - self.stats_logger.gauge("invalidated_cache", len(cache_keys)) + stats_logger_manager.instance.gauge( + "invalidated_cache", len(cache_keys) + ) logger.info( "Invalidated %s cache records for %s datasources", len(cache_keys), diff --git a/superset/dashboards/permalink/api.py b/superset/dashboards/permalink/api.py index 56b6ca311b..a8664f0ddd 100644 --- a/superset/dashboards/permalink/api.py +++ b/superset/dashboards/permalink/api.py @@ -17,10 +17,10 @@ import logging from flask import request, Response -from flask_appbuilder.api import BaseApi, expose, protect, safe +from flask_appbuilder.api import expose, protect, safe from marshmallow import ValidationError -from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod +from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP from superset.dashboards.commands.exceptions import ( DashboardAccessDeniedError, DashboardNotFoundError, @@ -33,20 +33,14 @@ from superset.dashboards.permalink.exceptions import DashboardPermalinkInvalidSt from superset.dashboards.permalink.schemas import DashboardPermalinkPostSchema from superset.extensions import event_logger from superset.key_value.exceptions import KeyValueAccessDeniedError -from superset.views.base_api import requires_json +from superset.views.base_api import BaseSupersetApi, requires_json logger = logging.getLogger(__name__) -class DashboardPermalinkRestApi(BaseApi): +class DashboardPermalinkRestApi(BaseSupersetApi): add_model_schema = DashboardPermalinkPostSchema() method_permission_name = MODEL_API_RW_METHOD_PERMISSION_MAP - include_route_methods = { - RouteMethod.POST, - RouteMethod.PUT, - RouteMethod.GET, - RouteMethod.DELETE, - } allow_browser_login = True class_permission_name = "DashboardPermalinkRestApi" resource_name = "dashboard" diff --git a/superset/databases/decorators.py b/superset/databases/decorators.py index 2cea2f96f8..eb05ccbea7 100644 --- a/superset/databases/decorators.py +++ b/superset/databases/decorators.py @@ -21,6 +21,7 @@ from typing import Any, Callable, Optional from flask import g from flask_babel import lazy_gettext as _ +from superset.extensions import stats_logger_manager from superset.models.core import Database from superset.sql_parse import Table from superset.utils.core import parse_js_uri_path_item @@ -46,14 +47,14 @@ def check_datasource_access(f: Callable[..., Any]) -> Callable[..., Any]: return self.response_422(message=_("Table name undefined")) database: Database = self.datamodel.get(pk) if not database: - self.stats_logger.incr( + stats_logger_manager.instance.incr( f"database_not_found_{self.__class__.__name__}.select_star" ) return self.response_404() if not self.appbuilder.sm.can_access_table( database, Table(table_name_parsed, schema_name_parsed) ): - self.stats_logger.incr( + stats_logger_manager.instance.incr( f"permisssion_denied_{self.__class__.__name__}.select_star" ) logger.warning( diff --git a/superset/explore/api.py b/superset/explore/api.py index 9eea542178..2c1ec4b8e4 100644 --- a/superset/explore/api.py +++ b/superset/explore/api.py @@ -17,10 +17,10 @@ import logging from flask import g, request, Response -from flask_appbuilder.api import BaseApi, expose, protect, safe +from flask_appbuilder.api import expose, protect, safe from superset.charts.commands.exceptions import ChartNotFoundError -from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod +from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP from superset.explore.commands.get import GetExploreCommand from superset.explore.commands.parameters import CommandParameters from superset.explore.exceptions import DatasetAccessDeniedError, WrongEndpointError @@ -31,13 +31,13 @@ from superset.temporary_cache.commands.exceptions import ( TemporaryCacheAccessDeniedError, TemporaryCacheResourceNotFoundError, ) +from superset.views.base_api import BaseSupersetApi, statsd_metrics logger = logging.getLogger(__name__) -class ExploreRestApi(BaseApi): +class ExploreRestApi(BaseSupersetApi): method_permission_name = MODEL_API_RW_METHOD_PERMISSION_MAP - include_route_methods = {RouteMethod.GET} allow_browser_login = True class_permission_name = "Explore" resource_name = "explore" @@ -47,6 +47,7 @@ class ExploreRestApi(BaseApi): @expose("/", methods=["GET"]) @protect() @safe + @statsd_metrics @event_logger.log_this_with_context( action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get", log_to_statsd=True, diff --git a/superset/explore/form_data/api.py b/superset/explore/form_data/api.py index 902156d05b..cc2fc75bb5 100644 --- a/superset/explore/form_data/api.py +++ b/superset/explore/form_data/api.py @@ -17,10 +17,10 @@ import logging from flask import request, Response -from flask_appbuilder.api import BaseApi, expose, protect, safe +from flask_appbuilder.api import expose, protect, safe from marshmallow import ValidationError -from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod +from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP from superset.explore.form_data.commands.create import CreateFormDataCommand from superset.explore.form_data.commands.delete import DeleteFormDataCommand from superset.explore.form_data.commands.get import GetFormDataCommand @@ -32,21 +32,15 @@ from superset.temporary_cache.commands.exceptions import ( TemporaryCacheAccessDeniedError, TemporaryCacheResourceNotFoundError, ) -from superset.views.base_api import requires_json +from superset.views.base_api import BaseSupersetApi, requires_json, statsd_metrics logger = logging.getLogger(__name__) -class ExploreFormDataRestApi(BaseApi): +class ExploreFormDataRestApi(BaseSupersetApi): add_model_schema = FormDataPostSchema() edit_model_schema = FormDataPutSchema() method_permission_name = MODEL_API_RW_METHOD_PERMISSION_MAP - include_route_methods = { - RouteMethod.POST, - RouteMethod.PUT, - RouteMethod.GET, - RouteMethod.DELETE, - } allow_browser_login = True class_permission_name = "ExploreFormDataRestApi" resource_name = "explore" @@ -56,6 +50,7 @@ class ExploreFormDataRestApi(BaseApi): @expose("/form_data", methods=["POST"]) @protect() @safe + @statsd_metrics @event_logger.log_this_with_context( action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.post", log_to_statsd=False, @@ -120,6 +115,7 @@ class ExploreFormDataRestApi(BaseApi): @expose("/form_data/", methods=["PUT"]) @protect() @safe + @statsd_metrics @event_logger.log_this_with_context( action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.put", log_to_statsd=True, @@ -193,6 +189,7 @@ class ExploreFormDataRestApi(BaseApi): @expose("/form_data/", methods=["GET"]) @protect() @safe + @statsd_metrics @event_logger.log_this_with_context( action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get", log_to_statsd=True, @@ -244,6 +241,7 @@ class ExploreFormDataRestApi(BaseApi): @expose("/form_data/", methods=["DELETE"]) @protect() @safe + @statsd_metrics @event_logger.log_this_with_context( action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.delete", log_to_statsd=True, diff --git a/superset/explore/permalink/api.py b/superset/explore/permalink/api.py index 7e2813decc..88e819aa2b 100644 --- a/superset/explore/permalink/api.py +++ b/superset/explore/permalink/api.py @@ -17,14 +17,14 @@ import logging from flask import request, Response -from flask_appbuilder.api import BaseApi, expose, protect, safe +from flask_appbuilder.api import expose, protect, safe from marshmallow import ValidationError from superset.charts.commands.exceptions import ( ChartAccessDeniedError, ChartNotFoundError, ) -from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod +from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP from superset.datasets.commands.exceptions import ( DatasetAccessDeniedError, DatasetNotFoundError, @@ -35,20 +35,14 @@ from superset.explore.permalink.exceptions import ExplorePermalinkInvalidStateEr from superset.explore.permalink.schemas import ExplorePermalinkPostSchema from superset.extensions import event_logger from superset.key_value.exceptions import KeyValueAccessDeniedError -from superset.views.base_api import requires_json +from superset.views.base_api import BaseSupersetApi, requires_json, statsd_metrics logger = logging.getLogger(__name__) -class ExplorePermalinkRestApi(BaseApi): +class ExplorePermalinkRestApi(BaseSupersetApi): add_model_schema = ExplorePermalinkPostSchema() method_permission_name = MODEL_API_RW_METHOD_PERMISSION_MAP - include_route_methods = { - RouteMethod.POST, - RouteMethod.PUT, - RouteMethod.GET, - RouteMethod.DELETE, - } allow_browser_login = True class_permission_name = "ExplorePermalinkRestApi" resource_name = "explore" @@ -58,6 +52,7 @@ class ExplorePermalinkRestApi(BaseApi): @expose("/permalink", methods=["POST"]) @protect() @safe + @statsd_metrics @event_logger.log_this_with_context( action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.post", log_to_statsd=False, @@ -118,6 +113,7 @@ class ExplorePermalinkRestApi(BaseApi): @expose("/permalink/", methods=["GET"]) @protect() @safe + @statsd_metrics @event_logger.log_this_with_context( action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.get", log_to_statsd=False, diff --git a/superset/extensions/__init__.py b/superset/extensions/__init__.py index cccf3a526f..e2e5592e1e 100644 --- a/superset/extensions/__init__.py +++ b/superset/extensions/__init__.py @@ -16,7 +16,6 @@ # under the License. import json import os -from pathlib import Path from typing import Any, Callable, Dict, List, Optional import celery @@ -29,6 +28,7 @@ from flask_wtf.csrf import CSRFProtect from werkzeug.local import LocalProxy from superset.extensions.ssh import SSHManagerFactory +from superset.extensions.stats_logger import BaseStatsLoggerManager from superset.utils.async_query_manager import AsyncQueryManager from superset.utils.cache_manager import CacheManager from superset.utils.encrypt import EncryptedFieldFactory @@ -127,5 +127,6 @@ migrate = Migrate() profiling = ProfilingExtension() results_backend_manager = ResultsBackendManager() security_manager = LocalProxy(lambda: appbuilder.sm) -talisman = Talisman() ssh_manager_factory = SSHManagerFactory() +stats_logger_manager = BaseStatsLoggerManager() +talisman = Talisman() diff --git a/superset/extensions/stats_logger.py b/superset/extensions/stats_logger.py new file mode 100644 index 0000000000..bb6407141a --- /dev/null +++ b/superset/extensions/stats_logger.py @@ -0,0 +1,31 @@ +# 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. +from flask import Flask + +from superset.stats_logger import BaseStatsLogger + + +class BaseStatsLoggerManager: + def __init__(self) -> None: + self._stats_logger = BaseStatsLogger() + + def init_app(self, app: Flask) -> None: + self._stats_logger = app.config["STATS_LOGGER"] + + @property + def instance(self) -> BaseStatsLogger: + return self._stats_logger diff --git a/superset/importexport/api.py b/superset/importexport/api.py index e20cc22b80..26bc78e5d7 100644 --- a/superset/importexport/api.py +++ b/superset/importexport/api.py @@ -20,7 +20,7 @@ from io import BytesIO from zipfile import is_zipfile, ZipFile from flask import request, Response, send_file -from flask_appbuilder.api import BaseApi, expose, protect +from flask_appbuilder.api import expose, protect from superset.commands.export.assets import ExportAssetsCommand from superset.commands.importers.exceptions import ( @@ -30,10 +30,10 @@ from superset.commands.importers.exceptions import ( from superset.commands.importers.v1.assets import ImportAssetsCommand from superset.commands.importers.v1.utils import get_contents_from_bundle from superset.extensions import event_logger -from superset.views.base_api import requires_form_data +from superset.views.base_api import BaseSupersetApi, requires_form_data, statsd_metrics -class ImportExportRestApi(BaseApi): +class ImportExportRestApi(BaseSupersetApi): """ API for exporting all assets or importing them. """ @@ -44,6 +44,7 @@ class ImportExportRestApi(BaseApi): @expose("/export/", methods=["GET"]) @protect() + @statsd_metrics @event_logger.log_this_with_context( action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.export", log_to_statsd=False, @@ -92,6 +93,7 @@ class ImportExportRestApi(BaseApi): @expose("/import/", methods=["POST"]) @protect() + @statsd_metrics @event_logger.log_this_with_context( action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.import_", log_to_statsd=False, diff --git a/superset/initialization/__init__.py b/superset/initialization/__init__.py index 1cffbd0dc2..bcfaf7839f 100644 --- a/superset/initialization/__init__.py +++ b/superset/initialization/__init__.py @@ -46,6 +46,7 @@ from superset.extensions import ( profiling, results_backend_manager, ssh_manager_factory, + stats_logger_manager, talisman, ) from superset.security import SupersetSecurityManager @@ -419,6 +420,7 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods self.configure_auth_provider() self.configure_async_queries() self.configure_ssh_manager() + self.configure_stats_manager() # Hook that provides administrators a handle on the Flask APP # after initialization @@ -479,6 +481,9 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods def configure_ssh_manager(self) -> None: ssh_manager_factory.init_app(self.superset_app) + def configure_stats_manager(self) -> None: + stats_logger_manager.init_app(self.superset_app) + def setup_event_logger(self) -> None: _event_logger["event_logger"] = get_event_logger_from_cfg_value( self.superset_app.config.get("EVENT_LOGGER", DBEventLogger()) diff --git a/superset/security/api.py b/superset/security/api.py index 4eb7ebe660..643681ae7d 100644 --- a/superset/security/api.py +++ b/superset/security/api.py @@ -19,7 +19,7 @@ from typing import Any, Dict from flask import request, Response from flask_appbuilder import expose -from flask_appbuilder.api import BaseApi, safe +from flask_appbuilder.api import safe from flask_appbuilder.security.decorators import permission_name, protect from flask_wtf.csrf import generate_csrf from marshmallow import EXCLUDE, fields, post_load, Schema, ValidationError @@ -30,6 +30,7 @@ from superset.embedded_dashboard.commands.exceptions import ( ) from superset.extensions import event_logger from superset.security.guest_token import GuestTokenResourceType +from superset.views.base_api import BaseSupersetApi, statsd_metrics logger = logging.getLogger(__name__) @@ -76,7 +77,7 @@ class GuestTokenCreateSchema(PermissiveSchema): guest_token_create_schema = GuestTokenCreateSchema() -class SecurityRestApi(BaseApi): +class SecurityRestApi(BaseSupersetApi): resource_name = "security" allow_browser_login = True openapi_spec_tag = "Security" @@ -85,6 +86,7 @@ class SecurityRestApi(BaseApi): @event_logger.log_this @protect() @safe + @statsd_metrics @permission_name("read") def csrf_token(self) -> Response: """ @@ -114,6 +116,7 @@ class SecurityRestApi(BaseApi): @event_logger.log_this @protect() @safe + @statsd_metrics @permission_name("grant_guest_token") def guest_token(self) -> Response: """Response diff --git a/superset/temporary_cache/api.py b/superset/temporary_cache/api.py index a2d2e287d1..b6376c63c3 100644 --- a/superset/temporary_cache/api.py +++ b/superset/temporary_cache/api.py @@ -21,7 +21,6 @@ from typing import Any from apispec import APISpec from apispec.exceptions import DuplicateComponentNameError from flask import request, Response -from flask_appbuilder.api import BaseApi from marshmallow import ValidationError from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod @@ -34,12 +33,12 @@ from superset.temporary_cache.schemas import ( TemporaryCachePostSchema, TemporaryCachePutSchema, ) -from superset.views.base_api import requires_json +from superset.views.base_api import BaseSupersetApi, requires_json logger = logging.getLogger(__name__) -class TemporaryCacheRestApi(BaseApi, ABC): +class TemporaryCacheRestApi(BaseSupersetApi, ABC): add_model_schema = TemporaryCachePostSchema() edit_model_schema = TemporaryCachePutSchema() method_permission_name = MODEL_API_RW_METHOD_PERMISSION_MAP diff --git a/superset/utils/log.py b/superset/utils/log.py index a52556ae64..f2379fe11c 100644 --- a/superset/utils/log.py +++ b/superset/utils/log.py @@ -42,6 +42,7 @@ from flask_appbuilder.const import API_URI_RIS_KEY from sqlalchemy.exc import SQLAlchemyError from typing_extensions import Literal +from superset.extensions import stats_logger_manager from superset.utils.core import get_user_id, LoggerLevel if TYPE_CHECKING: @@ -194,7 +195,7 @@ class AbstractEventLogger(ABC): slice_id = 0 if log_to_statsd: - self.stats_logger.incr(action) + stats_logger_manager.instance.incr(action) try: # bulk insert @@ -283,10 +284,6 @@ class AbstractEventLogger(ABC): """Decorator that instrument `update_log_payload` to kwargs""" return self._wrapper(f, allow_extra_payload=True) - @property - def stats_logger(self) -> BaseStatsLogger: - return current_app.config["STATS_LOGGER"] - def get_event_logger_from_cfg_value(cfg_value: Any) -> AbstractEventLogger: """ diff --git a/superset/views/base_api.py b/superset/views/base_api.py index 47fc611ba2..d27fad7eb2 100644 --- a/superset/views/base_api.py +++ b/superset/views/base_api.py @@ -14,13 +14,15 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. +from __future__ import annotations + import functools import logging from typing import Any, Callable, cast, Dict, List, Optional, Set, Tuple, Type, Union -from flask import Blueprint, request, Response -from flask_appbuilder import AppBuilder, Model, ModelRestApi -from flask_appbuilder.api import expose, protect, rison, safe +from flask import request, Response +from flask_appbuilder import Model, ModelRestApi +from flask_appbuilder.api import BaseApi, expose, protect, rison, safe from flask_appbuilder.models.filters import BaseFilter, Filters from flask_appbuilder.models.sqla.filters import FilterStartsWith from flask_appbuilder.models.sqla.interface import SQLAInterface @@ -30,13 +32,12 @@ from sqlalchemy import and_, distinct, func from sqlalchemy.orm.query import Query from superset.exceptions import InvalidPayloadFormatError -from superset.extensions import db, event_logger, security_manager +from superset.extensions import db, event_logger, security_manager, stats_logger_manager from superset.models.core import FavStar from superset.models.dashboard import Dashboard from superset.models.slice import Slice from superset.schemas import error_payload_content from superset.sql_lab import Query as SqllabQuery -from superset.stats_logger import BaseStatsLogger from superset.superset_typing import FlaskResponse from superset.utils.core import get_user_id, time_function from superset.views.base import handle_api_exception @@ -91,7 +92,7 @@ def requires_form_data(f: Callable[..., Any]) -> Callable[..., Any]: Require 'multipart/form-data' as request MIME type """ - def wraps(self: "BaseSupersetModelRestApi", *args: Any, **kwargs: Any) -> Response: + def wraps(self: BaseSupersetApiMixin, *args: Any, **kwargs: Any) -> Response: if not request.mimetype == "multipart/form-data": raise InvalidPayloadFormatError( message="Request MIME type is not 'multipart/form-data'" @@ -106,14 +107,15 @@ def statsd_metrics(f: Callable[..., Any]) -> Callable[..., Any]: Handle sending all statsd metrics from the REST API """ - def wraps(self: "BaseSupersetModelRestApi", *args: Any, **kwargs: Any) -> Response: + def wraps(self: BaseSupersetApiMixin, *args: Any, **kwargs: Any) -> Response: + func_name = f.__name__ try: duration, response = time_function(f, self, *args, **kwargs) except Exception as ex: - self.incr_stats("error", f.__name__) + self.incr_stats("error", func_name) raise ex - self.send_stats_metrics(response, f.__name__, duration) + self.send_stats_metrics(response, func_name, duration) return response return functools.update_wrapper(wraps, f) @@ -155,12 +157,68 @@ class BaseFavoriteFilter(BaseFilter): # pylint: disable=too-few-public-methods return query.filter(and_(~self.model.id.in_(users_favorite_query))) -class BaseSupersetModelRestApi(ModelRestApi): +class BaseSupersetApiMixin: + csrf_exempt = False + + responses = { + "400": {"description": "Bad request", "content": error_payload_content}, + "401": {"description": "Unauthorized", "content": error_payload_content}, + "403": {"description": "Forbidden", "content": error_payload_content}, + "404": {"description": "Not found", "content": error_payload_content}, + "422": { + "description": "Could not process entity", + "content": error_payload_content, + }, + "500": {"description": "Fatal error", "content": error_payload_content}, + } + + def incr_stats(self, action: str, func_name: str) -> None: + """ + Proxy function for statsd.incr to impose a key structure for REST API's + :param action: String with an action name eg: error, success + :param func_name: The function name + """ + stats_logger_manager.instance.incr( + f"{self.__class__.__name__}.{func_name}.{action}" + ) + + def timing_stats(self, action: str, func_name: str, value: float) -> None: + """ + Proxy function for statsd.incr to impose a key structure for REST API's + :param action: String with an action name eg: error, success + :param func_name: The function name + :param value: A float with the time it took for the endpoint to execute + """ + stats_logger_manager.instance.timing( + f"{self.__class__.__name__}.{func_name}.{action}", value + ) + + def send_stats_metrics( + self, response: Response, key: str, time_delta: Optional[float] = None + ) -> None: + """ + Helper function to handle sending statsd metrics + :param response: flask response object, will evaluate if it was an error + :param key: The function name + :param time_delta: Optional time it took for the endpoint to execute + """ + if 200 <= response.status_code < 400: + self.incr_stats("success", key) + else: + self.incr_stats("error", key) + if time_delta: + self.timing_stats("time", key, time_delta) + + +class BaseSupersetApi(BaseApi, BaseSupersetApiMixin): + ... + + +class BaseSupersetModelRestApi(ModelRestApi, BaseSupersetApiMixin): """ Extends FAB's ModelResApi to implement specific superset generic functionality """ - csrf_exempt = False method_permission_name = { "bulk_delete": "delete", "data": "list", @@ -246,22 +304,8 @@ class BaseSupersetModelRestApi(ModelRestApi): list_columns: List[str] show_columns: List[str] - responses = { - "400": {"description": "Bad request", "content": error_payload_content}, - "401": {"description": "Unauthorized", "content": error_payload_content}, - "403": {"description": "Forbidden", "content": error_payload_content}, - "404": {"description": "Not found", "content": error_payload_content}, - "422": { - "description": "Could not process entity", - "content": error_payload_content, - }, - "500": {"description": "Fatal error", "content": error_payload_content}, - } - def __init__(self) -> None: super().__init__() - # Setup statsd - self.stats_logger = BaseStatsLogger() # Add base API spec base query parameter schemas if self.apispec_parameter_schemas is None: # type: ignore self.apispec_parameter_schemas = {} @@ -273,12 +317,6 @@ class BaseSupersetModelRestApi(ModelRestApi): DistincResponseSchema, ) - def create_blueprint( - self, appbuilder: AppBuilder, *args: Any, **kwargs: Any - ) -> Blueprint: - self.stats_logger = self.appbuilder.get_app.config["STATS_LOGGER"] - return super().create_blueprint(appbuilder, *args, **kwargs) - def _init_properties(self) -> None: """ Lock down initial not configured REST API columns. We want to just expose @@ -372,44 +410,6 @@ class BaseSupersetModelRestApi(ModelRestApi): extra_rows = db.session.query(datamodel.obj).filter(pk_col.in_(ids)).all() result += self._get_result_from_rows(datamodel, extra_rows, column_name) - def incr_stats(self, action: str, func_name: str) -> None: - """ - Proxy function for statsd.incr to impose a key structure for REST API's - - :param action: String with an action name eg: error, success - :param func_name: The function name - """ - self.stats_logger.incr(f"{self.__class__.__name__}.{func_name}.{action}") - - def timing_stats(self, action: str, func_name: str, value: float) -> None: - """ - Proxy function for statsd.incr to impose a key structure for REST API's - - :param action: String with an action name eg: error, success - :param func_name: The function name - :param value: A float with the time it took for the endpoint to execute - """ - self.stats_logger.timing( - f"{self.__class__.__name__}.{func_name}.{action}", value - ) - - def send_stats_metrics( - self, response: Response, key: str, time_delta: Optional[float] = None - ) -> None: - """ - Helper function to handle sending statsd metrics - - :param response: flask response object, will evaluate if it was an error - :param key: The function name - :param time_delta: Optional time it took for the endpoint to execute - """ - if 200 <= response.status_code < 400: - self.incr_stats("success", key) - else: - self.incr_stats("error", key) - if time_delta: - self.timing_stats("time", key, time_delta) - @event_logger.log_this_with_context( action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.info", object_ref=False, diff --git a/superset/views/users/api.py b/superset/views/users/api.py index 29d376935a..675b918847 100644 --- a/superset/views/users/api.py +++ b/superset/views/users/api.py @@ -15,17 +15,17 @@ # specific language governing permissions and limitations # under the License. from flask import g, Response -from flask_appbuilder.api import BaseApi, expose, safe +from flask_appbuilder.api import expose, safe from flask_jwt_extended.exceptions import NoAuthorizationError +from superset.views.base_api import BaseSupersetApi +from superset.views.users.schemas import UserResponseSchema from superset.views.utils import bootstrap_user_data -from .schemas import UserResponseSchema - user_response_schema = UserResponseSchema() -class CurrentUserRestApi(BaseApi): +class CurrentUserRestApi(BaseSupersetApi): """An api to get information about the current user""" resource_name = "me"