Class: MovingCount
- Inherits:
-
ActiveRecord::Base
- Object
- ActiveRecord::Base
- MovingCount
- Defined in:
- lib/counter/moving_count.rb
Overview
Capture counts, then aggregate them across time. Think of this as an rrdtool for counts.
Setup
Inherit from this class
class PageView < MovingCount
end
Then drop the following into a migration:
create_table :page_views, :force => true do |t|
t.string :category, :null => false
t.integer :count, :null => false, :default => 0
t.datetime :sample_time, :null => false
end
add_index :page_views, :category
Two optional class-level settings:
-
set_history_to_keep, depth of history to keep in seconds - defaults to 1 hour of history
-
set_sample_interval, minimum distance between samples in seconds (enforced at record time) - defaults to 1 minute
Recording counts
PageView.record_counts(Time.at(1280420630)) do |c|
c.increment('key1')
c.increment('key1')
c.increment('key2')
end
records observed counts for Thu Jul 29 12:23:50. You can omit the param to record counts at Time.now.
A runtime error will be raised if the sample is too close to the previous (for example, sample_interval is 60s and you tried to record a sample at 50s). This prevents accidental duping of counts on restarts, etc.
Checking totals
PageView.totals
returns a [key,count] array of all keys ordered by count. An optional limit
param restricts results to the top n.
Class Method Summary collapse
-
.grand_total(opts = {}) ⇒ Object
Returns single sum across all categories, limited by options.
- .history_to_keep ⇒ Object
-
.record_counts(timestamp = Time.now) {|c| ... } ⇒ Object
Records counts at the specified timestamp.
- .sample_interval ⇒ Object
-
.set_history_to_keep(time_range) ⇒ Object
Sets the depth of history to keep, in seconds.
-
.set_sample_interval(interval) ⇒ Object
Sets the minimum distance between recorded samples, in seconds.
-
.totals(opts = {}) ⇒ Object
Returns totals grouped by category across entire history.
Class Method Details
.grand_total(opts = {}) ⇒ Object
Returns single sum across all categories, limited by options. Use this to get, say, total across all categories matching “myhost…” Optional filters:
* <tt>:window</tt> limits totaled to samples to those in the past <em>n</em> seconds (can of course specify as 1.hour with ActiveSupport)
* <tt>:category_like</tt> run a LIKE match against categories before totaling, useful for limiting scope of totals. '%' wildcards are allowed.
95 96 97 98 99 100 101 102 103 104 105 106 107 108 |
# File 'lib/counter/moving_count.rb', line 95 def self.grand_total opts={} latest_sample = self.maximum(:sample_time) return 0 if latest_sample.nil? # don't try to run counts against empty db q = "SELECT SUM(count) FROM #{self.table_name}" where = [] where << self.sanitize_sql(['category LIKE ?', opts[:category_like]]) if opts[:category_like] where << self.sanitize_sql(['sample_time > ?', latest_sample - opts[:window]]) if opts[:window] q += " WHERE #{where.join(' AND ')}" unless where.empty? self.connection.select_value(q).to_i end |
.history_to_keep ⇒ Object
52 53 54 |
# File 'lib/counter/moving_count.rb', line 52 def self.history_to_keep @history_to_keep || 1.hour end |
.record_counts(timestamp = Time.now) {|c| ... } ⇒ Object
Records counts at the specified timestamp. If timestamp omitted, counts are recorded at Time.now. Yields a Counter instance, all standard methods apply. Raises an exception if the timestamp would fall too soon under the sample_interval
.
69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
# File 'lib/counter/moving_count.rb', line 69 def self.record_counts =Time.now, &block = Time.at() if .is_a?(Integer) c = Counter.new yield(c) self.transaction do check_sample_valid() unless c.counts.empty? q = "INSERT INTO #{self.table_name} (sample_time, category, count) VALUES " c.counts.each { |key, count| q += self.sanitize_sql(['(?, ?, ?),', , key, count]) } q.chop! self.connection.execute(q) end self.delete_all(['sample_time < ?', ( - history_to_keep)]) end true end |
.sample_interval ⇒ Object
62 63 64 |
# File 'lib/counter/moving_count.rb', line 62 def self.sample_interval @sample_interval || 1.minute end |
.set_history_to_keep(time_range) ⇒ Object
Sets the depth of history to keep, in seconds. Default is 1 hour.
48 49 50 |
# File 'lib/counter/moving_count.rb', line 48 def self.set_history_to_keep time_range @history_to_keep = time_range end |
.set_sample_interval(interval) ⇒ Object
Sets the minimum distance between recorded samples, in seconds. Default is 1 minute.
58 59 60 |
# File 'lib/counter/moving_count.rb', line 58 def self.set_sample_interval interval @sample_interval = interval end |
.totals(opts = {}) ⇒ Object
Returns totals grouped by category across entire history. Optional filters can be used to filter totals:
* <tt>:limit</tt> limits results to top <em>n</em>
* <tt>:window</tt> limits totaled to samples to those in the past <em>n</em> seconds (can of course specify as 1.hour with ActiveSupport)
* <tt>:category_like</tt> run a LIKE match against categories before totaling, useful for limiting scope of totals. '%' wildcards are allowed.
115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 |
# File 'lib/counter/moving_count.rb', line 115 def self.totals opts={} latest_sample = self.maximum(:sample_time) return [] if latest_sample.nil? # don't try to run counts against empty db q = "SELECT category, SUM(count) AS cnt FROM #{self.table_name}" where = [] where << self.sanitize_sql(['category LIKE ?', opts[:category_like]]) if opts[:category_like] where << self.sanitize_sql(['sample_time > ?', latest_sample - opts[:window]]) if opts[:window] q += " WHERE #{where.join(' AND ')}" unless where.empty? q += ' GROUP BY category' q += ' ORDER BY cnt DESC' q += " LIMIT #{opts[:limit]}" if opts[:limit] values = self.connection.select_rows(q) values.map { |v| v[1] = v[1].to_i } return values end |