Commit bd59d872 authored by Alina Mihaila's avatar Alina Mihaila Committed by Alper Akgun

Add base framework and tooling for Usage Data metrics instrumentation

parent 4c0d2649
......@@ -11,6 +11,7 @@ milestone: "9.1"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/1521
time_frame: none
data_source: database
instrumentation_class: 'Gitlab::Usage::Metrics::Instrumentations::UuidMetric'
distribution:
- ee
- ce
......
......@@ -51,6 +51,10 @@
"type": "string",
"enum": ["database", "redis", "redis_hll", "prometheus", "ruby"]
},
"instrumentation_class": {
"type": "string",
"pattern": "^(Gitlab::Usage::Metrics::Instrumentations::)(([A-Z][a-z]+)+::)*(([A-Z][a-z]+)+)$"
},
"distribution": {
"type": "array",
"items": {
......
......@@ -65,6 +65,10 @@ module Gitlab
@definitions ||= load_all!
end
def all
@all ||= definitions.map { |_key_path, definition| definition }
end
def schemer
@schemer ||= ::JSONSchemer.schema(Pathname.new(METRIC_SCHEMA_PATH))
end
......
# frozen_string_literal: true
module Gitlab
module Usage
module Metrics
module Instrumentations
class BaseMetric
include Gitlab::Utils::UsageData
attr_reader :time_frame
def initialize(time_frame:)
@time_frame = time_frame
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Usage
module Metrics
module Instrumentations
class DatabaseMetric < BaseMetric
# Usage Example
#
# class CountUsersCreatingIssuesMetric < DatabaseMetric
# operation :distinct_count, column: :author_id
#
# relation do |database_time_constraints|
# ::Issue.where(database_time_constraints)
# end
# end
class << self
def start(&block)
@metric_start = block
end
def finish(&block)
@metric_finish = block
end
def relation(&block)
@metric_relation = block
end
def operation(symbol, column: nil)
@metric_operation = symbol
@column = column
end
attr_reader :metric_operation, :metric_relation, :metric_start, :metric_finish, :column
end
def value
method(self.class.metric_operation)
.call(relation,
self.class.column,
start: self.class.metric_start&.call,
finish: self.class.metric_finish&.call)
end
def relation
self.class.metric_relation.call.where(time_constraints)
end
private
def time_constraints
case time_frame
when '28d'
{ created_at: 30.days.ago..2.days.ago }
when 'all'
{}
when 'none'
nil
else
raise "Unknown time frame: #{time_frame} for DatabaseMetric"
end
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Usage
module Metrics
module Instrumentations
class GenericMetric < BaseMetric
# Usage example
#
# class UuidMetric < GenericMetric
# value do
# Gitlab::CurrentSettings.uuid
# end
# end
class << self
def value(&block)
@metric_value = block
end
attr_reader :metric_value
end
def value
alt_usage_data do
self.class.metric_value.call
end
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Usage
module Metrics
module Instrumentations
class RedisHLLMetric < BaseMetric
# Usage example
#
# class CountUsersVisitingAnalyticsValuestreamMetric < RedisHLLMetric
# event_names :g_analytics_valuestream
# end
class << self
def event_names(events = nil)
@mentric_events = events
end
attr_reader :metric_events
end
def value
redis_usage_data do
event_params = time_constraints.merge(event_names: self.class.metric_events)
Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(**event_params)
end
end
private
def time_constraints
case time_frame
when '28d'
{ start_date: 4.weeks.ago.to_date, end_date: Date.current }
when '7d'
{ start_date: 7.days.ago.to_date, end_date: Date.current }
else
raise "Unknown time frame: #{time_frame} for TimeConstraint"
end
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Usage
module Metrics
module Instrumentations
class UuidMetric < GenericMetric
value do
Gitlab::CurrentSettings.uuid
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Usage
module Metrics
class KeyPathProcessor
class << self
def process(key_path, value)
unflatten(key_path.split('.'), value)
end
private
def unflatten(keys, value)
loop do
value = { keys.pop.to_sym => value }
break if keys.blank?
end
value
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
class UsageDataMetrics
class << self
# Build the Usage Ping JSON payload from metrics YAML definitions which have instrumentation class set
def uncached_data
::Gitlab::Usage::MetricDefinition.all.map do |definition|
instrumentation_class = definition.attributes[:instrumentation_class]
if instrumentation_class.present?
metric_value = instrumentation_class.constantize.new(time_frame: definition.attributes[:time_frame]).value
metric_payload(definition.key_path, metric_value)
else
{}
end
end.reduce({}, :deep_merge)
end
private
def metric_payload(key_path, value)
::Gitlab::Usage::Metrics::KeyPathProcessor.process(key_path, value)
end
end
end
end
......@@ -29,5 +29,10 @@ namespace :gitlab do
items = Gitlab::Usage::MetricDefinition.definitions
Gitlab::Usage::Docs::Renderer.new(items).write
end
desc 'GitLab | UsageDataMetrics | Generate usage ping from metrics definition YAML files in JSON'
task generate_from_yaml: :environment do
puts Gitlab::Json.pretty_generate(Gitlab::UsageDataMetrics.uncached_data)
end
end
end
......@@ -31,6 +31,7 @@ CodeReuse/ActiveRecord:
- lib/gitlab/import_export/**/*.rb
- lib/gitlab/project_authorizations.rb
- lib/gitlab/sql/**/*.rb
- lib/gitlab/usage/metrics/instrumentations/**/*.rb
- lib/system_check/**/*.rb
- qa/**/*.rb
- rubocop/**/*.rb
......
......@@ -25,6 +25,12 @@ RSpec.describe Gitlab::Usage::MetricDefinition do
let(:definition) { described_class.new(path, attributes) }
let(:yaml_content) { attributes.deep_stringify_keys.to_yaml }
around do |example|
described_class.instance_variable_set(:@definitions, nil)
example.run
described_class.instance_variable_set(:@definitions, nil)
end
def write_metric(metric, path, content)
path = File.join(metric, path)
dir = File.dirname(path)
......@@ -62,6 +68,9 @@ RSpec.describe Gitlab::Usage::MetricDefinition do
:distribution | 'test'
:tier | %w(test ee)
:name | 'count_<adjective_describing>_boards'
:instrumentation_class | 'Gitlab::Usage::Metrics::Instrumentations::Metric_Class'
:instrumentation_class | 'Gitlab::Usage::Metrics::MetricClass'
end
with_them do
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Usage::Metrics::Instrumentations::UuidMetric do
it_behaves_like 'a correct instrumented metric value', { time_frame: 'none' }, Gitlab::CurrentSettings.uuid
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Usage::Metrics::KeyPathProcessor do
describe '#unflatten_default_path' do
using RSpec::Parameterized::TableSyntax
where(:key_path, :value, :expected_hash) do
'uuid' | nil | { uuid: nil }
'uuid' | '1111' | { uuid: '1111' }
'counts.issues' | nil | { counts: { issues: nil } }
'counts.issues' | 100 | { counts: { issues: 100 } }
'usage_activity_by_stage.verify.ci_builds' | 100 | { usage_activity_by_stage: { verify: { ci_builds: 100 } } }
end
with_them do
subject { described_class.process(key_path, value) }
it { is_expected.to eq(expected_hash) }
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::UsageDataMetrics do
describe '.uncached_data' do
subject { described_class.uncached_data }
around do |example|
described_class.instance_variable_set(:@definitions, nil)
example.run
described_class.instance_variable_set(:@definitions, nil)
end
before do
allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false)
end
context 'whith instrumentation_class' do
it 'includes top level keys' do
expect(subject).to include(:uuid)
end
end
end
end
# frozen_string_literal: true
RSpec.shared_examples 'a correct instrumented metric value' do |options, expected_value|
let(:time_frame) { options[:time_frame] }
before do
allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false)
end
it 'has correct value' do
expect(described_class.new(time_frame: time_frame).value).to eq(expected_value)
end
end
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