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