leafy 0.0.3 → 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.
- checksums.yaml +5 -5
- data/.gitignore +2 -0
- data/.rspec +1 -0
- data/.travis.yml +8 -0
- data/Gemfile +8 -0
- data/LICENSE +201 -0
- data/README.md +109 -0
- data/leafy.gemspec +28 -0
- data/lib/leafy/core/adder.rb +28 -0
- data/lib/leafy/core/clock.rb +29 -0
- data/lib/leafy/core/console_reporter.rb +157 -0
- data/lib/leafy/core/counter.rb +33 -0
- data/lib/leafy/core/ewma.rb +95 -0
- data/lib/leafy/core/gauge.rb +26 -0
- data/lib/leafy/core/histogram.rb +40 -0
- data/lib/leafy/core/meter.rb +92 -0
- data/lib/leafy/core/metric_registry.rb +213 -0
- data/lib/leafy/core/ratio_gauge.rb +56 -0
- data/lib/leafy/core/scheduled_reporter.rb +170 -0
- data/lib/leafy/core/sliding_window_reservoir.rb +42 -0
- data/lib/leafy/core/snapshot.rb +58 -0
- data/lib/leafy/core/timer.rb +105 -0
- data/lib/leafy/core/uniform_snapshot.rb +114 -0
- data/lib/leafy/core/version.rb +5 -0
- data/puma.rb +22 -0
- data/spec/console_reporter_spec.rb +362 -0
- data/spec/counter_spec.rb +50 -0
- data/spec/ewma_spec.rb +208 -0
- data/spec/histogram_spec.rb +20 -0
- data/spec/meter_spec.rb +40 -0
- data/spec/metric_registry_spec.rb +168 -0
- data/spec/ratio_gauge_spec.rb +47 -0
- data/spec/scheduled_reporter_spec.rb +135 -0
- data/spec/sliging_window_reservoir_spec.rb +22 -0
- data/spec/spec_helper.rb +123 -0
- data/spec/timer_spec.rb +100 -0
- data/spec/uniform_snapshot_spec.rb +126 -0
- metadata +120 -16
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'concurrent/thread_safe/util/adder'
|
2
|
+
|
3
|
+
# An incrementing and decrementing counter metric.
|
4
|
+
module Leafy
|
5
|
+
module Core
|
6
|
+
class Counter
|
7
|
+
def initialize
|
8
|
+
@count = Concurrent::ThreadSafe::Util::Adder.new
|
9
|
+
end
|
10
|
+
|
11
|
+
# Increment the counter by {@code n}, default by one.
|
12
|
+
#
|
13
|
+
# @param n the amount by which the counter will be increased
|
14
|
+
def inc(n = 1)
|
15
|
+
@count.add(n)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Decrement the counter by {@code n}, default by one.
|
19
|
+
#
|
20
|
+
# @param n the amount by which the counter will be decreased
|
21
|
+
def dec(n = 1)
|
22
|
+
@count.add(-n)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Returns the counter's current value.
|
26
|
+
#
|
27
|
+
# @return the counter's current value
|
28
|
+
def count
|
29
|
+
@count.sum
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'concurrent/thread_safe/util/volatile'
|
2
|
+
require_relative 'adder'
|
3
|
+
|
4
|
+
# An exponentially-weighted moving average.
|
5
|
+
#
|
6
|
+
# @see <a href="http://www.teamquest.com/pdfs/whitepaper/ldavg1.pdf">UNIX Load Average Part 1: How
|
7
|
+
# It Works</a>
|
8
|
+
# @see <a href="http://www.teamquest.com/pdfs/whitepaper/ldavg2.pdf">UNIX Load Average Part 2: Not
|
9
|
+
# Your Average Average</a>
|
10
|
+
# @see <a href="http://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average">EMA</a>
|
11
|
+
module Leafy
|
12
|
+
module Core
|
13
|
+
class EWMA
|
14
|
+
extend Concurrent::ThreadSafe::Util::Volatile
|
15
|
+
|
16
|
+
INTERVAL = 5 # 5 nanos
|
17
|
+
FIVE_NANOS = INTERVAL * 1000000000.0 # 5 nanos
|
18
|
+
ONE_NANOS = 1000000000.0 # 1 nanos
|
19
|
+
SECONDS_PER_MINUTE = 60.0
|
20
|
+
ONE_MINUTE = 1
|
21
|
+
FIVE_MINUTES = 5
|
22
|
+
FIFTEEN_MINUTES = 15
|
23
|
+
M1_ALPHA = 1 - Math.exp(-INTERVAL / SECONDS_PER_MINUTE / ONE_MINUTE)
|
24
|
+
M5_ALPHA = 1 - Math.exp(-INTERVAL / SECONDS_PER_MINUTE / FIVE_MINUTES)
|
25
|
+
M15_ALPHA = 1 - Math.exp(-INTERVAL / SECONDS_PER_MINUTE / FIFTEEN_MINUTES)
|
26
|
+
|
27
|
+
# Creates a new EWMA which is equivalent to the UNIX one minute load average and which expects
|
28
|
+
# to be ticked every 5 seconds.
|
29
|
+
#
|
30
|
+
# @return a one-minute EWMA
|
31
|
+
def self.oneMinuteEWMA
|
32
|
+
EWMA.new(M1_ALPHA);
|
33
|
+
end
|
34
|
+
|
35
|
+
# Creates a new EWMA which is equivalent to the UNIX five minute load average and which expects
|
36
|
+
# to be ticked every 5 seconds.
|
37
|
+
#
|
38
|
+
# @return a five-minute EWMA
|
39
|
+
def self.fiveMinuteEWMA
|
40
|
+
EWMA.new(M5_ALPHA);
|
41
|
+
end
|
42
|
+
|
43
|
+
# Creates a new EWMA which is equivalent to the UNIX fifteen minute load average and which
|
44
|
+
# expects to be ticked every 5 seconds.
|
45
|
+
#
|
46
|
+
# @return a fifteen-minute EWMA
|
47
|
+
def self.fifteenMinuteEWMA
|
48
|
+
EWMA.new(M15_ALPHA);
|
49
|
+
end
|
50
|
+
|
51
|
+
attr_volatile :initialized, :rate
|
52
|
+
|
53
|
+
# Create a new EWMA with a specific smoothing constant.
|
54
|
+
#
|
55
|
+
# @param alpha the smoothing constant
|
56
|
+
# @param interval the expected tick interval
|
57
|
+
# @param intervalUnit the time unit of the tick interval
|
58
|
+
def initialize(alpha)
|
59
|
+
@alpha = alpha;
|
60
|
+
@initialized = false # volatile
|
61
|
+
@rate = 0.0 # volatile
|
62
|
+
@uncounted = Adder.new
|
63
|
+
end
|
64
|
+
|
65
|
+
# Update the moving average with a new value.
|
66
|
+
#
|
67
|
+
# @param n the new value
|
68
|
+
def update(n)
|
69
|
+
@uncounted.add(n)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Mark the passage of time and decay the current rate accordingly.
|
73
|
+
def tick
|
74
|
+
count = @uncounted.sum_then_reset# @uncounted = 0 # uncounted.sumThenReset();
|
75
|
+
instantRate = count / FIVE_NANOS;
|
76
|
+
if @initialized
|
77
|
+
oldRate = @rate
|
78
|
+
@rate = oldRate + (@alpha * (instantRate - oldRate))
|
79
|
+
else
|
80
|
+
@rate = instantRate
|
81
|
+
@initialized = true
|
82
|
+
end
|
83
|
+
self
|
84
|
+
end
|
85
|
+
|
86
|
+
# Returns the rate in the given units of time.
|
87
|
+
#
|
88
|
+
# @param rateUnit the unit of time
|
89
|
+
# @return the rate
|
90
|
+
def rate
|
91
|
+
@rate * ONE_NANOS
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# A gauge.
|
2
|
+
module Leafy
|
3
|
+
module Core
|
4
|
+
class Gauge
|
5
|
+
include Concurrent::Synchronization::Volatile
|
6
|
+
|
7
|
+
attr_volatile :val
|
8
|
+
|
9
|
+
def initialize(&block)
|
10
|
+
@block = block
|
11
|
+
end
|
12
|
+
|
13
|
+
def value=(v)
|
14
|
+
self.val = v
|
15
|
+
end
|
16
|
+
|
17
|
+
def value
|
18
|
+
if @block
|
19
|
+
@block.call
|
20
|
+
else
|
21
|
+
self.val
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'concurrent/thread_safe/util/adder'
|
2
|
+
|
3
|
+
module Leafy
|
4
|
+
module Core
|
5
|
+
|
6
|
+
# A metric which calculates the distribution of a value.
|
7
|
+
#
|
8
|
+
# @see <a href="http://www.johndcook.com/standard_deviation.html">Accurately computing running
|
9
|
+
# variance</a>
|
10
|
+
class Histogram
|
11
|
+
|
12
|
+
# Creates a new {@link Histogram} with the given reservoir.
|
13
|
+
#
|
14
|
+
# @param reservoir the reservoir to create a histogram from
|
15
|
+
def initialize(reservoir)
|
16
|
+
@reservoir = reservoir
|
17
|
+
@count = Concurrent::ThreadSafe::Util::Adder.new
|
18
|
+
end
|
19
|
+
|
20
|
+
# Adds a recorded value.
|
21
|
+
#
|
22
|
+
# @param value the length of the value
|
23
|
+
def update(value)
|
24
|
+
@count.increment
|
25
|
+
@reservoir.update(value)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns the number of values recorded.
|
29
|
+
#
|
30
|
+
# @return the number of values recorded
|
31
|
+
def count
|
32
|
+
@count.sum
|
33
|
+
end
|
34
|
+
|
35
|
+
def snapshot
|
36
|
+
@reservoir.snapshot
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require_relative 'clock'
|
2
|
+
require_relative 'ewma'
|
3
|
+
require 'concurrent/thread_safe/util/adder'
|
4
|
+
require 'concurrent/atomic/atomic_fixnum'
|
5
|
+
|
6
|
+
# A meter metric which measures mean throughput and one-, five-, and fifteen-minute
|
7
|
+
# exponentially-weighted moving average throughputs.
|
8
|
+
#
|
9
|
+
# @see EWMA
|
10
|
+
module Leafy
|
11
|
+
module Core
|
12
|
+
class Meter
|
13
|
+
|
14
|
+
TICK_INTERVAL = 5 * 1000 * 1000 * 1000 # 5 Nano-Seconds
|
15
|
+
|
16
|
+
# Creates a new {@link Meter}.
|
17
|
+
# @param clock the clock to use for the meter ticks
|
18
|
+
def initialize(clock = Clock.default_clock)
|
19
|
+
@clock = clock;
|
20
|
+
@startTime = clock.tick
|
21
|
+
@lastTick = Concurrent::AtomicFixnum.new(@startTime)#AtomicLong.new(startTime)
|
22
|
+
@count = Adder.new
|
23
|
+
|
24
|
+
@m1Rate = EWMA.oneMinuteEWMA
|
25
|
+
@m5Rate = EWMA.fiveMinuteEWMA
|
26
|
+
@m15Rate = EWMA.fifteenMinuteEWMA
|
27
|
+
end
|
28
|
+
|
29
|
+
# Mark the occurrence of an event.
|
30
|
+
def mark(n = 1)
|
31
|
+
tickIfNecessary
|
32
|
+
@count.add(n)
|
33
|
+
@m1Rate.update(n)
|
34
|
+
@m5Rate.update(n)
|
35
|
+
@m15Rate.update(n)
|
36
|
+
n
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
def count
|
41
|
+
@count.sum
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
def fifteen_minute_rate
|
46
|
+
tickIfNecessary
|
47
|
+
@m15Rate.rate
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
def five_minute_rate
|
52
|
+
tickIfNecessary
|
53
|
+
@m5Rate.rate
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
def one_minute_rate
|
58
|
+
tickIfNecessary
|
59
|
+
@m1Rate.rate
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
def mean_rate
|
64
|
+
if count == 0
|
65
|
+
0.0
|
66
|
+
else
|
67
|
+
elapsed = @clock.tick - @startTime
|
68
|
+
count * 1000000000.0 / elapsed
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def tickIfNecessary
|
75
|
+
oldTick = @lastTick.value #.get
|
76
|
+
newTick = @clock.tick
|
77
|
+
age = newTick - oldTick
|
78
|
+
if age > TICK_INTERVAL
|
79
|
+
newIntervalStartTick = newTick - age % TICK_INTERVAL
|
80
|
+
if (@lastTick.compare_and_set(oldTick, newIntervalStartTick))
|
81
|
+
requiredTicks = age / TICK_INTERVAL
|
82
|
+
requiredTicks.to_i.times do
|
83
|
+
@m1Rate.tick
|
84
|
+
@m5Rate.tick
|
85
|
+
@m15Rate.tick
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,213 @@
|
|
1
|
+
require 'concurrent/map'
|
2
|
+
require_relative 'ratio_gauge'
|
3
|
+
require_relative 'counter'
|
4
|
+
require_relative 'meter'
|
5
|
+
require_relative 'histogram'
|
6
|
+
require_relative 'timer'
|
7
|
+
|
8
|
+
module Leafy
|
9
|
+
module Core
|
10
|
+
|
11
|
+
# A registry of metric instances.
|
12
|
+
class MetricRegistry
|
13
|
+
|
14
|
+
# Creates a new {@link MetricRegistry}.
|
15
|
+
def initialize
|
16
|
+
@metrics = build_map
|
17
|
+
end
|
18
|
+
|
19
|
+
# Creates a new {@link ConcurrentMap} implementation for use inside the registry. Override this
|
20
|
+
# to create a {@link MetricRegistry} with space- or time-bounded metric lifecycles, for
|
21
|
+
# example.
|
22
|
+
#
|
23
|
+
# @return a new {@link ConcurrentMap}
|
24
|
+
def build_map
|
25
|
+
Concurrent::Map.new
|
26
|
+
end
|
27
|
+
|
28
|
+
# Given a {@link Metric}, registers it under the given name.
|
29
|
+
#
|
30
|
+
# @param name the name of the metric
|
31
|
+
# @param metric the metric
|
32
|
+
# @param <T> the type of the metric
|
33
|
+
# @return {@code metric}
|
34
|
+
# @throws IllegalArgumentException if the name is already registered
|
35
|
+
def register(name, metric)
|
36
|
+
existing = @metrics.put_if_absent(name, metric)
|
37
|
+
if existing
|
38
|
+
raise ArgumentError.new("A metric named #{name} already exists")
|
39
|
+
end
|
40
|
+
metric
|
41
|
+
end
|
42
|
+
|
43
|
+
# Return the {@link Gauge} registered under this name or create and register
|
44
|
+
# a new {@link Gauge} if none is registered.
|
45
|
+
#
|
46
|
+
# @param name the name of the metric
|
47
|
+
# @param supplier a Builder that can be used to manufacture a Gauge
|
48
|
+
# @return a new or pre-existing {@link Gauge}
|
49
|
+
def gauge(name, builder)
|
50
|
+
getOrAdd(name, builder)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Return the {@link Counter} registered under this name or create and register
|
54
|
+
# a new {@link Counter} if none is registered.
|
55
|
+
#
|
56
|
+
# @param name the name of the metric
|
57
|
+
# @return a new or pre-existing {@link Counter}
|
58
|
+
def counter(name)
|
59
|
+
getOrAdd(name, Builder.counters)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Return the {@link Histogram} registered under this name or create and register
|
63
|
+
# a new {@link Histogram} if none is registered.
|
64
|
+
#
|
65
|
+
# @param name the name of the metric
|
66
|
+
# @return a new or pre-existing {@link Histogram}
|
67
|
+
def histogram(name)
|
68
|
+
getOrAdd(name, Builder.histograms)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Return the {@link Meter} registered under this name or create and register
|
72
|
+
# a new {@link Meter} if none is registered.
|
73
|
+
#
|
74
|
+
# @param name the name of the metric
|
75
|
+
# @return a new or pre-existing {@link Meter}
|
76
|
+
def meter(name)
|
77
|
+
getOrAdd(name, Builder.meters)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Return the {@link Timer} registered under this name or create and register
|
81
|
+
# a new {@link Timer} if none is registered.
|
82
|
+
#
|
83
|
+
# @param name the name of the metric
|
84
|
+
# @return a new or pre-existing {@link Timer}
|
85
|
+
def timer(name)
|
86
|
+
getOrAdd(name, Builder.timers)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Removes the metric with the given name.
|
90
|
+
#
|
91
|
+
# @param name the name of the metric
|
92
|
+
# @return whether or not the metric was removed
|
93
|
+
#/
|
94
|
+
def remove(name)
|
95
|
+
metric = @metrics.delete(name)
|
96
|
+
metric != nil
|
97
|
+
end
|
98
|
+
|
99
|
+
# Returns a set of the names of all the metrics in the registry.
|
100
|
+
#
|
101
|
+
# @return the names of all the metrics
|
102
|
+
def names
|
103
|
+
@metrics.keys.dup.freeze
|
104
|
+
end
|
105
|
+
|
106
|
+
# Returns a map of all the gauges in the registry and their names.
|
107
|
+
#
|
108
|
+
# @return all the gauges in the registry
|
109
|
+
def gauges
|
110
|
+
metrics(Gauge)
|
111
|
+
end
|
112
|
+
|
113
|
+
# Returns a map of all the counters in the registry and their names.
|
114
|
+
#
|
115
|
+
# @return all the counters in the registry
|
116
|
+
def counters
|
117
|
+
metrics(Counter)
|
118
|
+
end
|
119
|
+
|
120
|
+
# Returns a map of all the histograms in the registry and their names.
|
121
|
+
#
|
122
|
+
# @return all the histograms in the registry
|
123
|
+
def histograms
|
124
|
+
metrics(Histogram)
|
125
|
+
end
|
126
|
+
|
127
|
+
# Returns a map of all the meters in the registry and their names.
|
128
|
+
#
|
129
|
+
# @return all the meters in the registry
|
130
|
+
def meters
|
131
|
+
metrics(Meter)
|
132
|
+
end
|
133
|
+
|
134
|
+
# Returns a map of all the timers in the registry and their names.
|
135
|
+
#
|
136
|
+
# @return all the timers in the registry
|
137
|
+
def timers
|
138
|
+
metrics(Timer)
|
139
|
+
end
|
140
|
+
|
141
|
+
def metrics(klass = nil)
|
142
|
+
if klass
|
143
|
+
result = {}
|
144
|
+
@metrics.each do |k,v|
|
145
|
+
result[k] = v if v.is_a?(klass)
|
146
|
+
end
|
147
|
+
result.freeze
|
148
|
+
else
|
149
|
+
Hash[@metrics.keys.zip(@metrics.values)].freeze
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def getOrAdd(name, builder)
|
154
|
+
metric = @metrics[name]
|
155
|
+
if builder.instance?(metric)
|
156
|
+
return metric
|
157
|
+
elsif metric.nil?
|
158
|
+
begin
|
159
|
+
return register(name, builder.new_metric)
|
160
|
+
rescue ArgumentError
|
161
|
+
added = @metrics[name]
|
162
|
+
return added if builder.instance?(added)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
raise ArgumentError.new("#{name} is already used for a different type of metric")
|
166
|
+
end
|
167
|
+
private :getOrAdd
|
168
|
+
|
169
|
+
# A quick and easy way of capturing the notion of default metrics.
|
170
|
+
class Builder
|
171
|
+
def initialize(klass, &block)
|
172
|
+
@klass = klass
|
173
|
+
@block = (block || klass.method(:new))
|
174
|
+
end
|
175
|
+
|
176
|
+
def new_metric
|
177
|
+
@block.call
|
178
|
+
end
|
179
|
+
|
180
|
+
def instance?(metric)
|
181
|
+
metric.is_a?(@klass)
|
182
|
+
end
|
183
|
+
|
184
|
+
class << self
|
185
|
+
|
186
|
+
def counters
|
187
|
+
@counters ||= Builder.new(Counter)
|
188
|
+
end
|
189
|
+
|
190
|
+
def histograms
|
191
|
+
@histograms ||= begin
|
192
|
+
builder = Builder.new(Histogram)
|
193
|
+
def builder.new_metric
|
194
|
+
Histogram.new(SlidingWindowReservoir.new(16))
|
195
|
+
end
|
196
|
+
builder
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def meters
|
201
|
+
@meters ||= Builder.new(Meter)
|
202
|
+
end
|
203
|
+
|
204
|
+
def timers
|
205
|
+
@timers ||= Builder.new(Timer)
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|