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,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