tabs 0.5.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.
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