rubycut-metriks 0.9.9.4

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.
@@ -0,0 +1,44 @@
1
+ require 'atomic'
2
+
3
+ module Metriks
4
+ # Public: Counters are one of the simplest metrics whose only operations
5
+ # are increment and decrement.
6
+ class Counter
7
+ # Public: Initialize a new Counter.
8
+ def initialize
9
+ @count = Atomic.new(0)
10
+ end
11
+
12
+ # Public: Reset the counter back to 0
13
+ #
14
+ # Returns nothing.
15
+ def clear
16
+ @count.value = 0
17
+ end
18
+
19
+ # Public: Increment the counter.
20
+ #
21
+ # incr - The value to add to the counter.
22
+ #
23
+ # Returns nothing.
24
+ def increment(incr = 1)
25
+ @count.update { |v| v + incr }
26
+ end
27
+
28
+ # Public: Decrement the counter.
29
+ #
30
+ # decr - The value to subtract from the counter.
31
+ #
32
+ # Returns nothing.
33
+ def decrement(decr = 1)
34
+ @count.update { |v| v - decr }
35
+ end
36
+
37
+ # Public: The current count.
38
+ #
39
+ # Returns the count.
40
+ def count
41
+ @count.value
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,63 @@
1
+ require 'atomic'
2
+
3
+ module Metriks
4
+ class EWMA
5
+ INTERVAL = 5.0
6
+ SECONDS_PER_MINUTE = 60.0
7
+
8
+ ONE_MINUTE = 1
9
+ FIVE_MINUTES = 5
10
+ FIFTEEN_MINUTES = 15
11
+
12
+ M1_ALPHA = 1 - Math.exp(-INTERVAL / SECONDS_PER_MINUTE / ONE_MINUTE)
13
+ M5_ALPHA = 1 - Math.exp(-INTERVAL / SECONDS_PER_MINUTE / FIVE_MINUTES)
14
+ M15_ALPHA = 1 - Math.exp(-INTERVAL / SECONDS_PER_MINUTE / FIFTEEN_MINUTES)
15
+
16
+ def self.new_m1
17
+ new(M1_ALPHA, INTERVAL)
18
+ end
19
+
20
+ def self.new_m5
21
+ new(M5_ALPHA, INTERVAL)
22
+ end
23
+
24
+ def self.new_m15
25
+ new(M15_ALPHA, INTERVAL)
26
+ end
27
+
28
+ def initialize(alpha, interval)
29
+ @alpha = alpha
30
+ @interval = interval
31
+
32
+ @initialized = false
33
+ @rate = Atomic.new(0.0)
34
+ @uncounted = Atomic.new(0)
35
+ end
36
+
37
+ def clear
38
+ @initialized = false
39
+ @rate.value = 0.0
40
+ @uncounted.value = 0
41
+ end
42
+
43
+ def update(value)
44
+ @uncounted.update { |v| v + value }
45
+ end
46
+
47
+ def tick
48
+ count = @uncounted.swap(0)
49
+ instant_rate = count / @interval.to_f
50
+
51
+ if @initialized
52
+ @rate.update { |v| v + @alpha * (instant_rate - v) }
53
+ else
54
+ @rate.value = instant_rate
55
+ @initialized = true
56
+ end
57
+ end
58
+
59
+ def rate
60
+ @rate.value
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,102 @@
1
+ require 'atomic'
2
+ require 'red_black_tree'
3
+ require 'metriks/snapshot'
4
+
5
+ module Metriks
6
+ class ExponentiallyDecayingSample
7
+ RESCALE_THRESHOLD = 60 * 60 # 1 hour
8
+
9
+ def initialize(reservoir_size, alpha, values = nil)
10
+ @values = values || RedBlackTree.new
11
+ @count = Atomic.new(0)
12
+ @next_scale_time = Atomic.new(0)
13
+ @alpha = alpha
14
+ @reservoir_size = reservoir_size
15
+ @mutex = Mutex.new
16
+ clear
17
+ end
18
+
19
+ def clear
20
+ @mutex.synchronize do
21
+ @values.clear
22
+ @count.value = 0
23
+ @next_scale_time.value = Time.now + RESCALE_THRESHOLD
24
+ @start_time = Time.now
25
+ end
26
+ end
27
+
28
+ def size
29
+ count = @count.value
30
+ count < @reservoir_size ? count : @reservoir_size
31
+ end
32
+
33
+ def snapshot
34
+ @mutex.synchronize do
35
+ Snapshot.new(@values.values)
36
+ end
37
+ end
38
+
39
+ def update(value, timestamp = Time.now)
40
+ @mutex.synchronize do
41
+ priority = weight(timestamp - @start_time) / rand
42
+ priority = Float::MAX if priority.infinite?
43
+ new_count = @count.update { |v| v + 1 }
44
+
45
+ if priority.nan?
46
+ warn "ExponentiallyDecayingSample found priority of NaN. timestamp: #{timestamp.to_f} start_time: #{@start_time.to_f}"
47
+ return
48
+ end
49
+
50
+ if new_count <= @reservoir_size
51
+ @values[priority] = value
52
+ else
53
+ first_priority = @values.first[0]
54
+ if first_priority < priority
55
+ unless @values[priority]
56
+ @values[priority] = value
57
+
58
+ until @values.delete(first_priority)
59
+ first_priority = @values.first[0]
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ now = Time.new
67
+ next_time = @next_scale_time.value
68
+ if now >= next_time
69
+ rescale(now, next_time)
70
+ end
71
+ end
72
+
73
+ def weight(time)
74
+ Math.exp(@alpha * time)
75
+ end
76
+
77
+ def rescale(now, next_time)
78
+ if @next_scale_time.compare_and_swap(next_time, now + RESCALE_THRESHOLD)
79
+ @mutex.synchronize do
80
+ old_start_time = @start_time
81
+ @start_time = Time.now
82
+ @values.keys.each do |key|
83
+ value = @values.delete(key)
84
+ new_key = key * Math.exp(-@alpha * (@start_time - old_start_time))
85
+
86
+ if key.nan?
87
+ warn "ExponentiallyDecayingSample found a key of NaN. old_start_time: #{old_start_time.to_f} start_time: #{@start_time.to_f}"
88
+ next
89
+ end
90
+
91
+ if new_key.nan?
92
+ warn "ExponentiallyDecayingSample found a new_key of NaN. key: #{key} old_start_time: #{old_start_time.to_f} start_time: #{@start_time.to_f}"
93
+ next
94
+ end
95
+
96
+ @values[new_key] = value
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,112 @@
1
+ require 'atomic'
2
+ require 'metriks/uniform_sample'
3
+ require 'metriks/exponentially_decaying_sample'
4
+
5
+ module Metriks
6
+ class Histogram
7
+ DEFAULT_SAMPLE_SIZE = 1028
8
+ DEFAULT_ALPHA = 0.015
9
+
10
+ def self.new_uniform
11
+ new(Metriks::UniformSample.new(DEFAULT_SAMPLE_SIZE))
12
+ end
13
+
14
+ def self.new_exponentially_decaying
15
+ new(Metriks::ExponentiallyDecayingSample.new(DEFAULT_SAMPLE_SIZE, DEFAULT_ALPHA))
16
+ end
17
+
18
+ def initialize(sample)
19
+ @sample = sample
20
+ @count = Atomic.new(0)
21
+ @min = Atomic.new(nil)
22
+ @max = Atomic.new(nil)
23
+ @sum = Atomic.new(0)
24
+ @variance = Atomic.new([ -1, 0 ])
25
+ end
26
+
27
+ def clear
28
+ @sample.clear
29
+ @count.value = 0
30
+ @min.value = nil
31
+ @max.value = nil
32
+ @sum.value = 0
33
+ @variance.value = [ -1, 0 ]
34
+ end
35
+
36
+ def update(value)
37
+ @count.update { |v| v + 1 }
38
+ @sample.update(value)
39
+ self.max = value
40
+ self.min = value
41
+ @sum.update { |v| v + value }
42
+ update_variance(value)
43
+ end
44
+
45
+ def snapshot
46
+ @sample.snapshot
47
+ end
48
+
49
+ def count
50
+ @count.value
51
+ end
52
+
53
+ def max
54
+ count > 0 ? @max.value : 0.0
55
+ end
56
+
57
+ def min
58
+ count > 0 ? @min.value : 0.0
59
+ end
60
+
61
+ def mean
62
+ count > 0 ? @sum.value / count : 0.0
63
+ end
64
+
65
+ def stddev
66
+ count > 0 ? variance ** 0.5 : 0.0
67
+ end
68
+
69
+ def variance
70
+ count <= 1 ? 0.0 : @variance.value[1] / (count - 1)
71
+ end
72
+
73
+ def max=(potential_max)
74
+ done = false
75
+
76
+ while !done
77
+ current_max = @max.value
78
+ done = (!current_max.nil? && current_max >= potential_max) || @max.compare_and_swap(current_max, potential_max)
79
+ end
80
+ end
81
+
82
+ def min=(potential_min)
83
+ done = false
84
+
85
+ while !done
86
+ current_min = @min.value
87
+ done = (!current_min.nil? && current_min <= potential_min) || @min.compare_and_swap(current_min, potential_min)
88
+ end
89
+ end
90
+
91
+ def update_variance(value)
92
+ @variance.update do |old_values|
93
+ new_values = Array.new(2)
94
+ if old_values[0] == -1
95
+ new_values[0] = value
96
+ new_values[1] = 0
97
+ else
98
+ old_m = old_values[0]
99
+ old_s = old_values[1]
100
+
101
+ new_m = old_m + ((value - old_m) / count)
102
+ new_s = old_s + ((value - old_m) * (value - new_m))
103
+
104
+ new_values[0] = new_m
105
+ new_values[1] = new_s
106
+ end
107
+
108
+ new_values
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,85 @@
1
+ require 'atomic'
2
+
3
+ require 'metriks/ewma'
4
+
5
+ module Metriks
6
+ class Meter
7
+ TICK_INTERVAL = 5.0
8
+
9
+ def initialize(averager_klass = Metriks::EWMA)
10
+ @count = Atomic.new(0)
11
+ @start_time = Time.now.to_f
12
+ @last_tick = Atomic.new(@start_time)
13
+
14
+ @m1_rate = averager_klass.new_m1
15
+ @m5_rate = averager_klass.new_m5
16
+ @m15_rate = averager_klass.new_m15
17
+ end
18
+
19
+ def clear
20
+ @count.value = 0
21
+ @start_time = Time.now.to_f
22
+ @last_tick.value = @start_time
23
+ @m1_rate.clear
24
+ @m5_rate.clear
25
+ @m15_rate.clear
26
+ end
27
+
28
+ def tick
29
+ @m1_rate.tick
30
+ @m5_rate.tick
31
+ @m15_rate.tick
32
+ end
33
+
34
+ def tick_if_nessesary
35
+ old_tick = @last_tick.value
36
+ new_tick = Time.new.to_f
37
+ age = new_tick - old_tick
38
+ if age > TICK_INTERVAL && @last_tick.compare_and_swap(old_tick, new_tick)
39
+ required_ticks = age / TICK_INTERVAL
40
+ required_ticks.to_i.times do
41
+ tick
42
+ end
43
+ end
44
+ end
45
+
46
+ def mark(val = 1)
47
+ tick_if_nessesary
48
+ @count.update { |v| v + val }
49
+ @m1_rate.update(val)
50
+ @m5_rate.update(val)
51
+ @m15_rate.update(val)
52
+ end
53
+
54
+ def count
55
+ @count.value
56
+ end
57
+
58
+ def one_minute_rate
59
+ tick_if_nessesary
60
+ @m1_rate.rate
61
+ end
62
+
63
+ def five_minute_rate
64
+ tick_if_nessesary
65
+ @m5_rate.rate
66
+ end
67
+
68
+ def fifteen_minute_rate
69
+ tick_if_nessesary
70
+ @m15_rate.rate
71
+ end
72
+
73
+ def mean_rate
74
+ if count == 0
75
+ return 0.0
76
+ else
77
+ elapsed = Time.now.to_f - @start_time
78
+ count / elapsed
79
+ end
80
+ end
81
+
82
+ def stop
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,207 @@
1
+ require 'metriks/counter'
2
+ require 'metriks/timer'
3
+ require 'metriks/utilization_timer'
4
+ require 'metriks/meter'
5
+ require 'metriks/gauge'
6
+
7
+ module Metriks
8
+ # Public: A collection of metrics
9
+ class Registry
10
+ # Public: The default registry for the process.
11
+ #
12
+ # Returns the default Registry for the process.
13
+ def self.default
14
+ @default ||= new
15
+ end
16
+
17
+ # Public: Initializes a new Registry.
18
+ def initialize
19
+ @mutex = Mutex.new
20
+ @metrics = {}
21
+ end
22
+
23
+ # Public: Clear all of the metrics in the Registry. This ensures all
24
+ # metrics that have been added are stopped.
25
+ #
26
+ # Returns nothing.
27
+ def clear
28
+ @mutex.synchronize do
29
+ @metrics.each do |key, metric|
30
+ metric.stop if metric.respond_to?(:stop)
31
+ end
32
+
33
+ @metrics = {}
34
+ end
35
+ end
36
+
37
+ # Public: Clear all of the metrics in the Registry. This has the same
38
+ # effect as calling #clear.
39
+ #
40
+ # Returns nothing.
41
+ def stop
42
+ clear
43
+ end
44
+
45
+ # Public: Iterate over all of the counters.
46
+ #
47
+ # Examples
48
+ #
49
+ # registry.each do |name, metric|
50
+ # puts name
51
+ # end
52
+ #
53
+ # Returns nothing.
54
+ def each(&block)
55
+ metrics = @mutex.synchronize do
56
+ @metrics.dup
57
+ end
58
+
59
+ metrics.each(&block)
60
+ end
61
+
62
+ # Public: Fetch or create a new counter metric. Counters are one of the
63
+ # simplest metrics whose only operations are increment and decrement.
64
+ #
65
+ # name - The String name of the metric to define or fetch
66
+ #
67
+ # Examples
68
+ #
69
+ # registry.counter('method.calls')
70
+ #
71
+ # Returns the Metriks::Counter identified by the name.
72
+ def counter(name)
73
+ add_or_get(name, Metriks::Counter)
74
+ end
75
+
76
+ # Public: Fetch or create a new gauge metric.
77
+ #
78
+ # name - The String name of the metric to define or fetch
79
+ #
80
+ # Examples
81
+ #
82
+ # registry.gauge('disk_space.used') { 1 }
83
+ #
84
+ # Returns the Metriks::Gauge identified by the name.
85
+ def gauge(name, callable = nil, &block)
86
+ add_or_get(name, Metriks::Gauge) do
87
+ Metriks::Gauge.new(callable, &block)
88
+ end
89
+ end
90
+
91
+ # Public: Fetch or create a new meter metric. Meters are a counter that
92
+ # tracks throughput along with the count.
93
+ #
94
+ # name - The String name of the metric to define or fetch
95
+ #
96
+ # Examples
97
+ #
98
+ # registry.meter('resque.calls')
99
+ #
100
+ # Returns the Metriks::Meter identified by the name.
101
+ def meter(name)
102
+ add_or_get(name, Metriks::Meter)
103
+ end
104
+
105
+ # Public: Fetch or create a new timer metric. Timers provide the means to
106
+ # time the execution of a method including statistics on the number of
107
+ # invocations, average length of time, throughput.
108
+ #
109
+ # name - The String name of the metric to define or fetch
110
+ #
111
+ # Examples
112
+ #
113
+ # registry.timer('resque.worker')
114
+ #
115
+ # Returns the Metriks::Timer identified by the name.
116
+ def timer(name)
117
+ add_or_get(name, Metriks::Timer)
118
+ end
119
+
120
+ # Public: Fetch or create a new utilization timer metric.
121
+ #
122
+ # Utilization timers are a specialized version of a timer that calculate
123
+ # the percentage of wall-clock time (between 0 and 1) that was spent in
124
+ # the method. This metric is most valuable in a single-threaded
125
+ # environment where a processes is waiting on an external resource like a
126
+ # message queue or HTTP server.
127
+ #
128
+ # name - The String name of the metric to define or fetch
129
+ #
130
+ # Examples
131
+ #
132
+ # registry.utilization_timer('rack.utilization')
133
+ #
134
+ # Returns the Metriks::UtilizationTimer identified by the name.
135
+ def utilization_timer(name)
136
+ add_or_get(name, Metriks::UtilizationTimer)
137
+ end
138
+
139
+ # Public: Fetch or create a new histogram metric. Histograms record values
140
+ # and expose statistics about the distribution of the data like median and
141
+ # 95th percentile.
142
+ #
143
+ # name - The String name of the metric to define or fetch
144
+ #
145
+ # Examples
146
+ #
147
+ # registry.histogram('backlog.wait')
148
+ #
149
+ # Returns the Metriks::Histogram identified by the name.
150
+ def histogram(name)
151
+ add_or_get(name, Metriks::Histogram) do
152
+ Metriks::Histogram.new_exponentially_decaying
153
+ end
154
+ end
155
+
156
+ # Public: Fetch an existing metric.
157
+ #
158
+ # name - The String name of the metric to fetch
159
+ #
160
+ # Examples
161
+ #
162
+ # registry.get('rack.utilization')
163
+ #
164
+ # Returns the metric or nil.
165
+ def get(name)
166
+ @mutex.synchronize do
167
+ @metrics[name]
168
+ end
169
+ end
170
+
171
+ # Public: Add a new metric.
172
+ #
173
+ # name - The String name of the metric to add
174
+ # metric - The metric instance to add
175
+ #
176
+ # Examples
177
+ #
178
+ # registry.add('method.calls', Metriks::Counter.new)
179
+ #
180
+ # Returns nothing.
181
+ # Raises RuntimeError if the metric name is already defined
182
+ def add(name, metric)
183
+ @mutex.synchronize do
184
+ if @metrics[name]
185
+ raise "Metric '#{name}' already defined"
186
+ else
187
+ @metrics[name] = metric
188
+ end
189
+ end
190
+ end
191
+
192
+ protected
193
+ def add_or_get(name, klass, &create_metric)
194
+ @mutex.synchronize do
195
+ if metric = @metrics[name]
196
+ if !metric.is_a?(klass)
197
+ raise "Metric already defined as '#{metric.class}'"
198
+ else
199
+ return metric
200
+ end
201
+ else
202
+ @metrics[name] = create_metric ? create_metric.call : klass.new
203
+ end
204
+ end
205
+ end
206
+ end
207
+ end