metricstore 0.0.1 → 0.1.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.
- 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
|