mirror of https://github.com/apache/superset.git
chore: add statsd support to base API and refactor (#22887)
This commit is contained in:
parent
bed10a0e2b
commit
d00ba15c78
|
@ -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"
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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/<string:key>", 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/<string:key>", 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/<string:key>", 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,
|
||||
|
|
|
@ -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/<string:key>", 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,
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
|
@ -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,
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
"""
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue