Commit 2117fa0c authored by Nikolay Belokolodov's avatar Nikolay Belokolodov Committed by Mikołaj Wawrzyniak

Compute instrumentation metrics within ServicePingReport class

Adds support for instrumentation classes based metrics calculation for
ServicePingReport. Instrumentation metrics only computed if keys
are missing from the usage data payload.
parent 553e0207
---
name: merge_service_ping_instrumented_metrics
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/77629
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/352964
milestone: '14.8'
type: development
group: group::product intelligence
default_enabled: false
...@@ -83,6 +83,8 @@ RSpec.describe Gitlab::UsageData do ...@@ -83,6 +83,8 @@ RSpec.describe Gitlab::UsageData do
end end
it 'gathers usage counts', :aggregate_failures do it 'gathers usage counts', :aggregate_failures do
stub_feature_flags(merge_service_ping_instrumented_metrics: false)
expect(count_data[:boards]).to eq(1) expect(count_data[:boards]).to eq(1)
expect(count_data[:projects]).to eq(3) expect(count_data[:projects]).to eq(3)
......
...@@ -16,6 +16,7 @@ RSpec.describe ServicePing::BuildPayloadService do ...@@ -16,6 +16,7 @@ RSpec.describe ServicePing::BuildPayloadService do
before do before do
allow(User).to receive(:single_user).and_return(double(:user, requires_usage_stats_consent?: false)) allow(User).to receive(:single_user).and_return(double(:user, requires_usage_stats_consent?: false))
stub_feature_flags(merge_service_ping_instrumented_metrics: false)
end end
context 'GitLab instance have a license' do context 'GitLab instance have a license' do
......
...@@ -80,6 +80,10 @@ module Gitlab ...@@ -80,6 +80,10 @@ module Gitlab
@all ||= definitions.map { |_key_path, definition| definition } @all ||= definitions.map { |_key_path, definition| definition }
end end
def not_removed
all.select { |definition| definition.attributes[:status] != 'removed' }.index_by(&:key_path)
end
def with_instrumentation_class def with_instrumentation_class
all.select { |definition| definition.attributes[:instrumentation_class].present? && definition.available? } all.select { |definition| definition.attributes[:instrumentation_class].present? && definition.available? }
end end
......
# frozen_string_literal: true
# Service Ping payload build using the instrumentation classes
# for given metrics key_paths and output method
module Gitlab
module Usage
module ServicePing
class InstrumentedPayload
attr_reader :metrics_key_paths
attr_reader :output_method
def initialize(metrics_key_paths, output_method)
@metrics_key_paths = metrics_key_paths
@output_method = output_method
end
def build
metrics_key_paths.map do |key_path|
compute_instrumental_value(key_path, output_method)
end.reduce({}, :deep_merge)
end
private
# Not all metrics defintions have instrumentation classes
# The value can be computed only for those that have it
def instrumented_metrics_defintions
Gitlab::Usage::MetricDefinition.with_instrumentation_class
end
def compute_instrumental_value(key_path, output_method)
definition = instrumented_metrics_defintions.find { |df| df.key_path == key_path }
return {} unless definition.present?
Gitlab::Usage::Metric.new(definition).method(output_method).call
end
end
end
end
end
# frozen_string_literal: true
# Process the UsageData payload to get the keys that have a metric defintion
# Get the missing keys from the payload
module Gitlab
module Usage
module ServicePing
class PayloadKeysProcessor
attr_reader :old_payload
def initialize(old_payload)
@old_payload = old_payload
end
def key_paths
@key_paths ||= payload_keys.to_a.flatten.compact
end
def missing_instrumented_metrics_key_paths
@missing_key_paths ||= metrics_with_instrumentation.map(&:key_path) - key_paths
end
private
def payload_keys(payload = old_payload, parents = [])
return unless payload.is_a?(Hash)
payload.map do |key, value|
if has_metric_definition?(key, parents)
parents.dup.append(key).join('.')
else
payload_keys(value, parents.dup << key) if value.is_a?(Hash)
end
end
end
def has_metric_definition?(key, parent_keys)
key_path = parent_keys.dup.append(key).join('.')
metric_definitions.key?(key_path)
end
def metric_definitions
::Gitlab::Usage::MetricDefinition.not_removed
end
def metrics_with_instrumentation
::Gitlab::Usage::MetricDefinition.with_instrumentation_class
end
end
end
end
end
...@@ -7,16 +7,29 @@ module Gitlab ...@@ -7,16 +7,29 @@ module Gitlab
def for(output:, cached: false) def for(output:, cached: false)
case output.to_sym case output.to_sym
when :all_metrics_values when :all_metrics_values
all_metrics_values(cached) with_instrumentation_classes(all_metrics_values(cached), :with_value)
when :metrics_queries when :metrics_queries
metrics_queries with_instrumentation_classes(metrics_queries, :with_instrumentation)
when :non_sql_metrics_values when :non_sql_metrics_values
non_sql_metrics_values with_instrumentation_classes(non_sql_metrics_values, :with_instrumentation)
end end
end end
private private
def with_instrumentation_classes(old_payload, output_method)
if Feature.enabled?(:merge_service_ping_instrumented_metrics, default_enabled: :yaml)
instrumented_metrics_key_paths = Gitlab::Usage::ServicePing::PayloadKeysProcessor.new(old_payload).missing_instrumented_metrics_key_paths
instrumented_payload = Gitlab::Usage::ServicePing::InstrumentedPayload.new(instrumented_metrics_key_paths, output_method).build
old_payload.deep_merge(instrumented_payload)
else
old_payload
end
end
def all_metrics_values(cached) def all_metrics_values(cached)
Rails.cache.fetch('usage_data', force: !cached, expires_in: 2.weeks) do Rails.cache.fetch('usage_data', force: !cached, expires_in: 2.weeks) do
Gitlab::UsageData.data Gitlab::UsageData.data
......
...@@ -70,10 +70,9 @@ module Gitlab ...@@ -70,10 +70,9 @@ module Gitlab
def system_usage_data def system_usage_data
issues_created_manually_from_alerts = count(Issue.with_alert_management_alerts.not_authored_by(::User.alert_bot), start: minimum_id(Issue), finish: maximum_id(Issue)) issues_created_manually_from_alerts = count(Issue.with_alert_management_alerts.not_authored_by(::User.alert_bot), start: minimum_id(Issue), finish: maximum_id(Issue))
{ counts = {
counts: { counts: {
assignee_lists: count(List.assignee), assignee_lists: count(List.assignee),
boards: add_metric('CountBoardsMetric', time_frame: 'all'),
ci_builds: count(::Ci::Build), ci_builds: count(::Ci::Build),
ci_internal_pipelines: count(::Ci::Pipeline.internal), ci_internal_pipelines: count(::Ci::Pipeline.internal),
ci_external_pipelines: count(::Ci::Pipeline.external), ci_external_pipelines: count(::Ci::Pipeline.external),
...@@ -167,6 +166,12 @@ module Gitlab ...@@ -167,6 +166,12 @@ module Gitlab
data[:snippets] = add(data[:personal_snippets], data[:project_snippets]) data[:snippets] = add(data[:personal_snippets], data[:project_snippets])
end end
} }
if Feature.disabled?(:merge_service_ping_instrumented_metrics, default_enabled: :yaml)
counts[:counts][:boards] = add_metric('CountBoardsMetric', time_frame: 'all')
end
counts
end end
# rubocop: enable Metrics/AbcSize # rubocop: enable Metrics/AbcSize
......
...@@ -50,6 +50,28 @@ RSpec.describe Gitlab::Usage::MetricDefinition do ...@@ -50,6 +50,28 @@ RSpec.describe Gitlab::Usage::MetricDefinition do
expect { described_class.definitions }.not_to raise_error expect { described_class.definitions }.not_to raise_error
end end
describe 'not_removed' do
let(:all_definitions) do
metrics_definitions = [
{ key_path: 'metric1', instrumentation_class: 'RedisHLLMetric', status: 'active' },
{ key_path: 'metric2', instrumentation_class: 'RedisHLLMetric', status: 'broken' },
{ key_path: 'metric3', instrumentation_class: 'RedisHLLMetric', status: 'active' },
{ key_path: 'metric4', instrumentation_class: 'RedisHLLMetric', status: 'removed' }
]
metrics_definitions.map { |definition| described_class.new(definition[:key_path], definition.symbolize_keys) }
end
before do
allow(described_class).to receive(:all).and_return(all_definitions)
end
it 'includes metrics that are not removed' do
expect(described_class.not_removed.count).to eq(3)
expect(described_class.not_removed.keys).to match_array(%w(metric1 metric2 metric3))
end
end
describe '#with_instrumentation_class' do describe '#with_instrumentation_class' do
let(:metric_status) { 'active' } let(:metric_status) { 'active' }
let(:all_definitions) do let(:all_definitions) do
......
...@@ -27,8 +27,8 @@ RSpec.describe Gitlab::Usage::Metrics::NamesSuggestions::Generator do ...@@ -27,8 +27,8 @@ RSpec.describe Gitlab::Usage::Metrics::NamesSuggestions::Generator do
context 'for count with default column metrics' do context 'for count with default column metrics' do
it_behaves_like 'name suggestion' do it_behaves_like 'name suggestion' do
# corresponding metric is collected with count(Board) # corresponding metric is collected with count(Board)
let(:key_path) { 'counts.boards' } let(:key_path) { 'counts.issues' }
let(:name_suggestion) { /count_boards/ } let(:name_suggestion) { /count_issues/ }
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Usage::ServicePing::InstrumentedPayload do
let(:uuid) { "0000-0000-0000" }
before do
allow(ApplicationRecord.connection).to receive(:transaction_open?).and_return(false)
allow(Gitlab::CurrentSettings).to receive(:uuid).and_return(uuid)
end
context 'when building service ping with values' do
let(:metrics_key_paths) { %w(counts.boards uuid redis_hll_counters.search.i_search_total_monthly) }
let(:expected_payload) do
{
counts: { boards: 0 },
redis_hll_counters: { search: { i_search_total_monthly: 0 } },
uuid: uuid
}
end
it 'builds the service ping payload for the metrics key_paths' do
expect(described_class.new(metrics_key_paths, :with_value).build).to eq(expected_payload)
end
end
context 'when building service ping with instrumentations' do
let(:metrics_key_paths) { %w(counts.boards uuid redis_hll_counters.search.i_search_total_monthly) }
let(:expected_payload) do
{
counts: { boards: "SELECT COUNT(\"boards\".\"id\") FROM \"boards\"" },
redis_hll_counters: { search: { i_search_total_monthly: 0 } },
uuid: uuid
}
end
it 'builds the service ping payload for the metrics key_paths' do
expect(described_class.new(metrics_key_paths, :with_instrumentation).build).to eq(expected_payload)
end
end
context 'when missing instrumentation class' do
it 'returns empty hash' do
expect(described_class.new(['counts.ci_builds'], :with_instrumentation).build).to eq({})
expect(described_class.new(['counts.ci_builds'], :with_value).build).to eq({})
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Usage::ServicePing::PayloadKeysProcessor do
context 'with an object metric' do
let(:payload) { { counts: { issues: 1, boards: 1 }, topology: { duration_d: 100 }, redis_hll_counters: { search: { i_search_total_monthly: 1 } } } }
it 'returns the payload keys that have a metric definition' do
expect(described_class.new(payload).key_paths).to match_array(['counts.issues', 'counts.boards', 'topology', 'redis_hll_counters.search.i_search_total_monthly'])
end
end
context 'with a missing metric definition' do
let(:payload) { { counts: { issues: 1, boards: 1 }, missing_definition: 1, topology: { duration_d: 100 } } }
it 'returns the payload keys that have a metric definition' do
expect(described_class.new(payload).key_paths).to match_array(['counts.issues', 'counts.boards', 'topology'])
end
end
context 'with array metric' do
let(:payload) { { counts: { issues: 1, boards: 1 }, settings: { collected_data_categories: ['standard'] }, topology: { duration_d: 100 } } }
it 'returns the payload keys that have a metric definition' do
expect(described_class.new(payload).key_paths).to match_array(['counts.issues', 'counts.boards', 'topology', 'settings.collected_data_categories'])
end
end
context 'missing_instrumented_metrics_key_paths' do
let(:payload) do
{
counts: { issues: 1, boards: 1 },
topology: { duration_d: 100 },
redis_hll_counters: { search: { i_search_total_monthly: 1 } }
}
end
let(:metrics_definitions) do
[
double(:issues, key_path: 'counts.issues'),
double(:topology, key_path: 'topology'),
double(:i_search_total_monthly, key_path: 'redis_hll_counters.search.i_search_total_monthly'),
double(:collected_data_categories, key_path: 'settings.collected_data_categories')
]
end
before do
allow(::Gitlab::Usage::MetricDefinition).to receive(:with_instrumentation_class).and_return(metrics_definitions)
end
it 'returns the missing keys' do
expect(described_class.new(payload).missing_instrumented_metrics_key_paths).to match_array(['settings.collected_data_categories'])
end
end
end
...@@ -3,19 +3,44 @@ ...@@ -3,19 +3,44 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Gitlab::Usage::ServicePingReport, :use_clean_rails_memory_store_caching do RSpec.describe Gitlab::Usage::ServicePingReport, :use_clean_rails_memory_store_caching do
let(:usage_data) { { uuid: "1111" } } let(:usage_data) { { uuid: "1111", counts: { issue: 0 } } }
context 'for output: :all_metrics_values' do context 'when feature merge_service_ping_instrumented_metrics enabled' do
it 'generates the service ping' do before do
expect(Gitlab::UsageData).to receive(:data) stub_feature_flags(merge_service_ping_instrumented_metrics: true)
allow_next_instance_of(Gitlab::Usage::ServicePing::PayloadKeysProcessor) do |instance|
allow(instance).to receive(:missing_key_paths).and_return([])
end
described_class.for(output: :all_metrics_values) allow_next_instance_of(Gitlab::Usage::ServicePing::InstrumentedPayload) do |instance|
allow(instance).to receive(:build).and_return({})
end
end
context 'all_metrics_values' do
it 'generates the service ping when there are no missing values' do
expect(Gitlab::UsageData).to receive(:data).and_return(usage_data)
expect(described_class.for(output: :all_metrics_values)).to eq({ uuid: "1111", counts: { issue: 0 } })
end
it 'generates the service ping with the missing values' do
expect_next_instance_of(Gitlab::Usage::ServicePing::PayloadKeysProcessor, usage_data) do |instance|
expect(instance).to receive(:missing_instrumented_metrics_key_paths).and_return(['counts.boards'])
end
expect_next_instance_of(Gitlab::Usage::ServicePing::InstrumentedPayload, ['counts.boards'], :with_value) do |instance|
expect(instance).to receive(:build).and_return({ counts: { boards: 1 } })
end
expect(Gitlab::UsageData).to receive(:data).and_return(usage_data)
expect(described_class.for(output: :all_metrics_values)).to eq({ uuid: "1111", counts: { issue: 0, boards: 1 } })
end end
end end
context 'for output: :metrics_queries' do context 'for output: :metrics_queries' do
it 'generates the service ping' do it 'generates the service ping' do
expect(Gitlab::UsageDataQueries).to receive(:data) expect(Gitlab::UsageData).to receive(:data).and_return(usage_data)
described_class.for(output: :metrics_queries) described_class.for(output: :metrics_queries)
end end
...@@ -23,7 +48,7 @@ RSpec.describe Gitlab::Usage::ServicePingReport, :use_clean_rails_memory_store_c ...@@ -23,7 +48,7 @@ RSpec.describe Gitlab::Usage::ServicePingReport, :use_clean_rails_memory_store_c
context 'for output: :non_sql_metrics_values' do context 'for output: :non_sql_metrics_values' do
it 'generates the service ping' do it 'generates the service ping' do
expect(Gitlab::UsageDataNonSqlMetrics).to receive(:data) expect(Gitlab::UsageData).to receive(:data).and_return(usage_data)
described_class.for(output: :non_sql_metrics_values) described_class.for(output: :non_sql_metrics_values)
end end
...@@ -66,4 +91,34 @@ RSpec.describe Gitlab::Usage::ServicePingReport, :use_clean_rails_memory_store_c ...@@ -66,4 +91,34 @@ RSpec.describe Gitlab::Usage::ServicePingReport, :use_clean_rails_memory_store_c
end end
end end
end end
end
context 'when feature merge_service_ping_instrumented_metrics disabled' do
before do
stub_feature_flags(merge_service_ping_instrumented_metrics: false)
end
context 'all_metrics_values' do
it 'generates the service ping when there are no missing values' do
expect(Gitlab::UsageData).to receive(:data).and_return(usage_data)
expect(described_class.for(output: :all_metrics_values)).to eq({ uuid: "1111", counts: { issue: 0 } })
end
end
context 'for output: :metrics_queries' do
it 'generates the service ping' do
expect(Gitlab::UsageData).to receive(:data).and_return(usage_data)
described_class.for(output: :metrics_queries)
end
end
context 'for output: :non_sql_metrics_values' do
it 'generates the service ping' do
expect(Gitlab::UsageData).to receive(:data).and_return(usage_data)
described_class.for(output: :non_sql_metrics_values)
end
end
end
end end
...@@ -507,6 +507,8 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do ...@@ -507,6 +507,8 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
end end
it 'gathers usage counts', :aggregate_failures do it 'gathers usage counts', :aggregate_failures do
stub_feature_flags(merge_service_ping_instrumented_metrics: false)
count_data = subject[:counts] count_data = subject[:counts]
expect(count_data[:boards]).to eq(1) expect(count_data[:boards]).to eq(1)
expect(count_data[:projects]).to eq(4) expect(count_data[:projects]).to eq(4)
......
...@@ -4,6 +4,10 @@ require 'spec_helper' ...@@ -4,6 +4,10 @@ require 'spec_helper'
RSpec.describe ServicePing::BuildPayloadService do RSpec.describe ServicePing::BuildPayloadService do
describe '#execute', :without_license do describe '#execute', :without_license do
before do
stub_feature_flags(merge_service_ping_instrumented_metrics: false)
end
subject(:service_ping_payload) { described_class.new.execute } subject(:service_ping_payload) { described_class.new.execute }
include_context 'stubbed service ping metrics definitions' do include_context 'stubbed service ping metrics definitions' do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment