Put aggregated log query and log exports behind feature flags

This commit is contained in:
Joseph Schorr 2019-01-02 14:17:40 -05:00
parent 4ba4d9141b
commit 204eb74c4f
7 changed files with 60 additions and 20 deletions

View file

@ -553,3 +553,9 @@ class DefaultConfig(ImmutableConfig):
# Feature Flag: Whether to record when users were last accessed. # Feature Flag: Whether to record when users were last accessed.
FEATURE_USER_LAST_ACCESSED = True 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

View file

@ -6,11 +6,13 @@ from datetime import datetime, timedelta
from flask import request from flask import request
import features
from app import export_action_logs_queue from app import export_action_logs_queue
from endpoints.api import (resource, nickname, ApiResource, query_param, parse_args, from endpoints.api import (resource, nickname, ApiResource, query_param, parse_args,
RepositoryParamResource, require_repo_admin, related_user_resource, RepositoryParamResource, require_repo_admin, related_user_resource,
format_date, require_user_admin, path_param, require_scope, page_support, 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 data import model as data_model
from endpoints.api.logs_models_pre_oci import pre_oci_model as model from endpoints.api.logs_models_pre_oci import pre_oci_model as model
from endpoints.exception import Unauthorized, NotFound from endpoints.exception import Unauthorized, NotFound
@ -150,6 +152,7 @@ class OrgLogs(ApiResource):
@resource('/v1/repository/<apirepopath:repository>/aggregatelogs') @resource('/v1/repository/<apirepopath:repository>/aggregatelogs')
@show_if(features.AGGREGATED_LOG_COUNT_RETRIEVAL)
@path_param('repository', 'The full path of the repository. e.g. namespace/name') @path_param('repository', 'The full path of the repository. e.g. namespace/name')
class RepositoryAggregateLogs(RepositoryParamResource): class RepositoryAggregateLogs(RepositoryParamResource):
""" Resource for fetching aggregated logs for the specific repository. """ """ Resource for fetching aggregated logs for the specific repository. """
@ -170,6 +173,7 @@ class RepositoryAggregateLogs(RepositoryParamResource):
@resource('/v1/user/aggregatelogs') @resource('/v1/user/aggregatelogs')
@show_if(features.AGGREGATED_LOG_COUNT_RETRIEVAL)
class UserAggregateLogs(ApiResource): class UserAggregateLogs(ApiResource):
""" Resource for fetching aggregated logs for the current user. """ """ Resource for fetching aggregated logs for the current user. """
@ -191,6 +195,7 @@ class UserAggregateLogs(ApiResource):
@resource('/v1/organization/<orgname>/aggregatelogs') @resource('/v1/organization/<orgname>/aggregatelogs')
@show_if(features.AGGREGATED_LOG_COUNT_RETRIEVAL)
@path_param('orgname', 'The name of the organization') @path_param('orgname', 'The name of the organization')
@related_user_resource(UserLogs) @related_user_resource(UserLogs)
class OrgAggregateLogs(ApiResource): class OrgAggregateLogs(ApiResource):
@ -314,6 +319,7 @@ class ExportUserLogs(ApiResource):
@resource('/v1/organization/<orgname>/exportlogs') @resource('/v1/organization/<orgname>/exportlogs')
@show_if(features.LOG_EXPORT)
@path_param('orgname', 'The name of the organization') @path_param('orgname', 'The name of the organization')
@related_user_resource(ExportUserLogs) @related_user_resource(ExportUserLogs)
class ExportOrgLogs(ApiResource): class ExportOrgLogs(ApiResource):
@ -329,7 +335,7 @@ class ExportOrgLogs(ApiResource):
@require_scope(scopes.ORG_ADMIN) @require_scope(scopes.ORG_ADMIN)
@validate_json_request('ExportLogs') @validate_json_request('ExportLogs')
def post(self, orgname, parsed_args): def post(self, orgname, parsed_args):
""" Gets the aggregated logs for the specified organization. """ """ Exports the logs for the specified organization. """
permission = AdministerOrganizationPermission(orgname) permission = AdministerOrganizationPermission(orgname)
if permission.can(): if permission.can():
start_time = parsed_args['starttime'] start_time = parsed_args['starttime']

View file

@ -22,14 +22,16 @@
</span> </span>
<span class="hidden-xs right"> <span class="hidden-xs right">
<i class="fa fa-bar-chart-o toggle-icon" ng-class="chartVisible ? 'active' : ''" <i class="fa fa-bar-chart-o toggle-icon" ng-class="chartVisible ? 'active' : ''"
ng-click="toggleChart()" data-title="Toggle Chart" bs-tooltip="tooltip.title"></i> ng-click="toggleChart()" data-title="Toggle Chart" bs-tooltip="tooltip.title"
quay-show="Features.AGGREGATED_LOG_COUNT_RETRIEVAL"></i>
<button class="btn btn-default download-btn" ng-click="showExportLogs()" <button class="btn btn-default download-btn" ng-click="showExportLogs()"
ng-if="user || organization || repository"><i class="fa fa-download"></i>Export Logs</button> ng-if="(user || organization || repository) && Features.LOG_EXPORT"><i class="fa fa-download"></i>Export Logs</button>
</span> </span>
</div> </div>
<div> <div>
<div id="bar-chart" style="width: 800px; height: 500px;" ng-show="chartVisible"> <div id="bar-chart" style="width: 800px; height: 500px;"
quay-show="chartVisible && Features.AGGREGATED_LOG_COUNT_RETRIEVAL">
<svg style="width: 800px; height: 500px;"></svg> <svg style="width: 800px; height: 500px;"></svg>
<div class="cor-loader" ng-if="chartLoading"></div> <div class="cor-loader" ng-if="chartLoading"></div>
</div> </div>

View file

@ -19,7 +19,9 @@ angular.module('quay').directive('logsView', function () {
'allLogs': '@allLogs' 'allLogs': '@allLogs'
}, },
controller: function($scope, $element, $sce, Restangular, ApiService, TriggerService, controller: function($scope, $element, $sce, Restangular, ApiService, TriggerService,
StringBuilderService, ExternalNotificationData, UtilService) { StringBuilderService, ExternalNotificationData, UtilService,
Features) {
$scope.Features = Features;
$scope.loading = true; $scope.loading = true;
$scope.loadCounter = -1; $scope.loadCounter = -1;
$scope.logs = null; $scope.logs = null;
@ -405,20 +407,22 @@ angular.module('quay').directive('logsView', function () {
return; return;
} }
$scope.chartLoading = true; if (Features.AGGREGATED_LOG_COUNT_RETRIEVAL) {
$scope.chartLoading = true;
var aggregateUrl = getUrl('aggregatelogs').toString(); var aggregateUrl = getUrl('aggregatelogs').toString();
var loadAggregate = Restangular.one(aggregateUrl); var loadAggregate = Restangular.one(aggregateUrl);
loadAggregate.customGET().then(function(resp) { loadAggregate.customGET().then(function(resp) {
$scope.chart = new LogUsageChart(logKinds); $scope.chart = new LogUsageChart(logKinds);
$($scope.chart).bind('filteringChanged', function(e) { $($scope.chart).bind('filteringChanged', function(e) {
$scope.$apply(function() { $scope.kindsAllowed = e.allowed; }); $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.nextPageToken = null;
$scope.hasAdditional = true; $scope.hasAdditional = true;

View file

@ -784,6 +784,20 @@ CONFIG_SCHEMA = {
'pattern': '^[0-9]+(w|m|d|h|s)$', '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 Flag: User last accessed.
'FEATURE_USER_LAST_ACCESSED': { 'FEATURE_USER_LAST_ACCESSED': {
'type': 'boolean', 'type': 'boolean',

View file

@ -1,6 +1,7 @@
import logging import logging
import os.path import os.path
import json import json
import time
import uuid import uuid
from datetime import datetime, timedelta from datetime import datetime, timedelta
@ -8,6 +9,8 @@ from io import BytesIO
from enum import Enum, unique from enum import Enum, unique
import features
from app import app, export_action_logs_queue, storage, get_app_url from app import app, export_action_logs_queue, storage, get_app_url
from data import model from data import model
from endpoints.api import format_date from endpoints.api import format_date
@ -277,6 +280,11 @@ def _run_and_time(fn):
if __name__ == "__main__": if __name__ == "__main__":
logging.config.fileConfig(logfile_path(debug=False), disable_existing_loggers=False) 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') logger.debug('Starting export action logs worker')
worker = ExportActionLogsWorker(export_action_logs_queue, worker = ExportActionLogsWorker(export_action_logs_queue,
poll_period_seconds=POLL_PERIOD_SECONDS) poll_period_seconds=POLL_PERIOD_SECONDS)

View file

@ -129,8 +129,8 @@ def log_dict(log):
def main(): def main():
logging.config.fileConfig(logfile_path(debug=False), disable_existing_loggers=False) logging.config.fileConfig(logfile_path(debug=False), disable_existing_loggers=False)
if not features.ACTION_LOG_ROTATION or None in [SAVE_PATH, SAVE_LOCATION]: if not features.LOG_EXPORT:
logger.debug('Action log rotation worker not enabled; skipping') logger.debug('Log export not enabled; skipping')
while True: while True:
time.sleep(100000) time.sleep(100000)