abaci 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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