tabs 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ dump.rdb
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 1.9.3
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in tabs.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 JC Grubbs
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,40 @@
1
+ # Tabs
2
+
3
+ Tabs is a redis-backed metrics tracker that supports counts, sums,
4
+ averages, and min/max stats sliceable by the minute, hour, day, week,
5
+ month, and year.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ gem 'tabs'
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install tabs
20
+
21
+ ## Usage
22
+
23
+ To count events, simply call the `increment` or `record` methods to
24
+ write an event to the store.
25
+
26
+ ### Increment a counter
27
+
28
+ Tabs.increment(key)
29
+
30
+ ### Record a value
31
+
32
+ Tabs.record(key, 37)
33
+
34
+ ## Contributing
35
+
36
+ 1. Fork it
37
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
38
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
39
+ 4. Push to the branch (`git push origin my-new-feature`)
40
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,18 @@
1
+ module Tabs
2
+ module Config
3
+ extend self
4
+
5
+ def redis=(arg)
6
+ if arg.is_a?(Redis)
7
+ @redis = arg
8
+ else
9
+ @redis = Redis.new(arg)
10
+ end
11
+ end
12
+
13
+ def redis
14
+ @redis ||= Redis.new
15
+ end
16
+
17
+ end
18
+ end
@@ -0,0 +1,34 @@
1
+ module Tabs
2
+ module Helpers
3
+ extend self
4
+
5
+ def timestamp_range(period, resolution, default_value=0)
6
+ period = normalize_period(period, resolution)
7
+ dt = period.first
8
+ Hash[([].tap do |arr|
9
+ arr << dt
10
+ while (dt = dt + 1.send(resolution)) <= period.last
11
+ arr << dt.utc
12
+ end
13
+ end).map { |ts| [ts, default_value] }]
14
+ end
15
+
16
+ def normalize_period(period, resolution)
17
+ period_start = Tabs::Resolution.normalize(resolution, period.first)
18
+ period_end = Tabs::Resolution.normalize(resolution, period.last)
19
+ (period_start..period_end)
20
+ end
21
+
22
+ def extract_date_from_key(stat_key, resolution)
23
+ date_str = stat_key.split(":").last
24
+ Tabs::Resolution.deserialize(resolution, date_str)
25
+ end
26
+
27
+ def fill_missing_dates(period, date_value_pairs, resolution, default_value=0)
28
+ all_timestamps = timestamp_range(period, resolution, default_value)
29
+ merged = all_timestamps.merge(Hash[date_value_pairs])
30
+ merged.to_a
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,43 @@
1
+ module Tabs
2
+ module Metrics
3
+ class Counter
4
+ include Tabs::Storage
5
+ include Tabs::Helpers
6
+
7
+ attr_reader :key
8
+
9
+ def initialize(key)
10
+ @key = key
11
+ end
12
+
13
+ def increment
14
+ timestamp = Time.now.utc
15
+ Tabs::RESOLUTIONS.each do |resolution|
16
+ increment_resolution(resolution, timestamp)
17
+ end
18
+ true
19
+ end
20
+
21
+ def stats(period, resolution)
22
+ period = normalize_period(period, resolution)
23
+ keys = smembers("stat:keys:#{key}:#{resolution}")
24
+ dates = keys.map { |k| extract_date_from_key(k, resolution) }
25
+ values = mget(*keys).map(&:to_i)
26
+ pairs = dates.zip(values)
27
+ filtered_pairs = pairs.find_all { |p| period.cover?(p[0]) }
28
+ filtered_pairs = fill_missing_dates(period, filtered_pairs, resolution)
29
+ filtered_pairs.map { |p| Hash[[p]] }
30
+ end
31
+
32
+ private
33
+
34
+ def increment_resolution(resolution, timestamp)
35
+ formatted_time = Tabs::Resolution.serialize(resolution, timestamp)
36
+ stat_key = "stat:value:#{key}:count:#{formatted_time}"
37
+ sadd("stat:keys:#{key}:#{resolution}", stat_key)
38
+ incr(stat_key)
39
+ end
40
+
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,75 @@
1
+ module Tabs
2
+ module Metrics
3
+ class Value
4
+ include Tabs::Storage
5
+ include Tabs::Helpers
6
+
7
+ attr_reader :key
8
+
9
+ def initialize(key)
10
+ @key = key
11
+ end
12
+
13
+ def record(value)
14
+ timestamp = Time.now.utc
15
+ Tabs::RESOLUTIONS.each do |resolution|
16
+ formatted_time = Tabs::Resolution.serialize(resolution, timestamp)
17
+ stat_key = "stat:value:#{key}:#{formatted_time}"
18
+ sadd("stat:keys:#{key}:#{resolution}", stat_key)
19
+ update_values(stat_key, value)
20
+ end
21
+ true
22
+ end
23
+
24
+ def stats(period, resolution)
25
+ period = normalize_period(period, resolution)
26
+ keys = smembers("stat:keys:#{key}:#{resolution}")
27
+ dates = keys.map { |k| extract_date_from_key(k, resolution) }
28
+ values = mget(*keys).map { |v| JSON.parse(v) }
29
+ pairs = dates.zip(values)
30
+ filtered_pairs = pairs.find_all { |p| period.cover?(p[0]) }
31
+ filtered_pairs = fill_missing_dates(period, filtered_pairs, resolution, default_value(0))
32
+ filtered_pairs.map { |p| Hash[[p]] }
33
+ end
34
+
35
+ private
36
+
37
+ def update_values(stat_key, value)
38
+ hash = get_current_hash(stat_key)
39
+ increment(hash, value)
40
+ update_min(hash, value)
41
+ update_max(hash, value)
42
+ update_avg(hash)
43
+ set(stat_key, JSON.generate(hash))
44
+ end
45
+
46
+ def get_current_hash(stat_key)
47
+ hash = get(stat_key)
48
+ return JSON.parse(hash) if hash
49
+ default_value
50
+ end
51
+
52
+ def increment(hash, value)
53
+ hash["count"] += 1
54
+ hash["sum"] += value
55
+ end
56
+
57
+ def update_min(hash, value)
58
+ hash["min"] = value if hash["min"].nil? || value < hash["min"]
59
+ end
60
+
61
+ def update_max(hash, value)
62
+ hash["max"] = value if hash["max"].nil? || value > hash["max"]
63
+ end
64
+
65
+ def update_avg(hash)
66
+ hash["avg"] = hash["sum"] / hash["count"]
67
+ end
68
+
69
+ def default_value(nil_value=nil)
70
+ { "count" => 0, "min" => nil_value, "max" => nil_value, "sum" => 0, "avg" => 0 }
71
+ end
72
+
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,24 @@
1
+ module Tabs
2
+ module Resolution
3
+ extend self
4
+
5
+ def serialize(resolution, timestamp)
6
+ resolution_klass(resolution).serialize(timestamp)
7
+ end
8
+
9
+ def deserialize(resolution, str)
10
+ resolution_klass(resolution).deserialize(str)
11
+ end
12
+
13
+ def normalize(resolution, timestamp)
14
+ resolution_klass(resolution).normalize(timestamp)
15
+ end
16
+
17
+ private
18
+
19
+ def resolution_klass(resolution)
20
+ "Tabs::Resolutions::#{resolution.to_s.classify}".constantize
21
+ end
22
+
23
+ end
24
+ end
@@ -0,0 +1,23 @@
1
+ module Tabs
2
+ module Resolutions
3
+ module Day
4
+ extend self
5
+
6
+ PATTERN = "%Y-%m-%d"
7
+
8
+ def serialize(timestamp)
9
+ timestamp.strftime(PATTERN)
10
+ end
11
+
12
+ def deserialize(str)
13
+ dt = DateTime.strptime(str, PATTERN)
14
+ self.normalize(dt)
15
+ end
16
+
17
+ def normalize(ts)
18
+ Time.utc(ts.year, ts.month, ts.day)
19
+ end
20
+
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ module Tabs
2
+ module Resolutions
3
+ module Hour
4
+ extend self
5
+
6
+ PATTERN = "%Y-%m-%d-%H"
7
+
8
+ def serialize(timestamp)
9
+ timestamp.strftime(PATTERN)
10
+ end
11
+
12
+ def deserialize(str)
13
+ dt = DateTime.strptime(str, PATTERN)
14
+ self.normalize(dt)
15
+ end
16
+
17
+ def normalize(ts)
18
+ Time.utc(ts.year, ts.month, ts.day, ts.hour)
19
+ end
20
+
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ module Tabs
2
+ module Resolutions
3
+ module Minute
4
+ extend self
5
+
6
+ PATTERN = "%Y-%m-%d-%H-%M"
7
+
8
+ def serialize(timestamp)
9
+ timestamp.strftime(PATTERN)
10
+ end
11
+
12
+ def deserialize(str)
13
+ dt = DateTime.strptime(str, PATTERN)
14
+ self.normalize(dt)
15
+ end
16
+
17
+ def normalize(ts)
18
+ Time.utc(ts.year, ts.month, ts.day, ts.hour, ts.min)
19
+ end
20
+
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ module Tabs
2
+ module Resolutions
3
+ module Month
4
+ extend self
5
+
6
+ PATTERN = "%Y-%m"
7
+
8
+ def serialize(timestamp)
9
+ timestamp.strftime(PATTERN)
10
+ end
11
+
12
+ def deserialize(str)
13
+ dt = DateTime.strptime(str, PATTERN)
14
+ self.normalize(dt)
15
+ end
16
+
17
+ def normalize(ts)
18
+ Time.utc(ts.year, ts.month)
19
+ end
20
+
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,25 @@
1
+ module Tabs
2
+ module Resolutions
3
+ module Week
4
+ extend self
5
+
6
+ PATTERN = "%Y-%W"
7
+
8
+ def serialize(timestamp)
9
+ timestamp.strftime(PATTERN)
10
+ end
11
+
12
+ def deserialize(str)
13
+ year, week = str.split("-").map(&:to_i)
14
+ week = 1 if week == 0
15
+ dt = DateTime.strptime("#{year}-#{week}", PATTERN)
16
+ self.normalize(dt)
17
+ end
18
+
19
+ def normalize(ts)
20
+ Time.utc(ts.year, ts.month, ts.day).beginning_of_week
21
+ end
22
+
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,23 @@
1
+ module Tabs
2
+ module Resolutions
3
+ module Year
4
+ extend self
5
+
6
+ PATTERN = "%Y"
7
+
8
+ def serialize(timestamp)
9
+ timestamp.strftime(PATTERN)
10
+ end
11
+
12
+ def deserialize(str)
13
+ dt = DateTime.strptime(str, PATTERN)
14
+ self.normalize(dt)
15
+ end
16
+
17
+ def normalize(ts)
18
+ Time.utc(ts.year)
19
+ end
20
+
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,59 @@
1
+ module Tabs
2
+ module Storage
3
+ extend self
4
+
5
+ def redis
6
+ @redis ||= Config.redis
7
+ end
8
+
9
+ def exists(key)
10
+ redis.get("tabs:#{key}")
11
+ end
12
+
13
+ def get(key)
14
+ redis.get("tabs:#{key}")
15
+ end
16
+
17
+ def mget(*keys)
18
+ prefixed_keys = keys.map { |k| "tabs:#{k}" }
19
+ redis.mget(*prefixed_keys)
20
+ end
21
+
22
+ def set(key, value)
23
+ redis.set("tabs:#{key}", value)
24
+ end
25
+
26
+ def incr(key)
27
+ redis.incr("tabs:#{key}")
28
+ end
29
+
30
+ def rpush(key, value)
31
+ redis.rpush("tabs:#{key}", value)
32
+ end
33
+
34
+ def sadd(key, *values)
35
+ redis.sadd("tabs:#{key}", *values)
36
+ end
37
+
38
+ def smembers(key)
39
+ redis.smembers("tabs:#{key}")
40
+ end
41
+
42
+ def hget(key, field)
43
+ redis.hget("tabs:#{key}", field)
44
+ end
45
+
46
+ def hset(key, field, value)
47
+ redis.hset("tabs:#{key}", field, value)
48
+ end
49
+
50
+ def hdel(key, field)
51
+ redis.hdel("tabs:#{key}", field)
52
+ end
53
+
54
+ def hkeys(key)
55
+ redis.hkeys("tabs:#{key}")
56
+ end
57
+
58
+ end
59
+ end
data/lib/tabs/tabs.rb ADDED
@@ -0,0 +1,70 @@
1
+ module Tabs
2
+ extend self
3
+ extend Tabs::Storage
4
+
5
+ class UnknownTypeError < Exception; end
6
+ class DuplicateMetricError < Exception; end
7
+ class UnknownMetricError < Exception; end
8
+ class MetricTypeMismatchError < Exception; end
9
+
10
+ METRIC_TYPES = ["counter", "value"]
11
+
12
+ RESOLUTIONS = [:minute, :hour, :day, :week, :month, :year]
13
+
14
+ def configure
15
+ yield(Config)
16
+ end
17
+
18
+ def redis
19
+ Config.redis
20
+ end
21
+
22
+ def increment_counter(key)
23
+ raise UnknownMetricError.new("Unknown metric: #{key}") unless metric_exists?(key)
24
+ raise MetricTypeMismatchError.new("Only counter metrics can be incremented") unless metric_type(key) == "counter"
25
+ get_metric(key).increment
26
+ end
27
+
28
+ def record_value(key, value)
29
+ raise UnknownMetricError.new("Unknown metric: #{key}") unless metric_exists?(key)
30
+ raise MetricTypeMismatchError.new("Only value metrics can record a value") unless metric_type(key) == "value"
31
+ get_metric(key).record(value)
32
+ end
33
+
34
+ def create_metric(key, type)
35
+ raise UnknownTypeError.new("Unknown metric type: #{type}") unless METRIC_TYPES.include?(type)
36
+ raise DuplicateMetricError.new("Metric already exists: #{key}") if metric_exists?(key)
37
+ hset "metrics", key, type
38
+ metric_klass(type).new(key)
39
+ end
40
+
41
+ def get_metric(key)
42
+ metrics = get("metrics")
43
+ type = metrics[key]
44
+ metric_klass(type).new(key)
45
+ end
46
+
47
+ def metric_type(key)
48
+ hget "metrics", key
49
+ end
50
+
51
+ def drop_metric(key)
52
+ hdel "metrics", key
53
+ # TODO: Need to finish this
54
+ end
55
+
56
+ def list_metrics
57
+ hkeys "metrics"
58
+ end
59
+
60
+ def metric_exists?(key)
61
+ list_metrics.include? key
62
+ end
63
+
64
+ private
65
+
66
+ def metric_klass(type)
67
+ "Tabs::Metrics::#{type.classify}".constantize
68
+ end
69
+
70
+ end
@@ -0,0 +1,3 @@
1
+ module Tabs
2
+ VERSION = "0.5.0"
3
+ end
data/lib/tabs.rb ADDED
@@ -0,0 +1,21 @@
1
+ require "active_support/all"
2
+ require "redis"
3
+ require "json/ext"
4
+
5
+ require "tabs/version"
6
+ require "tabs/config"
7
+ require "tabs/storage"
8
+ require "tabs/helpers"
9
+
10
+ require "tabs/resolution"
11
+ require "tabs/resolutions/minute"
12
+ require "tabs/resolutions/hour"
13
+ require "tabs/resolutions/day"
14
+ require "tabs/resolutions/week"
15
+ require "tabs/resolutions/month"
16
+ require "tabs/resolutions/year"
17
+
18
+ require "tabs/metrics/counter"
19
+ require "tabs/metrics/value"
20
+
21
+ require "tabs/tabs"
@@ -0,0 +1,104 @@
1
+ require "spec_helper"
2
+
3
+ describe Tabs::Metrics::Counter do
4
+
5
+ let(:metric) { Tabs.create_metric("foo", "counter") }
6
+ let(:now) { Time.utc(2000, 1, 1, 0, 0) }
7
+
8
+ describe "incrementing stats" do
9
+
10
+ it "increments the value for the expected periods" do
11
+ Timecop.freeze(now)
12
+ metric.increment
13
+ time = Time.utc(now.year, now.month, now.day, now.hour)
14
+ stats = metric.stats(((now - 2.hours)..(now + 4.hours)), :hour)
15
+ expect(stats).to include({ time => 1 })
16
+ end
17
+ end
18
+
19
+ describe "retrieving stats" do
20
+
21
+ before do
22
+ Timecop.freeze(now)
23
+ end
24
+
25
+ after do
26
+ Timecop.return
27
+ end
28
+
29
+ def create_span(time_unit)
30
+ metric.increment
31
+ Timecop.freeze(now + 3.send(time_unit))
32
+ metric.increment
33
+ Timecop.freeze(now + 6.send(time_unit))
34
+ metric.increment
35
+ metric.increment
36
+ Timecop.freeze(now)
37
+ end
38
+
39
+ it "returns the expected results for an minutely metric" do
40
+ create_span(:minute)
41
+ stats = metric.stats(now..(now + 7.minutes), :minute)
42
+ expect(stats).to include({ (now + 3.minutes) => 1 })
43
+ expect(stats).to include({ (now + 6.minutes) => 2 })
44
+ end
45
+
46
+ it "returns the expected results for an hourly metric" do
47
+ create_span(:hours)
48
+ stats = metric.stats(now..(now + 7.hours), :hour)
49
+ expect(stats).to include({ (now + 3.hours) => 1 })
50
+ expect(stats).to include({ (now + 6.hours) => 2 })
51
+ end
52
+
53
+ it "returns the expected results for a daily metric" do
54
+ create_span(:days)
55
+ stats = metric.stats(now..(now + 7.days), :day)
56
+ expect(stats).to include({ (now + 3.days) => 1 })
57
+ expect(stats).to include({ (now + 6.days) => 2 })
58
+ end
59
+
60
+ it "returns the expected results for a monthly metric" do
61
+ create_span(:months)
62
+ stats = metric.stats(now..(now + 7.months), :month)
63
+ expect(stats).to include({ (now + 3.months) => 1 })
64
+ expect(stats).to include({ (now + 6.months) => 2 })
65
+ end
66
+
67
+ it "returns the expected results for a yearly metric" do
68
+ create_span(:years)
69
+ stats = metric.stats(now..(now + 7.years), :year)
70
+ expect(stats).to include({ (now + 3.years) => 1 })
71
+ expect(stats).to include({ (now + 6.years) => 2 })
72
+ end
73
+
74
+ it "returns zeros for time periods which do not have any events" do
75
+ create_span(:days)
76
+ stats = metric.stats(now..(now + 7.days), :day)
77
+ expect(stats).to include({ (now + 1.day) => 0 })
78
+ end
79
+
80
+ context "for weekly metrics" do
81
+
82
+ let(:period) do
83
+ (now - 2.days)..((now + 7.weeks) + 2.days)
84
+ end
85
+
86
+ it "returns the expected results for a weekly metric" do
87
+ create_span(:weeks)
88
+ stats = metric.stats(period, :week)
89
+ expect(stats).to include({ (now + 3.weeks).beginning_of_week => 1 })
90
+ expect(stats).to include({ (now + 6.weeks).beginning_of_week => 2 })
91
+ end
92
+
93
+ it "normalizes the period to the first day of the week" do
94
+ create_span(:weeks)
95
+ stats = metric.stats(period, :week)
96
+ expect(stats.first.keys[0]).to eq(period.first.beginning_of_week)
97
+ expect(stats.last.keys[0]).to eq(period.last.beginning_of_week)
98
+ end
99
+
100
+ end
101
+
102
+ end
103
+
104
+ end
@@ -0,0 +1,85 @@
1
+ require "spec_helper"
2
+
3
+ describe Tabs::Metrics::Value do
4
+
5
+ let(:metric) { Tabs.create_metric("foo", "value") }
6
+ let(:now) { Time.utc(2000, 1, 1, 0, 0) }
7
+
8
+ describe ".record" do
9
+
10
+ it "sets the expected values for the period" do
11
+ Timecop.freeze(now)
12
+ metric.record(17)
13
+ metric.record(42)
14
+ time = Time.utc(now.year, now.month, now.day, now.hour)
15
+ stats = metric.stats(((now - 2.hours)..(now + 4.hours)), :hour)
16
+ expect(stats).to include({ time => {"count"=>2, "min"=>17, "max"=>42, "sum"=>59, "avg"=>29} })
17
+ end
18
+
19
+ end
20
+
21
+ describe ".stats" do
22
+
23
+ before do
24
+ Timecop.freeze(now)
25
+ end
26
+
27
+ after do
28
+ Timecop.return
29
+ end
30
+
31
+ def create_span(time_unit)
32
+ metric.record(5)
33
+ Timecop.freeze(now + 3.send(time_unit))
34
+ metric.record(10)
35
+ Timecop.freeze(now + 6.send(time_unit))
36
+ metric.record(15)
37
+ metric.record(20)
38
+ Timecop.freeze(now)
39
+ end
40
+
41
+ it "returns the expected results for an minutely metric" do
42
+ create_span(:minutes)
43
+ stats = metric.stats(now..(now + 7.minutes), :minute)
44
+ expect(stats).to include({ (now + 3.minutes) => {"count"=>1, "min"=>10, "max"=>10, "sum"=>10, "avg"=>10} })
45
+ expect(stats).to include({ (now + 6.minutes) => {"count"=>2, "min"=>15, "max"=>20, "sum"=>35, "avg"=>17} })
46
+ end
47
+
48
+ it "returns the expected results for an hourly metric" do
49
+ create_span(:hours)
50
+ stats = metric.stats(now..(now + 7.hours), :hour)
51
+ expect(stats).to include({ (now + 3.hours) => {"count"=>1, "min"=>10, "max"=>10, "sum"=>10, "avg"=>10} })
52
+ expect(stats).to include({ (now + 6.hours) => {"count"=>2, "min"=>15, "max"=>20, "sum"=>35, "avg"=>17} })
53
+ end
54
+
55
+ it "returns the expected results for a daily metric" do
56
+ create_span(:days)
57
+ stats = metric.stats(now..(now + 7.days), :day)
58
+ expect(stats).to include({ (now + 3.days) => {"count"=>1, "min"=>10, "max"=>10, "sum"=>10, "avg"=>10} })
59
+ expect(stats).to include({ (now + 6.days) => {"count"=>2, "min"=>15, "max"=>20, "sum"=>35, "avg"=>17} })
60
+ end
61
+
62
+ it "returns the expected results for a weekly metric" do
63
+ create_span(:weeks)
64
+ stats = metric.stats(now..(now + 7.weeks), :week)
65
+ expect(stats).to include({ (now + 3.weeks).beginning_of_week => {"count"=>1, "min"=>10, "max"=>10, "sum"=>10, "avg"=>10} })
66
+ expect(stats).to include({ (now + 6.weeks).beginning_of_week => {"count"=>2, "min"=>15, "max"=>20, "sum"=>35, "avg"=>17} })
67
+ end
68
+
69
+ it "returns the expected results for a monthly metric" do
70
+ create_span(:months)
71
+ stats = metric.stats(now..(now + 7.months), :month)
72
+ expect(stats).to include({ (now + 3.months) => {"count"=>1, "min"=>10, "max"=>10, "sum"=>10, "avg"=>10} })
73
+ expect(stats).to include({ (now + 6.months) => {"count"=>2, "min"=>15, "max"=>20, "sum"=>35, "avg"=>17} })
74
+ end
75
+
76
+ it "returns the expected results for a yearly metric" do
77
+ create_span(:years)
78
+ stats = metric.stats(now..(now + 7.years), :year)
79
+ expect(stats).to include({ (now + 3.years) => {"count"=>1, "min"=>10, "max"=>10, "sum"=>10, "avg"=>10} })
80
+ expect(stats).to include({ (now + 6.years) => {"count"=>2, "min"=>15, "max"=>20, "sum"=>35, "avg"=>17} })
81
+ end
82
+
83
+ end
84
+
85
+ end
@@ -0,0 +1,121 @@
1
+ require "spec_helper"
2
+
3
+ describe Tabs do
4
+
5
+ describe "#create_metric" do
6
+
7
+ it "raises an error if the type is invalid" do
8
+ lambda { Tabs.create_metric("foo", "foobar") }.should raise_error(Tabs::UnknownTypeError)
9
+ end
10
+
11
+ it "raises an error if the metric already exists" do
12
+ Tabs.create_metric("foo", "counter")
13
+ lambda { Tabs.create_metric("foo", "counter") }.should raise_error(Tabs::DuplicateMetricError)
14
+ end
15
+
16
+ it "returns a Counter metric if 'counter' was the specified type" do
17
+ expect(Tabs.create_metric("foo", "counter")).to be_a_kind_of(Tabs::Metrics::Counter)
18
+ end
19
+
20
+ it "returns a Value metric if 'value' was the specified type" do
21
+ expect(Tabs.create_metric("foo", "value")).to be_a_kind_of(Tabs::Metrics::Value)
22
+ end
23
+
24
+ it "adds the metric's key to the list_metrics" do
25
+ Tabs.create_metric("foo", "value")
26
+ Tabs.create_metric("bar", "counter")
27
+ expect(Tabs.list_metrics).to include("foo")
28
+ expect(Tabs.list_metrics).to include("bar")
29
+ end
30
+
31
+ end
32
+
33
+ describe "#get_metric" do
34
+
35
+ it "returns the expected metric object" do
36
+ Tabs.create_metric("foo", "counter")
37
+ expect(Tabs.get_metric("foo")).to be_a_kind_of(Tabs::Metrics::Counter)
38
+ end
39
+
40
+ end
41
+
42
+ describe "#list_metrics" do
43
+
44
+ it "returns the list_metrics of metric names" do
45
+ Tabs.create_metric("foo", "counter")
46
+ Tabs.create_metric("bar", "value")
47
+ expect(Tabs.list_metrics).to eq(["foo", "bar"])
48
+ end
49
+
50
+ end
51
+
52
+ describe "#metric_exists?" do
53
+
54
+ it "returns true if the metric exists" do
55
+ Tabs.create_metric("foo", "counter")
56
+ expect(Tabs.metric_exists?("foo")).to be_true
57
+ end
58
+
59
+ it "returns false if the metric does not exist" do
60
+ expect(Tabs.metric_exists?("foo")).to be_false
61
+ end
62
+
63
+ end
64
+
65
+ describe "#drop_metric" do
66
+
67
+ before do
68
+ Tabs.create_metric("foo", "counter")
69
+ end
70
+
71
+ it "removes the metric from the list_metrics" do
72
+ Tabs.drop_metric("foo")
73
+ expect(Tabs.list_metrics).to_not include("foo")
74
+ expect(Tabs.metric_exists?("foo")).to be_false
75
+ end
76
+
77
+ it "removes the metrics values from redis"
78
+
79
+ end
80
+
81
+ describe "#increment_counter" do
82
+
83
+ it "raises an Tabs::UnknownMetricError if the metric does not exist" do
84
+ lambda { Tabs.increment_counter("foo") }.should raise_error(Tabs::UnknownMetricError)
85
+ end
86
+
87
+ it "raises a Tabs::MetricTypeMismatchError if the metric is the wrong type" do
88
+ Tabs.create_metric("foo", "value")
89
+ lambda { Tabs.increment_counter("foo") }.should raise_error(Tabs::MetricTypeMismatchError)
90
+ end
91
+
92
+ it "calls increment on the metric" do
93
+ metric = Tabs.create_metric("foo", "counter")
94
+ Tabs.stub(get_metric: metric)
95
+ metric.should_receive(:increment)
96
+ Tabs.increment_counter("foo")
97
+ end
98
+
99
+ end
100
+
101
+ describe "#increment_counter" do
102
+
103
+ it "raises an Tabs::UnknownMetricError if the metric does not exist" do
104
+ lambda { Tabs.record_value("foo", 27) }.should raise_error(Tabs::UnknownMetricError)
105
+ end
106
+
107
+ it "raises a Tabs::MetricTypeMismatchError if the metric is the wrong type" do
108
+ Tabs.create_metric("foo", "counter")
109
+ lambda { Tabs.record_value("foo", 27) }.should raise_error(Tabs::MetricTypeMismatchError)
110
+ end
111
+
112
+ it "calls record on the metric" do
113
+ metric = Tabs.create_metric("foo", "value")
114
+ Tabs.stub(get_metric: metric)
115
+ metric.should_receive(:record).with(42)
116
+ Tabs.record_value("foo", 42)
117
+ end
118
+
119
+ end
120
+
121
+ end
@@ -0,0 +1,9 @@
1
+ require "rubygems"
2
+ require "tabs"
3
+ require "fakeredis/rspec"
4
+ require "pry"
5
+ require "timecop"
6
+
7
+ RSpec.configure do |config|
8
+ config.mock_with :rspec
9
+ end
data/tabs.gemspec ADDED
@@ -0,0 +1,30 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'tabs/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+
8
+ gem.name = "tabs"
9
+ gem.version = Tabs::VERSION
10
+ gem.authors = ["JC Grubbs"]
11
+ gem.email = ["jc.grubbs@devmynd.com"]
12
+ gem.description = %q{A redis-backed metrics tracker for keeping tabs on pretty much anything ;)}
13
+ gem.summary = %q{A redis-backed metrics tracker for keeping tabs on pretty much anything ;)}
14
+ gem.homepage = "https://github.com/thegrubbsian/tabs"
15
+
16
+ gem.files = `git ls-files`.split($/)
17
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
18
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
19
+ gem.require_paths = ["lib"]
20
+
21
+ gem.add_dependency "redis", "~> 3.0.2"
22
+ gem.add_dependency "activesupport", "~> 3.2.11"
23
+ gem.add_dependency "json", "~> 1.7.7"
24
+
25
+ gem.add_development_dependency "pry"
26
+ gem.add_development_dependency "rspec"
27
+ gem.add_development_dependency "fakeredis"
28
+ gem.add_development_dependency "timecop"
29
+
30
+ end
metadata ADDED
@@ -0,0 +1,188 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tabs
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - JC Grubbs
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-02-22 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: json
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: 1.7.7
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: 1.7.7
62
+ - !ruby/object:Gem::Dependency
63
+ name: pry
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '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: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: rspec
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
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'
94
+ - !ruby/object:Gem::Dependency
95
+ name: fakeredis
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
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'
110
+ - !ruby/object:Gem::Dependency
111
+ name: timecop
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ description: A redis-backed metrics tracker for keeping tabs on pretty much anything
127
+ ;)
128
+ email:
129
+ - jc.grubbs@devmynd.com
130
+ executables: []
131
+ extensions: []
132
+ extra_rdoc_files: []
133
+ files:
134
+ - .gitignore
135
+ - .ruby-version
136
+ - Gemfile
137
+ - LICENSE.txt
138
+ - README.md
139
+ - Rakefile
140
+ - lib/tabs.rb
141
+ - lib/tabs/config.rb
142
+ - lib/tabs/helpers.rb
143
+ - lib/tabs/metrics/counter.rb
144
+ - lib/tabs/metrics/value.rb
145
+ - lib/tabs/resolution.rb
146
+ - lib/tabs/resolutions/day.rb
147
+ - lib/tabs/resolutions/hour.rb
148
+ - lib/tabs/resolutions/minute.rb
149
+ - lib/tabs/resolutions/month.rb
150
+ - lib/tabs/resolutions/week.rb
151
+ - lib/tabs/resolutions/year.rb
152
+ - lib/tabs/storage.rb
153
+ - lib/tabs/tabs.rb
154
+ - lib/tabs/version.rb
155
+ - spec/lib/tabs/metrics/counter_spec.rb
156
+ - spec/lib/tabs/metrics/value_spec.rb
157
+ - spec/lib/tabs_spec.rb
158
+ - spec/spec_helper.rb
159
+ - tabs.gemspec
160
+ homepage: https://github.com/thegrubbsian/tabs
161
+ licenses: []
162
+ post_install_message:
163
+ rdoc_options: []
164
+ require_paths:
165
+ - lib
166
+ required_ruby_version: !ruby/object:Gem::Requirement
167
+ none: false
168
+ requirements:
169
+ - - ! '>='
170
+ - !ruby/object:Gem::Version
171
+ version: '0'
172
+ required_rubygems_version: !ruby/object:Gem::Requirement
173
+ none: false
174
+ requirements:
175
+ - - ! '>='
176
+ - !ruby/object:Gem::Version
177
+ version: '0'
178
+ requirements: []
179
+ rubyforge_project:
180
+ rubygems_version: 1.8.23
181
+ signing_key:
182
+ specification_version: 3
183
+ summary: A redis-backed metrics tracker for keeping tabs on pretty much anything ;)
184
+ test_files:
185
+ - spec/lib/tabs/metrics/counter_spec.rb
186
+ - spec/lib/tabs/metrics/value_spec.rb
187
+ - spec/lib/tabs_spec.rb
188
+ - spec/spec_helper.rb