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 +46 -5
- data/Rakefile +1 -1
- data/lib/von.rb +42 -5
- data/lib/von/config.rb +54 -24
- data/lib/von/counter.rb +41 -83
- data/lib/von/counters/best.rb +68 -0
- data/lib/von/counters/commands.rb +41 -0
- data/lib/von/counters/current.rb +43 -0
- data/lib/von/counters/period.rb +62 -0
- data/lib/von/counters/total.rb +49 -0
- data/lib/von/period.rb +51 -30
- data/lib/von/version.rb +1 -1
- data/test/config_test.rb +31 -11
- data/test/counter_test.rb +40 -65
- data/test/counters/best_test.rb +53 -0
- data/test/counters/current_test.rb +55 -0
- data/test/counters/period_test.rb +78 -0
- data/test/counters/total_test.rb +33 -0
- data/test/period_test.rb +62 -40
- data/test/test_helper.rb +2 -62
- data/test/von_test.rb +25 -3
- data/von.gemspec +1 -0
- metadata +31 -3
- data/lib/von/model_counter.rb +0 -28
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
|
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
|
-
|
37
|
+
## Tracking the Current Time Period
|
38
38
|
|
39
|
-
|
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')
|
79
|
+
Von.count('downloads').total #=> 4
|
80
|
+
|
51
81
|
# get the monthly counts (returns an Array of Hashes)
|
52
|
-
Von.count('uploads'
|
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
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
|
-
|
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.
|
29
|
-
|
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
|
data/lib/von/config.rb
CHANGED
@@ -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
|
16
|
+
attr_reader :periods
|
17
|
+
attr_reader :bests
|
18
|
+
attr_reader :currents
|
16
19
|
|
17
20
|
def init!
|
18
|
-
@
|
19
|
-
@
|
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
|
30
|
+
self.yearly_format = '%Y'
|
26
31
|
# 2013-01
|
27
|
-
self.monthly_format
|
32
|
+
self.monthly_format = '%Y-%m'
|
28
33
|
# 2013-01-02
|
29
|
-
self.weekly_format
|
34
|
+
self.weekly_format = '%Y-%m-%d'
|
30
35
|
# 2013-01-02
|
31
|
-
self.daily_format
|
36
|
+
self.daily_format = '%Y-%m-%d'
|
32
37
|
# 2013-01-02 12:00
|
33
|
-
self.hourly_format
|
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
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
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
|
-
|
93
|
+
def set_period(field, period, length)
|
94
|
+
@periods[field] ||= []
|
95
|
+
@periods[field] << Period.new(period, length)
|
66
96
|
end
|
67
97
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
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
|
-
|
76
|
-
|
77
|
-
|
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
|
data/lib/von/counter.rb
CHANGED
@@ -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
|
-
|
13
|
-
|
14
|
-
|
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
|
-
|
18
|
-
|
19
|
-
|
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
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
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
|
-
|
35
|
-
|
36
|
-
return unless Von.config.periods.has_key?(@field.to_sym)
|
26
|
+
def per(unit)
|
27
|
+
periods = Von.config.periods[@field]
|
37
28
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
-
|
52
|
-
|
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
|
-
|
60
|
-
|
61
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
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
|