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 +90 -40
- data/lib/tabs.rb +3 -1
- data/lib/tabs/helpers.rb +2 -14
- data/lib/tabs/metrics/counter.rb +13 -9
- data/lib/tabs/metrics/counter/stats.rb +50 -0
- data/lib/tabs/metrics/task.rb +15 -7
- data/lib/tabs/metrics/value.rb +12 -9
- data/lib/tabs/metrics/value/stats.rb +54 -0
- data/lib/tabs/tabs.rb +7 -2
- data/lib/tabs/version.rb +1 -1
- data/spec/lib/tabs/metrics/counter/stats_spec.rb +37 -0
- data/spec/lib/tabs/metrics/counter_spec.rb +17 -17
- data/spec/lib/tabs/metrics/value/stats_spec.rb +41 -0
- data/spec/lib/tabs/metrics/value_spec.rb +20 -14
- data/spec/lib/tabs/task_spec.rb +5 -9
- data/spec/lib/tabs_spec.rb +24 -12
- data/tabs.gemspec +8 -6
- metadata +16 -6
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'
|
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
|
65
|
-
|
66
|
-
```ruby
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
{ 2000-01-01
|
72
|
-
{ 2000-01-01
|
73
|
-
{ 2000-01-01
|
74
|
-
{ 2000-01-01
|
75
|
-
{ 2000-01-01
|
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
|
-
|
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
|
-
|
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
|
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
|
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
|
-
|
119
|
-
|
120
|
-
{ 2000-
|
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
|
185
|
+
This will return a `Tabs::Metrics::Task::Stats` object:
|
159
186
|
|
160
187
|
```ruby
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
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
|
data/lib/tabs/metrics/counter.rb
CHANGED
@@ -20,14 +20,19 @@ module Tabs
|
|
20
20
|
end
|
21
21
|
|
22
22
|
def stats(period, resolution)
|
23
|
-
|
24
|
-
keys =
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
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
|
data/lib/tabs/metrics/task.rb
CHANGED
@@ -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
|
-
|
35
|
-
|
36
|
-
|
37
|
-
completion_rate
|
38
|
-
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!
|
data/lib/tabs/metrics/value.rb
CHANGED
@@ -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
|
-
|
26
|
-
keys =
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
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
@@ -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
|
117
|
-
expect(stats.last
|
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
|
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
|
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)
|
55
|
-
expect(stats).to include({ (now + 6.minutes)
|
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)
|
62
|
-
expect(stats).to include({ (now + 6.hours)
|
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)
|
69
|
-
expect(stats).to include({ (now + 6.days)
|
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
|
76
|
-
expect(stats).to include({ (now + 6.weeks).beginning_of_week
|
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)
|
83
|
-
expect(stats).to include({ (now + 6.months)
|
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)
|
90
|
-
expect(stats).to include({ (now + 6.years)
|
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
|
data/spec/lib/tabs/task_spec.rb
CHANGED
@@ -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
|
-
|
69
|
-
|
70
|
-
|
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
|
data/spec/lib/tabs_spec.rb
CHANGED
@@ -3,7 +3,7 @@ require "spec_helper"
|
|
3
3
|
describe Tabs do
|
4
4
|
include Tabs::Storage
|
5
5
|
|
6
|
-
describe "
|
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 "
|
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 "
|
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 "
|
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 "
|
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 "
|
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 "
|
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 "
|
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 "
|
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 = <<
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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.
|
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-
|
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: ! '
|
187
|
+
post_install_message: ! 'Tabs v0.8.0 - BREAKING CHANGES:
|
184
188
|
|
185
|
-
|
189
|
+
The get_stats method now returns a more robust object instead of just
|
186
190
|
|
187
|
-
|
191
|
+
an array of hashes. Existing data will continue to work (no changes were
|
188
192
|
|
189
|
-
|
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
|