tabs 0.7.1 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -9,7 +9,7 @@ averages, and min/max, and task based stats sliceable by the minute, hour, day,
9
9
 
10
10
  Add this line to your application's Gemfile:
11
11
 
12
- gem 'tabs', '~> 0.7.0'
12
+ gem 'tabs'
13
13
 
14
14
  And then execute:
15
15
 
@@ -19,14 +19,6 @@ Or install it yourself as:
19
19
 
20
20
  $ gem install tabs
21
21
 
22
- ## Breaking Changes in v0.6.0
23
-
24
- Please note that when the library version went from v0.5.6 to v0.6.0 some of
25
- the key patterns used to store metrics in Redis were changed. If you upgrade
26
- an app to v0.6.0 the previous set of data will not be picked up by tabs.
27
- Please use v0.6.x on new applications only. However, the 'Task' metric
28
- type will only be available in v0.6.0 and above.
29
-
30
22
  ## Usage
31
23
 
32
24
  Metrics come in three flavors: counters, values, and tasks.
@@ -61,29 +53,49 @@ To retrieve the counts for a given time period just call `Tabs#get_stats` with t
61
53
  Tabs.get_stats("website-visits", 10.days.ago..Time.now, :hour)
62
54
  ```
63
55
 
64
- This will return stats for the last 10 days by hour as an array of hashes in which the keys are an instance of `Time` and the value is the count for that time.
65
-
66
- ```ruby
67
- [
68
- { 2000-01-01 00:00:00 UTC => 1 },
69
- { 2000-01-01 01:00:00 UTC => 0 },
70
- { 2000-01-01 02:00:00 UTC => 10 },
71
- { 2000-01-01 03:00:00 UTC => 1 },
72
- { 2000-01-01 04:00:00 UTC => 0 },
73
- { 2000-01-01 05:00:00 UTC => 0 },
74
- { 2000-01-01 06:00:00 UTC => 3 },
75
- { 2000-01-01 07:00:00 UTC => 0 },
56
+ This will return stats for the last 10 days by hour in the form of a `Tabs::Metrics::Counter::Stats` object. This object is enumerable so you can iterate through the results like so:
57
+
58
+ ```ruby
59
+ results = Tabs.get_stats("website-visits", 10.days.ago..Time.now, :hour)
60
+ results.each { |r| puts r }
61
+
62
+ #=>
63
+ { timestamp: 2000-01-01 00:00:00 UTC, count: 1 }
64
+ { timestamp: 2000-01-01 01:00:00 UTC, count: 0 }
65
+ { timestamp: 2000-01-01 02:00:00 UTC, count: 10 }
66
+ { timestamp: 2000-01-01 03:00:00 UTC, count: 1 }
67
+ { timestamp: 2000-01-01 04:00:00 UTC, count: 0 }
68
+ { timestamp: 2000-01-01 05:00:00 UTC, count: 0 }
69
+ { timestamp: 2000-01-01 06:00:00 UTC, count: 3 }
70
+ { timestamp: 2000-01-01 07:00:00 UTC, count: 0 }
76
71
  ...
77
- ]
72
+ ```
73
+
74
+ The results object also provides the following methods:
75
+
76
+ ```ruby
77
+ results.total #=> The count total for the given period
78
+ results.min #=> The min count for any timestamp in the period
79
+ results.max #=> The max count for any timestamp in the period
80
+ results.avg #=> The avg count for timestamps in the period
81
+ results.period #=> The timestamp range that was requested
82
+ results.resolution #=> The resolution requested
78
83
  ```
79
84
 
80
- Times for the given period in which no events occurred will be "filled in" with a zero value to make visualizations easier.
85
+ Timestamps for the given period in which no events occurred will be "filled in" with a count value to make visualizations easier.
86
+
87
+ The timestamps are also normalized. For example, in hour resolution, the minutes and seconds of the timestamps are set to 00:00. Likewise for the week resolution, the day is set to the first day of the week.
81
88
 
82
- The `Time` keys are also normalized. For example, in hour resolution, the minutes and seconds of the `Time` object are set to 00:00. Likewise for the week resolution, the day is set to the first day of the week.
89
+ Lastly, you can access the overall total for a counter (for all time)
90
+ using the `counter_total` method.
91
+
92
+ ```ruby
93
+ Tabs.counter_total("website-visits") #=> 476873
94
+ ```
83
95
 
84
96
  ### Value Metrics
85
97
 
86
- Value metrics take a value and record the min, max, avg, and sum for a given time resolution. Creating a value metric is easy:
98
+ Value metrics record a value at a point in time and calculate the min, max, avg, and sum for a given time resolution. Creating a value metric is easy:
87
99
 
88
100
  To record a value, simply call `Tabs#record_value`.
89
101
 
@@ -111,15 +123,30 @@ Retrieving the stats for a value metric is just like retrieving a counter metric
111
123
  Tabs.get_stats("new-user-age", 6.months.ago..Time.now, :month)
112
124
  ```
113
125
 
114
- This will return a familiar value, but with an expanded set of values.
126
+ This will return a `Tabs::Metrics::Value::Stats` object. Again, this
127
+ object is enumerable and encapsulates all the timestamps within the
128
+ given period.
115
129
 
116
130
  ```ruby
117
- [
118
- { 2000-01-01 00:00:00 UTC => { count: 9, min: 19, max: 54, sum: 226, avg: 38 } },
119
- { 2000-02-01 01:00:00 UTC => { count: 0, min: 0, max: 0, sum: 0, avg: 0 } },
120
- { 2000-03-01 02:00:00 UTC => { count: 2, min: 22, max: 34, sum: 180, avg: 26 } },
131
+ results = Tabs.get_stats("new-user-age", 6.months.ago..Time.now, :month)
132
+ results.each { |r| puts r }
133
+ #=>
134
+ { timestamp: 2000-01-01 00:00:00, count: 9, min: 19, max: 54, sum: 226, avg: 38 }
135
+ { timestamp: 2000-02-01 01:00:00, count: 0, min: 0, max: 0, sum: 0, avg: 0 }
136
+ { timestamp: 2000-03-01 02:00:00, count: 2, min: 22, max: 34, sum: 180, avg: 26 }
121
137
  ...
122
- ]
138
+ ```
139
+
140
+ The results object also provides some aggregates and other methods:
141
+
142
+ ```ruby
143
+ results.count #=> The total count of recorded values for the period
144
+ results.sum #=> The sum of all values for the period
145
+ results.min #=> The min value for any timestamp in the period
146
+ results.max #=> The max value for any timestamp in the period
147
+ results.avg #=> The avg value for timestamps in the period
148
+ results.period #=> The timestamp range that was requested
149
+ results.resolution #=> The resolution requested
123
150
  ```
124
151
 
125
152
  ### Task Metrics
@@ -155,16 +182,15 @@ Retrieving stats for a task metric is just like the other types:
155
182
  Tabs.get_stats("mobile-to-purchase", 6.hours.ago..Time.now, :minute)
156
183
  ```
157
184
 
158
- This will return a hash like this:
185
+ This will return a `Tabs::Metrics::Task::Stats` object:
159
186
 
160
187
  ```ruby
161
- {
162
- started: 3, #=> number of items started within the period
163
- completed: 2, #=> number of items completed within the period
164
- completed_within_period: 2, #=> number started AND completed within the period
165
- completion_rate: 0.18, #=> rate of completion in the specified resolution (e.g. :minute)
166
- average_completion_time: 1.5 #=> average completion time in the specified resolution
167
- }
188
+ results = Tabs.get_stats("mobile-to-purchase", 6.hours.ago..Time.now, :minute)
189
+ results.started_within_period #=> Number of items started in period
190
+ results.completed_within_period #=> Number of items completed in period
191
+ results.started_and_completed_within_period #=> Items wholly started/completed in period
192
+ results.completion_rate #=> Rate of completion in the given resolution
193
+ results.average_completion_time #=> Average time for the task to be completed
168
194
  ```
169
195
 
170
196
  ### Resolutions
@@ -201,11 +227,17 @@ Tabs.metric_exists?("foobar") #=> false
201
227
  To drop a metric, just call `Tabs#drop_metric`
202
228
 
203
229
  ```ruby
204
- Tabs.drop_metric("website-visits")
230
+ Tabs.drop_metric!("website-visits")
205
231
  ```
206
232
 
207
233
  This will drop all recorded values for the metric so it may not be un-done...be careful.
208
234
 
235
+ Even more dangerous, you can drop all metrics...be very careful.
236
+
237
+ ```ruby
238
+ Tabs.drop_all_metrics!
239
+ ```
240
+
209
241
  ### Configuration
210
242
 
211
243
  There really isn’t much to configure with Tabs, it just works out of the box. You can use the following configure block to set the Redis connection instance that Tabs will use.
@@ -222,6 +254,24 @@ Tabs.configure do |config|
222
254
  end
223
255
  ```
224
256
 
257
+ ## Breaking Changes
258
+
259
+ ### Breaking Changes in v0.6.0
260
+
261
+ Please note that when the library version went from v0.5.6 to v0.6.0 some of
262
+ the key patterns used to store metrics in Redis were changed. If you upgrade
263
+ an app to v0.6.0 the previous set of data will not be picked up by tabs.
264
+ Please use v0.6.x on new applications only. However, the 'Task' metric
265
+ type will only be available in v0.6.0 and above.
266
+
267
+ ### Breaking Changes in v0.8.0
268
+
269
+ In version 0.8.0 and higher the get_stats method returns a more robust
270
+ object instead of just an array of hashes. These stats objects are
271
+ enumerable and most existing code utilizing tabs should continue to
272
+ function. However, please review the docs for more information if you
273
+ encounter issues when upgrading.
274
+
225
275
  ## Contributing
226
276
 
227
277
  1. Fork it
data/lib/tabs.rb CHANGED
@@ -15,9 +15,11 @@ require "tabs/resolutions/week"
15
15
  require "tabs/resolutions/month"
16
16
  require "tabs/resolutions/year"
17
17
 
18
+ require "tabs/metrics/counter/stats"
18
19
  require "tabs/metrics/counter"
20
+ require "tabs/metrics/value/stats"
19
21
  require "tabs/metrics/value"
20
- require "tabs/metrics/task"
21
22
  require "tabs/metrics/task/token"
23
+ require "tabs/metrics/task"
22
24
 
23
25
  require "tabs/tabs"
data/lib/tabs/helpers.rb CHANGED
@@ -14,23 +14,11 @@ module Tabs
14
14
  end
15
15
 
16
16
  def normalize_period(period, resolution)
17
- period_start = Tabs::Resolution.normalize(resolution, period.first)
18
- period_end = Tabs::Resolution.normalize(resolution, period.last)
17
+ period_start = Tabs::Resolution.normalize(resolution, period.first.utc)
18
+ period_end = Tabs::Resolution.normalize(resolution, period.last.utc)
19
19
  (period_start..period_end)
20
20
  end
21
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)
29
- default_value_timestamps = Hash[all_timestamps.map { |t| [t, default_value] }]
30
- merged = default_value_timestamps.merge(Hash[date_value_pairs])
31
- merged.to_a
32
- end
33
-
34
22
  def round_float(f)
35
23
  (f*100).round / 100.0
36
24
  end
@@ -20,14 +20,19 @@ module Tabs
20
20
  end
21
21
 
22
22
  def stats(period, resolution)
23
- period = normalize_period(period, resolution)
24
- keys = smembers("stat:counter:#{key}:keys:#{resolution}")
25
- dates = keys.map { |k| extract_date_from_key(k, resolution) }
26
- values = mget(*keys).map(&:to_i)
27
- pairs = dates.zip(values)
28
- filtered_pairs = pairs.find_all { |p| period.cover?(p[0]) }
29
- filtered_pairs = fill_missing_dates(period, filtered_pairs, resolution)
30
- filtered_pairs.map { |p| Hash[[p]] }
23
+ timestamps = timestamp_range period, resolution
24
+ keys = timestamps.map do |ts|
25
+ "stat:counter:#{key}:count:#{Tabs::Resolution.serialize(resolution, ts)}"
26
+ end
27
+
28
+ values = mget(*keys).map do |v|
29
+ {
30
+ "timestamp" => timestamps.shift,
31
+ "count" => (v || 0).to_i
32
+ }.with_indifferent_access
33
+ end
34
+
35
+ Stats.new(period, resolution, values)
31
36
  end
32
37
 
33
38
  def total
@@ -43,7 +48,6 @@ module Tabs
43
48
  def increment_resolution(resolution, timestamp)
44
49
  formatted_time = Tabs::Resolution.serialize(resolution, timestamp)
45
50
  stat_key = "stat:counter:#{key}:count:#{formatted_time}"
46
- sadd("stat:counter:#{key}:keys:#{resolution}", stat_key)
47
51
  incr(stat_key)
48
52
  end
49
53
 
@@ -0,0 +1,50 @@
1
+ module Tabs
2
+ module Metrics
3
+ class Counter
4
+ class Stats
5
+
6
+ include Enumerable
7
+ include Helpers
8
+
9
+ attr_reader :period, :resolution, :values
10
+
11
+ def initialize(period, resolution, values)
12
+ @period, @resolution, @values = period, resolution, values
13
+ end
14
+
15
+ def first
16
+ values.first
17
+ end
18
+
19
+ def last
20
+ values.last
21
+ end
22
+
23
+ def total
24
+ @total ||= values.map { |v| v["count"] }.sum
25
+ end
26
+
27
+ def min
28
+ @min ||= values.min_by { |v| v["count"] }["count"]
29
+ end
30
+
31
+ def max
32
+ @max ||= values.max_by { |v| v["count"] }["count"]
33
+ end
34
+
35
+ def avg
36
+ round_float(self.total.to_f / values.size.to_f)
37
+ end
38
+
39
+ def each(&block)
40
+ values.each(&block)
41
+ end
42
+
43
+ def to_a
44
+ values
45
+ end
46
+
47
+ end
48
+ end
49
+ end
50
+ end
@@ -6,6 +6,14 @@ module Tabs
6
6
 
7
7
  class UnstartedTaskMetricError < Exception; end
8
8
 
9
+ Stats = Struct.new(
10
+ :started_within_period,
11
+ :completed_within_period,
12
+ :started_and_completed_within_period,
13
+ :completion_rate,
14
+ :average_completion_time
15
+ )
16
+
9
17
  attr_reader :key
10
18
 
11
19
  def initialize(key)
@@ -30,13 +38,13 @@ module Tabs
30
38
  completion_rate = round_float(matching_tokens.size.to_f / range.size)
31
39
  elapsed_times = matching_tokens.map { |t| t.time_elapsed(resolution) }
32
40
  average_completion_time = (elapsed_times.inject(&:+)) / matching_tokens.size
33
- {
34
- started: started_tokens.size,
35
- completed: completed_tokens.size,
36
- completed_within_period: matching_tokens.size,
37
- completion_rate: completion_rate,
38
- average_completion_time: average_completion_time
39
- }
41
+ Stats.new(
42
+ started_tokens.size,
43
+ completed_tokens.size,
44
+ matching_tokens.size,
45
+ completion_rate,
46
+ average_completion_time
47
+ )
40
48
  end
41
49
 
42
50
  def drop!
@@ -15,21 +15,24 @@ module Tabs
15
15
  Tabs::RESOLUTIONS.each do |resolution|
16
16
  formatted_time = Tabs::Resolution.serialize(resolution, timestamp)
17
17
  stat_key = "stat:value:#{key}:data:#{formatted_time}"
18
- sadd("stat:value:#{key}:keys:#{resolution}", stat_key)
19
18
  update_values(stat_key, value)
20
19
  end
21
20
  true
22
21
  end
23
22
 
24
23
  def stats(period, resolution)
25
- period = normalize_period(period, resolution)
26
- keys = smembers("stat:value:#{key}:keys:#{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]] }
24
+ timestamps = timestamp_range period, resolution
25
+ keys = timestamps.map do |ts|
26
+ "stat:value:#{key}:data:#{Tabs::Resolution.serialize(resolution, ts)}"
27
+ end
28
+
29
+ values = mget(*keys).map do |v|
30
+ value = v.nil? ? default_value(0) : JSON.parse(v)
31
+ value["timestamp"] = timestamps.shift
32
+ value.with_indifferent_access
33
+ end
34
+
35
+ Stats.new(period, resolution, values)
33
36
  end
34
37
 
35
38
  def drop!
@@ -0,0 +1,54 @@
1
+ module Tabs
2
+ module Metrics
3
+ class Value
4
+ class Stats
5
+
6
+ include Enumerable
7
+ include Helpers
8
+
9
+ attr_reader :period, :resolution, :values
10
+
11
+ def initialize(period, resolution, values)
12
+ @period, @resolution, @values = period, resolution, values
13
+ end
14
+
15
+ def first
16
+ values.first
17
+ end
18
+
19
+ def last
20
+ values.last
21
+ end
22
+
23
+ def count
24
+ @count ||= values.map { |v| v["count"] }.sum
25
+ end
26
+
27
+ def sum
28
+ @sum ||= values.map { |v| v["sum"] }.sum
29
+ end
30
+
31
+ def min
32
+ @min ||= values.min_by { |v| v["min"] }["min"]
33
+ end
34
+
35
+ def max
36
+ @max ||= values.max_by { |v| v["max"] }["max"]
37
+ end
38
+
39
+ def avg
40
+ round_float(self.sum.to_f / self.count.to_f)
41
+ end
42
+
43
+ def each(&block)
44
+ values.each(&block)
45
+ end
46
+
47
+ def to_a
48
+ values
49
+ end
50
+
51
+ end
52
+ end
53
+ end
54
+ end
data/lib/tabs/tabs.rb CHANGED
@@ -59,7 +59,7 @@ module Tabs
59
59
  metric_klass(type).new(key)
60
60
  end
61
61
 
62
- def counter_total(key, period=nil)
62
+ def counter_total(key)
63
63
  raise UnknownMetricError.new("Unknown metric: #{key}") unless metric_exists?(key)
64
64
  raise MetricTypeMismatchError.new("Only counter metrics can be incremented") unless metric_type(key) == "counter"
65
65
  get_metric(key).total
@@ -84,13 +84,18 @@ module Tabs
84
84
  list_metrics.include? key
85
85
  end
86
86
 
87
- def drop_metric(key)
87
+ def drop_metric!(key)
88
88
  raise UnknownMetricError.new("Unknown metric: #{key}") unless metric_exists?(key)
89
89
  metric = get_metric(key)
90
90
  metric.drop!
91
91
  hdel "metrics", key
92
92
  end
93
93
 
94
+ def drop_all_metrics!
95
+ metrics = self.list_metrics
96
+ metrics.each { |key| self.drop_metric! key }
97
+ end
98
+
94
99
  private
95
100
 
96
101
  def metric_klass(type)
data/lib/tabs/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Tabs
2
- VERSION = "0.7.1"
2
+ VERSION = "0.8.0"
3
3
  end
@@ -0,0 +1,37 @@
1
+ require "spec_helper"
2
+
3
+ describe Tabs::Metrics::Counter::Stats do
4
+
5
+ let(:period) { (Time.now - 2.days..Time.now) }
6
+ let(:resolution) { :hour }
7
+ let(:values) do
8
+ [
9
+ { "timestamp" => Time.now - 30.hours, "count" => 44 },
10
+ { "timestamp" => Time.now - 20.hours, "count" => 123 },
11
+ { "timestamp" => Time.now - 10.hours, "count" => 92 }
12
+ ]
13
+ end
14
+ let(:stats) { Tabs::Metrics::Counter::Stats.new(period, resolution, values) }
15
+
16
+ it "is enumerable" do
17
+ expect(stats).to respond_to :each
18
+ expect(Tabs::Metrics::Counter::Stats.ancestors).to include Enumerable
19
+ end
20
+
21
+ it "#total returns the total count for the entire set" do
22
+ expect(stats.total).to eq 259
23
+ end
24
+
25
+ it "min returns the min for the entire set" do
26
+ expect(stats.min).to eq 44
27
+ end
28
+
29
+ it "max returns the max for the entire set" do
30
+ expect(stats.max).to eq 123
31
+ end
32
+
33
+ it "avg returns the average for the entire set" do
34
+ expect(stats.avg).to eq 86.33
35
+ end
36
+
37
+ end
@@ -15,14 +15,14 @@ describe Tabs::Metrics::Counter do
15
15
  metric.increment
16
16
  time = Time.utc(now.year, now.month, now.day, now.hour)
17
17
  stats = metric.stats(((now - 2.hours)..(now + 4.hours)), :hour)
18
- expect(stats).to include({ time => 1 })
18
+ expect(stats).to include({ "timestamp" => time, "count" => 1 })
19
19
  end
20
20
 
21
21
  it "applys the increment to the specified timestamp if one is supplied" do
22
22
  time = Time.utc(now.year, now.month, now.day, now.hour) - 2.hours
23
23
  metric.increment(time)
24
24
  stats = metric.stats(((now - 3.hours)..now), :hour)
25
- expect(stats).to include({ time => 1 })
25
+ expect(stats).to include({ "timestamp" => time, "count" => 1 })
26
26
  end
27
27
 
28
28
  end
@@ -59,42 +59,42 @@ describe Tabs::Metrics::Counter do
59
59
  it "returns the expected results for an minutely metric" do
60
60
  create_span(:minute)
61
61
  stats = metric.stats(now..(now + 7.minutes), :minute)
62
- expect(stats).to include({ (now + 3.minutes) => 1 })
63
- expect(stats).to include({ (now + 6.minutes) => 2 })
62
+ expect(stats).to include({ "timestamp" => (now + 3.minutes), "count" => 1 })
63
+ expect(stats).to include({ "timestamp" => (now + 6.minutes), "count" => 2 })
64
64
  end
65
65
 
66
66
  it "returns the expected results for an hourly metric" do
67
67
  create_span(:hours)
68
68
  stats = metric.stats(now..(now + 7.hours), :hour)
69
- expect(stats).to include({ (now + 3.hours) => 1 })
70
- expect(stats).to include({ (now + 6.hours) => 2 })
69
+ expect(stats).to include({ "timestamp" => (now + 3.hours), "count" => 1 })
70
+ expect(stats).to include({ "timestamp" => (now + 6.hours), "count" => 2 })
71
71
  end
72
72
 
73
73
  it "returns the expected results for a daily metric" do
74
74
  create_span(:days)
75
75
  stats = metric.stats(now..(now + 7.days), :day)
76
- expect(stats).to include({ (now + 3.days) => 1 })
77
- expect(stats).to include({ (now + 6.days) => 2 })
76
+ expect(stats).to include({ "timestamp" => (now + 3.days), "count" => 1 })
77
+ expect(stats).to include({ "timestamp" => (now + 6.days), "count" => 2 })
78
78
  end
79
79
 
80
80
  it "returns the expected results for a monthly metric" do
81
81
  create_span(:months)
82
82
  stats = metric.stats(now..(now + 7.months), :month)
83
- expect(stats).to include({ (now + 3.months) => 1 })
84
- expect(stats).to include({ (now + 6.months) => 2 })
83
+ expect(stats).to include({ "timestamp" => (now + 3.months), "count" => 1 })
84
+ expect(stats).to include({ "timestamp" => (now + 6.months), "count" => 2 })
85
85
  end
86
86
 
87
87
  it "returns the expected results for a yearly metric" do
88
88
  create_span(:years)
89
89
  stats = metric.stats(now..(now + 7.years), :year)
90
- expect(stats).to include({ (now + 3.years) => 1 })
91
- expect(stats).to include({ (now + 6.years) => 2 })
90
+ expect(stats).to include({ "timestamp" => (now + 3.years), "count" => 1 })
91
+ expect(stats).to include({ "timestamp" => (now + 6.years), "count" => 2 })
92
92
  end
93
93
 
94
94
  it "returns zeros for time periods which do not have any events" do
95
95
  create_span(:days)
96
96
  stats = metric.stats(now..(now + 7.days), :day)
97
- expect(stats).to include({ (now + 1.day) => 0 })
97
+ expect(stats).to include({ "timestamp" => (now + 1.day), "count" => 0 })
98
98
  end
99
99
 
100
100
  context "for weekly metrics" do
@@ -106,15 +106,15 @@ describe Tabs::Metrics::Counter do
106
106
  it "returns the expected results for a weekly metric" do
107
107
  create_span(:weeks)
108
108
  stats = metric.stats(period, :week)
109
- expect(stats).to include({ (now + 3.weeks).beginning_of_week => 1 })
110
- expect(stats).to include({ (now + 6.weeks).beginning_of_week => 2 })
109
+ expect(stats).to include({ "timestamp" => (now + 3.weeks).beginning_of_week, "count" => 1 })
110
+ expect(stats).to include({ "timestamp" => (now + 6.weeks).beginning_of_week, "count" => 2 })
111
111
  end
112
112
 
113
113
  it "normalizes the period to the first day of the week" do
114
114
  create_span(:weeks)
115
115
  stats = metric.stats(period, :week)
116
- expect(stats.first.keys[0]).to eq(period.first.beginning_of_week)
117
- expect(stats.last.keys[0]).to eq(period.last.beginning_of_week)
116
+ expect(stats.first["timestamp"]).to eq(period.first.beginning_of_week)
117
+ expect(stats.last["timestamp"]).to eq(period.last.beginning_of_week)
118
118
  end
119
119
 
120
120
  end
@@ -0,0 +1,41 @@
1
+ require "spec_helper"
2
+
3
+ describe Tabs::Metrics::Value::Stats do
4
+
5
+ let(:period) { (Time.now - 2.days..Time.now) }
6
+ let(:resolution) { :hour }
7
+ let(:values) do
8
+ [
9
+ { "timestamp" => Time.now - 30.hours, "count" => 10, "sum" => 145, "min" => 11, "max" => 204, "avg" => 14.5 },
10
+ { "timestamp" => Time.now - 20.hours, "count" => 15, "sum" => 288, "min" => 10, "max" => 199, "avg" => 19.2 },
11
+ { "timestamp" => Time.now - 10.hours, "count" => 25, "sum" => 405, "min" => 12, "max" => 210, "avg" => 16.2 }
12
+ ]
13
+ end
14
+ let(:stats) { Tabs::Metrics::Value::Stats.new(period, resolution, values) }
15
+
16
+ it "is enumerable" do
17
+ expect(stats).to respond_to :each
18
+ expect(Tabs::Metrics::Value::Stats.ancestors).to include Enumerable
19
+ end
20
+
21
+ it "#count returns the total count for the entire set" do
22
+ expect(stats.count).to eq 50
23
+ end
24
+
25
+ it "sum returns the sum for the entire set" do
26
+ expect(stats.sum).to eq 838
27
+ end
28
+
29
+ it "min returns the min for the entire set" do
30
+ expect(stats.min).to eq 10
31
+ end
32
+
33
+ it "max returns the max for the entire set" do
34
+ expect(stats.max).to eq 210
35
+ end
36
+
37
+ it "avg returns the average for the entire set" do
38
+ expect(stats.avg).to eq 16.76
39
+ end
40
+
41
+ end
@@ -16,14 +16,14 @@ describe Tabs::Metrics::Value do
16
16
  metric.record(42)
17
17
  time = Time.utc(now.year, now.month, now.day, now.hour)
18
18
  stats = metric.stats(((now - 2.hours)..(now + 4.hours)), :hour)
19
- expect(stats).to include({ time => {"count"=>2, "min"=>17, "max"=>42, "sum"=>59, "avg"=>29} })
19
+ expect(stats).to include({ "timestamp"=>time, "count"=>2, "min"=>17, "max"=>42, "sum"=>59, "avg"=>29})
20
20
  end
21
21
 
22
22
  it "applys the value to the specified timestamp if one is supplied" do
23
23
  time = Time.utc(now.year, now.month, now.day, now.hour) - 2.hours
24
24
  metric.record(42, time)
25
25
  stats = metric.stats(((now - 3.hours)..now), :hour)
26
- expect(stats).to include({ time => {"count"=>1, "min"=>42, "max"=>42, "sum"=>42, "avg"=>42} })
26
+ expect(stats).to include({ "timestamp"=>time, "count"=>1, "min"=>42, "max"=>42, "sum"=>42, "avg"=>42})
27
27
  end
28
28
 
29
29
  end
@@ -48,46 +48,52 @@ describe Tabs::Metrics::Value do
48
48
  Timecop.freeze(now)
49
49
  end
50
50
 
51
+ it "returns an instance of Tabs::Metrics::Value::Stats" do
52
+ create_span(:minutes)
53
+ stats = metric.stats(now..(now + 7.minutes), :minute)
54
+ expect(stats).to be_a_kind_of Tabs::Metrics::Value::Stats
55
+ end
56
+
51
57
  it "returns the expected results for an minutely metric" do
52
58
  create_span(:minutes)
53
59
  stats = metric.stats(now..(now + 7.minutes), :minute)
54
- expect(stats).to include({ (now + 3.minutes) => {"count"=>1, "min"=>10, "max"=>10, "sum"=>10, "avg"=>10} })
55
- expect(stats).to include({ (now + 6.minutes) => {"count"=>2, "min"=>15, "max"=>20, "sum"=>35, "avg"=>17} })
60
+ expect(stats).to include({ "timestamp" => (now + 3.minutes), "count"=>1, "min"=>10, "max"=>10, "sum"=>10, "avg"=>10})
61
+ expect(stats).to include({ "timestamp" => (now + 6.minutes), "count"=>2, "min"=>15, "max"=>20, "sum"=>35, "avg"=>17})
56
62
  end
57
63
 
58
64
  it "returns the expected results for an hourly metric" do
59
65
  create_span(:hours)
60
66
  stats = metric.stats(now..(now + 7.hours), :hour)
61
- expect(stats).to include({ (now + 3.hours) => {"count"=>1, "min"=>10, "max"=>10, "sum"=>10, "avg"=>10} })
62
- expect(stats).to include({ (now + 6.hours) => {"count"=>2, "min"=>15, "max"=>20, "sum"=>35, "avg"=>17} })
67
+ expect(stats).to include({ "timestamp" => (now + 3.hours), "count"=>1, "min"=>10, "max"=>10, "sum"=>10, "avg"=>10})
68
+ expect(stats).to include({ "timestamp" => (now + 6.hours), "count"=>2, "min"=>15, "max"=>20, "sum"=>35, "avg"=>17})
63
69
  end
64
70
 
65
71
  it "returns the expected results for a daily metric" do
66
72
  create_span(:days)
67
73
  stats = metric.stats(now..(now + 7.days), :day)
68
- expect(stats).to include({ (now + 3.days) => {"count"=>1, "min"=>10, "max"=>10, "sum"=>10, "avg"=>10} })
69
- expect(stats).to include({ (now + 6.days) => {"count"=>2, "min"=>15, "max"=>20, "sum"=>35, "avg"=>17} })
74
+ expect(stats).to include({ "timestamp" => (now + 3.days), "count"=>1, "min"=>10, "max"=>10, "sum"=>10, "avg"=>10})
75
+ expect(stats).to include({ "timestamp" => (now + 6.days), "count"=>2, "min"=>15, "max"=>20, "sum"=>35, "avg"=>17})
70
76
  end
71
77
 
72
78
  it "returns the expected results for a weekly metric" do
73
79
  create_span(:weeks)
74
80
  stats = metric.stats(now..(now + 7.weeks), :week)
75
- expect(stats).to include({ (now + 3.weeks).beginning_of_week => {"count"=>1, "min"=>10, "max"=>10, "sum"=>10, "avg"=>10} })
76
- expect(stats).to include({ (now + 6.weeks).beginning_of_week => {"count"=>2, "min"=>15, "max"=>20, "sum"=>35, "avg"=>17} })
81
+ expect(stats).to include({ "timestamp" => (now + 3.weeks).beginning_of_week, "count"=>1, "min"=>10, "max"=>10, "sum"=>10, "avg"=>10})
82
+ expect(stats).to include({ "timestamp" => (now + 6.weeks).beginning_of_week, "count"=>2, "min"=>15, "max"=>20, "sum"=>35, "avg"=>17})
77
83
  end
78
84
 
79
85
  it "returns the expected results for a monthly metric" do
80
86
  create_span(:months)
81
87
  stats = metric.stats(now..(now + 7.months), :month)
82
- expect(stats).to include({ (now + 3.months) => {"count"=>1, "min"=>10, "max"=>10, "sum"=>10, "avg"=>10} })
83
- expect(stats).to include({ (now + 6.months) => {"count"=>2, "min"=>15, "max"=>20, "sum"=>35, "avg"=>17} })
88
+ expect(stats).to include({ "timestamp" => (now + 3.months), "count"=>1, "min"=>10, "max"=>10, "sum"=>10, "avg"=>10})
89
+ expect(stats).to include({ "timestamp" => (now + 6.months), "count"=>2, "min"=>15, "max"=>20, "sum"=>35, "avg"=>17})
84
90
  end
85
91
 
86
92
  it "returns the expected results for a yearly metric" do
87
93
  create_span(:years)
88
94
  stats = metric.stats(now..(now + 7.years), :year)
89
- expect(stats).to include({ (now + 3.years) => {"count"=>1, "min"=>10, "max"=>10, "sum"=>10, "avg"=>10} })
90
- expect(stats).to include({ (now + 6.years) => {"count"=>2, "min"=>15, "max"=>20, "sum"=>35, "avg"=>17} })
95
+ expect(stats).to include({ "timestamp" => (now + 3.years), "count"=>1, "min"=>10, "max"=>10, "sum"=>10, "avg"=>10})
96
+ expect(stats).to include({ "timestamp" => (now + 6.years), "count"=>2, "min"=>15, "max"=>20, "sum"=>35, "avg"=>17})
91
97
  end
92
98
 
93
99
  end
@@ -63,15 +63,11 @@ describe Tabs::Metrics::Task do
63
63
  Timecop.freeze(now + 3.minutes)
64
64
  metric.complete(token_3)
65
65
  stats = metric.stats((now - 5.minutes)..(now + 5.minutes), :minute)
66
- expect(stats).to eq(
67
- {
68
- started: 3,
69
- completed: 2,
70
- completed_within_period: 2,
71
- completion_rate: 0.18,
72
- average_completion_time: 1.5
73
- }
74
- )
66
+ expect(stats.started_within_period).to eq 3
67
+ expect(stats.completed_within_period).to eq 2
68
+ expect(stats.started_and_completed_within_period).to eq 2
69
+ expect(stats.completion_rate).to eq 0.18
70
+ expect(stats.average_completion_time).to eq 1.5
75
71
  end
76
72
 
77
73
  end
@@ -3,7 +3,7 @@ require "spec_helper"
3
3
  describe Tabs do
4
4
  include Tabs::Storage
5
5
 
6
- describe "#create_metric" do
6
+ describe ".create_metric" do
7
7
 
8
8
  it "raises an error if the type is invalid" do
9
9
  lambda { Tabs.create_metric("foo", "foobar") }.should raise_error(Tabs::UnknownTypeError)
@@ -33,7 +33,7 @@ describe Tabs do
33
33
 
34
34
  end
35
35
 
36
- describe "#get_metric" do
36
+ describe ".get_metric" do
37
37
 
38
38
  it "returns the expected metric object" do
39
39
  Tabs.create_metric("foo", "counter")
@@ -42,7 +42,7 @@ describe Tabs do
42
42
 
43
43
  end
44
44
 
45
- describe "#list_metrics" do
45
+ describe ".list_metrics" do
46
46
 
47
47
  it "returns the list_metrics of metric names" do
48
48
  Tabs.create_metric("foo", "counter")
@@ -52,7 +52,7 @@ describe Tabs do
52
52
 
53
53
  end
54
54
 
55
- describe "#metric_exists?" do
55
+ describe ".metric_exists?" do
56
56
 
57
57
  it "returns true if the metric exists" do
58
58
  Tabs.create_metric("foo", "counter")
@@ -65,19 +65,19 @@ describe Tabs do
65
65
 
66
66
  end
67
67
 
68
- describe "#drop_metric" do
68
+ describe ".drop_metric" do
69
69
 
70
70
  before do
71
71
  Tabs.create_metric("foo", "counter")
72
72
  end
73
73
 
74
74
  it "removes the metric from the list_metrics" do
75
- Tabs.drop_metric("foo")
75
+ Tabs.drop_metric!("foo")
76
76
  expect(Tabs.list_metrics).to_not include("foo")
77
77
  end
78
78
 
79
79
  it "results in metric_exists? returning false" do
80
- Tabs.drop_metric("foo")
80
+ Tabs.drop_metric!("foo")
81
81
  expect(Tabs.metric_exists?("foo")).to be_false
82
82
  end
83
83
 
@@ -85,12 +85,24 @@ describe Tabs do
85
85
  metric = stub(:metric)
86
86
  Tabs.stub(get_metric: metric)
87
87
  metric.should_receive(:drop!)
88
- Tabs.drop_metric("foo")
88
+ Tabs.drop_metric!("foo")
89
89
  end
90
90
 
91
91
  end
92
92
 
93
- describe "#increment_counter" do
93
+ describe ".drop_all_metrics" do
94
+
95
+ it "drops all metrics" do
96
+ Tabs.create_metric("foo", "counter")
97
+ Tabs.create_metric("bar", "value")
98
+ Tabs.drop_all_metrics!
99
+ expect(Tabs.metric_exists?("foo")).to be_false
100
+ expect(Tabs.metric_exists?("bar")).to be_false
101
+ end
102
+
103
+ end
104
+
105
+ describe ".increment_counter" do
94
106
 
95
107
  it "raises a Tabs::MetricTypeMismatchError if the metric is the wrong type" do
96
108
  Tabs.create_metric("foo", "value")
@@ -112,7 +124,7 @@ describe Tabs do
112
124
 
113
125
  end
114
126
 
115
- describe "#record_value" do
127
+ describe ".record_value" do
116
128
 
117
129
  it "creates the metric if it doesn't exist" do
118
130
  expect(Tabs.metric_exists?("foo")).to be_false
@@ -134,7 +146,7 @@ describe Tabs do
134
146
 
135
147
  end
136
148
 
137
- describe "#list_metrics" do
149
+ describe ".list_metrics" do
138
150
 
139
151
  it "lists all metrics that are defined" do
140
152
  Tabs.create_metric("foo", "counter")
@@ -145,7 +157,7 @@ describe Tabs do
145
157
 
146
158
  end
147
159
 
148
- describe "#metric_type" do
160
+ describe ".metric_type" do
149
161
 
150
162
  it "returns the type of a counter metric" do
151
163
  Tabs.create_metric("foo", "counter")
data/tabs.gemspec CHANGED
@@ -18,12 +18,14 @@ Gem::Specification.new do |gem|
18
18
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
19
19
  gem.require_paths = ["lib"]
20
20
 
21
- gem.post_install_message = <<TXT
22
- **** NOTICE ****
23
- Please note there are breaking changes in this version of tabs!
24
- Existing data cannot be read because of changes to the redis key patterns.
25
- Please continue to use version 0.5.6 if you need to access existing metric data."
26
- TXT
21
+ gem.post_install_message = <<EOS
22
+ Tabs v0.8.0 - BREAKING CHANGES:
23
+ The get_stats method now returns a more robust object instead of just
24
+ an array of hashes. Existing data will continue to work (no changes were
25
+ made to the underlying Redis keys). However, application code using
26
+ tabs may need to be changed. Please review the README after installing
27
+ v0.8.0 or higher.
28
+ EOS
27
29
 
28
30
  gem.add_dependency "activesupport", ">= 3.2"
29
31
  gem.add_dependency "json", ">= 1.7"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tabs
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.1
4
+ version: 0.8.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-07-17 00:00:00.000000000 Z
12
+ date: 2013-07-22 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport
@@ -158,9 +158,11 @@ files:
158
158
  - lib/tabs/config.rb
159
159
  - lib/tabs/helpers.rb
160
160
  - lib/tabs/metrics/counter.rb
161
+ - lib/tabs/metrics/counter/stats.rb
161
162
  - lib/tabs/metrics/task.rb
162
163
  - lib/tabs/metrics/task/token.rb
163
164
  - lib/tabs/metrics/value.rb
165
+ - lib/tabs/metrics/value/stats.rb
164
166
  - lib/tabs/resolution.rb
165
167
  - lib/tabs/resolutions/day.rb
166
168
  - lib/tabs/resolutions/hour.rb
@@ -171,8 +173,10 @@ files:
171
173
  - lib/tabs/storage.rb
172
174
  - lib/tabs/tabs.rb
173
175
  - lib/tabs/version.rb
176
+ - spec/lib/tabs/metrics/counter/stats_spec.rb
174
177
  - spec/lib/tabs/metrics/counter_spec.rb
175
178
  - spec/lib/tabs/metrics/task/token_spec.rb
179
+ - spec/lib/tabs/metrics/value/stats_spec.rb
176
180
  - spec/lib/tabs/metrics/value_spec.rb
177
181
  - spec/lib/tabs/task_spec.rb
178
182
  - spec/lib/tabs_spec.rb
@@ -180,13 +184,17 @@ files:
180
184
  - tabs.gemspec
181
185
  homepage: https://github.com/thegrubbsian/tabs
182
186
  licenses: []
183
- post_install_message: ! '**** NOTICE ****
187
+ post_install_message: ! 'Tabs v0.8.0 - BREAKING CHANGES:
184
188
 
185
- Please note there are breaking changes in this version of tabs!
189
+ The get_stats method now returns a more robust object instead of just
186
190
 
187
- Existing data cannot be read because of changes to the redis key patterns.
191
+ an array of hashes. Existing data will continue to work (no changes were
188
192
 
189
- Please continue to use version 0.5.6 if you need to access existing metric data."
193
+ made to the underlying Redis keys). However, application code using
194
+
195
+ tabs may need to be changed. Please review the README after installing
196
+
197
+ v0.8.0 or higher.
190
198
 
191
199
  '
192
200
  rdoc_options: []
@@ -211,8 +219,10 @@ signing_key:
211
219
  specification_version: 3
212
220
  summary: A redis-backed metrics tracker for keeping tabs on pretty much anything ;)
213
221
  test_files:
222
+ - spec/lib/tabs/metrics/counter/stats_spec.rb
214
223
  - spec/lib/tabs/metrics/counter_spec.rb
215
224
  - spec/lib/tabs/metrics/task/token_spec.rb
225
+ - spec/lib/tabs/metrics/value/stats_spec.rb
216
226
  - spec/lib/tabs/metrics/value_spec.rb
217
227
  - spec/lib/tabs/task_spec.rb
218
228
  - spec/lib/tabs_spec.rb