debian-mirror-gitlab/lib/gitlab/usage_data_counters/hll_redis_counter.rb

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

201 lines
7.6 KiB
Ruby
Raw Normal View History

2020-10-24 23:57:45 +05:30
# frozen_string_literal: true
module Gitlab
module UsageDataCounters
module HLLRedisCounter
DEFAULT_WEEKLY_KEY_EXPIRY_LENGTH = 6.weeks
DEFAULT_DAILY_KEY_EXPIRY_LENGTH = 29.days
2023-05-27 22:25:52 +05:30
REDIS_SLOT = 'hll_counters'
2021-01-29 00:20:46 +05:30
EventError = Class.new(StandardError)
UnknownEvent = Class.new(EventError)
UnknownAggregation = Class.new(EventError)
AggregationMismatch = Class.new(EventError)
InvalidContext = Class.new(EventError)
KNOWN_EVENTS_PATH = File.expand_path('known_events/*.yml', __dir__)
2020-10-24 23:57:45 +05:30
ALLOWED_AGGREGATIONS = %i(daily weekly).freeze
# Track event on entity_id
# Increment a Redis HLL counter for unique event_name and entity_id
#
2021-01-29 00:20:46 +05:30
# All events should be added to known_events yml files lib/gitlab/usage_data_counters/known_events/
2020-10-24 23:57:45 +05:30
#
# Event example:
#
# - name: g_compliance_dashboard # Unique event name
# aggregation: daily # Aggregation level, keys are stored daily or weekly
#
# Usage:
#
2021-03-08 18:12:59 +05:30
# * Track event: Gitlab::UsageDataCounters::HLLRedisCounter.track_event('g_compliance_dashboard', values: user_id)
2020-10-24 23:57:45 +05:30
# * Get unique counts per user: Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(event_names: 'g_compliance_dashboard', start_date: 28.days.ago, end_date: Date.current)
class << self
2020-11-24 15:15:51 +05:30
include Gitlab::Utils::UsageData
2021-09-04 01:27:46 +05:30
include Gitlab::Usage::TimeFrame
2020-11-24 15:15:51 +05:30
2021-03-08 18:12:59 +05:30
# Track unique events
#
# event_name - The event name.
# values - One or multiple values counted.
# time - Time of the action, set to Time.current.
def track_event(event_name, values:, time: Time.current)
track(values, event_name, time: time)
2020-10-24 23:57:45 +05:30
end
2021-03-08 18:12:59 +05:30
# Track unique events
#
# event_name - The event name.
# values - One or multiple values counted.
# context - Event context, plan level tracking.
# time - Time of the action, set to Time.current.
def track_event_in_context(event_name, values:, context:, time: Time.zone.now)
2021-01-29 00:20:46 +05:30
return if context.blank?
return unless context.in?(valid_context_list)
2020-10-24 23:57:45 +05:30
2021-03-08 18:12:59 +05:30
track(values, event_name, context: context, time: time)
2021-01-29 00:20:46 +05:30
end
2020-10-24 23:57:45 +05:30
2022-06-21 17:19:12 +05:30
# Count unique events for a given time range.
#
# event_names - The list of the events to count.
# start_date - The start date of the time range.
# end_date - The end date of the time range.
# context - Event context, plan level tracking. Available if set when tracking.
2021-01-29 00:20:46 +05:30
def unique_events(event_names:, start_date:, end_date:, context: '')
count_unique_events(event_names: event_names, start_date: start_date, end_date: end_date, context: context) do |events|
raise AggregationMismatch, events unless events_same_aggregation?(events)
raise InvalidContext if context.present? && !context.in?(valid_context_list)
end
2020-11-24 15:15:51 +05:30
end
2021-03-11 19:13:27 +05:30
def known_event?(event_name)
event_for(event_name).present?
2021-01-29 00:20:46 +05:30
end
def known_events
@known_events ||= load_events(KNOWN_EVENTS_PATH)
end
2021-03-11 19:13:27 +05:30
def calculate_events_union(event_names:, start_date:, end_date:)
count_unique_events(event_names: event_names, start_date: start_date, end_date: end_date) do |events|
raise AggregationMismatch, events unless events_same_aggregation?(events)
end
2021-01-29 00:20:46 +05:30
end
2020-10-24 23:57:45 +05:30
private
2021-03-08 18:12:59 +05:30
def track(values, event_name, context: '', time: Time.zone.now)
2021-10-27 15:23:28 +05:30
return unless ::ServicePing::ServicePingSettings.enabled?
2021-01-29 00:20:46 +05:30
event = event_for(event_name)
2021-04-17 20:07:23 +05:30
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(UnknownEvent.new("Unknown event #{event_name}")) unless event.present?
2021-01-29 00:20:46 +05:30
2023-03-04 22:38:38 +05:30
return if event.blank?
2023-05-27 22:25:52 +05:30
return unless Feature.enabled?(:redis_hll_tracking, type: :ops)
2021-03-11 19:13:27 +05:30
2021-03-08 18:12:59 +05:30
Gitlab::Redis::HLL.add(key: redis_key(event, time, context), value: values, expiry: expiry(event))
2021-06-08 01:23:25 +05:30
rescue StandardError => e
2021-04-17 20:07:23 +05:30
# Ignore any exceptions unless is dev or test env
# The application flow should not be blocked by erros in tracking
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e)
2021-01-29 00:20:46 +05:30
end
2021-03-08 18:12:59 +05:30
# The array of valid context on which we allow tracking
2021-01-29 00:20:46 +05:30
def valid_context_list
Plan.all_plans
end
def count_unique_events(event_names:, start_date:, end_date:, context: '')
events = events_for(Array(event_names).map(&:to_s))
yield events if block_given?
aggregation = events.first[:aggregation]
keys = keys_for_aggregation(aggregation, events: events, start_date: start_date, end_date: end_date, context: context)
2021-04-29 21:17:54 +05:30
return FALLBACK unless keys.any?
2021-01-29 00:20:46 +05:30
redis_usage_data { Gitlab::Redis::HLL.count(keys: keys) }
end
def keys_for_aggregation(aggregation, events:, start_date:, end_date:, context: '')
2020-10-24 23:57:45 +05:30
if aggregation.to_sym == :daily
2021-01-29 00:20:46 +05:30
daily_redis_keys(events: events, start_date: start_date, end_date: end_date, context: context)
2020-10-24 23:57:45 +05:30
else
2021-01-29 00:20:46 +05:30
weekly_redis_keys(events: events, start_date: start_date, end_date: end_date, context: context)
2020-10-24 23:57:45 +05:30
end
end
2021-01-29 00:20:46 +05:30
def load_events(wildcard)
Dir[wildcard].each_with_object([]) do |path, events|
events.push(*load_yaml_from_path(path))
end
end
def load_yaml_from_path(path)
YAML.safe_load(File.read(path))&.map(&:with_indifferent_access)
2020-10-24 23:57:45 +05:30
end
def known_events_names
known_events.map { |event| event[:name] }
end
def events_same_aggregation?(events)
aggregation = events.first[:aggregation]
events.all? { |event| event[:aggregation] == aggregation }
end
def expiry(event)
event[:aggregation].to_sym == :daily ? DEFAULT_DAILY_KEY_EXPIRY_LENGTH : DEFAULT_WEEKLY_KEY_EXPIRY_LENGTH
end
def event_for(event_name)
2021-01-29 00:20:46 +05:30
known_events.find { |event| event[:name] == event_name.to_s }
2020-10-24 23:57:45 +05:30
end
def events_for(event_names)
known_events.select { |event| event_names.include?(event[:name]) }
end
# Compose the key in order to store events daily or weekly
2021-01-29 00:20:46 +05:30
def redis_key(event, time, context = '')
2021-06-08 01:23:25 +05:30
raise UnknownEvent, "Unknown event #{event[:name]}" unless known_events_names.include?(event[:name].to_s)
raise UnknownAggregation, "Use :daily or :weekly aggregation" unless ALLOWED_AGGREGATIONS.include?(event[:aggregation].to_sym)
2020-10-24 23:57:45 +05:30
2023-05-27 22:25:52 +05:30
key = "{#{REDIS_SLOT}}_#{event[:name]}"
2021-01-29 00:20:46 +05:30
key = apply_time_aggregation(key, time, event)
key = "#{context}_#{key}" if context.present?
key
end
def apply_time_aggregation(key, time, event)
2020-10-24 23:57:45 +05:30
if event[:aggregation].to_sym == :daily
year_day = time.strftime('%G-%j')
"#{year_day}-#{key}"
else
year_week = time.strftime('%G-%V')
"#{key}-#{year_week}"
end
end
2021-01-29 00:20:46 +05:30
def daily_redis_keys(events:, start_date:, end_date:, context: '')
2020-10-24 23:57:45 +05:30
(start_date.to_date..end_date.to_date).map do |date|
2021-01-29 00:20:46 +05:30
events.map { |event| redis_key(event, date, context) }
2020-10-24 23:57:45 +05:30
end.flatten
end
2021-01-29 00:20:46 +05:30
def weekly_redis_keys(events:, start_date:, end_date:, context: '')
2021-01-08 16:13:35 +05:30
end_date = end_date.end_of_week - 1.week
(start_date.to_date..end_date.to_date).map do |date|
2021-01-29 00:20:46 +05:30
events.map { |event| redis_key(event, date, context) }
2021-01-08 16:13:35 +05:30
end.flatten.uniq
2020-10-24 23:57:45 +05:30
end
end
end
end
end
2021-01-29 00:20:46 +05:30
2021-06-08 01:23:25 +05:30
Gitlab::UsageDataCounters::HLLRedisCounter.prepend_mod_with('Gitlab::UsageDataCounters::HLLRedisCounter')