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

Add basic metric yml defintion:

  - Add examples for metric definition
  - Use json schema validator
  - Use method_missing for defining methods for attributes
parent 8467c199
name: deployments
description: Total deployments count for recent 28 days
value_type: integer
stage: release
status: data_available
default_generation: generation_1
full_path:
generation_1: counts_monthy.deployments
milestone: 13.2
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/35493
group: 'group::ops release'
time_frame: 28d
data_source: database
distribution: [ee, ce]
tier: ['free', 'starter', 'premium', 'ultimate', 'bronze', 'silver', 'gold']
name: g_project_management_issue_title_changed_weekly
description: Distinct users count that changed issue title in a group for last recent week
value_type: integer
product_category: issue_tracking
stage: plan
status: data_available
default_generation: generation_1
full_path:
generation_1: redis_hll_counters.issues_edit.g_project_management_issue_title_changed_weekly
milestone: 13.6
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/issues/229918
group: 'group::project management'
time_frame: 7d
data_source: redis_hll
distribution: [ee, ce]
tier: ['free', 'starter', 'premium', 'ultimate', 'bronze', 'silver', 'gold']
name: deployments
description: Total deployments count
value_type: integer
stage: release
status: data_available
default_generation: generation_1
full_path:
generation_1: counts.deployments
milestone: 8.12
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/735
group: 'group::ops release'
time_frame: all
data_source: database
distribution: [ee, ce]
tier: ['free', 'starter', 'premium', 'ultimate', 'bronze', 'silver', 'gold']
name: recorded_at
description: When the Usage Ping computation was started
value_type: string
product_category: collection
stage: growth
status: data_available
default_generation: generation_1
full_path:
generation_1: recorded_at
milestone: 8.10
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/557
group: group::product analytics
time_frame: none
data_source: ruby
distribution: [ee, ce]
tier: ['free', 'starter', 'premium', 'ultimate', 'bronze', 'silver', 'gold']
name: uuid
description: GitLab instance unique identifier
value_type: string
product_category: collection
stage: growth
status: data_available
default_generation: generation_1
full_path:
generation_1: uuid
generation_2: license.uuid
milestone: 9.1
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/1521
group: group::product analytics
time_frame: none
data_source: database
distribution: [ee, ce]
tier: ['free', 'starter', 'premium', 'ultimate', 'bronze', 'silver', 'gold']
{
"type": "object",
"required": ["name", "description", "value_type", "status", "default_generation", "full_path", "group", "time_frame", "data_source", "distribution", "tier"],
"properties": {
"name": {
"type": "string"
},
"description": {
"type": "string"
},
"value_type": {
"type": "string",
"enum": ["integer", "string", "number", "boolean"]
},
"product_category": {
"type": ["string", "null"]
},
"stage": {
"type": ["string", "null"]
},
"status": {
"type": ["string"],
"enum": ["data_available", "planned", "in_progress", "implmented"]
},
"default_generation": {
"type": "string"
},
"full_path": {
"type": "object"
},
"milestone": {
"type": ["number", "null"]
},
"milestone_removed": {
"type": ["number", "null"]
},
"introduced_by_url": {
"type": ["string", "null"]
},
"group": {
"type": "string"
},
"time_frame": {
"type": "string",
"enum": ["7d", "28d", "all", "none"]
},
"data_source": {
"type": "string",
"enum": ["database", "redis", "redis_hll", "prometheus", "ruby"]
},
"distribution": {
"type": "array",
"items": {
"type": "string",
"enum": ["ee", "ce"]
}
},
"tier": {
"type": "array",
"items": {
"type": "string",
"enum": ["free", "starter", "premium", "ultimate", "bronze", "silver", "gold"]
}
}
}
}
name: adapter
description: This metric only returns a value of PostgreSQL in supported versions of GitLab. It could be removed from the usage ping. Historically MySQL was also supported.
value_type: string
product_category: collection
stage: growth
status: data_available
default_generation: generation_1
full_path:
generation_1: database.adapter
group: group::enablement distribution
time_frame: none
data_source: database
distribution: [ee, ce]
tier: ['free', 'starter', 'premium', 'ultimate', 'bronze', 'silver', 'gold']
# frozen_string_literal: true
module Gitlab
module Usage
class Metric
include ActiveModel::Model
InvalidMetricError = Class.new(RuntimeError)
attr_accessor :default_generation_path, :value
validates :default_generation_path, presence: true
def definition
self.class.definitions[default_generation_path]
end
def unflatten_default_path
unflatten(default_generation_path.split('.'), value)
end
class << self
def definitions
@definitions ||= Gitlab::Usage::MetricDefinition.definitions
end
def dictionary
definitions.map { |key, definition| definition.to_dictionary }
end
end
private
def unflatten(keys, value)
loop do
value = { keys.pop.to_sym => value }
break if keys.blank?
end
value
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Usage
class MetricDefinition
METRIC_SCHEMA_PATH = Rails.root.join('config', 'metrics', 'schema.json')
attr_reader :path
attr_reader :attributes
def initialize(path, opts = {})
@path = path
@attributes = opts
end
# The key is defined by default_generation and full_path
def key
full_path[default_generation.to_sym]
end
def to_h
attributes
end
def validate!
self.class.schemer.validate(attributes.stringify_keys).map do |error|
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(Metric::InvalidMetricError.new("#{error["details"] || error['data_pointer']} for `#{path}`"))
end
end
alias_method :to_dictionary, :to_h
class << self
def paths
@paths ||= [Rails.root.join('config', 'metrics', '**', '*.yml')]
end
def definitions
@definitions ||= load_all!
end
def schemer
@schemer ||= ::JSONSchemer.schema(Pathname.new(METRIC_SCHEMA_PATH))
end
private
def load_all!
paths.each_with_object({}) do |glob_path, definitions|
load_all_from_path!(definitions, glob_path)
end
end
def load_from_file(path)
definition = File.read(path)
definition = YAML.safe_load(definition)
definition.deep_symbolize_keys!
self.new(path, definition).tap(&:validate!)
rescue => e
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(Metric::InvalidMetricError.new(e.message))
end
def load_all_from_path!(definitions, glob_path)
Dir.glob(glob_path).each do |path|
definition = load_from_file(path)
if previous = definitions[definition.key]
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(Metric::InvalidMetricError.new("Metric '#{definition.key}' is already defined in '#{previous.path}'"))
end
definitions[definition.key] = definition
end
end
end
private
def method_missing(method, *args)
attributes[method] || super
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Usage::MetricDefinition do
let(:attributes) do
{
name: 'uuid',
description: 'GitLab instance unique identifier',
value_type: 'string',
product_category: 'collection',
stage: 'growth',
status: 'data_available',
default_generation: 'generation_1',
full_path: {
generation_1: 'uuid',
generation_2: 'license.uuid'
},
group: 'group::product analytics',
time_frame: 'none',
data_source: 'database',
distribution: %w(ee ce),
tier: %w(free starter premium ultimate bronze silver gold)
}
end
let(:path) { File.join('metrics', 'uuid.yml') }
let(:definition) { described_class.new(path, attributes) }
let(:yaml_content) { attributes.deep_stringify_keys.to_yaml }
it 'has all definitons valid' do
expect { described_class.definitions }.not_to raise_error(Gitlab::Usage::Metric::InvalidMetricError)
end
describe '#key' do
subject { definition.key }
it 'returns a symbol from name' do
is_expected.to eq('uuid')
end
end
describe '#validate' do
using RSpec::Parameterized::TableSyntax
where(:attribute, :value) do
:name | nil
:description | nil
:value_type | nil
:value_type | 'test'
:status | nil
:default_generation | nil
:group | nil
:time_frame | nil
:time_frame | '29d'
:data_source | 'other'
:data_source | nil
:distribution | nil
:distribution | 'test'
:tier | %w(test ee)
end
with_them do
before do
attributes[attribute] = value
end
it 'raise exception' do
expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).at_least(:once).with(instance_of(Gitlab::Usage::Metric::InvalidMetricError))
described_class.new(path, attributes).validate!
end
end
end
describe '.load_all!' do
let(:metric1) { Dir.mktmpdir('metric1') }
let(:metric2) { Dir.mktmpdir('metric2') }
let(:definitions) { {} }
before do
allow(described_class).to receive(:paths).and_return(
[
File.join(metric1, '**', '*.yml'),
File.join(metric2, '**', '*.yml')
]
)
end
subject { described_class.send(:load_all!) }
it 'has empty list when there are no definition files' do
is_expected.to be_empty
end
it 'has one metric when there is one file' do
write_metric(metric1, path, yaml_content)
is_expected.to be_one
end
it 'when the same meric is defined multiple times raises exception' do
write_metric(metric1, path, yaml_content)
write_metric(metric2, path, yaml_content)
expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).with(instance_of(Gitlab::Usage::Metric::InvalidMetricError))
subject
end
after do
FileUtils.rm_rf(metric1)
FileUtils.rm_rf(metric2)
end
def write_metric(metric, path, content)
path = File.join(metric, path)
dir = File.dirname(path)
FileUtils.mkdir_p(dir)
File.write(path, content)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Usage::Metric do
describe '#definition' do
it 'returns generation_1 metric definiton' do
expect(described_class.new(default_generation_path: 'uuid').definition).to be_an(Gitlab::Usage::MetricDefinition)
end
end
describe '#unflatten_default_path' do
using RSpec::Parameterized::TableSyntax
where(:default_generation_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.new(default_generation_path: default_generation_path, value: value).unflatten_default_path }
it { is_expected.to eq(expected_hash) }
end
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