von 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -19,7 +19,7 @@ Von.increment('downloads') # bumps the 'downloads' key 1 time
19
19
  Von.increment('downloads:app123') # bumps the 'downloads:app123' key 1 time AND the 'downloads' key 1 time
20
20
  ```
21
21
 
22
- ## Time Period Grouping and Limiting
22
+ ## Tracking Time Periods
23
23
 
24
24
  By default Von will only bump a "total" counter for the given key. This is great, but what makes Von really useful is that it can be configured to group certain keys by hour, day, week, month, and year. And you can set limits on how many of each you want to keep around. Here's how it works:
25
25
 
@@ -34,23 +34,64 @@ Von.configure do |config|
34
34
  end
35
35
  ```
36
36
 
37
- ### Incrementing Time Periods
37
+ ## Tracking the Current Time Period
38
38
 
39
- Once you've configured the keys you want to use Time Periods on, you just increment them like normal, Von handles the rest.
39
+ If just wanna track stats on the current minute/hour/day/week/etc, you can set that up pretty easily with Von:
40
+
41
+ ```ruby
42
+ Von.configure do |config|
43
+ # Track downloads stats for the current hour
44
+ config.counter 'downloads', :current => :hour
45
+
46
+ # Track page views for the current day and week
47
+ config.counter 'page-views', :current => [ :day, :week ]
48
+ end
49
+ ```
50
+
51
+ ## Tracking "Bests"
52
+
53
+ Time periods are pretty cool, but sometimes you wanna know when you did your best. You can track these with Von as well:
54
+
55
+ ```ruby
56
+ Von.configure do |config|
57
+ # Track the best day for downloads
58
+ config.counter 'downloads', :best => :day
59
+
60
+ # Track the best hour and week for page views
61
+ config.counter 'page-views', :best => [ :hour, :week ]
62
+ end
63
+ ```
64
+
65
+ ## Incrementing
66
+
67
+ Once you've configured the keys you don't have to do anything special, just increment the key, Von will handle this rest.
40
68
 
41
69
  ```ruby
42
70
  Von.increment('downloads')
43
71
  Von.increment('uploads')
72
+ Von.increment('page-views')
44
73
  ```
45
74
 
46
75
  ## Getting Stats
47
76
 
48
77
  ```ruby
49
78
  # get the total downloads (returns an Integer)
50
- Von.count('downloads') #=> 4
79
+ Von.count('downloads').total #=> 4
80
+
51
81
  # get the monthly counts (returns an Array of Hashes)
52
- Von.count('uploads', :monthly) #=> [ { '2012-03 => 3}, { '2013-04' => 1 }, { '2013-05' => 0 }]
82
+ Von.count('uploads').per(:month) #=> [ { :timestamp => '2012-03', :count => 3 }, { :timestamp => '2013-04', :count => 1 }, { :timestamp => '2013-05', :count => 0 }]
83
+
84
+ # get the download stats for the hour
85
+ Von.count('downloads').this(:hour) #=> 10
86
+ # or
87
+ Von.count('downloads').current(:hour) #=> 10
88
+
89
+ # get the page-views for today
90
+ Von.count('page-views').today #=> 78
91
+ Von.count('page-views').current(:day) #=> 78
53
92
 
93
+ # get the best day for downloads (returns a Hash)
94
+ Von.count('downloads').best(:day) #=> { :timestamp => '2012-03-01', :count => 10 }
54
95
  ```
55
96
 
56
97
  One nice thing to note, if you're counting a time period and there wasn't a value stored for the particular hour/day/week/etc, it'll be populated with a zero, this ensures that if you want 30 days of stats, you get 30 days of stats.
data/Rakefile CHANGED
@@ -4,7 +4,7 @@ require 'bundler/gem_tasks'
4
4
 
5
5
  Rake::TestTask.new do |t|
6
6
  t.libs << 'test'
7
- t.test_files = FileList['test/*_test.rb']
7
+ t.test_files = FileList['test/**/*_test.rb']
8
8
  end
9
9
 
10
10
  task :default => :test
data/lib/von.rb CHANGED
@@ -2,11 +2,18 @@ require 'redis'
2
2
  require 'active_support/time'
3
3
 
4
4
  require 'von/config'
5
- require 'von/counter'
6
5
  require 'von/period'
6
+ require 'von/counter'
7
+ require 'von/counters/commands'
8
+ require 'von/counters/total'
9
+ require 'von/counters/period'
10
+ require 'von/counters/best'
11
+ require 'von/counters/current'
7
12
  require 'von/version'
8
13
 
9
14
  module Von
15
+ PARENT_REGEX = /:?[^:]+\z/
16
+
10
17
  def self.connection
