abaci 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,10 @@
1
+ ## Abaci 0.2.0
2
+
3
+ * Added testing suite
4
+ * Storing of all stat metrics
5
+ * Stat summary with `Abaci.summary`
6
+ * If the `Stat` class is not defined, use it as a shortcut for `Abaci::Counter`
7
+
1
8
  ## Abaci 0.1.0
2
9
 
3
10
  * Initial build
@@ -2,6 +2,8 @@ require 'abaci/version'
2
2
 
3
3
  module Abaci
4
4
  autoload :Counter, 'abaci/counter'
5
+ autoload :DateRange, 'abaci/date_range'
6
+ autoload :Metric, 'abaci/metric'
5
7
  autoload :Store, 'abaci/store'
6
8
 
7
9
  # Configuration options
@@ -10,6 +12,11 @@ module Abaci
10
12
  Counter[key]
11
13
  end
12
14
 
15
+ # Gets all specific metrics stored, without date-specific keys
16
+ def metrics
17
+ Metric.all
18
+ end
19
+
13
20
  def options
14
21
  @options ||= {
15
22
  :redis => nil,
@@ -17,12 +24,36 @@ module Abaci
17
24
  }
18
25
  end
19
26
 
27
+ def prefix=(value)
28
+ @store = nil
29
+ options[:prefix] = value
30
+ end
31
+
20
32
  def prefix
21
33
  options[:prefix]
22
34
  end
23
35
 
36
+ def redis=(value)
37
+ @store = nil
38
+ options[:redis] = value
39
+ end
40
+
24
41
  def store
25
42
  @store ||= Store.new(options)
26
43
  end
44
+ alias_method :redis, :store
45
+
46
+ def summary
47
+ Counter.all
48
+ end
49
+
50
+ def method_missing(method, *args)
51
+ Counter.send(method, *args)
52
+ end
53
+ end
54
+
55
+ # Alias Stat to Abaci::Counter if nothing else is using the Stat namespace
56
+ unless defined?(::Stat)
57
+ ::Stat = Counter
27
58
  end
28
59
  end
@@ -18,6 +18,7 @@ module Abaci
18
18
 
19
19
  def del
20
20
  keys.each { |k| Abaci.store.del(k) }
21
+ Metric.remove(key)
21
22
  true
22
23
  end
23
24
 
@@ -27,9 +28,7 @@ module Abaci
27
28
  end
28
29
 
29
30
  def get_last_days(number_of_days = 30)
30
- seconds = number_of_days.to_i * 86400
31
- start = (Date.today - Rational(seconds, 86400)).to_date
32
- dates = (start..Date.today).map { |d| d.strftime('%Y:%-m:%-d') }
31
+ dates = DateRange.ago(number_of_days).keys
33
32
  dates.map { |date| Abaci.store.get("#{key}:#{date}" ).to_i }.reduce(:+)
34
33
  end
35
34
 
@@ -40,6 +39,7 @@ module Abaci
40
39
 
41
40
  def increment_at(date = nil, by = 1)
42
41
  date = Time.now unless date.respond_to?(:strftime)
42
+ Metric.add(key)
43
43
  run(:incrby, by, date)
44
44
  end
45
45
 
@@ -50,9 +50,14 @@ module Abaci
50
50
  ## Class methods ##
51
51
  ############################################################################
52
52
 
53
+ # Alias for Counter#new(key)
54
+ def self.[](key)
55
+ Counter.new(key)
56
+ end
57
+
53
58
  # Returns a hash of all current values
54
59
  def self.all
55
- keys.inject({}) { |hash, key| hash[key] = Abaci.store.get(key).to_i; hash }
60
+ Metric.all.inject({}) { |hash, key| hash[key.to_sym] = Abaci.store.get(key).to_i; hash }
56
61
  end
57
62
 
58
63
  # Gets all currently logged stat keys
@@ -60,11 +65,6 @@ module Abaci
60
65
  Abaci.store.keys(search).sort
61
66
  end
62
67
 
