tabstabs 2.0.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.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +20 -0
  3. data/.ruby-version +1 -0
  4. data/.travis.yml +7 -0
  5. data/Gemfile +3 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +421 -0
  8. data/Rakefile +5 -0
  9. data/lib/tabs_tabs.rb +26 -0
  10. data/lib/tabs_tabs/config.rb +65 -0
  11. data/lib/tabs_tabs/helpers.rb +27 -0
  12. data/lib/tabs_tabs/metrics/counter.rb +69 -0
  13. data/lib/tabs_tabs/metrics/counter/stats.rb +51 -0
  14. data/lib/tabs_tabs/metrics/task.rb +72 -0
  15. data/lib/tabs_tabs/metrics/task/token.rb +89 -0
  16. data/lib/tabs_tabs/metrics/value.rb +91 -0
  17. data/lib/tabs_tabs/metrics/value/stats.rb +55 -0
  18. data/lib/tabs_tabs/resolution.rb +65 -0
  19. data/lib/tabs_tabs/resolutionable.rb +48 -0
  20. data/lib/tabs_tabs/resolutions/day.rb +40 -0
  21. data/lib/tabs_tabs/resolutions/hour.rb +40 -0
  22. data/lib/tabs_tabs/resolutions/minute.rb +40 -0
  23. data/lib/tabs_tabs/resolutions/month.rb +40 -0
  24. data/lib/tabs_tabs/resolutions/week.rb +40 -0
  25. data/lib/tabs_tabs/resolutions/year.rb +40 -0
  26. data/lib/tabs_tabs/storage.rb +105 -0
  27. data/lib/tabs_tabs/tabs_tabs.rb +117 -0
  28. data/lib/tabs_tabs/version.rb +3 -0
  29. data/spec/lib/tabs_tabs/config_spec.rb +60 -0
  30. data/spec/lib/tabs_tabs/metrics/counter/stats_spec.rb +42 -0
  31. data/spec/lib/tabs_tabs/metrics/counter_spec.rb +196 -0
  32. data/spec/lib/tabs_tabs/metrics/task/token_spec.rb +18 -0
  33. data/spec/lib/tabs_tabs/metrics/task_spec.rb +103 -0
  34. data/spec/lib/tabs_tabs/metrics/value/stats_spec.rb +61 -0
  35. data/spec/lib/tabs_tabs/metrics/value_spec.rb +160 -0
  36. data/spec/lib/tabs_tabs/resolution_spec.rb +52 -0
  37. data/spec/lib/tabs_tabs/resolutionable_spec.rb +53 -0
  38. data/spec/lib/tabs_tabs/resolutions/day_spec.rb +23 -0
  39. data/spec/lib/tabs_tabs/resolutions/hour_spec.rb +23 -0
  40. data/spec/lib/tabs_tabs/resolutions/minute_spec.rb +23 -0
  41. data/spec/lib/tabs_tabs/resolutions/month_spec.rb +23 -0
  42. data/spec/lib/tabs_tabs/resolutions/week_spec.rb +24 -0
  43. data/spec/lib/tabs_tabs/resolutions/year_spec.rb +23 -0
  44. data/spec/lib/tabs_tabs/storage_spec.rb +138 -0
  45. data/spec/lib/tabs_tabs_spec.rb +223 -0
  46. data/spec/spec_helper.rb +17 -0
  47. data/spec/support/custom_resolutions.rb +40 -0
  48. data/tabs_tabs.gemspec +31 -0
  49. metadata +213 -0