11
18
  @connection ||= config.redis
12
19
  end
@@ -20,16 +27,46 @@ module Von
20
27
  end
21
28
 
22
29
  def self.increment(field)
23
- Counter.increment(field)
30
+ parents = field.to_s.sub(PARENT_REGEX, '')
31
+ total = increment_counts_for(field)
32
+
33
+ until parents.empty? do
34
+ increment_counts_for(parents)
35
+ parents.sub!(PARENT_REGEX, '')
36
+ end
37
+
38
+ total
24
39
  rescue Redis::BaseError => e
25
40
  raise e if config.raise_connection_errors
26
41
  end
27
42
 
28
- def self.count(field, period = nil)
29
- Counter.count(field, period)
43
+ def self.increment_counts_for(field)
44
+ counter = Counters::Total.new(field)
45
+ total = counter.increment
46
+
47
+ if config.periods_defined_for_counter?(counter)
48
+ periods = config.periods[counter.field]
49
+ Counters::Period.new(counter.field, periods).increment
50
+ end
51
+
52
+ if config.bests_defined_for_counter?(counter)
53
+ periods = config.bests[counter.field]
54
+ Counters::Best.new(counter.field, periods).increment
55
+ end
56
+
57
+ if config.currents_defined_for_counter?(counter)
58
+ periods = config.currents[counter.field]
59
+ Counters::Current.new(counter.field, periods).increment
60
+ end
61
+
62
+ total
63
+ end
64
+
65
+ def self.count(field)
66
+ Counter.new(field)
30
67
  rescue Redis::BaseError => e
31
68
  raise e if config.raise_connection_errors
32
69
  end
33
70
 
34
71
  config.init!
35
- end
72
+ end
@@ -11,26 +11,33 @@ module Von
11
11
  attr_accessor :weekly_format
12
12
  attr_accessor :daily_format
13
13
  attr_accessor :hourly_format
14
+ attr_accessor :minutely_format
14
15
 
15
- attr_reader :periods
16
+ attr_reader :periods
17
+ attr_reader :bests
18
+ attr_reader :currents
16
19
 
17
20
  def init!
18
- @counter_options = {}
19
- @periods = {}
21
+ @periods = {}
22
+ @bests = {}
23
+ @currents = {}
24
+ @totals = {}
20
25
  # all keys are prefixed with this namespace
21
26
  self.namespace = 'von'
22
27
  # rescue Redis connection errors
23
28
  self.raise_connection_errors = false
24
29
  # 2013
25
- self.yearly_format = '%Y'
30
+ self.yearly_format = '%Y'
26
31
  # 2013-01
27
- self.monthly_format = '%Y-%m'
32
+ self.monthly_format = '%Y-%m'
28
33
  # 2013-01-02
29
- self.weekly_format = '%Y-%m-%d'
34
+ self.weekly_format = '%Y-%m-%d'
30
35
  # 2013-01-02
31
- self.daily_format = '%Y-%m-%d'
36
+ self.daily_format = '%Y-%m-%d'
32
37
  # 2013-01-02 12:00
33
- self.hourly_format = '%Y-%m-%d %H:00'
38
+ self.hourly_format = '%Y-%m-%d %H:00'
39
+ # 2013-01-02 12:05
40
+ self.minutely_format = '%Y-%m-%d %H:%M'
34
41
  end
35
42
 
36
43
  # Set the Redis connection to use
@@ -54,28 +61,51 @@ module Von
54
61
  # Configure options for given Counter. Configures length of given time period
55
62
  # and any other options for the Counter
56
63
  def counter(field, options = {})
57
- options.each do |key, value|
58
- if Period::AVAILABLE_PERIODS.include?(key)
59
- @periods[field.to_sym] ||= {}
60
- @periods[field.to_sym][key.to_sym] = Period.new(field, key, value)
61
- options.delete(key)
62
- end
64
+ field = field.to_sym
65
+ options.each do |option, value|
66
+ set_period(field, option, value) if Period.exists?(option)
67
+ set_best(field, value) if option == :best
68
+ set_current(field, value) if option == :current
63
69
  end
70
+ end
71
+
72
+ # Returns a True if a period is defined for the
73
+ # given Counter
74
+ # TODO: these should just take the key, will fix when renaming field
75
+ def periods_defined_for_counter?(counter)
76
+ @periods.has_key?(counter.field)
77
+ end
78
+
79
+ # Returns a True if a best is defined for the
80
+ # given counter
81
+ def bests_defined_for_counter?(counter)
82
+ @bests.has_key?(counter.field)
83
+ end
84
+
85
+ # Returns a True if a current is defined for the
86
+ # given counter
87
+ def currents_defined_for_counter?(counter)
88
+ @currents.has_key?(counter.field)
89
+ end
90
+
91
+ private
64
92
 
