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,5 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+ task :default => :spec
@@ -0,0 +1,26 @@
1
+ require "active_support/all"
2
+ require "redis"
3
+ require "json/ext"
4
+
5
+ require "tabs_tabs/version"
6
+ require "tabs_tabs/config"
7
+ require "tabs_tabs/storage"
8
+ require "tabs_tabs/helpers"
9
+
10
+ require "tabs_tabs/resolutionable"
11
+ require "tabs_tabs/resolutions/minute"
12
+ require "tabs_tabs/resolutions/hour"
13
+ require "tabs_tabs/resolutions/day"
14
+ require "tabs_tabs/resolutions/week"
15
+ require "tabs_tabs/resolutions/month"
16
+ require "tabs_tabs/resolutions/year"
17
+ require "tabs_tabs/resolution"
18
+
19
+ require "tabs_tabs/metrics/counter/stats"
20
+ require "tabs_tabs/metrics/counter"
21
+ require "tabs_tabs/metrics/value/stats"
22
+ require "tabs_tabs/metrics/value"
23
+ require "tabs_tabs/metrics/task/token"
24
+ require "tabs_tabs/metrics/task"
25
+
26
+ require "tabs_tabs/tabs_tabs"
@@ -0,0 +1,65 @@
1
+ module TabsTabs
2
+ module Config
3
+ extend self
4
+
5
+ def decimal_precision
6
+ @decimal_precision ||= 5
7
+ end
8
+
9
+ def decimal_precision=(precision)
10
+ @decimal_precision = precision
11
+ end
12
+
13
+ def redis=(arg)
14
+ if arg.is_a?(Redis)
15
+ @redis = arg
16
+ else
17
+ @redis = Redis.new(arg)
18
+ end
19
+ end
20
+
21
+ def redis
22
+ @redis ||= Redis.new
23
+ end
24
+
25
+ def prefix=(arg)
26
+ @prefix = arg
27
+ end
28
+
29
+ def prefix
30
+ @prefix
31
+ end
32
+
33
+ def register_resolution(klass)
34
+ TabsTabs::Resolution.register(klass)
35
+ end
36
+
37
+ def unregister_resolutions(*resolutions)
38
+ TabsTabs::Resolution.unregister(resolutions)
39
+ end
40
+
41
+ def expiration_settings
42
+ @expiration_settings ||= {}
43
+ end
44
+
45
+ def set_expirations(resolution_hash)
46
+ resolution_hash.each do |resolution, expires_in_seconds|
47
+ raise TabsTabs::ResolutionMissingError.new(resolution) unless TabsTabs::Resolution.all.include? resolution
48
+ expiration_settings[resolution] = expires_in_seconds
49
+ end
50
+ end
51
+
52
+ def expires?(resolution)
53
+ expiration_settings.has_key?(resolution)
54
+ end
55
+
56
+ def expires_in(resolution)
57
+ expiration_settings[resolution]
58
+ end
59
+
60
+ def reset_expirations
61
+ @expiration_settings = {}
62
+ end
63
+
64
+ end
65
+ end
@@ -0,0 +1,27 @@
1
+ module TabsTabs
2
+ module Helpers
3
+ extend self
4
+
5
+ def timestamp_range(period, resolution)
6
+ period = normalize_period(period, resolution)
7
+ dt = period.first
8
+ [].tap do |arr|
9
+ arr << dt
10
+ while (dt = TabsTabs::Resolution.add(resolution, dt, 1)) <= period.last
11
+ arr << dt.utc
12
+ end
13
+ end
14
+ end
15
+
16
+ def normalize_period(period, resolution)
17
+ period_start = TabsTabs::Resolution.normalize(resolution, period.first.utc)
18
+ period_end = TabsTabs::Resolution.normalize(resolution, period.last.utc)
19
+ (period_start..period_end)
20
+ end
21
+
22
+ def to_numeric(v)
23
+ ((float = Float(v)) && (float % 1.0 == 0) ? float.to_i : float) rescue v
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,69 @@
1
+ module TabsTabs
2
+ module Metrics
3
+ class Counter
4
+ include Storage
5
+ include Helpers
6
+
7
+ attr_reader :key
8
+
9
+ def initialize(key)
10
+ @key = key
11
+ end
12
+
13
+ def increment(timestamp=Time.now)
14
+ timestamp.utc
15
+ TabsTabs::Resolution.all.each do |resolution|
16
+ increment_resolution(resolution, timestamp)
17
+ end
18
+ increment_total
19
+ true
20
+ end
21
+
22
+ def stats(period, resolution)
23
+ timestamps = timestamp_range period, resolution
24
+ keys = timestamps.map do |timestamp|
25
+ storage_key(resolution, timestamp)
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)
36
+ end
37
+
38
+ def total
39
+ (get("stat:counter:#{key}:total") || 0).to_i
40
+ end
41
+
42
+ def drop!
43
+ del_by_prefix("stat:counter:#{key}")
44
+ end
45
+
46
+ def drop_by_resolution!(resolution)
47
+ del_by_prefix("stat:counter:#{key}:count:#{resolution}")
48
+ end
49
+
50
+ def storage_key(resolution, timestamp)
51
+ formatted_time = TabsTabs::Resolution.serialize(resolution, timestamp)
52
+ "stat:counter:#{key}:count:#{resolution}:#{formatted_time}"
53
+ end
54
+
55
+ private
56
+
57
+ def increment_resolution(resolution, timestamp)
58
+ store_key = storage_key(resolution, timestamp)
59
+ incr(store_key)
60
+ TabsTabs::Resolution.expire(resolution, store_key, timestamp)
61
+ end
62
+
63
+ def increment_total
64
+ incr("stat:counter:#{key}:total")
65
+ end
66
+
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,51 @@
1
+ module TabsTabs
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
+ return 0 if values.size.zero?
37
+ (self.total.to_f / values.size.to_f).round(Config.decimal_precision)
38
+ end
39
+
40
+ def each(&block)
41
+ values.each(&block)
42
+ end
43
+
44
+ def to_a
45
+ values
46
+ end
47
+
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,72 @@
1
+ module TabsTabs
2
+ module Metrics
3
+ class Task
4
+ include TabsTabs::Storage
5
+ include TabsTabs::Helpers
6
+
7
+ class UnstartedTaskMetricError < Exception; end
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
+
17
+ attr_reader :key
18
+
19
+ def initialize(key)
20
+ @key = key
21
+ end
22
+
23
+ def start(token, timestamp=Time.now)
24
+ Token.new(token, key).start(timestamp)
25
+ true
26
+ end
27
+
28
+ def complete(token, timestamp=Time.now)
29
+ Token.new(token, key).complete(timestamp)
30
+ true
31
+ end
32
+
33
+ def stats(period, resolution)
34
+ range = timestamp_range(period, resolution)
35
+ started_tokens = tokens_for_period(range, resolution, "started")
36
+ completed_tokens = tokens_for_period(range, resolution, "completed")
37
+ matching_tokens = started_tokens.select { |token| completed_tokens.include? token }
38
+ completion_rate = (matching_tokens.size.to_f / range.size).round(Config.decimal_precision)
39
+ elapsed_times = matching_tokens.map { |t| t.time_elapsed(resolution) }
40
+ average_completion_time = matching_tokens.blank? ? 0.0 : (elapsed_times.sum) / matching_tokens.size
41
+ Stats.new(
42
+ started_tokens.size,
43
+ completed_tokens.size,
44
+ matching_tokens.size,
45
+ completion_rate,
46
+ average_completion_time
47
+ )
48
+ end
49
+
50
+ def drop!
51
+ del_by_prefix("stat:task:#{key}")
52
+ end
53
+
54
+ def storage_key(resolution, timestamp, type)
55
+ formatted_time = TabsTabs::Resolution.serialize(resolution, timestamp)
56
+ "stat:task:#{key}:#{type}:#{resolution}:#{formatted_time}"
57
+ end
58
+
59
+ private
60
+
61
+ def tokens_for_period(range, resolution, type)
62
+ keys = keys_for_range(range, resolution, type)
63
+ smembers_all(*keys).compact.map(&:to_a).flatten.map { |t| Token.new(t, key) }
64
+ end
65
+
66
+ def keys_for_range(range, resolution, type)
67
+ range.map { |timestamp| storage_key(resolution, timestamp, type) }
68
+ end
69
+
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,89 @@
1
+ module TabsTabs
2
+ module Metrics
3
+ class Task
4
+ class Token
5
+ include Storage
6
+
7
+ attr_reader :key
8
+ attr_reader :token
9
+
10
+ def initialize(token, key)
11
+ @key = key
12
+ @token = token
13
+ end
14
+
15
+ def start(timestamp=Time.now)
16
+ self.start_time = timestamp.utc
17
+ sadd(tokens_storage_key, token)
18
+ TabsTabs::Resolution.all.each { |res| record_start(res, start_time) }
19
+ end
20
+
21
+ def complete(timestamp=Time.now)
22
+ self.complete_time = timestamp.utc
23
+ unless sismember(tokens_storage_key, token)
24
+ raise UnstartedTaskMetricError.new("No task for metric '#{key}' was started with token '#{token}'")
25
+ end
26
+ TabsTabs::Resolution.all.each { |res| record_complete(res, complete_time) }
27
+ end
28
+
29
+ def time_elapsed(resolution)
30
+ TabsTabs::Resolution.from_seconds(resolution, complete_time - start_time)
31
+ end
32
+
33
+ def ==(other_token)
34
+ self.token == other_token.token
35
+ end
36
+
37
+ def to_s
38
+ "#{super}:#{token}"
39
+ end
40
+
41
+ private
42
+
43
+ def storage_key(resolution, timestamp, type)
44
+ formatted_time = TabsTabs::Resolution.serialize(resolution, timestamp)
45
+ "stat:task:#{key}:#{type}:#{resolution}:#{formatted_time}"
46
+ end
47
+
48
+ def started_storage_key
49
+ "stat:task:#{key}:#{token}:started_time"
50
+ end
51
+
52
+ def completed_storage_key
53
+ "stat:task:#{key}:#{token}:completed_time"
54
+ end
55
+
56
+ def tokens_storage_key
57
+ "stat:task:#{key}:tokens"
58
+ end
59
+
60
+ def record_start(resolution, timestamp)
61
+ sadd(storage_key(resolution, timestamp, "started"), token)
62
+ end
63
+
64
+ def record_complete(resolution, timestamp)
65
+ sadd(storage_key(resolution, timestamp, "completed"), token)
66
+ end
67
+
68
+ def start_time=(timestamp)
69
+ set(started_storage_key, timestamp)
70
+ @start_time = timestamp
71
+ end
72
+
73
+ def start_time
74
+ Time.parse(get(started_storage_key))
75
+ end
76
+
77
+ def complete_time=(timestamp)
78
+ set(completed_storage_key, timestamp)
79
+ @complete_time = timestamp
80
+ end
81
+
82
+ def complete_time
83
+ Time.parse(get(completed_storage_key))
84
+ end
85
+
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,91 @@
1
+ module TabsTabs
2
+ module Metrics
3
+ class Value
4
+ include Storage
5
+ include Helpers
6
+
7
+ attr_reader :key
8
+
9
+ def initialize(key)
10
+ @key = key
11
+ end
12
+
13
+ def record(value, timestamp=Time.now)
14
+ timestamp.utc
15
+ TabsTabs::Resolution.all.each do |resolution|
16
+ store_key = storage_key(resolution, timestamp)
17
+ update_values(store_key, value)
18
+ TabsTabs::Resolution.expire(resolution, store_key, timestamp)
19
+ end
20
+ true
21
+ end
22
+
23
+ def stats(period, resolution)
24
+ timestamps = timestamp_range period, resolution
25
+ keys = timestamps.map do |timestamp|
26
+ storage_key(resolution, timestamp)
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)
36
+ end
37
+
38
+ def drop!
39
+ del_by_prefix("stat:value:#{key}")
40
+ end
41
+
42
+ def drop_by_resolution!(resolution)
43
+ del_by_prefix("stat:value:#{key}:data:#{resolution}")
44
+ end
45
+
46
+ def storage_key(resolution, timestamp)
47
+ formatted_time = TabsTabs::Resolution.serialize(resolution, timestamp)
48
+ "stat:value:#{key}:data:#{resolution}:#{formatted_time}"
49
+ end
50
+
51
+ private
52
+
53
+ def update_values(stat_key, value)
54
+ hash = get_current_hash(stat_key)
55
+ increment(hash, value)
56
+ update_min(hash, value)
57
+ update_max(hash, value)
58
+ update_avg(hash)
59
+ set(stat_key, JSON.generate(hash))
60
+ end
61
+
62
+ def get_current_hash(stat_key)
63
+ hash = get(stat_key)
64
+ return JSON.parse(hash) if hash
65
+ default_value
66
+ end
67
+
68
+ def increment(hash, value)
69
+ hash["count"] += 1
70
+ hash["sum"] += value.to_f
71
+ end
72
+
73
+ def update_min(hash, value)
74
+ hash["min"] = value.to_f if hash["min"].nil? || value.to_f < hash["min"]
75
+ end
76
+
77
+ def update_max(hash, value)
78
+ hash["max"] = value.to_f if hash["max"].nil? || value.to_f > hash["max"]
79
+ end
80
+
81
+ def update_avg(hash)
82
+ hash["avg"] = hash["sum"].to_f / hash["count"]
83
+ end
84
+
85
+ def default_value(nil_value=nil)
86
+ { "count" => 0, "min" => nil_value, "max" => nil_value, "sum" => 0.0, "avg" => 0.0 }
87
+ end
88
+
89
+ end
90
+ end
91
+ end