tabstabs 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +20 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +22 -0
- data/README.md +421 -0
- data/Rakefile +5 -0
- data/lib/tabs_tabs.rb +26 -0
- data/lib/tabs_tabs/config.rb +65 -0
- data/lib/tabs_tabs/helpers.rb +27 -0
- data/lib/tabs_tabs/metrics/counter.rb +69 -0
- data/lib/tabs_tabs/metrics/counter/stats.rb +51 -0
- data/lib/tabs_tabs/metrics/task.rb +72 -0
- data/lib/tabs_tabs/metrics/task/token.rb +89 -0
- data/lib/tabs_tabs/metrics/value.rb +91 -0
- data/lib/tabs_tabs/metrics/value/stats.rb +55 -0
- data/lib/tabs_tabs/resolution.rb +65 -0
- data/lib/tabs_tabs/resolutionable.rb +48 -0
- data/lib/tabs_tabs/resolutions/day.rb +40 -0
- data/lib/tabs_tabs/resolutions/hour.rb +40 -0
- data/lib/tabs_tabs/resolutions/minute.rb +40 -0
- data/lib/tabs_tabs/resolutions/month.rb +40 -0
- data/lib/tabs_tabs/resolutions/week.rb +40 -0
- data/lib/tabs_tabs/resolutions/year.rb +40 -0
- data/lib/tabs_tabs/storage.rb +105 -0
- data/lib/tabs_tabs/tabs_tabs.rb +117 -0
- data/lib/tabs_tabs/version.rb +3 -0
- data/spec/lib/tabs_tabs/config_spec.rb +60 -0
- data/spec/lib/tabs_tabs/metrics/counter/stats_spec.rb +42 -0
- data/spec/lib/tabs_tabs/metrics/counter_spec.rb +196 -0
- data/spec/lib/tabs_tabs/metrics/task/token_spec.rb +18 -0
- data/spec/lib/tabs_tabs/metrics/task_spec.rb +103 -0
- data/spec/lib/tabs_tabs/metrics/value/stats_spec.rb +61 -0
- data/spec/lib/tabs_tabs/metrics/value_spec.rb +160 -0
- data/spec/lib/tabs_tabs/resolution_spec.rb +52 -0
- data/spec/lib/tabs_tabs/resolutionable_spec.rb +53 -0
- data/spec/lib/tabs_tabs/resolutions/day_spec.rb +23 -0
- data/spec/lib/tabs_tabs/resolutions/hour_spec.rb +23 -0
- data/spec/lib/tabs_tabs/resolutions/minute_spec.rb +23 -0
- data/spec/lib/tabs_tabs/resolutions/month_spec.rb +23 -0
- data/spec/lib/tabs_tabs/resolutions/week_spec.rb +24 -0
- data/spec/lib/tabs_tabs/resolutions/year_spec.rb +23 -0
- data/spec/lib/tabs_tabs/storage_spec.rb +138 -0
- data/spec/lib/tabs_tabs_spec.rb +223 -0
- data/spec/spec_helper.rb +17 -0
- data/spec/support/custom_resolutions.rb +40 -0
- data/tabs_tabs.gemspec +31 -0
- metadata +213 -0
data/Rakefile
ADDED
data/lib/tabs_tabs.rb
ADDED
@@ -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
|