65
- @counter_options[field.to_sym] = options
93
+ def set_period(field, period, length)
94
+ @periods[field] ||= []
95
+ @periods[field] << Period.new(period, length)
66
96
  end
67
97
 
68
- # Returns a True if a Period is defined for the
69
- # given period identifier and the period has a length
70
- # False if not
71
- def period_defined_for?(key, period)
72
- @periods.has_key?(key) && @periods[key].has_key?(period)
98
+ def set_best(field, time_unit)
99
+ @bests[field] = [ time_unit ].flatten.map { |u|
100
+ Period.new(u)
101
+ }
73
102
  end
74
103
 
75
- # TODO: rename
76
- def counter_options(field)
77
- @counter_options[field.to_sym] ||= {}
104
+ def set_current(field, time_unit)
105
+ @currents[field] = [ time_unit ].flatten.map { |u|
106
+ Period.new(u)
107
+ }
78
108
  end
79
109
 
80
110
  end
81
- end
111
+ end
@@ -1,112 +1,70 @@
1
1
  module Von
2
2
  class Counter
3
- PARENT_REGEX = /:?[^:]+\z/
4
3
 
5
- # Initialize a new Counter
6
- #
7
- # field - counter field name
8
4
  def initialize(field)
9
5
  @field = field.to_sym
10
6
  end
11
7
 
12
- # Returns options specified in config for this Counter
13
- def options
14
- @options ||= Von.config.counter_options(@field)
8
+ def to_s
9
+ Counters::Total.new(@field).count.to_s
10
+ rescue Redis::BaseError => e
11
+ raise e if Von.config.raise_connection_errors
15
12
  end
16
13
 
17
- # Returns the Redis hash key used for storing counts for this Counter
18
- def hash_key
19
- @hash_key ||= "#{Von.config.namespace}:counters:#{@field}"
14
+ def to_i
15
+ Counters::Total.new(@field).count
16
+ rescue Redis::BaseError => e
17
+ raise e if Von.config.raise_connection_errors
20
18
  end
21
19
 
22
- # Increment the total count for this Counter
23
- # If the key has time periods specified, increment those.
24
- #
25
- # Returns the Integer total for the key
26
- def increment
27
- total = Von.connection.hincrby(hash_key, 'total', 1)
28
-
29
- increment_periods
30
-
31
- total
20
+ def total
21
+ Counters::Total.new(@field).count
22
+ rescue Redis::BaseError => e
23
+ raise e if Von.config.raise_connection_errors
32
24
  end
33
25
 
34
- # Increment periods associated with this key
35
- def increment_periods
36
- return unless Von.config.periods.has_key?(@field.to_sym)
26
+ def per(unit)
27
+ periods = Von.config.periods[@field]
37
28
 
38
- Von.config.periods[@field.to_sym].each do |key, period|
39
- Von.connection.hincrby(period.hash_key, period.field, 1)
40
- unless Von.connection.lrange(period.list_key, 0, -1).include?(period.field)
41
- Von.connection.rpush(period.list_key, period.field)
42
- end
43
-
44
- if Von.connection.llen(period.list_key) > period.length
45
- expired_counter = Von.connection.lpop(period.list_key)
46
- Von.connection.hdel(period.hash_key, expired_counter)
47
- end
29
+ if !Period.time_unit_exists?(unit)
30
+ raise ArgumentError, "`#{unit}' is an unknown time unit"
31
+ else
32
+ Counters::Period.new(@field, periods).count(unit)
48
33
  end
34
+ rescue Redis::BaseError => e
35
+ raise e if Von.config.raise_connection_errors
49
36
  end
50
37
 
51
- # Increment the Redis count for this Counter.
52
- # If the key has parents, increment them as well.
53
- #
54
- # Returns the Integer total for the key
55
- def self.increment(field)
56
- total = Counter.new(field).increment
57
- parents = field.sub(PARENT_REGEX, '')
38
+ def best(unit)
39
+ periods = Von.config.bests[@field]
58
40
 
59
- until parents.empty? do
60
- Counter.new(parents).increment
61
- parents.sub!(PARENT_REGEX, '')
41
+ if !Period.time_unit_exists?(unit)
42
+ raise ArgumentError, "`#{unit}' is an unknown time unit"
43
+ else
44
+ Counters::Best.new(@field, periods).count(unit)
62
45
  end
