Module: Metrics::ClassMethods

Defined in:
lib/has_metrics/metrics.rb

Instance Method Summary collapse

Instance Method Details

#aggregate_metricsObject



92
93
94
# File 'lib/has_metrics/metrics.rb', line 92

def aggregate_metrics
  metrics.select{ |metric, options| options.has_key?(:aggregate) }
end

#collect_metrics(warmup) ⇒ Object



134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/has_metrics/metrics.rb', line 134

def collect_metrics(warmup)
  detected_aggregate_metrics = {}
  metrics.select{|m,o| o[:infer_aggregate] }.each do |metric_name, options|
    next if options[:aggregate]
    existing_logger = ActiveRecord::Base.logger
    sql_capturer = ActiveRecord::Base.logger = SqlCapturer.new(existing_logger)
    warmup.instance_exec(&options[:single])
    ActiveRecord::Base.logger = existing_logger

    unless sql_capturer.query.count != 1
      subquery = sql_capturer.query.first.gsub(warmup.id.to_s, "#{metrics_class.table_name}.id")
      unless subquery == %Q{SELECT "#{metrics_class.table_name}".* FROM "#{metrics_class.table_name}" WHERE "#{metrics_class.table_name}"."id" = #{metrics_class.table_name}.id LIMIT 1}
        update_sql = %Q{
          UPDATE #{metrics_class.table_name}
             SET #{metric_name} = (#{subquery}),
                 updated__#{metric_name}__at = '#{Time.current.to_s(:db)}';
          }
          detected_aggregate_metrics[metric_name] = update_sql
      end
    end
  end
  return detected_aggregate_metrics, (metrics.keys - aggregate_metrics.keys - detected_aggregate_metrics.keys)
end

#crank_aggregate_metrics!Object



190
191
192
193
194
195
# File 'lib/has_metrics/metrics.rb', line 190

def crank_aggregate_metrics!
  aggregate_metrics.each do |metric_name, options|
    ActiveRecord::Base.connection.execute options[:aggregate]
    metrics_class.update_all "updated__#{metric_name}__at" => Time.current
  end
end

#crank_detected_aggregate_metrics!(detected_aggregate_metrics) ⇒ Object



158
159
160
161
162
163
164
165
166
167
168
# File 'lib/has_metrics/metrics.rb', line 158

def crank_detected_aggregate_metrics!(detected_aggregate_metrics)
  bad_guesses = []
  detected_aggregate_metrics.each do |metric_name, update_sql|
    begin
      ActiveRecord::Base.connection.execute update_sql
    rescue
      bad_guesses << metric_name
    end
  end
  bad_guesses
end

#crank_singular_metrics!(args, singular_metrics) ⇒ Object



170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/has_metrics/metrics.rb', line 170

def crank_singular_metrics!(args, singular_metrics)
  unless ENV['RAILS_ENV'] == 'test'
    puts "Slow Metrics Found :: #{singular_metrics.count} \n"
    puts " --  --  --  -- \n"
    puts "Go implement their aggregate methods in #{self} to speed this up.\n"
    puts singular_metrics.join(', ')
    puts "\n ≈ ≈ ≈ ≈ ≈ ≈ ≈ "
  end

  find_in_batches do |batch|
    metrics_class.transaction do
      batch.each do |record|
        singular_metrics.each do |singular_metric|
          record.send(singular_metric, *args)
        end
      end
    end
  end
end

#define_single_method(name, options) ⇒ Object



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/has_metrics/metrics.rb', line 54

def define_single_method(name, options)
  define_method name do |*args|
    previous_result = metrics.attributes[name.to_s] unless options[:every] == :always
    datestamp_column = "updated__#{name}__at"
    force = [:force, true].include?(args[0])
    if !force && (options.has_key?(:aggregate) || options[:infer_aggregate])
      previous_result
    else
      result = instance_exec(&options[:single])
      result = nil if result.is_a?(Float) && !result.finite?
      begin
        metrics.send "#{name}=", result
        metrics.send "#{datestamp_column}=", Time.current
      rescue NoMethodError => e
        raise e unless e.name == "#{name}=".to_sym
        # This happens if the migrations haven't run yet for this metric. We should still calculate & return the metric.
      end
      unless changed?
        metrics.save
      end
      result
    end
  end

  if defined?(::NewRelic)
    ::NewRelic::Agent.logger.debug "Installing instrumentation for #{name}"
    add_transaction_tracer name, category: :task
  end
end

#has_aggregate_metric(name, sql) ⇒ Object



50
51
52
# File 'lib/has_metrics/metrics.rb', line 50

def has_aggregate_metric(name, sql)
  has_metric name, {}.merge(aggregate: sql)
end

#has_metric(name, options = {}, &block) ⇒ Object



39
40
41
42
43
44
45
46
47
48
# File 'lib/has_metrics/metrics.rb', line 39

def has_metric name, options={}, &block
  options.merge!(single: block) if block
  define_single_method(name, options) if options[:single]

  metrics[name.to_sym] ||= {}
  metrics[name.to_sym].merge!(options)
  metrics_class.class_eval do
    attr_accessible(name, "updated__#{name}__at")
  end
end

#metricsObject



84
85
86
# File 'lib/has_metrics/metrics.rb', line 84

def metrics
  @metrics ||= {}
end

#metrics_classObject

CLASS METHODS ADDED



35
36
37
# File 'lib/has_metrics/metrics.rb', line 35

def metrics_class
  @metrics_class
end

#metrics_column_type(column) ⇒ Object



96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/has_metrics/metrics.rb', line 96

def metrics_column_type(column)
  case
  when (metric = metrics.select { |metric, options| metric == column.to_sym && options[:type] }).any?
    metric.values.first[:type]
  when (column.to_s =~ /^by_(.+)$/) && respond_to?(:segment_categories) && segment_categories.include?($1.to_sym) # TODO: carve out segementation functionality into this gem
    :string
  when (column.to_s =~ /_at$/)
    :datetime
  else
    :integer
  end
end

#single_only_metricsObject



88
89
90
# File 'lib/has_metrics/metrics.rb', line 88

def single_only_metrics
  metrics.select{ |metric, options| !options.has_key?(:aggregate) }
end

#update_all_metrics!(*args) ⇒ Object



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/has_metrics/metrics.rb', line 109

def update_all_metrics!(*args)
  metrics_class.migrate!

  return unless warmup = first

  where("not exists(select id from #{metrics_class.table_name} where #{metrics_class.table_name}.id = #{table_name}.id)").each do |resource|
    resource.metrics.save!
  end

  detected_aggregate_metrics, singular_metrics = collect_metrics(warmup)

  bad_guesses = crank_detected_aggregate_metrics!(detected_aggregate_metrics)
  
  puts "#{bad_guesses.count} bad guesses found, scheduling as singular metrics." if bad_guesses.any?
  singular_metrics = singular_metrics | bad_guesses

  if singular_metrics.any?
    crank_singular_metrics!(args, singular_metrics) 
  end

  crank_aggregate_metrics!

  metrics
end