@@ -0,0 +1,117 @@
1
+ module TabsTabs
2
+ extend self
3
+ extend TabsTabs::Storage
4
+
5
+ class UnknownTypeError < StandardError; end
6
+ class DuplicateMetricError < StandardError; end
7
+ class UnknownMetricError < StandardError; end
8
+ class MetricTypeMismatchError < StandardError; end
9
+ class ResolutionMissingError < StandardError; end
10
+
11
+ METRIC_TYPES = ["counter", "value", "task"]
12
+
13
+ def configure
14
+ yield(Config)
15
+ end
16
+
17
+ def redis
18
+ Config.redis
19
+ end
20
+
21
+ def config
22
+ Config
23
+ end
24
+
25
+ def increment_counter(key, timestamp=Time.now)
26
+ create_metric(key, "counter") unless metric_exists?(key)
27
+ raise MetricTypeMismatchError.new("Only counter metrics can be incremented") unless metric_type(key) == "counter"
28
+ get_metric(key).increment(timestamp)
29
+ end
30
+
31
+ def record_value(key, value, timestamp=Time.now)
32
+ create_metric(key, "value") unless metric_exists?(key)
33
+ raise MetricTypeMismatchError.new("Only value metrics can record a value") unless metric_type(key) == "value"
34
+ get_metric(key).record(value, timestamp)
35
+ end
36
+
37
+ def start_task(key, token, timestamp=Time.now)
38
+ create_metric(key, "task")
39
+ raise MetricTypeMismatchError.new("Only task metrics can start a task") unless metric_type(key) == "task"
40
+ get_metric(key).start(token, timestamp)
41
+ end
42
+
43
+ def complete_task(key, token, timestamp=Time.now)
44
+ raise MetricTypeMismatchError.new("Only task metrics can complete a task") unless metric_type(key) == "task"
45
+ get_metric(key).complete(token, timestamp)
46
+ end
47
+
48
+ def create_metric(key, type)
49
+ raise UnknownTypeError.new("Unknown metric type: #{type}") unless METRIC_TYPES.include?(type)
50
+ raise DuplicateMetricError.new("Metric already exists: #{key}") if metric_exists?(key)
51
+ hset "metrics", key, type
52
+ metric_klass(type).new(key)
53
+ end
54
+
55
+ def get_metric(key)
56
+ raise UnknownMetricError.new("Unknown metric: #{key}") unless metric_exists?(key)
57
+ type = hget("metrics", key)
58
+ metric_klass(type).new(key)
59
+ end
60
+
61
+ def counter_total(key)
62
+ unless metric_exists?(key)
63
+ if block_given?
64
+ return yield
65
+ else
66
+ raise UnknownMetricError.new("Unknown metric: #{key}")
67
+ end
68
+ end
69
+ raise MetricTypeMismatchError.new("Only counter metrics can be incremented") unless metric_type(key) == "counter"
70
+ get_metric(key).total
71
+ end
72
+
73
+ def get_stats(key, period, resolution)
74
+ raise UnknownMetricError.new("Unknown metric: #{key}") unless metric_exists?(key)
75
+ metric = get_metric(key)
76
+ metric.stats(period, resolution)
77
+ end
78
+
79
+ def metric_type(key)
80
+ raise UnknownMetricError.new("Unknown metric: #{key}") unless metric_exists?(key)
81
+ hget "metrics", key
82
+ end
83
+
84
+ def list_metrics
85
+ hkeys "metrics"
86
+ end
87
+
88
+ def metric_exists?(key)
89
+ list_metrics.include? key
90
+ end
91
+
92
+ def drop_metric!(key)
93
+ raise UnknownMetricError.new("Unknown metric: #{key}") unless metric_exists?(key)
94
+ metric = get_metric(key)
95
+ metric.drop!
96
+ hdel "metrics", key
97
+ end
98
+
99
+ def drop_all_metrics!
100
+ metrics = self.list_metrics
101
+ metrics.each { |key| self.drop_metric! key }
102
+ end
103
+
104
+ def drop_resolution_for_metric!(key, resolution)
105
+ raise UnknownMetricError.new("Unknown metric: #{key}") unless metric_exists?(key)
106
+ raise ResolutionMissingError.new(resolution) unless TabsTabs::Resolution.all.include? resolution
107
+ metric = get_metric(key)
108
+ metric.drop_by_resolution!(resolution) unless metric_type(key) == "task"
109
+ end
110
+
111
+ private
112
+
113
+ def metric_klass(type)
114
+ "TabsTabs::Metrics::#{type.classify}".constantize
115
+ end
116
+
117
+ end
@@ -0,0 +1,3 @@
1
+ module TabsTabs
2
+ VERSION = "2.0.0"
3
+ end
@@ -0,0 +1,60 @@
1
+ require "spec_helper"
2
+ require File.expand_path("../../../support/custom_resolutions", __FILE__)
3
+
4
+ describe TabsTabs::Config do
5
+ context "#decimal_precision" do
6
+
7
+ before do
8
+ @precision = TabsTabs::Config.decimal_precision
9
+ end
10
+
11
+ after do
12
+ TabsTabs::Config.decimal_precision = @precision
13
+ end
14
+
15
+ it "should set/get the decimal precision" do
16
+ TabsTabs::Config.decimal_precision = 4
17
+ expect(TabsTabs::Config.decimal_precision).to eq(4)
18
+ end
19
+ end
20
+
21
+ context "#register_resolution" do
22
+ it "should register a resolution" do
23
+ TabsTabs::Resolution.register(WellFormedResolution)
24
+ expect(TabsTabs::Resolution.all).to include(:seconds)
25
+ end
26
+ end
27
+
28
+ context "#unregister_resolution" do
29
+ it "should unregister a resolution" do
30
+ TabsTabs::Resolution.unregister(:minute)
31
+ expect(TabsTabs::Resolution.all).to_not include(:minute)
32
+ end
33
+ end
34
+
35
+ context "#set_expirations" do
36
+
37
+ after do
38
+ TabsTabs::Config.reset_expirations
39
+ end
40
+
41
+ it "should allow multiple resolutions to be expired" do
42
+ TabsTabs::Config.set_expirations({minute: 1.day, hour: 1.week })
43
+ expect(TabsTabs::Config.expiration_settings[:minute]).to eq(1.day)
44
+ expect(TabsTabs::Config.expiration_settings[:hour]).to eq(1.week)
45
+ end
46
+
47
+ it "should raise ResolutionMissingError if expiration passed in for invalid resolution" do
48
+ expect{ TabsTabs::Config.set_expirations({missing_resolution: 1.day }) }
49
+ .to raise_error(TabsTabs::ResolutionMissingError)
50
+ end
51
+
52
+ end
53
+
54
+ context "#prefix" do
55
+ it "should allow custom prefix for tabstabs keys" do
56
+ TabsTabs::Config.prefix = "rspec"
57
+ expect(TabsTabs::Config.prefix).to eq("rspec")
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,42 @@
1
+ require "spec_helper"
2
+
3
+ describe TabsTabs::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) { TabsTabs::Metrics::Counter::Stats.new(period, resolution, values) }
15
+
16
+ it "is enumerable" do
17
+ expect(stats).to respond_to :each
18
+ expect(TabsTabs::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.33333
35
+ end
36
+
37
+ it "avg returns 0 if values empty" do
38
+ stats = TabsTabs::Metrics::Counter::Stats.new(period, resolution, [])
39
+ expect(stats.avg).to be_zero
40
+ end
41
+
42
+ end
@@ -0,0 +1,196 @@
1
+ require "spec_helper"
2
+
3
+ describe TabsTabs::Metrics::Counter do
4
+
5
+ include TabsTabs::Storage
6
+
7
+ let(:metric) { TabsTabs.create_metric("foo", "counter") }
8
+ let(:now) { Time.utc(2000, 1, 1, 0, 0) }
9
+
10
+ describe "incrementing stats" do
11
+
12
+ before { Timecop.freeze(now) }
13
+
14
+ it "increments the value for the expected periods" do
15
+ metric.increment
16
+ time = Time.utc(now.year, now.month, now.day, now.hour)
17
+ stats = metric.stats(((now - 2.hours)..(now + 4.hours)), :hour)
18
+ expect(stats).to include({ "timestamp" => time, "count" => 1 })
19
+ end
20
+
21
+ it "applys the increment to the specified timestamp if one is supplied" do
22
+ time = Time.utc(now.year, now.month, now.day, now.hour) - 2.hours
23
+ metric.increment(time)
24
+ stats = metric.stats(((now - 3.hours)..now), :hour)
25
+ expect(stats).to include({ "timestamp" => time, "count" => 1 })
26
+ end
27
+
28
+ it "raises ResolutionMissingError if unregistered resolution requested" do
29
+ time = Time.utc(now.year, now.month, now.day, now.hour) - 2.hours
30
+ metric.increment(time)
31
+ TabsTabs::Resolution.unregister(:hour)
32
+ expect { metric.stats(((now - 3.hours)..now), :hour) }.to raise_error(TabsTabs::ResolutionMissingError)
33
+ end
34
+
35
+ end
36
+
37
+ describe "total count" do
38
+
39
+ it "is incremented every time regardless of resolution" do
40
+ 30.times { metric.increment }
41
+ expect(metric.total).to eq(30)
42
+ end
43
+
44
+ end
45
+
46
+ describe "retrieving stats" do
47
+
48
+ before do
49
+ Timecop.freeze(now)
50
+ end
51
+
52
+ after do
53
+ Timecop.return
54
+ end
55
+
56
+ def create_span(time_unit)
57
+ metric.increment
58
+ Timecop.freeze(now + 1.send(time_unit))
59
+ metric.increment
60
+ Timecop.freeze(now + 3.send(time_unit))
61
+ metric.increment
62
+ Timecop.freeze(now + 6.send(time_unit))
63
+ metric.increment
64
+ metric.increment
65
+ Timecop.freeze(now)
66
+ end
67
+
68
+ it "returns the expected results for an minutely metric" do
69
+ create_span(:minute)
70
+ stats = metric.stats(now..(now + 7.minutes), :minute)
71
+ expect(stats).to include({ "timestamp" => (now + 3.minutes), "count" => 1 })
72
+ expect(stats).to include({ "timestamp" => (now + 6.minutes), "count" => 2 })
73
+ end
74
+
75
+ it "returns the expected results for an hourly metric" do
76
+ create_span(:hours)
77
+ stats = metric.stats(now..(now + 7.hours), :hour)
78
+ expect(stats).to include({ "timestamp" => (now + 3.hours), "count" => 1 })
79
+ expect(stats).to include({ "timestamp" => (now + 6.hours), "count" => 2 })
80
+ end
81
+
82
+ it "returns the expected results for a daily metric" do
83
+ create_span(:days)
84
+ stats = metric.stats(now..(now + 7.days), :day)
85
+ expect(stats).to include({ "timestamp" => (now + 3.days), "count" => 1 })
86
+ expect(stats).to include({ "timestamp" => (now + 6.days), "count" => 2 })
87
+ end
88
+
89
+ it "returns the expected results for a monthly metric" do
90
+ create_span(:months)
91
+ stats = metric.stats(now..(now + 7.months), :month)
92
+ expect(stats).to include({ "timestamp" => (now + 3.months), "count" => 1 })
93
+ expect(stats).to include({ "timestamp" => (now + 6.months), "count" => 2 })
94
+ end
95
+
96
+ it "returns the expected results for a yearly metric" do
97
+ create_span(:years)
98
+ stats = metric.stats(now..(now + 7.years), :year)
99
+ expect(stats).to include({ "timestamp" => (now + 3.years), "count" => 1 })
100
+ expect(stats).to include({ "timestamp" => (now + 6.years), "count" => 2 })
101
+ end
102
+
103
+ it "returns zeros for time periods which do not have any events" do
104
+ create_span(:days)
105
+ stats = metric.stats(now..(now + 7.days), :day)
106
+ expect(stats.detect{|s| s["timestamp"] == (now + 2.day)}["count"]).to eq(0)
107
+ end
108
+
109
+ context "for weekly metrics" do
110
+
111
+ let(:period) do
112
+ (now - 2.days)..((now + 7.weeks) + 2.days)
113
+ end
114
+
115
+ it "returns the expected results for a weekly metric" do
116
+ create_span(:weeks)
117
+ stats = metric.stats(period, :week)
118
+ expect(stats.detect{|s| s["timestamp"] == (now + 1.week).beginning_of_week}["count"]).to eq(1)
119
+ expect(stats).to include({ "timestamp" => (now + 3.weeks).beginning_of_week, "count" => 1 })
120
+ expect(stats).to include({ "timestamp" => (now + 6.weeks).beginning_of_week, "count" => 2 })
121
+ end
122
+
123
+ it "normalizes the period to the first day of the week" do
124
+ create_span(:weeks)
125
+ stats = metric.stats(period, :week)
126
+ expect(stats.first["timestamp"]).to eq(period.first.beginning_of_week)
127
+ expect(stats.last["timestamp"]).to eq(period.last.beginning_of_week)
128
+ end
129
+
130
+ end
131
+
132
+ end
133
+
134
+ describe ".drop!" do
135
+
136
+ before do
137
+ 3.times { metric.increment }
138
+ expect(exists("stat:counter:foo:total")).to be_truthy
139
+ @count_keys = (TabsTabs::Resolution.all.map do |res|
140
+ smembers("stat:counter:foo:keys:#{res}")
141
+ end).flatten
142
+ metric.drop!
143
+ end
144
+
145
+ it "deletes the counter total key" do
146
+ expect(exists("stat:counter:foo:total")).to be_falsey
147
+ end
148
+
149
+ it "deletes all resolution count keys" do
150
+ @count_keys.each do |key|
151
+ expect(exists(key)).to be_falsey
152
+ end
153
+ end
154
+
155
+ it "deletes all resolution key collection keys" do
156
+ TabsTabs::Resolution.all.each do |res|
157
+ expect(exists("stat:counter:foo:keys:#{res}")).to be_falsey
158
+ end
159
+ end
160
+
161
+ end
162
+
163
+ describe ".drop_by_resolution!" do
164
+ before do
165
+ Timecop.freeze(now)
166
+ 2.times { metric.increment }
167
+ metric.drop_by_resolution!(:minute)
168
+ end
169
+
170
+ it "deletes all metrics for a resolution" do
171
+ stats = metric.stats((now - 1.minute)..(now + 1.minute), :minute)
172
+ expect(stats.total).to eq(0)
173
+ end
174
+ end
175
+
176
+ describe "expiration of counter metrics" do
177
+ let(:expires_setting){ 6.hours }
178
+ let(:now){ Time.utc(2050, 1, 1, 0, 0) }
179
+
180
+ before do
181
+ TabsTabs::Config.set_expirations({minute: expires_setting })
182
+ end
183
+
184
+ after do
185
+ TabsTabs::Config.reset_expirations
186
+ end
187
+
188
+ it "sets an expiration when recording a value" do
189
+ metric.increment(now)
190
+ redis_expire_date = Time.now + TabsTabs::Storage.ttl(metric.storage_key(:minute, now))
191
+ expire_date = now + expires_setting + TabsTabs::Resolutions::Minute.to_seconds
192
+ expect(redis_expire_date).to be_within(2.seconds).of(expire_date)
193
+ end
194
+ end
195
+
196
+ end
@@ -0,0 +1,18 @@
1
+ require "spec_helper"
2
+
3
+ describe TabsTabs::Metrics::Task::Token do
4
+
5
+ describe "#time_elapsed" do
6
+
7
+ let(:token) { TabsTabs::Metrics::Task::Token.new("foo", "bar") }
8
+ let(:time) { Time.now }
9
+
10
+ it "should return the time between when the task/token started and completed" do
11
+ token.start(time - 2.days)
12
+ token.complete(time - 1.day)
13
+ expect(token.time_elapsed(:hour)).to eq(24)
14
+ end
15
+
16
+ end
17
+
18
+ end
@@ -0,0 +1,103 @@
1
+ require "spec_helper"
2
+
3
+ describe TabsTabs::Metrics::Task do
4
+
5
+ let(:metric) { TabsTabs.create_metric("foo", "task") }
6
+ let(:now) { Time.utc(2000, 1, 1, 0, 0) }
7
+ let(:token_1) { "aaaa" }
8
+ let(:token_2) { "bbbb" }
9
+ let(:token_3) { "cccc" }
10
+
11
+ describe ".start" do
12
+
13
+ let(:token) { double(:token) }
14
+ let(:time) { Time.now }
15
+
16
+ it "calls start on the given token" do
17
+ expect(TabsTabs::Metrics::Task::Token).to receive(:new).with(token_1, "foo").and_return(token)
18
+ expect(token).to receive(:start)
19
+ metric.start(token_1)
20
+ end
21
+
22
+ it "passes through the specified timestamp" do
23
+ allow(TabsTabs::Metrics::Task::Token).to receive(:new).and_return(token)
24
+ expect(token).to receive(:start).with(time)
25
+ metric.start(token_1, time)
26
+ end
27
+
28
+ end
29
+
30
+ describe ".complete" do
31
+
32
+ let(:token) { double(:token) }
33
+ let(:time) { Time.now }
34
+
35
+ it "calls complete on the given token" do
36
+ expect(TabsTabs::Metrics::Task::Token).to receive(:new).with(token_1, "foo").and_return(token)
37
+ expect(token).to receive(:complete)
38
+ metric.complete(token_1)
39
+ end
40
+
41
+ it "passes through the specified timestamp" do
42
+ allow(TabsTabs::Metrics::Task::Token).to receive(:new).and_return(token)
43
+ expect(token).to receive(:complete).with(time)
44
+ metric.complete(token_1, time)
45
+ end
46
+
47
+ it "raises an UnstartedTaskMetricError if the metric has not yet been started" do
48
+ expect { metric.complete("foobar") }.to raise_error(TabsTabs::Metrics::Task::UnstartedTaskMetricError)
49
+ end
50
+
51
+ end
52
+
53
+ describe ".stats" do
54
+
55
+ it "returns zeroes across the board for no stats" do
56
+ stats = metric.stats((now - 5.minutes)..(now + 5.minutes), :minute)
57
+
58
+ expect(stats.started_within_period).to eq 0
59
+ expect(stats.completed_within_period).to eq 0
60
+ expect(stats.started_and_completed_within_period).to eq 0
61
+ expect(stats.completion_rate).to eq 0.0
62
+ expect(stats.average_completion_time).to eq 0.0
63
+ end
64
+
65
+ it "returns the expected value" do
66
+ Timecop.freeze(now)
67
+ metric.start(token_1)
68
+ metric.start(token_2)
69
+ Timecop.freeze(now + 2.minutes)
70
+ metric.complete(token_1)
71
+ metric.start(token_3)
72
+ Timecop.freeze(now + 3.minutes)
73
+ metric.complete(token_3)
74
+ stats = metric.stats((now - 5.minutes)..(now + 5.minutes), :minute)
75
+
76
+ expect(stats.started_within_period).to eq 3
77
+ expect(stats.completed_within_period).to eq 2
78
+ expect(stats.started_and_completed_within_period).to eq 2
79
+ expect(stats.completion_rate).to eq 0.18182
80
+ expect(stats.average_completion_time).to eq 1.5
81
+ end
82
+
83
+ it "returns the expected value for a week" do
84
+ Timecop.freeze(now)
85
+ metric.start(token_1)
86
+ metric.start(token_2)
87
+ Timecop.freeze(now + 1.week)
88
+ metric.complete(token_1)
89
+ metric.start(token_3)
90
+ Timecop.freeze(now + 3.weeks)
91
+ metric.complete(token_3)
92
+ stats = metric.stats((now - 5.weeks)..(now + 5.weeks), :week)
93
+
94
+ expect(stats.started_within_period).to eq 3
95
+ expect(stats.completed_within_period).to eq 2
96
+ expect(stats.started_and_completed_within_period).to eq 2
97
+ expect(stats.completion_rate).to eq 0.18182
98
+ expect(stats.average_completion_time).to eq 1.5
99
+ end
100
+
101
+ end
102
+
103
+ end