63
-
64
- total
65
- end
66
-
67
- # Count the "total" field for this Counter.
68
- #
69
- # Returns an Integer count
70
- def count
71
- Von.connection.hget(hash_key, 'total')
46
+ rescue Redis::BaseError => e
47
+ raise e if Von.config.raise_connection_errors
72
48
  end
73
49
 
74
- # Count the fields for the given time period for this Counter.
75
- #
76
- # Returns an Array of Hashes representing the count
77
- def count_period(period)
78
- return unless Von.config.period_defined_for?(@field, period)
79
-
80
- _counts = []
81
- _period = Von.config.periods[@field][period]
82
- now = DateTime.now.beginning_of_hour
50
+ def this(unit)
51
+ periods = Von.config.currents[@field]
83
52
 
84
- _period.length.times do
85
- this_period = now.strftime(_period.format)
86
- _counts.unshift(this_period)
87
- now = _period.hours? ? now.ago(3600) : now.send(:"prev_#{_period.time_unit}")
53
+ if !Period.time_unit_exists?(unit)
54
+ raise ArgumentError, "`#{unit}' is an unknown time unit"
55
+ else
56
+ Counters::Current.new(@field, periods).count(unit)
88
57
  end
89
-
90
- keys = Von.connection.hgetall("#{hash_key}:#{period}")
91
- _counts.map { |date| { date => keys.fetch(date, 0) }}
58
+ rescue Redis::BaseError => e
59
+ raise e if Von.config.raise_connection_errors
92
60
  end
93
61
 
94
- # Lookup the count for this Counter in Redis.
95
- # If a Period argument is given we lookup the count for
96
- # all of the possible units (not expired), zeroing ones that
97
- # aren't set in Redis already.
98
- #
99
- # period - A Period to lookup
100
- #
101
- # Returns an Integer representing the count or an Array of counts.
102
- def self.count(field, period = nil)
103
- counter = Counter.new(field)
62
+ alias :current :this
104
63
 
105
- if period.nil?
106
- counter.count
107
- else
108
- counter.count_period(period.to_sym)
109
- end
64
+ def today
65
+ periods = Von.config.currents[@field]
66
+
67
+ Counters::Current.new(@field, periods).count(:day)
110
68
  end
111
69
 
112
70
  end
@@ -0,0 +1,68 @@
1
+ module Von
2
+ module Counters
3
+ class Best
4
+ include Von::Counters::Commands
5
+
6
+ def initialize(field, periods = nil)
7
+ @field = field.to_sym
8
+ @periods = periods || []
9
+ end
10
+
11
+ def hash_key
12
+ @hash_key ||= "#{Von.config.namespace}:counters:bests:#{@field}"
13
+ end
14
+
15
+ def best_total(time_unit)
16
+ hget("#{hash_key}:#{time_unit}:best", 'total').to_i
17
+ end
18
+
19
+ def best_timestamp(time_unit)
20
+ hget("#{hash_key}:#{time_unit}:best", 'timestamp')
21
+ end
22
+
23
+ def current_total(time_unit)
24
+ hget("#{hash_key}:#{time_unit}:current", 'total').to_i
25
+ end
26
+
27
+ def current_timestamp(time_unit)
28
+ hget("#{hash_key}:#{time_unit}:current", 'timestamp')
29
+ end
30
+
31
+ def increment
32
+ return if @periods.empty?
33
+
34
+ @periods.each do |period|
35
+ _current_timestamp = current_timestamp(period.time_unit)
36
+ _current_total = current_total(period.time_unit)
37
+
38
+ if period.timestamp != _current_timestamp
39
+ # changing current period
40
+ hset("#{hash_key}:#{period.time_unit}:current", 'total', 1)
41
+ hset("#{hash_key}:#{period.time_unit}:current", 'timestamp', period.timestamp)
42
+
43
+ if best_total(period) < _current_total
44
+ hset("#{hash_key}:#{period.time_unit}:best", 'total', _current_total)
45
+ hset("#{hash_key}:#{period.time_unit}:best", 'timestamp', _current_timestamp)
46
+ end
47
+ else
48
+ hincrby("#{hash_key}:#{period.time_unit}:current", 'total', 1)
49
+ end
50
+ end
51
+ end
52
+
53
+ def count(time_unit)
54
+ _current_timestamp = current_timestamp(time_unit)
55
+ _current_total = current_total(time_unit)
56
+ _best_timestamp = best_timestamp(time_unit)
57
+ _best_total = best_total(time_unit)
58
+
59
+ if _current_total > _best_total
60
+ { :timestamp => _current_timestamp, :count => _current_total }
61
+ else
62
+ { :timestamp => _best_timestamp, :count => _best_total }
63
+ end
64
+ end
65
+
66
+ end
67
+ end
68
+ end