From aad6fcea77fa2a1397c5cc16fdfc364e66954f8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Wielich?= <mwielich@gitlab.com> Date: Thu, 21 Apr 2022 08:41:36 +0000 Subject: [PATCH] Add metric availability to instrumentation classes Make it possible to define availability on a per-metric-class basis --- .../service_ping/metrics_instrumentation.md | 66 +++++++++++++++++++ .../metrics/every_metric_definition_spec.rb | 1 + lib/gitlab/usage/metric.rb | 16 +++-- .../metrics/instrumentations/base_metric.rb | 16 +++++ spec/lib/gitlab/usage/metric_spec.rb | 27 ++++++++ .../instrumentations/database_metric_spec.rb | 27 ++++++++ .../instrumentations/redis_hll_metric_spec.rb | 24 +++++++ .../instrumentations/redis_metric_spec.rb | 24 +++++++ 8 files changed, 196 insertions(+), 5 deletions(-) diff --git a/doc/development/service_ping/metrics_instrumentation.md b/doc/development/service_ping/metrics_instrumentation.md index 3d56f3e777fcb..42f93d6ae24e6 100644 --- a/doc/development/service_ping/metrics_instrumentation.md +++ b/doc/development/service_ping/metrics_instrumentation.md @@ -40,6 +40,7 @@ We have built a domain-specific language (DSL) to define the metrics instrumenta - `start`: Specifies the start value of the batch counting, by default is `relation.minimum(:id)`. - `finish`: Specifies the end value of the batch counting, by default is `relation.maximum(:id)`. - `cache_start_and_finish_as`: Specifies the cache key for `start` and `finish` values and sets up caching them. Use this call when `start` and `finish` are expensive queries that should be reused between different metric calculations. +- `available?`: Specifies whether the metric should be reported. The default is `true`. [Example of a merge request that adds a database metric](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60022). @@ -123,6 +124,37 @@ options: counter_class: SourceCodeCounter ``` +### Availability-restrained Redis metrics + +If the Redis metric should only be available in the report under some conditions, then you must specify these conditions in a new class that is a child of the `RedisMetric` class. + +```ruby +# frozen_string_literal: true + +module Gitlab + module Usage + module Metrics + module Instrumentations + class MergeUsageCountRedisMetric < RedisMetric + available? { Feature.enabled?(:merge_usage_data_missing_key_paths) } + end + end + end + end +end +``` + +You must also use the class's name in the YAML setup. + +```yaml +time_frame: all +data_source: redis +instrumentation_class: 'MergeUsageCountRedisMetric' +options: + event: pushes + counter_class: SourceCodeCounter +``` + ## Redis HyperLogLog metrics [Example of a merge request that adds a `RedisHLL` metric](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61685). @@ -138,8 +170,42 @@ options: - i_quickactions_approve ``` +### Availability-restrained Redis HyperLogLog metrics + +If the Redis HyperLogLog metric should only be available in the report under some conditions, then you must specify these conditions in a new class that is a child of the `RedisHLLMetric` class. + +```ruby +# frozen_string_literal: true + +module Gitlab + module Usage + module Metrics + module Instrumentations + class MergeUsageCountRedisHLLMetric < RedisHLLMetric + available? { Feature.enabled?(:merge_usage_data_missing_key_paths) } + end + end + end + end +end +``` + +You must also use the class's name in the YAML setup. + +```yaml +time_frame: 28d +data_source: redis_hll +instrumentation_class: 'MergeUsageCountRedisHLLMetric' +options: + events: + - i_quickactions_approve +``` + ## Generic metrics +- `value`: Specifies the value of the metric. +- `available?`: Specifies whether the metric should be reported. The default is `true`. + [Example of a merge request that adds a generic metric](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60256). ```ruby diff --git a/ee/spec/config/metrics/every_metric_definition_spec.rb b/ee/spec/config/metrics/every_metric_definition_spec.rb index dcffdbcbe2ced..655781d668f96 100644 --- a/ee/spec/config/metrics/every_metric_definition_spec.rb +++ b/ee/spec/config/metrics/every_metric_definition_spec.rb @@ -104,6 +104,7 @@ def assert_uses_all_nested_classes(parent_module) if constant.is_a? Class metric_class_instance = instance_double(constant) expect(constant).to receive(:new).at_least(:once).and_return(metric_class_instance) + allow(metric_class_instance).to receive(:available?).and_return(true) expect(metric_class_instance).to receive(:value).at_least(:once) elsif constant.is_a? Module assert_uses_all_nested_classes(constant) diff --git a/lib/gitlab/usage/metric.rb b/lib/gitlab/usage/metric.rb index 24e044c574002..cf48aa49938c3 100644 --- a/lib/gitlab/usage/metric.rb +++ b/lib/gitlab/usage/metric.rb @@ -18,19 +18,25 @@ def all end def with_value - unflatten_key_path(intrumentation_object.value) + with_availability(proc { instrumentation_object.value }) end def with_instrumentation - unflatten_key_path(intrumentation_object.instrumentation) + with_availability(proc { instrumentation_object.instrumentation }) end def with_suggested_name - unflatten_key_path(intrumentation_object.suggested_name) + with_availability(proc { instrumentation_object.suggested_name }) end private + def with_availability(value_proc) + return {} unless instrumentation_object.available? + + unflatten_key_path(value_proc.call) + end + def unflatten_key_path(value) ::Gitlab::Usage::Metrics::KeyPathProcessor.process(definition.key_path, value) end @@ -39,8 +45,8 @@ def instrumentation_class "Gitlab::Usage::Metrics::Instrumentations::#{definition.instrumentation_class}" end - def intrumentation_object - instrumentation_class.constantize.new( + def instrumentation_object + @instrumentation_object ||= instrumentation_class.constantize.new( time_frame: definition.time_frame, options: definition.attributes[:options] ) diff --git a/lib/gitlab/usage/metrics/instrumentations/base_metric.rb b/lib/gitlab/usage/metrics/instrumentations/base_metric.rb index a264f9484f3a8..f76ed1753b28f 100644 --- a/lib/gitlab/usage/metrics/instrumentations/base_metric.rb +++ b/lib/gitlab/usage/metrics/instrumentations/base_metric.rb @@ -11,6 +11,18 @@ class BaseMetric attr_reader :time_frame attr_reader :options + class << self + def available?(&block) + return @metric_available = block if block_given? + + return @metric_available.call if instance_variable_defined?('@metric_available') + + true + end + + attr_reader :metric_available + end + def initialize(time_frame:, options: {}) @time_frame = time_frame @options = options @@ -19,6 +31,10 @@ def initialize(time_frame:, options: {}) def instrumentation value end + + def available? + self.class.available? + end end end end diff --git a/spec/lib/gitlab/usage/metric_spec.rb b/spec/lib/gitlab/usage/metric_spec.rb index 19d2d3048eb7b..10ae94e746b43 100644 --- a/spec/lib/gitlab/usage/metric_spec.rb +++ b/spec/lib/gitlab/usage/metric_spec.rb @@ -51,4 +51,31 @@ expect(described_class.new(issue_count_metric_definiton).with_suggested_name).to eq({ counts: { issues: 'count_issues' } }) end end + + context 'unavailable metric' do + let(:instrumentation_class) { "UnavailableMetric" } + let(:issue_count_metric_definiton) do + double(:issue_count_metric_definiton, + attributes.merge({ attributes: attributes, instrumentation_class: instrumentation_class }) + ) + end + + before do + unavailable_metric_class = Class.new(Gitlab::Usage::Metrics::Instrumentations::CountIssuesMetric) do + def available? + false + end + end + + stub_const("Gitlab::Usage::Metrics::Instrumentations::#{instrumentation_class}", unavailable_metric_class) + end + + [:with_value, :with_instrumentation, :with_suggested_name].each do |method_name| + describe "##{method_name}" do + it 'returns an empty hash' do + expect(described_class.new(issue_count_metric_definiton).public_send(method_name)).to eq({}) + end + end + end + end end diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb index ea5ae1970dedf..cd7eb44c83d6c 100644 --- a/spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb +++ b/spec/lib/gitlab/usage/metrics/instrumentations/database_metric_spec.rb @@ -71,6 +71,33 @@ end end + context 'with availability defined' do + subject do + described_class.tap do |metric_class| + metric_class.relation { Issue } + metric_class.operation :count + metric_class.available? { false } + end.new(time_frame: 'all') + end + + it 'responds to #available? properly' do + expect(subject.available?).to eq(false) + end + end + + context 'with availability not defined' do + subject do + Class.new(described_class) do + relation { Issue } + operation :count + end.new(time_frame: 'all') + end + + it 'responds to #available? properly' do + expect(subject.available?).to eq(true) + end + end + context 'with cache_start_and_finish_as called' do subject do described_class.tap do |metric_class| diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/redis_hll_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/redis_hll_metric_spec.rb index 347a2c779cb2a..9730605153326 100644 --- a/spec/lib/gitlab/usage/metrics/instrumentations/redis_hll_metric_spec.rb +++ b/spec/lib/gitlab/usage/metrics/instrumentations/redis_hll_metric_spec.rb @@ -25,4 +25,28 @@ it 'raise exception if events options is not present' do expect { described_class.new(time_frame: '28d') }.to raise_error(ArgumentError) end + + describe 'children classes' do + let(:options) { { events: ['i_quickactions_approve'] } } + + context 'availability not defined' do + subject { Class.new(described_class).new(time_frame: nil, options: options) } + + it 'returns default availability' do + expect(subject.available?).to eq(true) + end + end + + context 'availability defined' do + subject do + Class.new(described_class) do + available? { false } + end.new(time_frame: nil, options: options) + end + + it 'returns defined availability' do + expect(subject.available?).to eq(false) + end + end + end end diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/redis_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/redis_metric_spec.rb index fb3bd1ba8341d..831f775ec9aaa 100644 --- a/spec/lib/gitlab/usage/metrics/instrumentations/redis_metric_spec.rb +++ b/spec/lib/gitlab/usage/metrics/instrumentations/redis_metric_spec.rb @@ -20,4 +20,28 @@ it 'raises an exception if counter_class option is not present' do expect { described_class.new(event: 'pushes') }.to raise_error(ArgumentError) end + + describe 'children classes' do + let(:options) { { event: 'pushes', counter_class: 'SourceCodeCounter' } } + + context 'availability not defined' do + subject { Class.new(described_class).new(time_frame: nil, options: options) } + + it 'returns default availability' do + expect(subject.available?).to eq(true) + end + end + + context 'availability defined' do + subject do + Class.new(described_class) do + available? { false } + end.new(time_frame: nil, options: options) + end + + it 'returns defined availability' do + expect(subject.available?).to eq(false) + end + end + end end -- GitLab