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 +18 -0
- data/.ruby-version +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +40 -0
- data/Rakefile +1 -0
- data/lib/tabs/config.rb +18 -0
- data/lib/tabs/helpers.rb +34 -0
- data/lib/tabs/metrics/counter.rb +43 -0
- data/lib/tabs/metrics/value.rb +75 -0
- data/lib/tabs/resolution.rb +24 -0
- data/lib/tabs/resolutions/day.rb +23 -0
- data/lib/tabs/resolutions/hour.rb +23 -0
- data/lib/tabs/resolutions/minute.rb +23 -0
- data/lib/tabs/resolutions/month.rb +23 -0
- data/lib/tabs/resolutions/week.rb +25 -0
- data/lib/tabs/resolutions/year.rb +23 -0
- data/lib/tabs/storage.rb +59 -0
- data/lib/tabs/tabs.rb +70 -0
- data/lib/tabs/version.rb +3 -0
- data/lib/tabs.rb +21 -0
- data/spec/lib/tabs/metrics/counter_spec.rb +104 -0
- data/spec/lib/tabs/metrics/value_spec.rb +85 -0
- data/spec/lib/tabs_spec.rb +121 -0
- data/spec/spec_helper.rb +9 -0
- data/tabs.gemspec +30 -0
- metadata +188 -0
data/.gitignore
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.9.3
|
data/Gemfile
ADDED
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"
|
data/lib/tabs/config.rb
ADDED
data/lib/tabs/helpers.rb
ADDED
@@ -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
|
data/lib/tabs/storage.rb
ADDED
@@ -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
|
data/lib/tabs/version.rb
ADDED
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
|
data/spec/spec_helper.rb
ADDED
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
|