63
- # Alias for Counter#new(key)
64
- def self.[](key)
65
- Counter.new(key)
66
- end
67
-
68
68
  def self.method_missing(method, *args)
69
69
  ms = method.to_s.downcase.strip
70
70
 
@@ -74,11 +74,9 @@ module Abaci
74
74
  return self[$2].del
75
75
  elsif ms =~ /^last_(\d*)_days_of_([a-z_]*)$/
76
76
  return self[$2].get_last_days($1)
77
- elsif ms =~ /[a-z_]*/
78
- return self[ms].get(*args)
77
+ else
78
+ self[ms].get(*args)
79
79
  end
80
-
81
- super
82
80
  end
83
81
 
84
82
  ## Protected methods ##
@@ -0,0 +1,41 @@
1
+ module Abaci
2
+ class DateRange
3
+ attr_reader :start, :finish
4
+
5
+ def initialize(start, finish)
6
+ if start < finish
7
+ @start = start
8
+ @finish = finish
9
+ else
10
+ @start = finish
11
+ @finish = start
12
+ end
13
+ end
14
+
15
+ def days
16
+ range.to_a
17
+ end
18
+
19
+ def keys
20
+ days.map { |d| d.strftime('%Y:%-m:%-d') }
21
+ end
22
+
23
+ def range
24
+ start..finish
25
+ end
26
+
27
+ def self.ago(days = 30)
28
+ seconds = days.to_i * 86400
29
+ start = (Date.today - Rational(seconds, 86400)).to_date
30
+ new(start, Date.today)
31
+ end
32
+
33
+ def self.between(start, finish)
34
+ new(start, finish)
35
+ end
36
+
37
+ def self.since(date)
38
+ new(date, Date.today)
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,15 @@
1
+ module Abaci
2
+ class Metric
3
+ def self.add(key)
4
+ Abaci.store.sadd('_metrics', key)
5
+ end
6
+
7
+ def self.all
8
+ Abaci.store.smembers('_metrics')
9
+ end
10
+
11
+ def self.remove(key)
12
+ Abaci.store.srem('_metrics', key)
13
+ end
14
+ end
15
+ end
@@ -4,6 +4,8 @@ module Abaci
4
4
  # Common interface for Redis. In the future this could be
5
5
  # swapped out for an alternate datastore.
6
6
  class Store
7
+ attr_reader :prefix, :redis
8
+
7
9
  def initialize(options)
8
10
  @redis = options[:redis] || Redis.current
9
11
  @prefix = options[:prefix] || 'ab'
@@ -25,8 +27,8 @@ module Abaci
25
27
  exec(:incrby, key, by)
26
28
  end
27
29
 
28
- def keys(pattern)
29
- sub = Regexp.new("^#{Abaci.prefix}:")
30
+ def keys(pattern = "*")
31
+ sub = Regexp.new("^#{prefix}:")
30
32
  exec(:keys, pattern).map { |k| k.gsub(sub, '') }
31
33
  end
32
34
 
@@ -34,6 +36,18 @@ module Abaci
34
36
  exec(:set, key, value)
35
37
  end
36
38
 
39
+ def sadd(key, member)
40
+ exec_without_prefix(:sadd, "#{prefix}-#{key}", member)
41
+ end
42
+
43
+ def smembers(key)
44
+ exec_without_prefix(:smembers, "#{prefix}-#{key}")
45
+ end
46
+
47
+ def srem(key, member)
48
+ exec_without_prefix(:srem, "#{prefix}-#{key}", member)
49
+ end
50
+
37
51
  protected
38
52
  def exec(command, key, *args)
39
53
  if @redis and @redis.respond_to?(command)
@@ -41,8 +55,14 @@ module Abaci
41
55
  end
42
56
  end
43
57
 
58
+ def exec_without_prefix(command, key, *args)
59
+ if @redis and @redis.respond_to?(command)
60
+ @redis.send(command, key, *args)
61
+ end
62
+ end
63
+
44
64
  def prefixed_key(key)
