diff --git a/datameta/api/__init__.py b/datameta/api/__init__.py index eb961659..d5d21fae 100644 --- a/datameta/api/__init__.py +++ b/datameta/api/__init__.py @@ -12,15 +12,15 @@ # See the License for the specific language governing permissions and # limitations under the License. -from pyramid.config import Configurator -from pyramid.view import view_config -from pyramid.request import Request -from pyramid.httpexceptions import HTTPFound - -from dataclasses import dataclass -from dataclasses_json import dataclass_json, LetterCase import os +from dataclasses import dataclass + import yaml +from dataclasses_json import LetterCase, dataclass_json +from pyramid.config import Configurator +from pyramid.httpexceptions import HTTPFound +from pyramid.request import Request +from pyramid.view import view_config openapi_spec_path = os.path.join(os.path.dirname(__file__), "openapi.yaml") # read base url from openapi.yaml: @@ -34,6 +34,7 @@ @dataclass class DataHolderBase: """Base class for data classes intended to be used as API response bodies""" + def __json__(self, request): return self.to_dict() @@ -49,6 +50,7 @@ def includeme(config: Configurator) -> None: config.add_route("totp_secret_id", base_url + "/users/{id}/totp-secret") config.add_route("rpc_whoami", base_url + "/rpc/whoami") config.add_route("user_id", base_url + "/users/{id}") + config.add_route("metrics", base_url + "/metrics") config.add_route("metadata", base_url + "/metadata") config.add_route("metadata_id", base_url + "/metadata/{id}") config.add_route("metadatasets", base_url + "/metadatasets") @@ -64,11 +66,13 @@ def includeme(config: Configurator) -> None: config.add_route("rpc_delete_files", base_url + "/rpc/delete-files") config.add_route("rpc_delete_metadatasets", base_url + "/rpc/delete-metadatasets") config.add_route("rpc_get_file_url", base_url + "/rpc/get-file-url/{id}") - config.add_route('register_submit', base_url + "/registrations") + config.add_route("register_submit", base_url + "/registrations") config.add_route("register_settings", base_url + "/registrationsettings") config.add_route("services", base_url + "/services") config.add_route("services_id", base_url + "/services/{id}") - config.add_route("service_execution", base_url + "/service-execution/{serviceId}/{metadatasetId}") + config.add_route( + "service_execution", base_url + "/service-execution/{serviceId}/{metadatasetId}" + ) # Endpoint outside of openapi config.add_route("upload", base_url + "/upload/{id}") @@ -77,7 +81,7 @@ def includeme(config: Configurator) -> None: @view_config( route_name="api", - renderer='json', + renderer="json", request_method="GET", ) def get(request: Request): diff --git a/datameta/api/metrics.py b/datameta/api/metrics.py new file mode 100644 index 00000000..d143082c --- /dev/null +++ b/datameta/api/metrics.py @@ -0,0 +1,59 @@ +# Copyright 2021 Universität Tübingen, DKFZ and EMBL for the German Human Genome-Phenome Archive (GHGA) +# +# Licensed 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 +# +# https://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. + +import logging +from dataclasses import dataclass + +from pyramid.request import Request +from pyramid.view import view_config + +from ..models import MetaDataSet +from . import DataHolderBase + +log = logging.getLogger(__name__) + + +@dataclass +class MetricsBase(DataHolderBase): + """Base class for Metrics communication to OpenApi""" + + pass + + +@dataclass +class MetricsResponse(MetricsBase): + """MetricsResponse container for OpenApi communication""" + + metadatasets_submitted_count: int + + +@view_config( + route_name="metrics", + renderer="json", + request_method="GET", + openapi=True, +) +def metrics(request: Request) -> MetricsResponse: + """Get metrics of the server.""" + db = request.dbsession + + query = db.query(MetaDataSet).filter(MetaDataSet.submission_id.isnot(None)) + + metadatasets_submitted_count = int(query.count()) + + log.info("Server metrics requested.") + + return MetricsResponse( + metadatasets_submitted_count=metadatasets_submitted_count, + ) diff --git a/datameta/api/openapi.yaml b/datameta/api/openapi.yaml index 30b352fa..c09a43af 100644 --- a/datameta/api/openapi.yaml +++ b/datameta/api/openapi.yaml @@ -15,7 +15,7 @@ openapi: 3.0.0 info: description: DataMeta - version: 1.5.0 + version: 1.5.1 title: DataMeta servers: @@ -1242,6 +1242,23 @@ paths: $ref: "#/components/schemas/ServerInfoResponse" "500": description: Internal Server Error + + /metrics: + get: + summary: Get server metrics + description: Get metrics of the DataMeta server serving this API + tags: + - Server + operationId: GetMetrics + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/MetricsResponse" + "500": + description: Internal Server Error /registrations: post: @@ -1895,6 +1912,15 @@ components: items: $ref: "#/components/schemas/MetaDataResponse" + MetricsResponse: + type: object + properties: + metadatasetsSubmittedCount: + type: integer + required: + - metadatasetsSubmittedCount + additionalProperties: false + ServerInfoResponse: type: object properties: diff --git a/setup.py b/setup.py index 7a2d9aa5..f3533d2a 100644 --- a/setup.py +++ b/setup.py @@ -67,7 +67,7 @@ setup( name = 'datameta', - version = '1.1.1', + version = '1.1.2', description = 'DataMeta - submission server for data and associated metadata', long_description = README + '\n\n' + CHANGES, author = 'Leon Kuchenbecker', diff --git a/tests/integration/test_metrics.py b/tests/integration/test_metrics.py new file mode 100644 index 00000000..22d6b6b6 --- /dev/null +++ b/tests/integration/test_metrics.py @@ -0,0 +1,82 @@ +# Copyright 2021 Universität Tübingen, DKFZ and EMBL for the German Human Genome-Phenome Archive (GHGA) +# +# Licensed 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 +# +# https://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 datameta.api import base_url + +from . import BaseIntegrationTest + +FIXTURES_LOAD = [ + "groups", + "users", + "apikeys", + "services", + "metadata", + "files_msets", + "submissions", + "metadatasets", + "serviceexecutions", +] + +# Required, with empty dict 401 is returned for a not registered user +EMPTY_AUTH_HEADER = {"Authorization": f"Bearer {None}"} + + +class TestGetMetricsEmptyServer(BaseIntegrationTest): + """Test get metrics with an empty server.""" + + def setUp(self): + super().setUp() + + def test_metrics( + self, + ): + response = self.testapp.get( + url=f"{base_url}/metrics", + status=200, + headers=EMPTY_AUTH_HEADER, + ) + + assert response.json["metadatasetsSubmittedCount"] == 0 + + +class TestGetMetricsServer(BaseIntegrationTest): + """Test get metrics with data.""" + + def setUp(self): + super().setUp() + for fixture in FIXTURES_LOAD: + self.fixture_manager.load_fixtureset(fixture) + self.fixture_manager.copy_files_to_storage() + self.fixture_manager.populate_metadatasets() + + def test_metrics( + self, + ): + # Get submitted metadatasets + user = self.fixture_manager.get_fixture("users", "admin") + auth_headers = self.apikey_auth(user) if user else {} + + response_metadatasets = self.testapp.get( + url=f"{base_url}/metadatasets", headers=auth_headers, status=200 + ) + + response_metrics = self.testapp.get( + url=f"{base_url}/metrics", + status=200, + headers=EMPTY_AUTH_HEADER, + ) + + assert response_metrics.json["metadatasetsSubmittedCount"] == len( + response_metadatasets.json + )