von 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +105 -0
- data/Rakefile +10 -0
- data/lib/von/config.rb +81 -0
- data/lib/von/counter.rb +113 -0
- data/lib/von/model_counter.rb +28 -0
- data/lib/von/period.rb +58 -0
- data/lib/von/version.rb +3 -0
- data/lib/von.rb +35 -0
- data/test/config_test.rb +41 -0
- data/test/counter_test.rb +97 -0
- data/test/period_test.rb +65 -0
- data/test/test_helper.rb +73 -0
- data/test/von_test.rb +24 -0
- data/von.gemspec +27 -0
- metadata +164 -0
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 blahed
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
# Von [![Build Status](https://travis-ci.org/blahed/von.png?branch=master)](https://travis-ci.org/blahed/von)
|
2
|
+
|
3
|
+
Von is an opinionated Redis stats tracker. It works with keys, you choose one, Von increments it. It has a few built in conveniences:
|
4
|
+
|
5
|
+
## Requirements
|
6
|
+
|
7
|
+
Von uses Redis for storing counters so you'll need it get going. If you're on OS X you can use homebrew:
|
8
|
+
|
9
|
+
```bash
|
10
|
+
$ brew install redis
|
11
|
+
```
|
12
|
+
|
13
|
+
## Auto Incrementing Parent Keys
|
14
|
+
|
15
|
+
Keys are namespaced and every parent key is incremented when you increment a child key, for example:
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
Von.increment('downloads') # bumps the 'downloads' key 1 time
|
19
|
+
Von.increment('downloads:app123') # bumps the 'downloads:app123' key 1 time AND the 'downloads' key 1 time
|
20
|
+
```
|
21
|
+
|
22
|
+
## Time Period Grouping and Limiting
|
23
|
+
|
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
|
+
|
26
|
+
### Configuring Time Periods
|
27
|
+
```ruby
|
28
|
+
Von.configure do |config|
|
29
|
+
# Keep daily stats for 30 days
|
30
|
+
config.counter 'downloads', :daily => 30
|
31
|
+
|
32
|
+
# Keep monthly stats for 3 months and yearly stats for 2 years
|
33
|
+
config.counter 'uploads', :monthly => 3, :yearly => 2
|
34
|
+
end
|
35
|
+
```
|
36
|
+
|
37
|
+
### Incrementing Time Periods
|
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.
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
Von.increment('downloads')
|
43
|
+
Von.increment('uploads')
|
44
|
+
```
|
45
|
+
|
46
|
+
## Getting Stats
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
# get the total downloads (returns an Integer)
|
50
|
+
Von.count('downloads') #=> 4
|
51
|
+
# get the monthly counts (returns an Array of Hashes)
|
52
|
+
Von.count('uploads', :monthly) #=> [ { '2012-03 => 3}, { '2013-04' => 1 }, { '2013-05' => 0 }]
|
53
|
+
|
54
|
+
```
|
55
|
+
|
56
|
+
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.
|
57
|
+
|
58
|
+
## Configuration
|
59
|
+
|
60
|
+
There are a few things you might want to configure in Von, you can do this in the configure block where you would also set time periods and expirations.
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
Von.configure do |config|
|
64
|
+
# set the Redis connection to an already existing connection
|
65
|
+
config.redis = Redis.current
|
66
|
+
# Initialize a new Redis connection given options
|
67
|
+
config.redis = { :host => 'localhost', :port => 6379 }
|
68
|
+
|
69
|
+
# rescue Redis connection errors
|
70
|
+
# if the connection fails, no errors are raised by default
|
71
|
+
config.raise_connection_errors = false
|
72
|
+
|
73
|
+
# set the top level Redis key namespace
|
74
|
+
config.namespace = 'von'
|
75
|
+
|
76
|
+
# set the various formatting for time periods (defaults shown)
|
77
|
+
config.yearly_format = '%Y'
|
78
|
+
config.monthly_format = '%Y-%m'
|
79
|
+
config.weekly_format = '%Y-%m-%d'
|
80
|
+
config.daily_format = '%Y-%m-%d'
|
81
|
+
config.hourly_format = '%Y-%m-%d %H:00'
|
82
|
+
end
|
83
|
+
```
|
84
|
+
|
85
|
+
## Installation
|
86
|
+
|
87
|
+
Add this line to your application's Gemfile:
|
88
|
+
|
89
|
+
gem 'von'
|
90
|
+
|
91
|
+
And then execute:
|
92
|
+
|
93
|
+
$ bundle
|
94
|
+
|
95
|
+
Or install it yourself as:
|
96
|
+
|
97
|
+
$ gem install von
|
98
|
+
|
99
|
+
## Contributing
|
100
|
+
|
101
|
+
1. Fork it
|
102
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
103
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
104
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
105
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/lib/von/config.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module Von
|
4
|
+
module Config
|
5
|
+
extend self
|
6
|
+
|
7
|
+
attr_accessor :namespace
|
8
|
+
attr_accessor :raise_connection_errors
|
9
|
+
attr_accessor :yearly_format
|
10
|
+
attr_accessor :monthly_format
|
11
|
+
attr_accessor :weekly_format
|
12
|
+
attr_accessor :daily_format
|
13
|
+
attr_accessor :hourly_format
|
14
|
+
|
15
|
+
attr_reader :periods
|
16
|
+
|
17
|
+
def init!
|
18
|
+
@counter_options = {}
|
19
|
+
@periods = {}
|
20
|
+
# all keys are prefixed with this namespace
|
21
|
+
self.namespace = 'von'
|
22
|
+
# rescue Redis connection errors
|
23
|
+
self.raise_connection_errors = false
|
24
|
+
# 2013
|
25
|
+
self.yearly_format = '%Y'
|
26
|
+
# 2013-01
|
27
|
+
self.monthly_format = '%Y-%m'
|
28
|
+
# 2013-01-02
|
29
|
+
self.weekly_format = '%Y-%m-%d'
|
30
|
+
# 2013-01-02
|
31
|
+
self.daily_format = '%Y-%m-%d'
|
32
|
+
# 2013-01-02 12:00
|
33
|
+
self.hourly_format = '%Y-%m-%d %H:00'
|
34
|
+
end
|
35
|
+
|
36
|
+
# Set the Redis connection to use
|
37
|
+
#
|
38
|
+
# arg - A Redis connection or a Hash of Redis connection options
|
39
|
+
#
|
40
|
+
# Returns the Redis client
|
41
|
+
def redis=(arg)
|
42
|
+
if arg.is_a? Redis
|
43
|
+
@redis = arg
|
44
|
+
else
|
45
|
+
@redis = Redis.new(arg)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Returns the Redis connection
|
50
|
+
def redis
|
51
|
+
@redis ||= Redis.new
|
52
|
+
end
|
53
|
+
|
54
|
+
# Configure options for given Counter. Configures length of given time period
|
55
|
+
# and any other options for the Counter
|
56
|
+
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
|
63
|
+
end
|
64
|
+
|
65
|
+
@counter_options[field.to_sym] = options
|
66
|
+
end
|
67
|
+
|
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)
|
73
|
+
end
|
74
|
+
|
75
|
+
# TODO: rename
|
76
|
+
def counter_options(field)
|
77
|
+
@counter_options[field.to_sym] ||= {}
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
end
|
data/lib/von/counter.rb
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
module Von
|
2
|
+
class Counter
|
3
|
+
PARENT_REGEX = /:?[^:]+\z/
|
4
|
+
|
5
|
+
# Initialize a new Counter
|
6
|
+
#
|
7
|
+
# field - counter field name
|
8
|
+
def initialize(field)
|
9
|
+
@field = field.to_sym
|
10
|
+
end
|
11
|
+
|
12
|
+
# Returns options specified in config for this Counter
|
13
|
+
def options
|
14
|
+
@options ||= Von.config.counter_options(@field)
|
15
|
+
end
|
16
|
+
|
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}"
|
20
|
+
end
|
21
|
+
|
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
|
32
|
+
end
|
33
|
+
|
34
|
+
# Increment periods associated with this key
|
35
|
+
def increment_periods
|
36
|
+
return unless Von.config.periods.has_key?(@field.to_sym)
|
37
|
+
|
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
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
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, '')
|
58
|
+
|
59
|
+
until parents.empty? do
|
60
|
+
Counter.new(parents).increment
|
61
|
+
parents.sub!(PARENT_REGEX, '')
|
62
|
+
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')
|
72
|
+
end
|
73
|
+
|
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
|
83
|
+
|
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}")
|
88
|
+
end
|
89
|
+
|
90
|
+
keys = Von.connection.hgetall("#{hash_key}:#{period}")
|
91
|
+
_counts.map { |date| { date => keys.fetch(date, 0) }}
|
92
|
+
end
|
93
|
+
|
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)
|
104
|
+
|
105
|
+
if period.nil?
|
106
|
+
counter.count
|
107
|
+
else
|
108
|
+
counter.count_period(period.to_sym)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Von
|
2
|
+
module ModelCounter
|
3
|
+
extend ::ActiveSupport::Concern
|
4
|
+
|
5
|
+
module ClassMethods
|
6
|
+
def increments_stat(key, options = {})
|
7
|
+
increment_method = "increment_stat_#{key.gsub(/:/, '_')}".to_sym
|
8
|
+
|
9
|
+
define_method increment_method do
|
10
|
+
Von.increment(key)
|
11
|
+
end
|
12
|
+
|
13
|
+
case options[:on]
|
14
|
+
when :create
|
15
|
+
after_create increment_method
|
16
|
+
when :save
|
17
|
+
after_save increment_method
|
18
|
+
when :update
|
19
|
+
after_update increment_method
|
20
|
+
else
|
21
|
+
# default to create
|
22
|
+
after_create increment_method
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
data/lib/von/period.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
module Von
|
2
|
+
class Period
|
3
|
+
AVAILABLE_PERIODS = [ :hourly, :daily, :weekly, :monthly, :yearly ]
|
4
|
+
|
5
|
+
attr_reader :counter_key
|
6
|
+
attr_reader :length
|
7
|
+
attr_reader :format
|
8
|
+
|
9
|
+
# Initialize a Period object
|
10
|
+
#
|
11
|
+
# counter - the field name for the counter
|
12
|
+
# period - the time period one of AVAILABLE_PERIODS
|
13
|
+
# length - length of period
|
14
|
+
def initialize(counter_key, period, length)
|
15
|
+
@counter_key = counter_key
|
16
|
+
@period = period
|
17
|
+
@length = length
|
18
|
+
@format = Von.config.send(:"#{@period}_format")
|
19
|
+
end
|
20
|
+
|
21
|
+
# Returns a Symbol representing the time unit
|
22
|
+
# for the current period.
|
23
|
+
def time_unit
|
24
|
+
@time_unit ||= case @period
|
25
|
+
when :hourly
|
26
|
+
:hour
|
27
|
+
when :daily
|
28
|
+
:day
|
29
|
+
when :weekly
|
30
|
+
:week
|
31
|
+
when :monthly
|
32
|
+
:month
|
33
|
+
when :yearly
|
34
|
+
:year
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns True or False if the period is hourly
|
39
|
+
def hours?
|
40
|
+
@period == :hourly
|
41
|
+
end
|
42
|
+
|
43
|
+
# Returns the Redis hash key used for storing counts for this Period
|
44
|
+
def hash_key
|
45
|
+
@hash ||= "#{Von.config.namespace}:counters:#{@counter_key}:#{@period}"
|
46
|
+
end
|
47
|
+
|
48
|
+
# Returns the Redis list key used for storing current "active" counters
|
49
|
+
def list_key
|
50
|
+
@list ||= "#{Von.config.namespace}:lists:#{@counter_key}:#{@period}"
|
51
|
+
end
|
52
|
+
|
53
|
+
# Returns the Redis field representation used for storing the count value
|
54
|
+
def field
|
55
|
+
Time.now.strftime(format)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
data/lib/von/version.rb
ADDED
data/lib/von.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'redis'
|
2
|
+
require 'active_support/time'
|
3
|
+
|
4
|
+
require 'von/config'
|
5
|
+
require 'von/counter'
|
6
|
+
require 'von/period'
|
7
|
+
require 'von/version'
|
8
|
+
|
9
|
+
module Von
|
10
|
+
def self.connection
|
11
|
+
@connection ||= config.redis
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.config
|
15
|
+
Config
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.configure
|
19
|
+
yield(config)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.increment(field)
|
23
|
+
Counter.increment(field)
|
24
|
+
rescue Redis::BaseError => e
|
25
|
+
raise e if config.raise_connection_errors
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.count(field, period = nil)
|
29
|
+
Counter.count(field, period)
|
30
|
+
rescue Redis::BaseError => e
|
31
|
+
raise e if config.raise_connection_errors
|
32
|
+
end
|
33
|
+
|
34
|
+
config.init!
|
35
|
+
end
|
data/test/config_test.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
describe Von::Config do
|
4
|
+
|
5
|
+
before :each do
|
6
|
+
@config = Von::Config
|
7
|
+
@config.init!
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'intiializes a config with defaults' do
|
11
|
+
@config.namespace.must_equal 'von'
|
12
|
+
@config.hourly_format.must_equal '%Y-%m-%d %H:00'
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'initializes a config and overloads it with a block' do
|
16
|
+
@config.namespace = 'something'
|
17
|
+
|
18
|
+
@config.namespace.must_equal 'something'
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'stores counter options per key and retrieves them' do
|
22
|
+
options = { :monthly => 3, :total => false }
|
23
|
+
|
24
|
+
@config.counter 'bar', options
|
25
|
+
|
26
|
+
@config.namespace.must_equal 'von'
|
27
|
+
@config.counter_options('bar').must_equal options
|
28
|
+
end
|
29
|
+
|
30
|
+
it "allows config options to be updated via configure" do
|
31
|
+
options = { :monthly => 3, :total => false }
|
32
|
+
|
33
|
+
Von.configure do |config|
|
34
|
+
config.counter 'bar', options
|
35
|
+
end
|
36
|
+
|
37
|
+
Von.config.namespace.must_equal 'von'
|
38
|
+
Von.config.counter_options('bar').must_equal options
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
describe Von::Counter do
|
4
|
+
Counter = Von::Counter
|
5
|
+
|
6
|
+
before :each do
|
7
|
+
Timecop.freeze(Time.local(2013, 01))
|
8
|
+
Von.config.init!
|
9
|
+
mock_connection!
|
10
|
+
end
|
11
|
+
|
12
|
+
it "increments the total counter if given a single key" do
|
13
|
+
Counter.increment('foo')
|
14
|
+
|
15
|
+
@store.has_key?('von:counters:foo').must_equal true
|
16
|
+
@store['von:counters:foo']['total'].must_equal 1
|
17
|
+
|
18
|
+
Counter.increment('foo')
|
19
|
+
@store['von:counters:foo']['total'].must_equal 2
|
20
|
+
end
|
21
|
+
|
22
|
+
it "increments the total counter for a key and it's parent keys" do
|
23
|
+
Counter.increment('foo:bar')
|
24
|
+
|
25
|
+
@store.has_key?('von:counters:foo').must_equal true
|
26
|
+
@store['von:counters:foo']['total'].must_equal 1
|
27
|
+
@store.has_key?('von:counters:foo:bar').must_equal true
|
28
|
+
@store['von:counters:foo:bar']['total'].must_equal 1
|
29
|
+
|
30
|
+
Counter.increment('foo:bar')
|
31
|
+
@store['von:counters:foo']['total'].must_equal 2
|
32
|
+
@store['von:counters:foo:bar']['total'].must_equal 2
|
33
|
+
end
|
34
|
+
|
35
|
+
it "increments a month counter" do
|
36
|
+
Von.configure do |config|
|
37
|
+
config.counter 'foo', :monthly => 1
|
38
|
+
end
|
39
|
+
|
40
|
+
Counter.increment('foo')
|
41
|
+
Counter.increment('foo')
|
42
|
+
|
43
|
+
@store.has_key?('von:counters:foo').must_equal true
|
44
|
+
@store.has_key?('von:counters:foo:monthly').must_equal true
|
45
|
+
@store['von:counters:foo']['total'].must_equal 2
|
46
|
+
@store['von:counters:foo:monthly']['2013-01'].must_equal 2
|
47
|
+
@store['von:lists:foo:monthly'].size.must_equal 1
|
48
|
+
end
|
49
|
+
|
50
|
+
it "expires counters past the limit" do
|
51
|
+
Von.configure do |config|
|
52
|
+
config.counter 'foo', :monthly => 1
|
53
|
+
end
|
54
|
+
|
55
|
+
Counter.increment('foo')
|
56
|
+
Timecop.freeze(Time.local(2013, 02))
|
57
|
+
Counter.increment('foo')
|
58
|
+
|
59
|
+
@store.has_key?('von:counters:foo').must_equal true
|
60
|
+
@store.has_key?('von:counters:foo:monthly').must_equal true
|
61
|
+
@store['von:counters:foo']['total'].must_equal 2
|
62
|
+
@store['von:counters:foo:monthly'].has_key?('2013-02').must_equal true
|
63
|
+
@store['von:lists:foo:monthly'].size.must_equal 1
|
64
|
+
end
|
65
|
+
|
66
|
+
it "gets a total count for a counter" do
|
67
|
+
Counter.increment('foo')
|
68
|
+
Counter.increment('foo')
|
69
|
+
Counter.increment('foo')
|
70
|
+
|
71
|
+
Von.count('foo').must_equal 3
|
72
|
+
end
|
73
|
+
|
74
|
+
it "gets a count for a time period and 0s missing entries" do
|
75
|
+
Von.configure do |config|
|
76
|
+
config.counter 'foo', :monthly => 1, :hourly => 6
|
77
|
+
end
|
78
|
+
|
79
|
+
Timecop.freeze(Time.local(2013, 02, 01, 05))
|
80
|
+
Counter.increment('foo')
|
81
|
+
Timecop.freeze(Time.local(2013, 02, 01, 07))
|
82
|
+
Counter.increment('foo')
|
83
|
+
|
84
|
+
Von.count('foo').must_equal 2
|
85
|
+
|
86
|
+
Von.count('foo', :monthly).must_equal [{"2013-02" => 2}]
|
87
|
+
Von.count('foo', :hourly).must_equal [
|
88
|
+
{"2013-02-01 02:00"=>0},
|
89
|
+
{"2013-02-01 03:00"=>0},
|
90
|
+
{"2013-02-01 04:00"=>0},
|
91
|
+
{"2013-02-01 05:00"=>1},
|
92
|
+
{"2013-02-01 06:00"=>0},
|
93
|
+
{"2013-02-01 07:00"=>1}
|
94
|
+
]
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
data/test/period_test.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
describe Von::Period do
|
2
|
+
Period = Von::Period
|
3
|
+
|
4
|
+
before :each do
|
5
|
+
@config = Von::Config
|
6
|
+
@config.init!
|
7
|
+
end
|
8
|
+
|
9
|
+
it "intiializes given a counter, period, and length" do
|
10
|
+
period = Period.new('foo', :monthly, 6)
|
11
|
+
period.counter_key.must_equal 'foo'
|
12
|
+
period.length.must_equal 6
|
13
|
+
period.format.must_equal '%Y-%m'
|
14
|
+
end
|
15
|
+
|
16
|
+
it "checks if the period is an hourly period" do
|
17
|
+
Period.new('foo', :hourly, 6).must_be :hours?
|
18
|
+
Period.new('foo', :daily, 6).wont_be :hours?
|
19
|
+
Period.new('foo', :weekly, 6).wont_be :hours?
|
20
|
+
Period.new('foo', :monthly, 6).wont_be :hours?
|
21
|
+
Period.new('foo', :yearly, 6).wont_be :hours?
|
22
|
+
end
|
23
|
+
|
24
|
+
it "knows what time unit it is" do
|
25
|
+
Period.new('foo', :hourly, 6).time_unit.must_equal :hour
|
26
|
+
Period.new('foo', :daily, 6).time_unit.must_equal :day
|
27
|
+
Period.new('foo', :weekly, 6).time_unit.must_equal :week
|
28
|
+
Period.new('foo', :monthly, 6).time_unit.must_equal :month
|
29
|
+
Period.new('foo', :yearly, 6).time_unit.must_equal :year
|
30
|
+
end
|
31
|
+
|
32
|
+
it "pulls a time format from config options" do
|
33
|
+
Period.new('foo', :hourly, 6).format.must_equal Von.config.hourly_format
|
34
|
+
Period.new('foo', :daily, 6).format.must_equal Von.config.daily_format
|
35
|
+
Period.new('foo', :weekly, 6).format.must_equal Von.config.weekly_format
|
36
|
+
Period.new('foo', :monthly, 6).format.must_equal Von.config.monthly_format
|
37
|
+
Period.new('foo', :yearly, 6).format.must_equal Von.config.yearly_format
|
38
|
+
end
|
39
|
+
|
40
|
+
it "builds a redis hash key string" do
|
41
|
+
field = 'foo'
|
42
|
+
period = :hourly
|
43
|
+
period_obj = Period.new(field, period, 6)
|
44
|
+
|
45
|
+
period_obj.hash_key.must_equal "#{@config.namespace}:counters:#{field}:#{period}"
|
46
|
+
end
|
47
|
+
|
48
|
+
it "builds a redis list key string" do
|
49
|
+
field = 'foo'
|
50
|
+
period = :hourly
|
51
|
+
period_obj = Period.new(field, period, 6)
|
52
|
+
|
53
|
+
period_obj.list_key.must_equal "#{@config.namespace}:lists:#{field}:#{period}"
|
54
|
+
end
|
55
|
+
|
56
|
+
it "builds a redis field for the given period and current time" do
|
57
|
+
Timecop.freeze(Time.local(2013, 02, 01, 05))
|
58
|
+
Period.new('foo', :hourly, 6).field.must_equal '2013-02-01 05:00'
|
59
|
+
Period.new('foo', :daily, 6).field.must_equal '2013-02-01'
|
60
|
+
Period.new('foo', :weekly, 6).field.must_equal '2013-02-01'
|
61
|
+
Period.new('foo', :monthly, 6).field.must_equal '2013-02'
|
62
|
+
Period.new('foo', :yearly, 6).field.must_equal '2013'
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.expand_path('../../test', __FILE__))
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler'
|
5
|
+
Bundler.setup
|
6
|
+
|
7
|
+
require 'minitest/autorun'
|
8
|
+
require 'minitest/pride'
|
9
|
+
require 'mocha'
|
10
|
+
require 'timecop'
|
11
|
+
|
12
|
+
require 'von'
|
13
|
+
|
14
|
+
module Von
|
15
|
+
class TestConnection
|
16
|
+
attr_reader :store
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
@store = {}
|
20
|
+
end
|
21
|
+
|
22
|
+
def hincrby(hash, key, counter)
|
23
|
+
@store[hash] ||= {}
|
24
|
+
|
25
|
+
if @store[hash].has_key?(key)
|
26
|
+
@store[hash][key] += counter
|
27
|
+
else
|
28
|
+
@store[hash][key] = counter
|
29
|
+
end
|
30
|
+
|
31
|
+
@store[hash][key]
|
32
|
+
end
|
33
|
+
|
34
|
+
def hget(hash, key)
|
35
|
+
@store[hash][key]
|
36
|
+
end
|
37
|
+
|
38
|
+
def hgetall(hash)
|
39
|
+
@store.fetch(hash, {})
|
40
|
+
end
|
41
|
+
|
42
|
+
def hdel(hash, key)
|
43
|
+
@store[hash].delete(key)
|
44
|
+
end
|
45
|
+
|
46
|
+
def rpush(list, member)
|
47
|
+
@store[list] ||= []
|
48
|
+
@store[list] << member
|
49
|
+
end
|
50
|
+
|
51
|
+
def lpop(list)
|
52
|
+
@store[list].shift
|
53
|
+
end
|
54
|
+
|
55
|
+
def lrange(list, start, stop)
|
56
|
+
@store[list] ||= []
|
57
|
+
@store[list][start..stop]
|
58
|
+
end
|
59
|
+
|
60
|
+
def llen(list)
|
61
|
+
@store[list].length
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
module MiniTest::Expectations
|
68
|
+
def mock_connection!
|
69
|
+
connection = Von::TestConnection.new
|
70
|
+
@store = connection.store
|
71
|
+
Von.expects(:connection).returns(connection).at_least_once
|
72
|
+
end
|
73
|
+
end
|
data/test/von_test.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
describe Von do
|
4
|
+
|
5
|
+
before :each do
|
6
|
+
Von.config.init!
|
7
|
+
end
|
8
|
+
|
9
|
+
it "increments a counter and counts it" do
|
10
|
+
mock_connection!
|
11
|
+
|
12
|
+
Von.increment('foo')
|
13
|
+
Von.increment('foo')
|
14
|
+
Von.count('foo').must_equal 2
|
15
|
+
end
|
16
|
+
|
17
|
+
it "raises a Redis connection errors if raise_connection_errors is true" do
|
18
|
+
Von.config.raise_connection_errors = true
|
19
|
+
Redis.expects(:new).raises(Redis::CannotConnectError)
|
20
|
+
|
21
|
+
lambda { Von.increment('foo') }.must_raise Redis::CannotConnectError
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
data/von.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'von/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "von"
|
8
|
+
gem.version = Von::VERSION
|
9
|
+
gem.authors = ["blahed"]
|
10
|
+
gem.email = ["tdunn13@gmail.com"]
|
11
|
+
gem.description = "Von is an opinionated Redis stats tracker."
|
12
|
+
gem.summary = "Von is an opinionated Redis stats tracker."
|
13
|
+
gem.homepage = ""
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
|
20
|
+
gem.add_dependency 'redis', '~> 3.0.2'
|
21
|
+
gem.add_dependency 'activesupport', '~> 3.2.11'
|
22
|
+
|
23
|
+
gem.add_development_dependency 'rake', '~> 10.0.3'
|
24
|
+
gem.add_development_dependency 'minitest', '~> 3.0.0'
|
25
|
+
gem.add_development_dependency 'mocha', '~> 0.11.4'
|
26
|
+
gem.add_development_dependency 'timecop', '~> 0.5.9.1'
|
27
|
+
end
|
metadata
ADDED
@@ -0,0 +1,164 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: von
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- blahed
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-01-28 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: redis
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 3.0.2
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 3.0.2
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: activesupport
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 3.2.11
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 3.2.11
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rake
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ~>
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 10.0.3
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 10.0.3
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: minitest
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ~>
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: 3.0.0
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ~>
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: 3.0.0
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: mocha
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ~>
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: 0.11.4
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ~>
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: 0.11.4
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: timecop
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ~>
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: 0.5.9.1
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ~>
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: 0.5.9.1
|
110
|
+
description: Von is an opinionated Redis stats tracker.
|
111
|
+
email:
|
112
|
+
- tdunn13@gmail.com
|
113
|
+
executables: []
|
114
|
+
extensions: []
|
115
|
+
extra_rdoc_files: []
|
116
|
+
files:
|
117
|
+
- .gitignore
|
118
|
+
- .travis.yml
|
119
|
+
- Gemfile
|
120
|
+
- LICENSE.txt
|
121
|
+
- README.md
|
122
|
+
- Rakefile
|
123
|
+
- lib/von.rb
|
124
|
+
- lib/von/config.rb
|
125
|
+
- lib/von/counter.rb
|
126
|
+
- lib/von/model_counter.rb
|
127
|
+
- lib/von/period.rb
|
128
|
+
- lib/von/version.rb
|
129
|
+
- test/config_test.rb
|
130
|
+
- test/counter_test.rb
|
131
|
+
- test/period_test.rb
|
132
|
+
- test/test_helper.rb
|
133
|
+
- test/von_test.rb
|
134
|
+
- von.gemspec
|
135
|
+
homepage: ''
|
136
|
+
licenses: []
|
137
|
+
post_install_message:
|
138
|
+
rdoc_options: []
|
139
|
+
require_paths:
|
140
|
+
- lib
|
141
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
142
|
+
none: false
|
143
|
+
requirements:
|
144
|
+
- - ! '>='
|
145
|
+
- !ruby/object:Gem::Version
|
146
|
+
version: '0'
|
147
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
148
|
+
none: false
|
149
|
+
requirements:
|
150
|
+
- - ! '>='
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
153
|
+
requirements: []
|
154
|
+
rubyforge_project:
|
155
|
+
rubygems_version: 1.8.23
|
156
|
+
signing_key:
|
157
|
+
specification_version: 3
|
158
|
+
summary: Von is an opinionated Redis stats tracker.
|
159
|
+
test_files:
|
160
|
+
- test/config_test.rb
|
161
|
+
- test/counter_test.rb
|
162
|
+
- test/period_test.rb
|
163
|
+
- test/test_helper.rb
|
164
|
+
- test/von_test.rb
|