metricstore 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/Guardfile +15 -0
- data/README.md +54 -35
- data/Rakefile +11 -1
- data/lib/metricstore.rb +24 -2
- data/lib/metricstore/client.rb +423 -0
- data/lib/metricstore/couchbase_client.rb +58 -4
- data/lib/metricstore/count_incrementer.rb +23 -0
- data/lib/metricstore/exceptions.rb +4 -0
- data/lib/metricstore/hyper_log_log.rb +91 -0
- data/lib/metricstore/incrementer.rb +41 -0
- data/lib/metricstore/inserter.rb +78 -0
- data/lib/metricstore/mock_key_value_client.rb +39 -0
- data/lib/metricstore/monkey_patches.rb +27 -0
- data/lib/metricstore/range_updater.rb +58 -0
- data/lib/metricstore/updater.rb +192 -0
- data/lib/metricstore/version.rb +1 -1
- data/metricstore.gemspec +4 -0
- data/spec/lib/metricstore/client_spec.rb +91 -0
- data/spec/lib/metricstore/couchbase_client_spec.rb +0 -0
- data/spec/lib/metricstore/incrementer_spec.rb +0 -0
- data/spec/lib/metricstore/inserter_spec.rb +0 -0
- data/spec/lib/metricstore/monkey_patches_spec.rb +0 -0
- data/spec/lib/metricstore/range_updater_spec.rb +0 -0
- data/spec/lib/metricstore/updater_spec.rb +0 -0
- data/spec/lib/metricstore_spec.rb +1 -0
- data/spec/spec_helper.rb +7 -0
- data/tasks/console.rake +5 -0
- data/tasks/coverage.rake +10 -0
- data/tasks/environment.rake +4 -0
- data/tasks/spec.rake +12 -0
- metadata +100 -18
- data/lib/metricstore/base_client.rb +0 -24
data/.gitignore
CHANGED
data/Guardfile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
guard 'bundler' do
|
5
|
+
watch('Gemfile')
|
6
|
+
watch(%r{.+\.gemspec})
|
7
|
+
end
|
8
|
+
|
9
|
+
guard 'rspec', :cli => '-c --format documentation -r ./spec/spec_helper.rb',
|
10
|
+
:version => 2 do
|
11
|
+
watch(%r{^spec/.+_spec\.rb})
|
12
|
+
watch(%r{^lib/(.+)\.rb}) { |m| "spec/lib/#{m[1]}_spec.rb" }
|
13
|
+
watch('spec/spec_helper.rb') { "spec/" }
|
14
|
+
watch('lib/metricstore.rb') { "spec/" }
|
15
|
+
end
|
data/README.md
CHANGED
@@ -31,91 +31,110 @@ Or install it yourself as:
|
|
31
31
|
|
32
32
|
# Configure...
|
33
33
|
m.ttl_of_hours = 31_556_926 # 1 year, default
|
34
|
-
m.
|
35
|
-
m.ttl_of_group_members = 7200 # 2 hours, default
|
34
|
+
m.max_ttl_of_dimension[:session_id] = 7200 # 2 hours
|
36
35
|
m.list_threshold = 1000 # default
|
37
36
|
|
37
|
+
# Open the connection
|
38
|
+
m.open
|
39
|
+
|
38
40
|
# (Suppose that the current time is 17:05 UTC on April 13, 2012.)
|
39
41
|
|
40
|
-
m.counter(:when => Time.now, :
|
42
|
+
m.counter(:when => Time.now, :what => "logins", :where =>
|
41
43
|
{:user => 'joe', :ip => '10.20.30.40'})
|
42
|
-
m.counter(:when => Time.now, :
|
44
|
+
m.counter(:when => Time.now, :what => "logins", :where =>
|
43
45
|
{:user => 'bob', :ip => '10.20.30.40'})
|
44
|
-
m.counter(:when => Time.now, :
|
46
|
+
m.counter(:when => Time.now, :what => "logins", :where =>
|
45
47
|
{:user => 'joe', :ip => '10.20.30.50'})
|
46
48
|
|
47
|
-
m.measure(:when => Time.now, :
|
49
|
+
m.measure(:when => Time.now, :what => "load_time", :value => 340, :where =>
|
48
50
|
{:page => '/welcome/', :session_id => "h0zhmb1c-u1xfgw305e"})
|
49
|
-
m.measure(:when => Time.now, :
|
51
|
+
m.measure(:when => Time.now, :what => "load_time", :value => 501, :where =>
|
50
52
|
{:page => '/welcome/', :session_id => "h0zhmb2q-643dotlcgd"})
|
51
|
-
m.measure(:when => Time.now, :
|
53
|
+
m.measure(:when => Time.now, :what => "load_time", :value => 212, :where =>
|
52
54
|
{:page => '/welcome/', :session_id => "h0zhmb1c-u1xfgw305e"})
|
53
|
-
m.measure(:when => Time.now, :
|
55
|
+
m.measure(:when => Time.now, :what => "load_time", :value => 343, :where =>
|
54
56
|
{:page => '/welcome/', :session_id => "h0zhmb2q-643dotlcgd"})
|
55
57
|
|
56
58
|
# Now we can query...
|
57
59
|
|
58
|
-
m.count(:when => "2012-04-13-17", :
|
60
|
+
m.count(:when => "2012-04-13-17", :what => "logins")
|
59
61
|
=> 3
|
60
|
-
m.list(:when => "2012-04-13-17", :
|
62
|
+
m.list(:when => "2012-04-13-17", :what => "logins", :list => :user)
|
61
63
|
=> ['joe', 'bob']
|
62
|
-
m.count(:when => "2012-04-13-17", :
|
64
|
+
m.count(:when => "2012-04-13-17", :what => "logins", :where => {:user => 'joe'})
|
63
65
|
=> 2
|
64
|
-
m.count(:when => "2012-04-13-17", :
|
66
|
+
m.count(:when => "2012-04-13-17", :what => "logins", :where => {:user => 'bob'})
|
65
67
|
=> 1
|
66
|
-
m.list(:when => "2012-04-13-17", :
|
68
|
+
m.list(:when => "2012-04-13-17", :what => "logins", :where => {:user => 'joe'}, :list => :ip)
|
67
69
|
=> ['10.20.30.40', '10.20.30.50']
|
68
|
-
m.list(:when => "2012-04-13-17", :
|
70
|
+
m.list(:when => "2012-04-13-17", :what => "logins", :where => {:user => 'bob'}, :list => :ip)
|
69
71
|
=> ['10.20.30.40']
|
70
|
-
m.count(:when => "2012-04-13-17", :
|
72
|
+
m.count(:when => "2012-04-13-17", :what => "logins", :where => {:user => 'joe', :ip => '10.20.30.40'})
|
71
73
|
=> 1
|
72
74
|
|
73
|
-
m.count(:when => "2012-04-13-17", :
|
75
|
+
m.count(:when => "2012-04-13-17", :what => "load_time")
|
74
76
|
=> 4
|
75
|
-
m.sum(:when => "2012-04-13-17", :
|
77
|
+
m.sum(:when => "2012-04-13-17", :what => "load_time")
|
76
78
|
=> 1396
|
77
|
-
m.average(:when => "2012-04-13-17", :
|
79
|
+
m.average(:when => "2012-04-13-17", :what => "load_time")
|
78
80
|
=> 349.0
|
79
|
-
m.maximum(:when => "2012-04-13-17", :
|
81
|
+
m.maximum(:when => "2012-04-13-17", :what => "load_time")
|
80
82
|
=> 501
|
81
|
-
m.minimum(:when => "2012-04-13-17", :
|
83
|
+
m.minimum(:when => "2012-04-13-17", :what => "load_time")
|
82
84
|
=> 212
|
83
|
-
m.stddev(:when => "2012-04-13-17", :
|
85
|
+
m.stddev(:when => "2012-04-13-17", :what => "load_time")
|
84
86
|
=> 102.45730818248154
|
85
|
-
m.list(:when => "2012-04-13-17", :
|
87
|
+
m.list(:when => "2012-04-13-17", :what => "load_time", :list => :page)
|
86
88
|
=> ['/welcome/']
|
87
89
|
|
88
90
|
# We can do queries related to groups as well, with some limitations.
|
89
|
-
# We only guarantee the accuracy of
|
90
|
-
#
|
91
|
+
# We only guarantee the accuracy of a particular group summary if for every
|
92
|
+
# member in the group, all the metrics related to that member were loaded
|
93
|
+
# from start-to-finish before the preceeding such metric expired its TTL.
|
94
|
+
#
|
91
95
|
# Note: a range is the difference between the minimum and maximum metric,
|
92
96
|
# for an individual group.
|
93
|
-
|
97
|
+
|
98
|
+
m.count_of_groups(:when => "2012-04-13-17", :what => "load_time", :group => :session_id)
|
94
99
|
=> 2
|
95
|
-
m.sum_of_ranges(:when => "2012-04-13-17", :
|
100
|
+
m.sum_of_ranges(:when => "2012-04-13-17", :what => "load_time", :group => :session_id)
|
96
101
|
=> 286
|
97
|
-
m.average_range(:when => "2012-04-13-17", :
|
102
|
+
m.average_range(:when => "2012-04-13-17", :what => "load_time", :group => :session_id)
|
98
103
|
=> 143
|
99
|
-
m.maximum_range(:when => "2012-04-13-17", :
|
104
|
+
m.maximum_range(:when => "2012-04-13-17", :what => "load_time", :group => :session_id)
|
100
105
|
=> 158
|
101
|
-
m.minimum_range(:when => "2012-04-13-17", :
|
106
|
+
m.minimum_range(:when => "2012-04-13-17", :what => "load_time", :group => :session_id)
|
102
107
|
=> 128
|
103
|
-
m.stddev_of_ranges(:when => "2012-04-13-17", :
|
108
|
+
m.stddev_of_ranges(:when => "2012-04-13-17", :what => "load_time", :group => :session_id)
|
104
109
|
=> 15.0
|
105
110
|
|
106
111
|
|
107
112
|
# Supposing there were instead millions of counter and measure operations,
|
108
113
|
# metricstore may reach its list_threshold. Some queries will fail.
|
109
114
|
|
110
|
-
m.list(:
|
115
|
+
m.list(:when => "2012-04-13-17", :what => "load_time", :list => :page)
|
111
116
|
=> ['/welcome/', '/projects/']
|
112
117
|
|
113
|
-
m.list(:
|
114
|
-
|
118
|
+
m.list(:when => "2012-04-13-17", :what => "load_time", :list => :session_id)
|
119
|
+
Metricstore::DataLossError: Too many session_id for "2012-04-13-17", "load_time".
|
115
120
|
|
116
|
-
m.estimated_list_size(:
|
121
|
+
m.estimated_list_size(:when => "2012-04-13-17", :what => "load_time", :list => :session_id)
|
117
122
|
=> 3560831
|
118
123
|
|
124
|
+
m.close
|
125
|
+
|
126
|
+
## EventMachine
|
127
|
+
|
128
|
+
The Metricstore client's write methods (counter, measure) are designed to run
|
129
|
+
within an [EventMachine](http://rubyeventmachine.com/) reactor. This allows
|
130
|
+
writes to be batched up together (only when there's a backlog), and to re-try
|
131
|
+
in the case of intermittent connection problems or other non-fatal errors. You
|
132
|
+
will want to design your app to leave the reactor running.
|
133
|
+
|
134
|
+
If it does not make sense to leave a reactor running in your app, you can
|
135
|
+
make your updates within a temporary reactor using the client's "run" method.
|
136
|
+
Be aware though, that the "run" method itself will block until the write backlog
|
137
|
+
is clear again.
|
119
138
|
|
120
139
|
## Contributing
|
121
140
|
|
data/Rakefile
CHANGED
@@ -1,2 +1,12 @@
|
|
1
|
-
#!/usr/bin/env rake
|
2
1
|
require "bundler/gem_tasks"
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
Bundler.require(:default)
|
5
|
+
|
6
|
+
Dir['tasks/*.rake'].sort.each { |task| load task }
|
7
|
+
|
8
|
+
# Add rake tasks from selected gems
|
9
|
+
gem_names = []
|
10
|
+
gem_names.each do |gem_name|
|
11
|
+
Dir[File.join(Gem.searcher.find(gem_name).full_gem_path, '**', '*.rake')].each{|rake_file| load rake_file }
|
12
|
+
end
|
data/lib/metricstore.rb
CHANGED
@@ -1,5 +1,27 @@
|
|
1
|
-
require "
|
1
|
+
require "metricstore/version"
|
2
|
+
require "metricstore/exceptions"
|
3
|
+
require "metricstore/monkey_patches"
|
4
|
+
require "metricstore/client"
|
5
|
+
require "metricstore/couchbase_client"
|
6
|
+
require "metricstore/mock_key_value_client"
|
7
|
+
require "metricstore/updater"
|
8
|
+
require "metricstore/count_incrementer"
|
9
|
+
require "metricstore/incrementer"
|
10
|
+
require "metricstore/inserter"
|
11
|
+
require "metricstore/range_updater"
|
12
|
+
require "metricstore/hyper_log_log"
|
2
13
|
|
3
14
|
module Metricstore
|
4
|
-
|
15
|
+
|
16
|
+
def self.couchbase(*args, &callback)
|
17
|
+
couchbase_client = CouchbaseClient.new(*args, &callback)
|
18
|
+
Client.new(
|
19
|
+
:kvstore => couchbase_client,
|
20
|
+
:sleep_interval => 0.1,
|
21
|
+
:max_healthy_errors => 2,
|
22
|
+
:max_unhandled_errors => 8,
|
23
|
+
:max_retry_delay_in_seconds => 60.0
|
24
|
+
)
|
25
|
+
end
|
26
|
+
|
5
27
|
end
|
@@ -0,0 +1,423 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
|
3
|
+
module Metricstore
|
4
|
+
class Client
|
5
|
+
|
6
|
+
CARDINALITY_ESTIMATOR_ERROR_RATE = 0.05
|
7
|
+
|
8
|
+
# :kvstore - the underlying key-value store.
|
9
|
+
# :sleep_interval - sleep cycle length in seconds (default: 0.1).
|
10
|
+
# :max_retry_delay_in_seconds - maximum time to wait after an error.
|
11
|
+
# :max_unhandled_errors - maximum retries before handling errors.
|
12
|
+
# Set this >= max_healthy_errors.
|
13
|
+
# :max_healthy_errors - maximum retries before healthy? returns false.
|
14
|
+
# Set this <= max_unhandled_errors.
|
15
|
+
def initialize(opts={})
|
16
|
+
@ttl_of_hours = 31_556_926 # 1 year
|
17
|
+
|
18
|
+
@kvstore = required(opts, :kvstore)
|
19
|
+
@sleep_interval = required(opts, :sleep_interval)
|
20
|
+
@max_healthy_errors = required(opts, :max_healthy_errors)
|
21
|
+
@max_unhandled_errors = required(opts, :max_unhandled_errors)
|
22
|
+
@max_retry_delay_in_seconds = required(opts, :max_retry_delay_in_seconds)
|
23
|
+
@max_ttl_of_dimension = {}
|
24
|
+
|
25
|
+
updater_options = {
|
26
|
+
:kvstore => @kvstore,
|
27
|
+
:sleep_interval => @sleep_interval,
|
28
|
+
:max_healthy_errors => @max_healthy_errors,
|
29
|
+
:max_unhandled_errors => @max_unhandled_errors,
|
30
|
+
:max_retry_delay_in_seconds => @max_retry_delay_in_seconds
|
31
|
+
}
|
32
|
+
@open = false
|
33
|
+
@inserter = Inserter.new(updater_options)
|
34
|
+
bucket_count = 1 << HyperLogLog.bits_needed(CARDINALITY_ESTIMATOR_ERROR_RATE)
|
35
|
+
@inserter.list_threshold = (2.5 * bucket_count).ceil
|
36
|
+
@incrementer = Incrementer.new(updater_options)
|
37
|
+
@range_updater = RangeUpdater.new(updater_options)
|
38
|
+
@count_incrementer = CountIncrementer.new(updater_options)
|
39
|
+
|
40
|
+
range_updater.handle_update_result = Proc.new do |key, result, ttl|
|
41
|
+
if key.start_with?("range:") && !result.nil?
|
42
|
+
new_or_grew, amount = result
|
43
|
+
if new_or_grew == :new || new_or_grew == :grew
|
44
|
+
_, time_block, metric_name, dimensions = key.split(/[\/\?]/)
|
45
|
+
unless dimensions.nil?
|
46
|
+
dimensions = dimensions.split('&')
|
47
|
+
dimensions.size.times do |i|
|
48
|
+
dimensions2 = dimensions.clone
|
49
|
+
group, dimension_value = dimensions2.delete_at(i).split('=')
|
50
|
+
key_suffix = "#{time_block}/#{metric_name}/#{group}?#{dimensions2.join('&')}"
|
51
|
+
incrementer.increment("rangesum:/#{key_suffix}", amount, ttl)
|
52
|
+
incrementer.increment("rangesumsqr:/#{key_suffix}", amount * amount, ttl)
|
53
|
+
range_updater.update_range("rangerange:/#{key_suffix}", amount, ttl)
|
54
|
+
if new_or_grew == :new
|
55
|
+
count_incrementer.increment("rangecount:/#{key_suffix}", 1, ttl)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Use of this method is discouraged.
|
65
|
+
# Set up your own EventMachine reactor instead.
|
66
|
+
# Nevertheless, for a one-off, infrequent connection, this works too.
|
67
|
+
def run(&callback)
|
68
|
+
require 'em-synchrony'
|
69
|
+
raise("Already running") if @open
|
70
|
+
EM.synchrony do
|
71
|
+
open
|
72
|
+
callback.call
|
73
|
+
timer = EM.add_periodic_timer(0.01) do
|
74
|
+
if backlog == 0
|
75
|
+
EM.cancel_timer(timer)
|
76
|
+
EM.next_tick do
|
77
|
+
close
|
78
|
+
EM.stop
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def open
|
86
|
+
inserter.start!
|
87
|
+
incrementer.start!
|
88
|
+
range_updater.start!
|
89
|
+
count_incrementer.start!
|
90
|
+
@open = true
|
91
|
+
end
|
92
|
+
|
93
|
+
def close
|
94
|
+
@open = false
|
95
|
+
inserter.stop!
|
96
|
+
incrementer.stop!
|
97
|
+
range_updater.stop!
|
98
|
+
count_incrementer.stop!
|
99
|
+
end
|
100
|
+
|
101
|
+
def backlog
|
102
|
+
inserter.backlog + incrementer.backlog + range_updater.backlog + count_incrementer.backlog
|
103
|
+
end
|
104
|
+
|
105
|
+
attr_accessor :ttl_of_hours
|
106
|
+
attr_accessor :max_ttl_of_dimension
|
107
|
+
|
108
|
+
def list_threshold
|
109
|
+
inserter.list_threshold
|
110
|
+
end
|
111
|
+
|
112
|
+
def list_threshold=(threshold)
|
113
|
+
inserter.list_threshold = threshold
|
114
|
+
end
|
115
|
+
|
116
|
+
# A write method.
|
117
|
+
# :what => a String. Required.
|
118
|
+
# :when => a Time. Defaults to "now".
|
119
|
+
# :where => a Hash<String, String> (dimension_name => value).
|
120
|
+
# Time complexity of this method grows factorially with the size of the :where hash.
|
121
|
+
def counter(args={})
|
122
|
+
assert_open!
|
123
|
+
hour = date_as_hour((args[:when] || Time.now).utc)
|
124
|
+
metric = escape(required(args, :what).to_s)
|
125
|
+
where = (args[:where] || {}).map{|k,v| [k, v, escape(k) << '=' << escape(v), max_ttl_of_dimension[k]] }
|
126
|
+
where.all_combinations do |dimensions|
|
127
|
+
key = counter_key(hour, metric, dimensions.sort.map{|k,v,s,ttl| s}.join('&'))
|
128
|
+
ttl = (dimensions.map{|k,v,s,ttl| ttl} << ttl_of_hours).compact.min
|
129
|
+
count_incrementer.increment(key, 1, ttl)
|
130
|
+
end
|
131
|
+
where.size.times do |i|
|
132
|
+
where2 = where.clone
|
133
|
+
list, dimension_value, _ = where2.delete_at(i)
|
134
|
+
list = escape(list)
|
135
|
+
key_middle = "#{hour}/#{metric}/#{list}?"
|
136
|
+
where2.all_combinations do |dimensions|
|
137
|
+
key_suffix = "#{key_middle}#{dimensions.sort.map{|k,v,s,ttl| s}.join('&')}"
|
138
|
+
ttl = (dimensions.map{|k,v,s,ttl| ttl} << ttl_of_hours).compact.min
|
139
|
+
inserter.insert("list:/#{key_suffix}", dimension_value, ttl)
|
140
|
+
estimator = HyperLogLog::Builder.new(CARDINALITY_ESTIMATOR_ERROR_RATE, Proc.new do |idx, val|
|
141
|
+
range_updater.update_range("hyperloglog:#{idx.to_i}:/#{key_suffix}", val, ttl)
|
142
|
+
end)
|
143
|
+
estimator.add(dimension_value)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
# A write method.
|
149
|
+
# :value => an integer. Required.
|
150
|
+
# :what => a String. Required.
|
151
|
+
# :when => a Time. Defaults to "now".
|
152
|
+
# :where => a Hash<String, String> (dimension_name => value).
|
153
|
+
# Time complexity of this method grows factorially with the size of the :where hash.
|
154
|
+
def measure(args={})
|
155
|
+
assert_open!
|
156
|
+
value = required(args, :value).to_i
|
157
|
+
hour = date_as_hour((args[:when] || Time.now).utc)
|
158
|
+
metric = escape(required(args, :what).to_s)
|
159
|
+
where = (args[:where] || {}).map{|k,v| [k, v, escape(k) << '=' << escape(v), max_ttl_of_dimension[k]] }
|
160
|
+
where.all_combinations do |dimensions|
|
161
|
+
dimensions_string = dimensions.sort.map{|k,v,s,ttl| s}.join('&')
|
162
|
+
ttl = (dimensions.map{|k,v,s,ttl| ttl} << ttl_of_hours).compact.min
|
163
|
+
suffix = build_key('', hour, metric, dimensions_string)
|
164
|
+
count_incrementer.increment("count#{suffix}", 1, ttl)
|
165
|
+
incrementer.increment("sum#{suffix}", value, ttl)
|
166
|
+
range_updater.update_range("range#{suffix}", value, ttl)
|
167
|
+
incrementer.increment("sumsqr#{suffix}", value*value, ttl)
|
168
|
+
end
|
169
|
+
where.size.times do |i|
|
170
|
+
where2 = where.clone
|
171
|
+
list, dimension_value, _ = where2.delete_at(i)
|
172
|
+
list = escape(list)
|
173
|
+
key_middle = "#{hour}/#{metric}/#{list}?"
|
174
|
+
where2.all_combinations do |dimensions|
|
175
|
+
key_suffix = "#{key_middle}#{dimensions.sort.map{|k,v,s,ttl| s}.join('&')}"
|
176
|
+
ttl = (dimensions.map{|k,v,s,ttl| ttl} << ttl_of_hours).compact.min
|
177
|
+
inserter.insert("list:/#{key_suffix}", dimension_value, ttl)
|
178
|
+
estimator = HyperLogLog::Builder.new(CARDINALITY_ESTIMATOR_ERROR_RATE, Proc.new do |idx, val|
|
179
|
+
range_updater.update_range("hyperloglog:#{idx.to_i}:/#{key_suffix}", val, ttl)
|
180
|
+
end)
|
181
|
+
estimator.add(dimension_value)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def count(args={})
|
187
|
+
time_block = required(args, :hour)
|
188
|
+
metric_name = escape(required(args, :what).to_s)
|
189
|
+
dimensions = (args[:where] || {}).sort.map{|k,v| escape(k) << '=' << escape(v)}.join('&')
|
190
|
+
result, cas = kvstore.fetch(counter_key(time_block, metric_name, dimensions))
|
191
|
+
result || 0
|
192
|
+
end
|
193
|
+
|
194
|
+
def list(args={})
|
195
|
+
time_block = required(args, :hour)
|
196
|
+
metric_name = escape(required(args, :what).to_s)
|
197
|
+
list_name = escape(required(args, :list).to_s)
|
198
|
+
dimensions = (args[:where] || {}).sort.map{|k,v| escape(k) << '=' << escape(v)}.join('&')
|
199
|
+
result, cas = kvstore.fetch(list_key(time_block, metric_name, list_name, dimensions))
|
200
|
+
if result == 'overflow'
|
201
|
+
error_message = "Too many #{args[:list]} for #{time_block}, #{args[:what]}"
|
202
|
+
error_message << ", where #{args[:where].inspect}" unless dimensions.empty?
|
203
|
+
raise(Metricstore::DataLossError, error_message)
|
204
|
+
else
|
205
|
+
result || []
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
def sum(args={})
|
210
|
+
time_block = required(args, :hour)
|
211
|
+
metric_name = escape(required(args, :what).to_s)
|
212
|
+
dimensions = (args[:where] || {}).sort.map{|k,v| escape(k) << '=' << escape(v)}.join('&')
|
213
|
+
result, cas = kvstore.fetch(sum_key(time_block, metric_name, dimensions))
|
214
|
+
result || 0
|
215
|
+
end
|
216
|
+
|
217
|
+
def average(args={})
|
218
|
+
time_block = required(args, :hour)
|
219
|
+
metric_name = escape(required(args, :what).to_s)
|
220
|
+
dimensions = (args[:where] || {}).sort.map{|k,v| escape(k) << '=' << escape(v)}.join('&')
|
221
|
+
count, cas = kvstore.fetch(counter_key(time_block, metric_name, dimensions))
|
222
|
+
sum, cas = kvstore.fetch(sum_key(time_block, metric_name, dimensions))
|
223
|
+
return nil if count.nil? || sum.nil? || count == 0
|
224
|
+
sum.to_f / count
|
225
|
+
end
|
226
|
+
|
227
|
+
def maximum(args={})
|
228
|
+
time_block = required(args, :hour)
|
229
|
+
metric_name = escape(required(args, :what).to_s)
|
230
|
+
dimensions = (args[:where] || {}).sort.map{|k,v| escape(k) << '=' << escape(v)}.join('&')
|
231
|
+
range, cas = kvstore.fetch(range_key(time_block, metric_name, dimensions))
|
232
|
+
range.nil? ? nil : range[1]
|
233
|
+
end
|
234
|
+
|
235
|
+
def minimum(args={})
|
236
|
+
time_block = required(args, :hour)
|
237
|
+
metric_name = escape(required(args, :what).to_s)
|
238
|
+
dimensions = (args[:where] || {}).sort.map{|k,v| escape(k) << '=' << escape(v)}.join('&')
|
239
|
+
range, cas = kvstore.fetch(range_key(time_block, metric_name, dimensions))
|
240
|
+
range.nil? ? nil : range[0]
|
241
|
+
end
|
242
|
+
|
243
|
+
def stddev(args={})
|
244
|
+
time_block = required(args, :hour)
|
245
|
+
metric_name = escape(required(args, :what).to_s)
|
246
|
+
dimensions = (args[:where] || {}).sort.map{|k,v| escape(k) << '=' << escape(v)}.join('&')
|
247
|
+
count, cas = kvstore.fetch(counter_key(time_block, metric_name, dimensions))
|
248
|
+
sum, cas = kvstore.fetch(sum_key(time_block, metric_name, dimensions))
|
249
|
+
sumsqr, cas = kvstore.fetch(sumsqr_key(time_block, metric_name, dimensions))
|
250
|
+
return nil if count.nil? || sum.nil? || sumsqr.nil? || count == 0
|
251
|
+
Math.sqrt(count * sumsqr - sum*sum) / count
|
252
|
+
end
|
253
|
+
|
254
|
+
def count_of_groups(args={})
|
255
|
+
group = escape(required(args, :group))
|
256
|
+
time_block = required(args, :hour)
|
257
|
+
metric_name = escape(required(args, :what).to_s)
|
258
|
+
dimensions = (args[:where] || {}).sort.map{|k,v| escape(k) << '=' << escape(v)}.join('&')
|
259
|
+
result, cas = kvstore.fetch(group_counter_key(time_block, metric_name, group, dimensions))
|
260
|
+
result || 0
|
261
|
+
end
|
262
|
+
|
263
|
+
def sum_of_ranges(args={})
|
264
|
+
group = escape(required(args, :group))
|
265
|
+
time_block = required(args, :hour)
|
266
|
+
metric_name = escape(required(args, :what).to_s)
|
267
|
+
dimensions = (args[:where] || {}).sort.map{|k,v| escape(k) << '=' << escape(v)}.join('&')
|
268
|
+
result, cas = kvstore.fetch(range_sum_key(time_block, metric_name, group, dimensions))
|
269
|
+
result || 0
|
270
|
+
end
|
271
|
+
|
272
|
+
def average_range(args={})
|
273
|
+
group = escape(required(args, :group))
|
274
|
+
time_block = required(args, :hour)
|
275
|
+
metric_name = escape(required(args, :what).to_s)
|
276
|
+
dimensions = (args[:where] || {}).sort.map{|k,v| escape(k) << '=' << escape(v)}.join('&')
|
277
|
+
count, cas = kvstore.fetch(group_counter_key(time_block, metric_name, group, dimensions))
|
278
|
+
sum, cas = kvstore.fetch(range_sum_key(time_block, metric_name, group, dimensions))
|
279
|
+
return nil if count.nil? || sum.nil? || count == 0
|
280
|
+
sum.to_f / count
|
281
|
+
end
|
282
|
+
|
283
|
+
def maximum_range(args={})
|
284
|
+
group = escape(required(args, :group))
|
285
|
+
time_block = required(args, :hour)
|
286
|
+
metric_name = escape(required(args, :what).to_s)
|
287
|
+
dimensions = (args[:where] || {}).sort.map{|k,v| escape(k) << '=' << escape(v)}.join('&')
|
288
|
+
range, cas = kvstore.fetch(group_range_key(time_block, metric_name, group, dimensions))
|
289
|
+
range.nil? ? nil : range[1]
|
290
|
+
end
|
291
|
+
|
292
|
+
def minimum_range(args={})
|
293
|
+
group = escape(required(args, :group))
|
294
|
+
time_block = required(args, :hour)
|
295
|
+
metric_name = escape(required(args, :what).to_s)
|
296
|
+
dimensions = (args[:where] || {}).sort.map{|k,v| escape(k) << '=' << escape(v)}.join('&')
|
297
|
+
range, cas = kvstore.fetch(group_range_key(time_block, metric_name, group, dimensions))
|
298
|
+
range.nil? ? nil : range[0]
|
299
|
+
end
|
300
|
+
|
301
|
+
def stddev_of_ranges(args={})
|
302
|
+
group = escape(required(args, :group))
|
303
|
+
time_block = required(args, :hour)
|
304
|
+
metric_name = escape(required(args, :what).to_s)
|
305
|
+
dimensions = (args[:where] || {}).sort.map{|k,v| escape(k) << '=' << escape(v)}.join('&')
|
306
|
+
count, cas = kvstore.fetch(group_counter_key(time_block, metric_name, group, dimensions))
|
307
|
+
sum, cas = kvstore.fetch(range_sum_key(time_block, metric_name, group, dimensions))
|
308
|
+
sumsqr, cas = kvstore.fetch(range_sumsqr_key(time_block, metric_name, group, dimensions))
|
309
|
+
return nil if count.nil? || sum.nil? || sumsqr.nil? || count == 0
|
310
|
+
Math.sqrt(count * sumsqr - sum*sum) / count
|
311
|
+
end
|
312
|
+
|
313
|
+
def estimated_list_size(args={})
|
314
|
+
time_block = required(args, :hour)
|
315
|
+
metric_name = escape(required(args, :what).to_s)
|
316
|
+
list_name = escape(required(args, :list).to_s)
|
317
|
+
dimensions = (args[:where] || {}).sort.map{|k,v| escape(k) << '=' << escape(v)}.join('&')
|
318
|
+
list, cas = kvstore.fetch(list_key(time_block, metric_name, list_name, dimensions))
|
319
|
+
if list == 'overflow'
|
320
|
+
bucket_count = 1 << HyperLogLog.bits_needed(CARDINALITY_ESTIMATOR_ERROR_RATE)
|
321
|
+
buckets = Enumerator.new do |yielder|
|
322
|
+
bucket_count.times do |i|
|
323
|
+
key = hyperloglog_key(i, time_block, metric_name, list_name, dimensions)
|
324
|
+
range, cas = kvstore.fetch(key)
|
325
|
+
yielder << (range.nil? ? nil : range[1])
|
326
|
+
end
|
327
|
+
end
|
328
|
+
HyperLogLog.estimate_cardinality(buckets)
|
329
|
+
else
|
330
|
+
list.size
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
private
|
335
|
+
|
336
|
+
attr_reader :kvstore
|
337
|
+
attr_reader :inserter
|
338
|
+
attr_reader :incrementer
|
339
|
+
attr_reader :range_updater
|
340
|
+
attr_reader :count_incrementer
|
341
|
+
|
342
|
+
attr_reader :sleep_interval
|
343
|
+
attr_reader :max_healthy_errors
|
344
|
+
attr_reader :max_unhandled_errors
|
345
|
+
attr_reader :max_retry_delay_in_seconds
|
346
|
+
|
347
|
+
def date_as_hour(date)
|
348
|
+
date.strftime('%Y-%m-%d-%H')
|
349
|
+
end
|
350
|
+
|
351
|
+
def build_key(prefix, time_block, metric, dimensions, list_group=nil)
|
352
|
+
key = ''
|
353
|
+
key << prefix
|
354
|
+
key << ':/'
|
355
|
+
key << time_block
|
356
|
+
key << '/'
|
357
|
+
key << metric
|
358
|
+
unless list_group.nil?
|
359
|
+
key << '/'
|
360
|
+
key << list_group
|
361
|
+
end
|
362
|
+
key << '?'
|
363
|
+
key << dimensions
|
364
|
+
key
|
365
|
+
end
|
366
|
+
|
367
|
+
def counter_key(time_block, metric, dimensions)
|
368
|
+
build_key('count', time_block, metric, dimensions)
|
369
|
+
end
|
370
|
+
|
371
|
+
def list_key(time_block, metric, list_name, dimensions)
|
372
|
+
build_key('list', time_block, metric, dimensions, list_name)
|
373
|
+
rescue Exception => e
|
374
|
+
puts e.message
|
375
|
+
puts e.backtrace
|
376
|
+
raise
|
377
|
+
end
|
378
|
+
|
379
|
+
def sum_key(time_block, metric, dimensions)
|
380
|
+
build_key('sum', time_block, metric, dimensions)
|
381
|
+
end
|
382
|
+
|
383
|
+
def sumsqr_key(time_block, metric, dimensions)
|
384
|
+
build_key('sumsqr', time_block, metric, dimensions)
|
385
|
+
end
|
386
|
+
|
387
|
+
def range_key(time_block, metric, dimensions)
|
388
|
+
build_key('range', time_block, metric, dimensions)
|
389
|
+
end
|
390
|
+
|
391
|
+
def group_counter_key(time_block, metric, group_name, dimensions)
|
392
|
+
build_key('rangecount', time_block, metric, dimensions, group_name)
|
393
|
+
end
|
394
|
+
|
395
|
+
def group_range_key(time_block, metric, group_name, dimensions)
|
396
|
+
build_key('rangerange', time_block, metric, dimensions, group_name)
|
397
|
+
end
|
398
|
+
|
399
|
+
def range_sum_key(time_block, metric, group_name, dimensions)
|
400
|
+
build_key('rangesum', time_block, metric, dimensions, group_name)
|
401
|
+
end
|
402
|
+
|
403
|
+
def range_sumsqr_key(time_block, metric, group_name, dimensions)
|
404
|
+
build_key('rangesumsqr', time_block, metric, dimensions, group_name)
|
405
|
+
end
|
406
|
+
|
407
|
+
def hyperloglog_key(index, time_block, metric, list_name, dimensions)
|
408
|
+
build_key("hyperloglog:#{index.to_i}", time_block, metric, dimensions, list_name)
|
409
|
+
end
|
410
|
+
|
411
|
+
def required(args, argument_name)
|
412
|
+
args[argument_name] || raise(ArgumentError, "missing argument: #{argument_name}")
|
413
|
+
end
|
414
|
+
|
415
|
+
def assert_open!
|
416
|
+
raise "Client has not been opened" unless @open
|
417
|
+
end
|
418
|
+
|
419
|
+
def escape(s)
|
420
|
+
CGI.escape(s.to_s)
|
421
|
+
end
|
422
|
+
end
|
423
|
+
end
|