diff --git a/config.py b/config.py index 4d78b1abf..5d138e6a0 100644 --- a/config.py +++ b/config.py @@ -553,3 +553,9 @@ class DefaultConfig(ImmutableConfig): # Feature Flag: Whether to record when users were last accessed. FEATURE_USER_LAST_ACCESSED = True + + # Feature Flag: Whether to allow users to retrieve aggregated log counts. + FEATURE_AGGREGATED_LOG_COUNT_RETRIEVAL = True + + # Feature Flag: Whether to support log exporting. + FEATURE_LOG_EXPORT = True diff --git a/endpoints/api/logs.py b/endpoints/api/logs.py index 4c3928c43..0cae6b095 100644 --- a/endpoints/api/logs.py +++ b/endpoints/api/logs.py @@ -6,11 +6,13 @@ from datetime import datetime, timedelta from flask import request +import features + from app import export_action_logs_queue from endpoints.api import (resource, nickname, ApiResource, query_param, parse_args, RepositoryParamResource, require_repo_admin, related_user_resource, format_date, require_user_admin, path_param, require_scope, page_support, - validate_json_request, InvalidRequest) + validate_json_request, InvalidRequest, show_if) from data import model as data_model from endpoints.api.logs_models_pre_oci import pre_oci_model as model from endpoints.exception import Unauthorized, NotFound @@ -150,6 +152,7 @@ class OrgLogs(ApiResource): @resource('/v1/repository//aggregatelogs') +@show_if(features.AGGREGATED_LOG_COUNT_RETRIEVAL) @path_param('repository', 'The full path of the repository. e.g. namespace/name') class RepositoryAggregateLogs(RepositoryParamResource): """ Resource for fetching aggregated logs for the specific repository. """ @@ -170,6 +173,7 @@ class RepositoryAggregateLogs(RepositoryParamResource): @resource('/v1/user/aggregatelogs') +@show_if(features.AGGREGATED_LOG_COUNT_RETRIEVAL) class UserAggregateLogs(ApiResource): """ Resource for fetching aggregated logs for the current user. """ @@ -191,6 +195,7 @@ class UserAggregateLogs(ApiResource): @resource('/v1/organization//aggregatelogs') +@show_if(features.AGGREGATED_LOG_COUNT_RETRIEVAL) @path_param('orgname', 'The name of the organization') @related_user_resource(UserLogs) class OrgAggregateLogs(ApiResource): @@ -314,6 +319,7 @@ class ExportUserLogs(ApiResource): @resource('/v1/organization//exportlogs') +@show_if(features.LOG_EXPORT) @path_param('orgname', 'The name of the organization') @related_user_resource(ExportUserLogs) class ExportOrgLogs(ApiResource): @@ -329,7 +335,7 @@ class ExportOrgLogs(ApiResource): @require_scope(scopes.ORG_ADMIN) @validate_json_request('ExportLogs') def post(self, orgname, parsed_args): - """ Gets the aggregated logs for the specified organization. """ + """ Exports the logs for the specified organization. """ permission = AdministerOrganizationPermission(orgname) if permission.can(): start_time = parsed_args['starttime'] diff --git a/static/directives/logs-view.html b/static/directives/logs-view.html index 79ce3d7a2..9cd1cfee5 100644 --- a/static/directives/logs-view.html +++ b/static/directives/logs-view.html @@ -22,14 +22,16 @@
-
+
diff --git a/static/js/directives/ui/logs-view.js b/static/js/directives/ui/logs-view.js index a73e9f5f6..ab79ec045 100644 --- a/static/js/directives/ui/logs-view.js +++ b/static/js/directives/ui/logs-view.js @@ -19,7 +19,9 @@ angular.module('quay').directive('logsView', function () { 'allLogs': '@allLogs' }, controller: function($scope, $element, $sce, Restangular, ApiService, TriggerService, - StringBuilderService, ExternalNotificationData, UtilService) { + StringBuilderService, ExternalNotificationData, UtilService, + Features) { + $scope.Features = Features; $scope.loading = true; $scope.loadCounter = -1; $scope.logs = null; @@ -405,20 +407,22 @@ angular.module('quay').directive('logsView', function () { return; } - $scope.chartLoading = true; + if (Features.AGGREGATED_LOG_COUNT_RETRIEVAL) { + $scope.chartLoading = true; - var aggregateUrl = getUrl('aggregatelogs').toString(); - var loadAggregate = Restangular.one(aggregateUrl); - loadAggregate.customGET().then(function(resp) { - $scope.chart = new LogUsageChart(logKinds); - $($scope.chart).bind('filteringChanged', function(e) { - $scope.$apply(function() { $scope.kindsAllowed = e.allowed; }); + var aggregateUrl = getUrl('aggregatelogs').toString(); + var loadAggregate = Restangular.one(aggregateUrl); + loadAggregate.customGET().then(function(resp) { + $scope.chart = new LogUsageChart(logKinds); + $($scope.chart).bind('filteringChanged', function(e) { + $scope.$apply(function() { $scope.kindsAllowed = e.allowed; }); + }); + + $scope.chart.draw('bar-chart', resp.aggregated, $scope.options.logStartDate, + $scope.options.logEndDate); + $scope.chartLoading = false; }); - - $scope.chart.draw('bar-chart', resp.aggregated, $scope.options.logStartDate, - $scope.options.logEndDate); - $scope.chartLoading = false; - }); + } $scope.nextPageToken = null; $scope.hasAdditional = true; diff --git a/util/config/schema.py b/util/config/schema.py index 64226fc4e..844359008 100644 --- a/util/config/schema.py +++ b/util/config/schema.py @@ -784,6 +784,20 @@ CONFIG_SCHEMA = { 'pattern': '^[0-9]+(w|m|d|h|s)$', }, + # Feature Flag: Aggregated log retrieval. + 'FEATURE_AGGREGATED_LOG_COUNT_RETRIEVAL': { + 'type': 'boolean', + 'description': 'Whether to allow retrieval of aggregated log counts. Defaults to True', + 'x-example': True, + }, + + # Feature Flag: Log export. + 'FEATURE_LOG_EXPORT': { + 'type': 'boolean', + 'description': 'Whether to allow exporting of action logs. Defaults to True', + 'x-example': True, + }, + # Feature Flag: User last accessed. 'FEATURE_USER_LAST_ACCESSED': { 'type': 'boolean', diff --git a/workers/exportactionlogsworker.py b/workers/exportactionlogsworker.py index 3c14ec006..857bdd094 100644 --- a/workers/exportactionlogsworker.py +++ b/workers/exportactionlogsworker.py @@ -1,6 +1,7 @@ import logging import os.path import json +import time import uuid from datetime import datetime, timedelta @@ -8,6 +9,8 @@ from io import BytesIO from enum import Enum, unique +import features + from app import app, export_action_logs_queue, storage, get_app_url from data import model from endpoints.api import format_date @@ -277,6 +280,11 @@ def _run_and_time(fn): if __name__ == "__main__": logging.config.fileConfig(logfile_path(debug=False), disable_existing_loggers=False) + if not features.LOG_EXPORT: + logger.debug('Log export not enabled; skipping') + while True: + time.sleep(100000) + logger.debug('Starting export action logs worker') worker = ExportActionLogsWorker(export_action_logs_queue, poll_period_seconds=POLL_PERIOD_SECONDS) diff --git a/workers/logrotateworker.py b/workers/logrotateworker.py index e8486403c..a0ad7d314 100644 --- a/workers/logrotateworker.py +++ b/workers/logrotateworker.py @@ -129,8 +129,8 @@ def log_dict(log): def main(): logging.config.fileConfig(logfile_path(debug=False), disable_existing_loggers=False) - if not features.ACTION_LOG_ROTATION or None in [SAVE_PATH, SAVE_LOCATION]: - logger.debug('Action log rotation worker not enabled; skipping') + if not features.LOG_EXPORT: + logger.debug('Log export not enabled; skipping') while True: time.sleep(100000)