Files
gitlab-foss/lib/gitlab/usage/event_selection_rule.rb
2025-05-21 18:12:25 +00:00

143 lines
4.1 KiB
Ruby

# frozen_string_literal: true
# Represents the relationship between a event definition and a metric definition.
#
# Example usages:
# EventSelectionRule.new('pull_package', time_framed: true, filter: { label: 'rubygems' })
#
# The above rule represents a defines a metric that "counts weekly number of pull_package events with label rubygems"
#
# EventSelectionRule.new('pull_package', time_framed: false)
#
# The above rule represents a defines a metric that "counts the total number of pull_package events"
#
module Gitlab
module Usage
class EventSelectionRule
include Gitlab::Usage::TimeSeriesStorable
include Gitlab::Usage::TimeFrame
TOTAL_COUNTER_KEY_PREFIX = "event_counters"
SUM_KEY_PREFIX = "event_sums"
attr_reader :filter, :name, :unique_identifier_name, :operator
def initialize(name:, time_framed:, filter: {}, unique_identifier_name: nil, operator: nil)
@name = name
@time_framed = time_framed
@filter = filter || {}
@unique_identifier_name = unique_identifier_name
@operator = operator
end
def redis_key_for_date(date = Date.today)
redis_key(nil, date)
end
def redis_keys_for_time_frame(time_frame)
if time_frame == 'all'
[redis_key]
else
keys_for_aggregation(events: [path_part_of_redis_key], **time_constraint(time_frame))
end
end
def time_framed?
@time_framed
end
def total_counter?
unique_identifier_name.nil? && !sum?
end
def sum?
operator == 'sum(value)'
end
def unique_total?
unique_identifier_name && operator == 'total'
end
def matches?(additional_properties)
filter.all? do |property_name, value|
additional_properties[property_name] == value
end
end
# Implementing `==` to make sure that `a == b` is true if and only if `a` and `b` have equal properties
# Checks equality by comparing each attribute.
# @param [Object] Object to be compared
def ==(other)
other.is_a?(self.class) &&
name == other.name &&
time_framed? == other.time_framed? &&
filter == other.filter &&
unique_identifier_name == other.unique_identifier_name &&
operator == other.operator
end
alias_method :eql?, :==
# Calculates hash code according to all attributes.
# @return [Integer] Hash code
# Ensures 2 objects with the same attributes can't be keys
# in the same hash twice
def hash
[name, time_framed?, filter, unique_identifier_name, operator].hash
end
private
def redis_key(_event_name = nil, date = Date.today, _used_in_aggregate_metric = false)
return base_key unless time_framed?
apply_time_aggregation(base_key, date)
end
def base_key
@base_key ||= "{#{redis_slot}}_" + path_part_of_redis_key
end
def redis_slot
return SUM_KEY_PREFIX if sum?
total_counter? ? TOTAL_COUNTER_KEY_PREFIX : Gitlab::UsageDataCounters::HLLRedisCounter::REDIS_SLOT
end
def time_constraint(time_frame)
case time_frame
when '28d'
monthly_time_range
when '7d'
weekly_time_range
else
raise "Unknown time frame: #{time_frame}"
end
end
def path_part_of_redis_key
path = name
if operator.present?
# operator should be serialized and used in the path here
path = "#{path}-operator:#{operator}"
end
if filter.present?
sorted_filter_keys = filter.keys.sort
serialized_filter = sorted_filter_keys.map { |key| "#{key}:#{filter[key]}" }.join(',')
path = "#{path}-filter:[#{serialized_filter}]"
end
path = "#{path}-#{unique_identifier_name}" if unique_identifier_name.present?
key_overrides.fetch(path, path)
end
def key_overrides
YAML.safe_load(File.read(Gitlab::UsageDataCounters::HLLRedisCounter::KEY_OVERRIDES_PATH))
end
end
end
end