From 71bed7de3b9d0b79b1ada35253a6a24d56518bf9 Mon Sep 17 00:00:00 2001 From: Pedro Pombeiro <noreply@pedro.pombei.ro> Date: Fri, 22 Dec 2023 10:34:15 +0000 Subject: [PATCH] GraphQL: Add time window arguments to RunnersExportUsageInput EE: true --- doc/api/graphql/reference/index.md | 2 + .../mutations/ci/runners/export_usage.rb | 34 +++++++++---- .../ci/runners/generate_usage_csv_service.rb | 17 +++---- .../ci/runners/send_usage_csv_service.rb | 12 ++--- .../ci/runners/export_usage_csv_worker.rb | 4 +- .../mutations/ci/runners/export_usage_spec.rb | 51 ++++++++++++++++--- .../generate_usage_csv_service_spec.rb | 37 ++++---------- .../ci/runners/send_usage_csv_service_spec.rb | 2 +- .../runners/export_usage_csv_worker_spec.rb | 8 ++- 9 files changed, 105 insertions(+), 62 deletions(-) diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index e8dcf831d9f78..ed858aa534295 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -6747,7 +6747,9 @@ Input type: `RunnersExportUsageInput` | Name | Type | Description | | ---- | ---- | ----------- | | <a id="mutationrunnersexportusageclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| <a id="mutationrunnersexportusagefromdate"></a>`fromDate` | [`ISO8601Date`](#iso8601date) | UTC start date of the period to report on. Defaults to the start of last full month. | | <a id="mutationrunnersexportusagemaxprojectcount"></a>`maxProjectCount` | [`Int`](#int) | Maximum number of projects to return. All other runner usage will be attributed to a '<Other projects>' entry. Defaults to 1000 projects. | +| <a id="mutationrunnersexportusagetodate"></a>`toDate` | [`ISO8601Date`](#iso8601date) | UTC end date of the period to report on. " \ "Defaults to the end of the month specified by `fromDate`. | | <a id="mutationrunnersexportusagetype"></a>`type` | [`CiRunnerType`](#cirunnertype) | Scope of the runners to include in the report. | #### Fields diff --git a/ee/app/graphql/mutations/ci/runners/export_usage.rb b/ee/app/graphql/mutations/ci/runners/export_usage.rb index 8fd316962afe4..13aa87f83571e 100644 --- a/ee/app/graphql/mutations/ci/runners/export_usage.rb +++ b/ee/app/graphql/mutations/ci/runners/export_usage.rb @@ -6,23 +6,31 @@ module Runners class ExportUsage < BaseMutation graphql_name 'RunnersExportUsage' + DEFAULT_PROJECT_COUNT = 1_000 + argument :type, ::Types::Ci::RunnerTypeEnum, required: false, description: 'Scope of the runners to include in the report.' + argument :from_date, ::GraphQL::Types::ISO8601Date, + required: false, + description: 'UTC start date of the period to report on. Defaults to the start of last full month.' + argument :to_date, ::GraphQL::Types::ISO8601Date, + required: false, + description: 'UTC end date of the period to report on. " \ + "Defaults to the end of the month specified by `fromDate`.' + argument :max_project_count, ::GraphQL::Types::Int, required: false, + default_value: DEFAULT_PROJECT_COUNT, description: "Maximum number of projects to return. All other runner usage will be attributed " \ - "to a '<Other projects>' entry. " \ - "Defaults to #{::Ci::Runners::GenerateUsageCsvService::DEFAULT_PROJECT_COUNT} projects." + "to a '<Other projects>' entry. Defaults to #{DEFAULT_PROJECT_COUNT} projects." def ready?(**args) raise_resource_not_available_error! unless Ability.allowed?(current_user, :read_runner_usage) - max_project_count = args.fetch( - :max_project_count, ::Ci::Runners::GenerateUsageCsvService::DEFAULT_PROJECT_COUNT - ) + max_project_count = args.fetch(:max_project_count, DEFAULT_PROJECT_COUNT) unless max_project_count.between?(1, ::Ci::Runners::GenerateUsageCsvService::MAX_PROJECT_COUNT) raise Gitlab::Graphql::Errors::ArgumentError, @@ -32,10 +40,18 @@ def ready?(**args) super end - def resolve(type: nil, max_project_count: nil) - ::Ci::Runners::ExportUsageCsvWorker.perform_async( # rubocop: disable CodeReuse/Worker -- this worker sends out emails - current_user.id, { runner_type: ::Ci::Runner.runner_types[type], max_project_count: max_project_count } - ) + def resolve(type: nil, from_date: nil, to_date: nil, max_project_count: nil) + from_date ||= Date.current.prev_month.beginning_of_month + to_date ||= from_date.end_of_month + + args = { + runner_type: ::Ci::Runner.runner_types[type], + from_date: from_date, + to_date: to_date, + max_project_count: max_project_count + } + + ::Ci::Runners::ExportUsageCsvWorker.perform_async(current_user.id, args) # rubocop: disable CodeReuse/Worker -- this worker sends out emails { errors: [] diff --git a/ee/app/services/ci/runners/generate_usage_csv_service.rb b/ee/app/services/ci/runners/generate_usage_csv_service.rb index 3e33b66fbd750..43e35eb128eba 100644 --- a/ee/app/services/ci/runners/generate_usage_csv_service.rb +++ b/ee/app/services/ci/runners/generate_usage_csv_service.rb @@ -8,24 +8,23 @@ module Runners class GenerateUsageCsvService attr_reader :project_ids, :runner_type, :from_date, :to_date - DEFAULT_PROJECT_COUNT = 1_000 MAX_PROJECT_COUNT = 1_000 OTHER_PROJECTS_NAME = '<Other projects>' # @param [User] current_user The user performing the reporting - # @param [Symbol] runner_type The type of runners to report on. Defaults to nil, reporting on all runner types - # @param [Date] from_date The start date of the period to examine. Defaults to start of last full month - # @param [Date] to_date The end date of the period to examine. Defaults to end of month + # @param [Symbol] runner_type The type of runners to report on, or nil to report on all types + # @param [Date] from_date The start date of the period to examine + # @param [Date] to_date The end date of the period to examine # @param [Integer] max_project_count The maximum number of projects in the report. All others will be folded - # into an 'Other projects' entry. Defaults to 1000 - def initialize(current_user:, runner_type: nil, from_date: nil, to_date: nil, max_project_count: nil) + # into an 'Other projects' entry + def initialize(current_user:, runner_type:, from_date:, to_date:, max_project_count:) runner_type = Ci::Runner.runner_types[runner_type] if runner_type.is_a?(Symbol) @current_user = current_user @runner_type = runner_type - @from_date = from_date || Date.current.prev_month.beginning_of_month - @to_date = to_date || @from_date.end_of_month - @max_project_count = [MAX_PROJECT_COUNT, max_project_count || DEFAULT_PROJECT_COUNT].min + @max_project_count = [MAX_PROJECT_COUNT, max_project_count].min + @from_date = from_date + @to_date = to_date end def execute diff --git a/ee/app/services/ci/runners/send_usage_csv_service.rb b/ee/app/services/ci/runners/send_usage_csv_service.rb index 6ae6ed50b397c..23dfa8a0a0b1d 100644 --- a/ee/app/services/ci/runners/send_usage_csv_service.rb +++ b/ee/app/services/ci/runners/send_usage_csv_service.rb @@ -7,12 +7,12 @@ module Runners # class SendUsageCsvService # @param [User] current_user The user performing the reporting - # @param [Symbol] runner_type The type of runners to report on. Defaults to nil, reporting on all runner types - # @param [Date] from_date The start date of the period to examine. Defaults to start of last full month - # @param [Date] to_date The end date of the period to examine. Defaults to end of month + # @param [Symbol] runner_type The type of runners to report on, or nil to report on all types + # @param [Date] from_date The start date of the period to examine + # @param [Date] to_date The end date of the period to examine # @param [Integer] max_project_count The maximum number of projects in the report. All others will be folded - # into an 'Other projects' entry. Defaults to 1000 - def initialize(current_user:, runner_type: nil, from_date: nil, to_date: nil, max_project_count: nil) + # into an 'Other projects' entry + def initialize(current_user:, runner_type:, from_date:, to_date:, max_project_count:) @current_user = current_user @runner_type = runner_type @from_date = from_date @@ -33,7 +33,7 @@ def execute return result if result.error? Notify.runner_usage_by_project_csv_email( - user: @current_user, from_date: generate_csv_service.from_date, to_date: generate_csv_service.to_date, + user: @current_user, from_date: @from_date, to_date: @to_date, csv_data: result.payload[:csv_data], export_status: result.payload[:status] ).deliver_now diff --git a/ee/app/workers/ci/runners/export_usage_csv_worker.rb b/ee/app/workers/ci/runners/export_usage_csv_worker.rb index 4a86028332545..22127cecbebda 100644 --- a/ee/app/workers/ci/runners/export_usage_csv_worker.rb +++ b/ee/app/workers/ci/runners/export_usage_csv_worker.rb @@ -18,8 +18,10 @@ def perform(current_user_id, params) params.symbolize_keys! user = User.find(current_user_id) + from_date = Date.parse(params[:from_date]) + to_date = Date.parse(params[:to_date]) result = Ci::Runners::SendUsageCsvService.new( - current_user: user, **params.slice(:runner_type, :max_project_count) + current_user: user, from_date: from_date, to_date: to_date, **params.slice(:runner_type, :max_project_count) ).execute log_extra_metadata_on_done(:status, result.status) log_extra_metadata_on_done(:message, result.message) if result.message diff --git a/ee/spec/requests/api/graphql/mutations/ci/runners/export_usage_spec.rb b/ee/spec/requests/api/graphql/mutations/ci/runners/export_usage_spec.rb index f0c9cfab27fec..76b92d4521f6c 100644 --- a/ee/spec/requests/api/graphql/mutations/ci/runners/export_usage_spec.rb +++ b/ee/spec/requests/api/graphql/mutations/ci/runners/export_usage_spec.rb @@ -22,9 +22,17 @@ end let(:runner_type) { 'group_type' } - let(:max_project_count) { 7 } + let(:mutation_args) do + { + type: runner_type.upcase, + from_date: Date.new(2023, 11, 1), + to_date: Date.new(2023, 11, 30), + max_project_count: 7 + } + end + let(:mutation) do - graphql_mutation(:runners_export_usage, type: runner_type.upcase, max_project_count: max_project_count) do + graphql_mutation(:runners_export_usage, mutation_args) do <<~QL errors QL @@ -45,11 +53,12 @@ it 'sends email with report' do expect(::Ci::Runners::ExportUsageCsvWorker).to receive(:perform_async) .with(current_user.id, { - runner_type: ::Ci::Runner.runner_types[runner_type], max_project_count: max_project_count + runner_type: ::Ci::Runner.runner_types[runner_type], + **mutation_args.slice(:from_date, :to_date, :max_project_count) }).and_call_original expect(Notify).to receive(:runner_usage_by_project_csv_email) .with( - user: current_user, from_date: Date.new(2023, 11, 1), to_date: Date.new(2023, 11, 1).end_of_month, + user: current_user, from_date: mutation_args[:from_date], to_date: mutation_args[:to_date], csv_data: anything, export_status: anything ) do |args| expect(args.dig(:export_status, :rows_written)).to eq 1 @@ -64,9 +73,37 @@ expect_graphql_errors_to_be_empty end + context 'with default args' do + let(:mutation_args) { {} } + + it 'sends email with report' do + expect(::Ci::Runners::ExportUsageCsvWorker).to receive(:perform_async) + .with(current_user.id, { + runner_type: nil, from_date: Date.new(2023, 11, 1), to_date: Date.new(2023, 11, 30), max_project_count: 1_000 + }).and_call_original + + post_response + expect_graphql_errors_to_be_empty + end + end + + context 'with only from_date' do + let(:mutation_args) { { from_date: Date.new(2023, 9, 1) } } + + it 'sends email with report of the month of September' do + expect(::Ci::Runners::ExportUsageCsvWorker).to receive(:perform_async) + .with(current_user.id, { + runner_type: nil, from_date: Date.new(2023, 9, 1), to_date: Date.new(2023, 9, 30), max_project_count: 1_000 + }).and_call_original + + post_response + expect_graphql_errors_to_be_empty + end + end + context 'when max_project_count is out-of-range' do context 'and is below acceptable range' do - let(:max_project_count) { 0 } + let(:mutation_args) { { type: runner_type.upcase, max_project_count: 0 } } it 'returns an error' do post_response @@ -75,7 +112,9 @@ end context 'and is above acceptable range' do - let(:max_project_count) { ::Ci::Runners::GenerateUsageCsvService::MAX_PROJECT_COUNT + 1 } + let(:mutation_args) do + { type: runner_type.upcase, max_project_count: ::Ci::Runners::GenerateUsageCsvService::MAX_PROJECT_COUNT + 1 } + end it 'returns an error' do post_response diff --git a/ee/spec/services/ci/runners/generate_usage_csv_service_spec.rb b/ee/spec/services/ci/runners/generate_usage_csv_service_spec.rb index b50f18da2a9ad..9e49e0b662cdd 100644 --- a/ee/spec/services/ci/runners/generate_usage_csv_service_spec.rb +++ b/ee/spec/services/ci/runners/generate_usage_csv_service_spec.rb @@ -6,7 +6,7 @@ feature_category: :fleet_visibility do include ClickHouseHelpers - let_it_be(:current_user) { create(:admin) } + let_it_be(:current_user) { build_stubbed(:admin) } let_it_be(:instance_runner) { create(:ci_runner, :instance, :with_runner_manager) } let_it_be(:group) { create(:group) } let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group]) } @@ -26,9 +26,9 @@ end let(:runner_type) { nil } - let(:from_date) { nil } - let(:to_date) { nil } - let(:max_project_count) { nil } + let(:from_date) { Date.new(2023, 12, 1) } + let(:to_date) { Date.new(2023, 12, 31) } + let(:max_project_count) { 50 } let(:response_status) { response.payload[:status] } let(:response_csv_lines) { response.payload[:csv_data].lines } let(:service) do @@ -37,8 +37,6 @@ end let(:expected_header) { "Project ID,Project path,Build count,Total duration (minutes),Total duration\n" } - let(:expected_from_date) { Date.new(2023, 12, 1) } - let(:expected_to_date) { Date.new(2023, 12, 31) } subject(:response) { service.execute } @@ -165,20 +163,18 @@ end end - context 'when from_date is beginning of current month' do + context 'when time window is current month' do let(:from_date) { Date.new(2024, 1, 1) } - let(:expected_from_date) { from_date } - let(:expected_to_date) { from_date.end_of_month } + let(:to_date) { Date.new(2024, 1, 31) } it 'exports usage data for runners which finished builds before date' do expect(response_status).to eq({ rows_expected: 16, rows_written: 16, truncated: false }) end end - context 'when from_date is next month' do + context 'when time window is next month' do let(:from_date) { Date.new(2024, 2, 1) } - let(:expected_from_date) { from_date } - let(:expected_to_date) { from_date.end_of_month } + let(:to_date) { Date.new(2024, 2, 29) } it 'exports usage data for runners which finished builds before date' do expect(response_status).to eq({ rows_expected: 0, rows_written: 0, truncated: false }) @@ -186,9 +182,8 @@ end context 'when to_date is an hour ago, almost at the end of the year' do + let(:from_date) { Date.new(2023, 11, 1) } let(:to_date) { Date.new(2023, 12, 31) } - let(:expected_from_date) { Date.new(2023, 11, 1) } - let(:expected_to_date) { to_date } before do travel_to DateTime.new(2023, 12, 31, 23, 59, 59) @@ -199,20 +194,6 @@ end end - context 'when to_date is end of last month' do - let(:to_date) { Date.new(2024, 1, 31) } - let(:expected_from_date) { Date.new(2024, 1, 1) } - let(:expected_to_date) { to_date } - - before do - travel_to DateTime.new(2024, 2, 10) - end - - it 'exports usage data for runners which finished builds after date' do - expect(response_status).to eq({ rows_expected: 16, rows_written: 16, truncated: false }) - end - end - def create_build(runner, project, created_at, duration = 14.minutes) started_at = created_at + 6.minutes diff --git a/ee/spec/services/ci/runners/send_usage_csv_service_spec.rb b/ee/spec/services/ci/runners/send_usage_csv_service_spec.rb index 305ceec08df8a..59f3837c7af5a 100644 --- a/ee/spec/services/ci/runners/send_usage_csv_service_spec.rb +++ b/ee/spec/services/ci/runners/send_usage_csv_service_spec.rb @@ -6,7 +6,7 @@ feature_category: :fleet_visibility do include ClickHouseHelpers - let_it_be(:current_user) { create(:admin) } + let_it_be(:current_user) { build_stubbed(:admin) } let_it_be(:instance_runner) { create(:ci_runner, :instance, :with_runner_manager) } let(:from_date) { 1.month.ago } diff --git a/ee/spec/workers/ci/runners/export_usage_csv_worker_spec.rb b/ee/spec/workers/ci/runners/export_usage_csv_worker_spec.rb index 01c27a9fb137b..58d1d55671b2b 100644 --- a/ee/spec/workers/ci/runners/export_usage_csv_worker_spec.rb +++ b/ee/spec/workers/ci/runners/export_usage_csv_worker_spec.rb @@ -12,7 +12,9 @@ subject(:perform) { worker.perform(current_user.id, params) } let(:current_user) { admin } - let(:params) { { runner_type: 1, max_project_count: 25 } } + let(:params) do + { runner_type: 1, from_date: '2023-11-01', to_date: '2023-11-30', max_project_count: 25 } + end before do stub_licensed_features(runner_performance_insights: true) @@ -20,7 +22,9 @@ it 'delegates to Ci::Runners::SendUsageCsvService' do expect_next_instance_of(Ci::Runners::SendUsageCsvService, { - current_user: current_user, runner_type: params[:runner_type], max_project_count: params[:max_project_count] + current_user: current_user, runner_type: params[:runner_type], + from_date: Date.new(2023, 11, 1), to_date: Date.new(2023, 11, 30), + max_project_count: params[:max_project_count] }) do |service| expect(service).to receive(:execute).and_call_original end -- GitLab