45
- [ @prefix, key ].compact.join(':')
65
+ [ prefix, key ].compact.join(':')
46
66
  end
47
67
  end
48
68
  end
@@ -1,3 +1,3 @@
1
1
  module Abaci
2
- VERSION = "0.1.0" unless defined?(::Abaci::VERSION)
2
+ VERSION = "0.2.0" unless defined?(::Abaci::VERSION)
3
3
  end
@@ -0,0 +1,199 @@
1
+ require 'helper'
2
+
3
+ class CounterTest < Test::Unit::TestCase
4
+ context "A counter" do
5
+ should "increment a stat" do
6
+ assert_equal 0, Abaci['testers'].get
7
+
8
+ Abaci['testers'].increment
9
+
10
+ assert_equal 1, Abaci['testers'].get
11
+
12
+ Abaci['testers'].increment(2)
13
+
14
+ assert_equal 3, Abaci['testers'].get
15
+ end
16
+
17
+ should "decrement a stat" do
18
+ assert_equal 0, Abaci['testers'].get
19
+
20
+ Abaci['testers'].increment(5)
21
+ assert_equal 5, Abaci['testers'].get
22
+
23
+ Abaci['testers'].decrement
24
+ assert_equal 4, Abaci['testers'].get
25
+
26
+ Abaci['testers'].decrement(2)
27
+ assert_equal 2, Abaci['testers'].get
28
+ end
29
+
30
+ should "increment a stat down to the minute" do
31
+ now = Time.now
32
+
33
+ Abaci['people'].increment
34
+
35
+ expected_keys = [
36
+ 'people',
37
+ "people:#{now.strftime('%Y')}",
38
+ "people:#{now.strftime('%Y:%-m')}",
39
+ "people:#{now.strftime('%Y:%-m:%-d')}",
40
+ "people:#{now.strftime('%Y:%-m:%-d:%-k')}",
41
+ "people:#{now.strftime('%Y:%-m:%-d:%-k:%-M')}"
42
+ ].sort
43
+
44
+ assert_equal expected_keys, Abaci['people'].keys.sort
45
+
46
+ assert_equal 1, Abaci['people'].get
47
+ assert_equal 1, Abaci['people'].get(now.year)
48
+ assert_equal 1, Abaci['people'].get(now.year, now.month)
49
+ assert_equal 1, Abaci['people'].get(now.year, now.month, now.day)
50
+ assert_equal 1, Abaci['people'].get(now.year, now.month, now.day, now.hour)
51
+ assert_equal 1, Abaci['people'].get(now.year, now.month, now.day, now.hour, now.min)
52
+ end
53
+
54
+ should "increment a stat at a certain date" do
55
+ time = Time.new(2012, 2, 29, 23, 29)
56
+
57
+ Abaci['people'].increment_at(time)
58
+
59
+ expected_keys = [
60
+ 'people',
61
+ "people:2012",
62
+ "people:2012:2",
63
+ "people:2012:2:29",
64
+ "people:2012:2:29:23",
65
+ "people:2012:2:29:23:29"
66
+ ].sort
67
+
68
+ assert_equal expected_keys, Abaci['people'].keys.sort
69
+
70
+ assert_equal 1, Abaci['people'].get
71
+ assert_equal 1, Abaci['people'].get(2012)
72
+ assert_equal 1, Abaci['people'].get(2012, 2)
73
+ assert_equal 1, Abaci['people'].get(2012, 2, 29)
74
+ assert_equal 1, Abaci['people'].get(2012, 2, 29, 23)
75
+ assert_equal 1, Abaci['people'].get(2012, 2, 29, 23, 29)
76
+ end
77
+
78
+ should "get stats summary for last 5 days" do
79
+ time = Time.new(2012, 5, 30, 5, 0)
80
+ time2 = Time.new(2012, 5, 31, 5, 0)
81
+ time3 = Time.new(2012, 6, 1, 5, 0)
82
+ time4 = Time.new(2012, 6, 2, 5, 0)
83
+
84
+ Abaci['people'].increment_at(time)
85
+ Abaci['people'].increment_at(time2)
86
+ Abaci['people'].increment_at(time3)
87
+ Abaci['people'].increment_at(time4)
88
+ Abaci['people'].increment
89
+
90
+ Date.stubs(:today).returns(Date.new(2012, 6, 2))
91
+
92
+ assert_equal 5, Abaci['people'].get
93
+ assert_equal 4, Abaci['people'].get_last_days(30)
94
+ assert_equal 3, Abaci['people'].get_last_days(2)
95
+ assert_equal 2, Abaci['people'].get_last_days(1)
96
+
97
+ Date.unstub(:today)
98
+ end
99
+
100
+ should "delete stats for a given key" do
101
+ time = Time.new(2012, 2, 29, 23, 29)
102
+
103
+ Abaci['people'].increment_at(time)
104
+
105
+ expected_keys = [
106
+ 'people',
107
+ "people:2012",
108
+ "people:2012:2",
109
+ "people:2012:2:29",
110
+ "people:2012:2:29:23",
111
+ "people:2012:2:29:23:29"
112
+ ].sort
113
+
114
+ assert_equal expected_keys, Abaci['people'].keys.sort
115
+
116
+ Abaci['people'].del
117
+
118
+ assert_equal [], Abaci['people'].keys
119
+ end
120
+
121
+ should "return all stat keys across all metrics" do
122
+ time = Time.new(2012, 2, 29, 23, 29)
123
+
124
+ Abaci['people'].increment_at(time)
125
+ Abaci['places'].increment_at(time)
126
+ Abaci['things'].increment_at(time)
127
+
128
+ expected_keys = [
129
+ 'people',
130
+ "people:2012",
131
+ "people:2012:2",
132
+ "people:2012:2:29",
133
+ "people:2012:2:29:23",
134
+ "people:2012:2:29:23:29",
135
+ 'places',
136
+ "places:2012",
137
+ "places:2012:2",
138
+ "places:2012:2:29",
139
+ "places:2012:2:29:23",
140
+ "places:2012:2:29:23:29",
141
+ 'things',
142
+ "things:2012",
143
+ "things:2012:2",
144
+ "things:2012:2:29",
145
+ "things:2012:2:29:23",
146
+ "things:2012:2:29:23:29"
147
+ ].sort
148
+
149
+ assert_equal expected_keys, Abaci.keys.sort
150
+ end
151
+
152
+ should "return all metrics stores, without date-specific keys" do
153
+ time = Time.new(2012, 2, 29, 23, 29)
154
+
155
+ Abaci['people'].increment_at(time)
156
+ Abaci['places'].increment_at(time)
157
+ Abaci['things'].increment_at(time)
158
+
159
+ expected_keys = %w( people places things ).sort
160
+
161
+ assert_equal expected_keys, Abaci.metrics.sort
162
+ end
163
+
164
+ should "return a hash of all top-level metrics and current values" do
165
+ Abaci['people'].increment
166
+ Abaci['places'].increment
167
+ Abaci['things'].increment(2)
168
+
169
+ expected_result = {
170
+ :people => 1,
171
+ :places => 1,
172
+ :things => 2
173
+ }
174
+
175
+ assert_equal expected_result, Abaci.summary
176
+ end
177
+
178
+ should "have handy shortcuts for incrementing simple metrics" do
179
+ Abaci.increment_people
180
+ Abaci.increment_places(2)
181
+ Abaci.increase_things
182
+ Abaci.incr_people
183
+
184
+ assert_equal 2, Abaci.people
185
+ assert_equal 2, Abaci.places
186
+ assert_equal 1, Abaci.things
187
+
188
+ Abaci.decrease_things
189
+
190
+ assert_equal 0, Abaci.things
191
+
192
+ Abaci.clear_people!
193
+
194
+ assert_equal 0, Abaci.people
195
+
196
+ assert_equal 2, Abaci.last_20_days_of_places
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,95 @@
1
+ require 'helper'
2
+
3
+ class DateRangeTest < Test::Unit::TestCase
4
+ context "The date range calculator" do
5
+ should "get a day X days in the past" do
6
+ time = Date.new(2012, 11, 6)
7
+ expected_time = Date.new(2012, 11, 1)
8
+
9
+ Date.stubs(:today).returns(time)
10
+
11
+ assert_equal expected_time, Abaci::DateRange.ago(5).start
12
+
13
+ Date.unstub(:today)
14
+ end
15
+
16
+ should "get days since a given date" do
17
+ time = Date.new(2012, 11, 6)
18
+ expected_time = Date.new(2012, 11, 1)
19
+
20
+ Date.stubs(:today).returns(time)
21
+
22
+ since = Abaci::DateRange.since(expected_time)
23
+
24
+ assert_equal expected_time, since.start
25
+ assert_equal time, since.finish
26
+
27
+ Date.unstub(:today)
28
+ end
29
+
30
+ should "get days between a given date range" do
31
+ day1 = Date.new(2012, 11, 1)
32
+ day2 = Date.new(2012, 11, 6)
33
+
34
+ range = Abaci::DateRange.between(day1, day2)
35
+
36
+ assert_equal day1, range.start
37
+ assert_equal day2, range.finish
38
+ end
39
+
40
+ should "make days are in order" do
41
+ day1 = Date.new(2012, 11, 1)
42
+ day2 = Date.new(2012, 11, 6)
43
+
44
+ range = Abaci::DateRange.between(day2, day1)
45
+
46
+ assert_equal day1, range.start
47
+ assert_equal day2, range.finish
48
+ end
49
+
50
+ should "have a range of days" do
51
+ day1 = Date.new(2012, 11, 1)
52
+ day2 = Date.new(2012, 11, 6)
53
+
54
+ range = Abaci::DateRange.between(day2, day1)
55
+
56
+ assert_equal (day1..day2), range.range
57
+ end
58
+
59
+ should "have an array of days" do
60
+ day1 = Date.new(2012, 11, 1)
61
+ day2 = Date.new(2012, 11, 6)
62
+
63
+ range = Abaci::DateRange.between(day2, day1)
64
+
65
+ expected_result = [
66
+ Date.new(2012, 11, 1),
67
+ Date.new(2012, 11, 2),
68
+ Date.new(2012, 11, 3),
69
+ Date.new(2012, 11, 4),
70
+ Date.new(2012, 11, 5),
71
+ Date.new(2012, 11, 6)
72
+ ]
73
+
74
+ assert_equal expected_result, range.days
75
+ end
76
+
77
+ should "have a list of store keys for a range" do
78
+ day1 = Date.new(2012, 11, 1)
79
+ day2 = Date.new(2012, 11, 6)
80
+
81
+ range = Abaci::DateRange.between(day2, day1)
82
+
83
+ expected_result = [
84
+ '2012:11:1',
85
+ '2012:11:2',
86
+ '2012:11:3',
87
+ '2012:11:4',
88
+ '2012:11:5',
89
+ '2012:11:6'
90
+ ]
91
+
92
+ assert_equal expected_result, range.keys
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,23 @@
1
+ # SimpleCov provides test coverage in ruby 1.9 environments only, fail silently
2
+ # if it is not available
3
+ begin; require 'simplecov'; rescue LoadError; end
4
+
5
+ require 'shoulda'
6
+ require 'test/unit'
7
+ require 'mocha'
8
+ require 'redis'
9
+ require 'abaci'
10
+
11
+ # Fail silently if Turn is not available (runs in 1.9+ only)
12
+ begin; require 'turn/autorun'; rescue LoadError; end
13
+
14
+ REDIS = Redis.new(:db => 1)
15
+
16
+ Abaci.redis = REDIS
17
+
18
+ class Test::Unit::TestCase
19
+ def setup
20
+ REDIS.flushdb
21
+ Abaci.prefix = 'ab-test'
22
+ end
23
+ end
@@ -0,0 +1,81 @@
1
+ require 'helper'
2
+
3
+ class StoreTest < Test::Unit::TestCase
4
+ context "The abaci store" do
5
+ should "use a prefix for storage" do
6
+ Abaci.prefix = 'ab-test'
7
+ assert_equal 'ab-test', Abaci.prefix
8
+
9
+ assert_equal 0, Abaci['prefixes'].get
10
+
11
+ assert_equal nil, REDIS.get("ab-test:prefixes")
12
+
13
+ Abaci['prefixes'].increment
14
+ assert_equal 1, Abaci['prefixes'].get
15
+
16
+ assert_equal "1", REDIS.get("ab-test:prefixes")
17
+ end
18
+
19
+ should "be able to change the prefix" do
20
+ Abaci.prefix = 'ab-test-too'
21
+
22
+ assert_equal nil, REDIS.get("ab-test:prefixes")
23
+ assert_equal nil, REDIS.get("ab-test-too:prefixes")
24
+
25
+ Abaci.store.set 'prefixes', 1
26
+
27
+ assert_equal nil, REDIS.get("ab-test:prefixes")
28
+ assert_equal "1", REDIS.get("ab-test-too:prefixes")
29
+ end
30
+
31
+ should "list out all keys without prefixes" do
32
+ now = Time.now
33
+
34
+ Abaci['people'].increment
35
+
36
+ expected_redis_keys = [
37
+ 'ab-test:people',
38
+ "ab-test:people:#{now.strftime('%Y')}",
39
+ "ab-test:people:#{now.strftime('%Y:%-m')}",
40
+ "ab-test:people:#{now.strftime('%Y:%-m:%-d')}",
41
+ "ab-test:people:#{now.strftime('%Y:%-m:%-d:%-k')}",
42
+ "ab-test:people:#{now.strftime('%Y:%-m:%-d:%-k:%-M')}"
43
+ ].sort
44
+
45
+ assert_equal expected_redis_keys, REDIS.keys("ab-test:*").sort
46
+
47
+ expected_abaci_keys = [
48
+ 'people',
49
+ "people:#{now.strftime('%Y')}",
50
+ "people:#{now.strftime('%Y:%-m')}",
51
+ "people:#{now.strftime('%Y:%-m:%-d')}",
52
+ "people:#{now.strftime('%Y:%-m:%-d:%-k')}",
53
+ "people:#{now.strftime('%Y:%-m:%-d:%-k:%-M')}"
54
+ ].sort
55
+
56
+ assert_equal expected_abaci_keys, Abaci.store.keys.sort
57
+ end
58
+
59
+ should "be able to delete keys" do
60
+ Abaci['ipods'].increment
61
+
62
+ assert_equal 1, Abaci['ipods'].get
63
+
64
+ Abaci.store.del('ipods')
65
+
66
+ assert_equal 0, Abaci['ipods'].get
67
+ end
68
+
69
+ should "add list and remove items from a set" do
70
+ Abaci.store.sadd('_metrics', 'people')
71
+ Abaci.store.sadd('_metrics', 'places')
72
+ Abaci.store.sadd('_metrics', 'things')
73
+
74
+ assert_equal %w( people places things ).sort, Abaci.store.smembers('_metrics').sort
75
+
76
+ Abaci.store.srem('_metrics', 'things')
77
+
78
+ assert_equal %w( people places ).sort, Abaci.store.smembers('_metrics').sort
79
+ end
80
+ end
81
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: abaci
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -35,7 +35,13 @@ executables: []
35
35
  extensions: []
36
36
  extra_rdoc_files: []
37
37
  files:
38
+ - test/counter_test.rb
39
+ - test/date_range_test.rb
40
+ - test/helper.rb
41
+ - test/store_test.rb
38
42
  - lib/abaci/counter.rb
43
+ - lib/abaci/date_range.rb
44
+ - lib/abaci/metric.rb
39
45
  - lib/abaci/store.rb
40
46
  - lib/abaci/version.rb
41
47
  - lib/abaci.rb