Skip to content

Commit

Permalink
Monitor deprecated API usage via Prometheus metrics (#910)
Browse files Browse the repository at this point in the history
  • Loading branch information
psrok1 committed Feb 15, 2024
1 parent 3be33c6 commit d01ad64
Show file tree
Hide file tree
Showing 12 changed files with 118 additions and 24 deletions.
5 changes: 2 additions & 3 deletions mwdb/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from mwdb.core.app import api, app
from mwdb.core.config import app_config
from mwdb.core.deprecated import DeprecatedFeature, uses_deprecated_api
from mwdb.core.log import getLogger, setup_logger
from mwdb.core.metrics import metric_api_requests, metrics_enabled
from mwdb.core.plugins import PluginAppContext, load_plugins
Expand Down Expand Up @@ -206,9 +207,7 @@ def require_auth():
if g.auth_user is None:
g.auth_user = User.verify_legacy_token(token)
if g.auth_user is not None:
getLogger().debug(
"'%s' used legacy auth token for authentication", g.auth_user.login
)
uses_deprecated_api(DeprecatedFeature.legacy_api_key_v1)

if g.auth_user:
if (
Expand Down
74 changes: 74 additions & 0 deletions mwdb/core/deprecated.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""
Utilities for marking deprecated features to monitor usage
"""
from enum import Enum
from functools import wraps
from typing import Optional

from flask import g, request

from mwdb.core.log import getLogger
from mwdb.core.metrics import metric_deprecated_usage

logger = getLogger()


class DeprecatedFeature(Enum):
# Unmanageable API keys, deprecated in v2.0.0
legacy_api_key_v1 = "legacy_api_key_v1"
# API keys non-complaint with RFC7519
# Deprecated in v2.7.0
legacy_api_key_v2 = "legacy_api_key_v2"
# Legacy PUT/POST /api/<object_type>/<parent>
# Use POST /api/<object_type> instead
# Deprecated in v2.0.0
legacy_object_upload = "legacy_file_upload"
# Legacy /request/sample/<token>
# Use /file/<id>/download instead
# Deprecated in v2.2.0
legacy_file_download = "legacy_file_download"
# Legacy /search
# Use GET /<object_type> instead
# Deprecated in v2.0.0
legacy_search = "legacy_search"
# Legacy ?page parameter in object listing endpoints
# Use "?older_than" instead
# Deprecated in v2.0.0
legacy_page_parameter = "legacy_page_parameter"
# Legacy Metakey API
# Use Attribute API instead
# Deprecated in v2.6.0
legacy_metakey_api = "legacy_metakey_api"
# Legacy Metakey API
# Use Attribute API instead
# Deprecated in v2.6.0
legacy_metakeys_upload_option = "legacy_metakeys_upload_option"


def uses_deprecated_api(
feature: DeprecatedFeature,
endpoint: Optional[str] = None,
method: Optional[str] = None,
):
user = g.auth_user.login if g.auth_user is not None else None
metric_deprecated_usage.inc(
feature=str(feature.value), endpoint=endpoint, method=method, user=user
)
logger.debug(
f"Used deprecated feature: {feature}"
+ (f" ({method} {endpoint})" if endpoint is not None else "")
)


def deprecated_endpoint(feature: DeprecatedFeature):
def method_wrapper(f):
@wraps(f)
def endpoint(*args, **kwargs):
uses_deprecated_api(
feature, endpoint=request.endpoint, method=request.method
)
return f(*args, **kwargs)

return endpoint

return method_wrapper
10 changes: 9 additions & 1 deletion mwdb/core/metrics/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
from .redis_counter import RedisCounter, collect, metrics_enabled

metric_api_requests = RedisCounter(
"api_requests", "API request metrics", ("method", "endpoint", "user", "status_code")
"mwdb_api_requests",
"API request metrics",
("method", "endpoint", "user", "status_code"),
)
metric_deprecated_usage = RedisCounter(
"mwdb_deprecated_usage",
"Deprecated API usage metrics",
("feature", "method", "endpoint", "user"),
)

__all__ = [
"collect",
"metrics_enabled",
"metric_api_requests",
"metric_deprecated_usage",
]
4 changes: 4 additions & 0 deletions mwdb/model/api_key.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from sqlalchemy.orm.exc import NoResultFound

from mwdb.core.auth import AuthScope, generate_token, verify_legacy_token, verify_token
from mwdb.core.deprecated import DeprecatedFeature, uses_deprecated_api

from . import db

Expand Down Expand Up @@ -35,6 +36,9 @@ def verify_token(token):
data = verify_legacy_token(token, required_fields={"login", "api_key_id"})
if data is None:
return None
else:
# Note a deprecated usage
uses_deprecated_api(DeprecatedFeature.legacy_api_key_v2)

try:
api_key_obj = APIKey.query.filter(
Expand Down
15 changes: 1 addition & 14 deletions mwdb/resources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from functools import wraps
from json import JSONDecodeError

from flask import g, request
from flask import g
from marshmallow import EXCLUDE, ValidationError
from werkzeug.exceptions import BadRequest, Forbidden, NotFound, Unauthorized

Expand Down Expand Up @@ -49,19 +49,6 @@ def endpoint(*args, **kwargs):
return endpoint


def deprecated(f):
"""
Decorator for deprecated methods
"""

@wraps(f)
def endpoint(*args, **kwargs):
logger.debug("Used deprecated endpoint: %s", request.path)
return f(*args, **kwargs)

return endpoint


def get_type_from_str(s):
object_types = {"object": Object, "file": File, "config": Config, "blob": TextBlob}
if s not in object_types:
Expand Down
2 changes: 2 additions & 0 deletions mwdb/resources/blob.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from werkzeug.exceptions import Conflict

from mwdb.core.capabilities import Capabilities
from mwdb.core.deprecated import DeprecatedFeature, deprecated_endpoint
from mwdb.core.plugins import hooks
from mwdb.core.rate_limit import rate_limited_resource
from mwdb.model import TextBlob
Expand Down Expand Up @@ -224,6 +225,7 @@ def get(self, identifier):
"""
return super().get(identifier)

@deprecated_endpoint(DeprecatedFeature.legacy_object_upload)
@requires_authorization
@requires_capabilities(Capabilities.adding_blobs)
def put(self, identifier):
Expand Down
2 changes: 2 additions & 0 deletions mwdb/resources/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from werkzeug.exceptions import BadRequest, Conflict, Forbidden, NotFound

from mwdb.core.capabilities import Capabilities
from mwdb.core.deprecated import DeprecatedFeature, deprecated_endpoint
from mwdb.core.plugins import hooks
from mwdb.core.rate_limit import rate_limited_resource
from mwdb.model import Config, TextBlob, db
Expand Down Expand Up @@ -347,6 +348,7 @@ def get(self, identifier):
"""
return super().get(identifier)

@deprecated_endpoint(DeprecatedFeature.legacy_object_upload)
@requires_authorization
@requires_capabilities(Capabilities.adding_configs)
def put(self, identifier):
Expand Down
7 changes: 4 additions & 3 deletions mwdb/resources/download.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,17 @@
from werkzeug.exceptions import Forbidden, NotFound

from mwdb.core.app import api
from mwdb.core.deprecated import DeprecatedFeature, deprecated_endpoint
from mwdb.core.rate_limit import rate_limited_resource
from mwdb.model import File
from mwdb.schema.download import DownloadURLResponseSchema

from . import deprecated, requires_authorization
from . import requires_authorization


@rate_limited_resource
class DownloadResource(Resource):
@deprecated
@deprecated_endpoint(DeprecatedFeature.legacy_file_download)
def get(self, access_token):
"""
---
Expand Down Expand Up @@ -58,7 +59,7 @@ def get(self, access_token):

@rate_limited_resource
class RequestSampleDownloadResource(Resource):
@deprecated
@deprecated_endpoint(DeprecatedFeature.legacy_file_download)
@requires_authorization
def post(self, identifier):
"""
Expand Down
2 changes: 2 additions & 0 deletions mwdb/resources/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from werkzeug.exceptions import BadRequest, Conflict, Forbidden, NotFound, Unauthorized

from mwdb.core.capabilities import Capabilities
from mwdb.core.deprecated import DeprecatedFeature, deprecated_endpoint
from mwdb.core.plugins import hooks
from mwdb.core.rate_limit import rate_limited_resource
from mwdb.model import File
Expand Down Expand Up @@ -241,6 +242,7 @@ def get(self, identifier):
"""
return super().get(identifier)

@deprecated_endpoint(DeprecatedFeature.legacy_object_upload)
@requires_authorization
@requires_capabilities(Capabilities.adding_files)
def post(self, identifier):
Expand Down
12 changes: 12 additions & 0 deletions mwdb/resources/metakey.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from werkzeug.exceptions import BadRequest, Forbidden, NotFound

from mwdb.core.capabilities import Capabilities
from mwdb.core.deprecated import DeprecatedFeature, deprecated_endpoint
from mwdb.core.rate_limit import rate_limited_resource
from mwdb.model import AttributeDefinition, AttributePermission, Group, db
from mwdb.schema.metakey import (
Expand Down Expand Up @@ -35,6 +36,7 @@

@rate_limited_resource
class MetakeyResource(Resource):
@deprecated_endpoint(DeprecatedFeature.legacy_metakey_api)
@requires_authorization
def get(self, type, identifier):
"""
Expand Down Expand Up @@ -104,6 +106,7 @@ def get(self, type, identifier):
schema = MetakeyListResponseSchema()
return schema.dump({"metakeys": metakeys})

@deprecated_endpoint(DeprecatedFeature.legacy_metakey_api)
@requires_authorization
def post(self, type, identifier):
"""
Expand Down Expand Up @@ -183,6 +186,7 @@ def post(self, type, identifier):
schema = MetakeyListResponseSchema()
return schema.dump({"metakeys": metakeys})

@deprecated_endpoint(DeprecatedFeature.legacy_metakey_api)
@requires_authorization
@requires_capabilities("removing_attributes")
def delete(self, type, identifier):
Expand Down Expand Up @@ -261,6 +265,7 @@ def delete(self, type, identifier):

@rate_limited_resource
class MetakeyListDefinitionResource(Resource):
@deprecated_endpoint(DeprecatedFeature.legacy_metakey_api)
@requires_authorization
def get(self, access):
"""
Expand Down Expand Up @@ -309,6 +314,7 @@ def get(self, access):

@rate_limited_resource
class MetakeyListDefinitionManageResource(Resource):
@deprecated_endpoint(DeprecatedFeature.legacy_metakey_api)
@requires_authorization
@requires_capabilities(Capabilities.manage_users)
def get(self):
Expand Down Expand Up @@ -349,6 +355,7 @@ def get(self):

@rate_limited_resource
class MetakeyDefinitionManageResource(Resource):
@deprecated_endpoint(DeprecatedFeature.legacy_metakey_api)
@requires_authorization
@requires_capabilities(Capabilities.manage_users)
def get(self, key):
Expand Down Expand Up @@ -396,6 +403,7 @@ def get(self, key):
schema = MetakeyDefinitionManageItemResponseSchema()
return schema.dump(metakey)

@deprecated_endpoint(DeprecatedFeature.legacy_metakey_api)
@requires_authorization
@requires_capabilities(Capabilities.manage_users)
def post(self, key):
Expand Down Expand Up @@ -458,6 +466,7 @@ def post(self, key):
schema = MetakeyDefinitionItemResponseSchema()
return schema.dump(metakey_definition)

@deprecated_endpoint(DeprecatedFeature.legacy_metakey_api)
@requires_authorization
@requires_capabilities(Capabilities.manage_users)
def put(self, key):
Expand Down Expand Up @@ -537,6 +546,7 @@ def put(self, key):
schema = MetakeyDefinitionItemResponseSchema()
return schema.dump(metakey)

@deprecated_endpoint(DeprecatedFeature.legacy_metakey_api)
@requires_authorization
@requires_capabilities(Capabilities.manage_users)
def delete(self, key):
Expand Down Expand Up @@ -584,6 +594,7 @@ def delete(self, key):

@rate_limited_resource
class MetakeyPermissionResource(Resource):
@deprecated_endpoint(DeprecatedFeature.legacy_metakey_api)
@requires_authorization
@requires_capabilities(Capabilities.manage_users)
def put(self, key, group_name):
Expand Down Expand Up @@ -668,6 +679,7 @@ def put(self, key, group_name):
schema = MetakeyDefinitionManageItemResponseSchema()
return schema.dump(metakey_definition)

@deprecated_endpoint(DeprecatedFeature.legacy_metakey_api)
@requires_authorization
@requires_capabilities(Capabilities.manage_users)
def delete(self, key, group_name):
Expand Down
4 changes: 3 additions & 1 deletion mwdb/resources/object.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from mwdb.core.capabilities import Capabilities
from mwdb.core.config import app_config
from mwdb.core.deprecated import DeprecatedFeature, uses_deprecated_api
from mwdb.core.plugins import hooks
from mwdb.core.rate_limit import rate_limited_resource
from mwdb.core.search import SQLQueryBuilder, SQLQueryBuilderBaseException
Expand Down Expand Up @@ -79,6 +80,7 @@ def create_object(self, params):
analysis_id = params.get("karton_id")

if params["metakeys"]:
uses_deprecated_api(DeprecatedFeature.legacy_metakeys_upload_option)
# If 'metakeys' are defined: keep legacy behavior
if "attributes" in params and params["attributes"]:
raise BadRequest("'attributes' and 'metakeys' options can't be mixed")
Expand Down Expand Up @@ -218,7 +220,7 @@ def get(self):
Request canceled due to database statement timeout.
"""
if "page" in request.args:
logger.warning("'%s' used legacy 'page' parameter", g.auth_user.login)
uses_deprecated_api(DeprecatedFeature.legacy_page_parameter)

obj = load_schema(request.args, ObjectListRequestSchema())

Expand Down
5 changes: 3 additions & 2 deletions mwdb/resources/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,19 @@
from luqum.parser import ParseError
from werkzeug.exceptions import BadRequest

from mwdb.core.deprecated import DeprecatedFeature, deprecated_endpoint
from mwdb.core.rate_limit import rate_limited_resource
from mwdb.core.search import SQLQueryBuilder, SQLQueryBuilderBaseException
from mwdb.model import Object
from mwdb.schema.object import ObjectListItemResponseSchema
from mwdb.schema.search import SearchRequestSchema

from . import deprecated, loads_schema, requires_authorization
from . import loads_schema, requires_authorization


@rate_limited_resource
class SearchResource(Resource):
@deprecated
@deprecated_endpoint(DeprecatedFeature.legacy_search)
@requires_authorization
def post(self):
"""
Expand Down

0 comments on commit d01ad64

Please sign in to comment.