tabstabs 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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