leafy 0.0.3 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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,56 @@
|
|
1
|
+
require_relative 'gauge'
|
2
|
+
|
3
|
+
# A gauge which measures the ratio of one value to another.
|
4
|
+
#
|
5
|
+
# If the denominator is zero, not a number, or infinite, the resulting ratio is not a number.
|
6
|
+
module Leafy
|
7
|
+
module Core
|
8
|
+
class RatioGauge < Gauge
|
9
|
+
|
10
|
+
# A ratio of one quantity to another.
|
11
|
+
class Ratio
|
12
|
+
|
13
|
+
# Creates a new ratio with the given numerator and denominator.
|
14
|
+
#
|
15
|
+
# @param numerator the numerator of the ratio
|
16
|
+
# @param denominator the denominator of the ratio
|
17
|
+
# @return {@code numerator:denominator}
|
18
|
+
def self.of(numerator, denominator)
|
19
|
+
Ratio.new(numerator, denominator)
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(numerator, denominator)
|
23
|
+
@numerator = numerator
|
24
|
+
@denominator = denominator
|
25
|
+
end
|
26
|
+
|
27
|
+
# Returns the ratio, which is either a {@code double} between 0 and 1 (inclusive) or
|
28
|
+
# {@code NaN}.
|
29
|
+
#
|
30
|
+
# @return the ratio
|
31
|
+
def value
|
32
|
+
if !(@denominator.is_a?(Float)) || @denominator.infinite? || @denominator.nan? || @denominator == 0
|
33
|
+
return Float::NAN
|
34
|
+
end
|
35
|
+
@numerator.to_f / @denominator.to_f
|
36
|
+
end
|
37
|
+
|
38
|
+
def to_s
|
39
|
+
"#{@numerator.to_f}:#{@denominator.to_f}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Returns the {@link Ratio} which is the gauge's current value.
|
44
|
+
#
|
45
|
+
# @return the {@link Ratio} which is the gauge's current value
|
46
|
+
def ratio
|
47
|
+
@block.call
|
48
|
+
end
|
49
|
+
protected :ratio
|
50
|
+
|
51
|
+
def value
|
52
|
+
ratio.value
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,170 @@
|
|
1
|
+
require 'concurrent/thread_safe/util/cheap_lockable'
|
2
|
+
require 'concurrent'
|
3
|
+
require_relative 'metric_registry'
|
4
|
+
|
5
|
+
module Leafy
|
6
|
+
module Core
|
7
|
+
class ScheduledReporter
|
8
|
+
include Concurrent::ThreadSafe::Util::CheapLockable
|
9
|
+
|
10
|
+
def self.logger(logger = nil)
|
11
|
+
@logger ||= logger || (require 'logger'; Logger.new(STDERR))
|
12
|
+
end
|
13
|
+
|
14
|
+
def logger
|
15
|
+
self.class.logger
|
16
|
+
end
|
17
|
+
|
18
|
+
#FACTORY_ID = Concurrent::AtomicFixnum.new
|
19
|
+
def self.createDefaultExecutor(_name)
|
20
|
+
Concurrent::SingleThreadExecutor.new
|
21
|
+
#return Executors.newSingleThreadScheduledExecutor(new NamedThreadFactory(name + '-' + FACTORY_ID.incrementAndGet()));
|
22
|
+
end
|
23
|
+
|
24
|
+
def initialize(registry, name, executor = nil, shutdownExecutorOnStop = true)
|
25
|
+
super() # for cheap_lockable
|
26
|
+
@registry = registry;
|
27
|
+
@executor = executor.nil? ? self.class.createDefaultExecutor(name) : executor
|
28
|
+
@shutdownExecutorOnStop = shutdownExecutorOnStop
|
29
|
+
@rateFactor = 1.0
|
30
|
+
@durationFactor = 1000000.0
|
31
|
+
end
|
32
|
+
|
33
|
+
class ReportedTask
|
34
|
+
|
35
|
+
attr_reader :period
|
36
|
+
|
37
|
+
def initialize(reporter, start, period)
|
38
|
+
@reporter = reporter
|
39
|
+
@start = start
|
40
|
+
@period = period
|
41
|
+
end
|
42
|
+
|
43
|
+
def delay
|
44
|
+
@period - (Concurrent.monotonic_time - @start) % @period
|
45
|
+
end
|
46
|
+
|
47
|
+
def call
|
48
|
+
@reporter.report
|
49
|
+
end
|
50
|
+
|
51
|
+
def task(task = nil)
|
52
|
+
@task ||= task
|
53
|
+
end
|
54
|
+
|
55
|
+
def to_s
|
56
|
+
"start: #{Time.at(@start).utc} period: #{@period}"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
# obserer callback from scheduled task used to trigger the task for the
|
62
|
+
# next report
|
63
|
+
def update(_time, _value, reason)
|
64
|
+
return if reason.is_a? Concurrent::CancelledOperationError
|
65
|
+
cheap_synchronize do
|
66
|
+
raise ArgumentError.new("Reporter not started started") unless @scheduledFuture
|
67
|
+
task = Concurrent::ScheduledTask.new(@scheduledFuture.delay, executor: @executor, &@scheduledFuture.method(:call))
|
68
|
+
task.add_observer(self)
|
69
|
+
task.execute
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Starts the reporter polling at the given period.
|
74
|
+
#
|
75
|
+
# @param initialDelay the time to delay the first execution
|
76
|
+
# @param period the amount of time between polls
|
77
|
+
# @param unit the unit for {@code period}
|
78
|
+
def start(initial_delay, period = initial_delay)
|
79
|
+
cheap_synchronize do
|
80
|
+
raise ArgumentError.new("Reporter already started") if @scheduledFuture
|
81
|
+
start = Concurrent.monotonic_time + initial_delay
|
82
|
+
|
83
|
+
@scheduledFuture = ReportedTask.new(self, start, period)
|
84
|
+
task = Concurrent::ScheduledTask.new(initial_delay, executor: @executor, &@scheduledFuture.method(:call))
|
85
|
+
task.add_observer(self)
|
86
|
+
@scheduledFuture.task(task)
|
87
|
+
task.execute
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Stops the reporter and if shutdownExecutorOnStop is true then shuts down its thread of execution.
|
92
|
+
# <p>
|
93
|
+
# Uses the shutdown pattern from http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ExecutorService.html
|
94
|
+
def stop
|
95
|
+
if @shutdownExecutorOnStop
|
96
|
+
@executor.shutdown # Disable new tasks from being submitted
|
97
|
+
# Wait a while for existing tasks to terminate
|
98
|
+
unless @executor.wait_for_termination(1)
|
99
|
+
@executor.shutdown # Cancel currently executing tasks
|
100
|
+
# Wait a while for tasks to respond to being cancelled
|
101
|
+
unless @executor.wait_for_termination(1)
|
102
|
+
logger.warn "#{self.class.name}: ScheduledExecutorService did not terminate"
|
103
|
+
end
|
104
|
+
end
|
105
|
+
else
|
106
|
+
# The external manager(like JEE container) responsible for lifecycle of executor
|
107
|
+
cheap_synchronize do
|
108
|
+
return if @scheduledFuture.nil? # was never started
|
109
|
+
return if @scheduledFuture.task.cancelled? # already cancelled
|
110
|
+
# just cancel the scheduledFuture and exit
|
111
|
+
@scheduledFuture.task.cancel
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Report the current values of all metrics in the registry.
|
117
|
+
def report
|
118
|
+
cheap_synchronize do
|
119
|
+
do_report(@registry.gauges,
|
120
|
+
@registry.counters,
|
121
|
+
@registry.histograms,
|
122
|
+
@registry.meters,
|
123
|
+
@registry.timers)
|
124
|
+
end
|
125
|
+
rescue => ex
|
126
|
+
logger.error("Exception thrown from #{self.class.name}#report. Exception was suppressed: #{ex.message}", e)
|
127
|
+
end
|
128
|
+
|
129
|
+
# Called periodically by the polling thread. Subclasses should report all the given metrics.
|
130
|
+
#
|
131
|
+
# @param gauges all of the gauges in the registry
|
132
|
+
# @param counters all of the counters in the registry
|
133
|
+
# @param histograms all of the histograms in the registry
|
134
|
+
# @param meters all of the meters in the registry
|
135
|
+
# @param timers all of the timers in tdhe registry
|
136
|
+
def do_report(_gauges,
|
137
|
+
_counters,
|
138
|
+
_histograms,
|
139
|
+
_meters,
|
140
|
+
_timers)
|
141
|
+
raise 'not implemented'
|
142
|
+
end
|
143
|
+
|
144
|
+
|
145
|
+
def rate_unit
|
146
|
+
'second'
|
147
|
+
end
|
148
|
+
protected :rate_unit
|
149
|
+
|
150
|
+
def duration_unit
|
151
|
+
'milliseconds'
|
152
|
+
end
|
153
|
+
protected :duration_unit
|
154
|
+
|
155
|
+
def convert_duration(duration)
|
156
|
+
duration / @durationFactor;
|
157
|
+
end
|
158
|
+
protected :convert_duration
|
159
|
+
|
160
|
+
def convert_rate(rate)
|
161
|
+
rate * @rateFactor
|
162
|
+
end
|
163
|
+
protected :convert_rate
|
164
|
+
|
165
|
+
def to_s
|
166
|
+
"#{self.class}: #{@scheduledFuture}"
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require_relative 'uniform_snapshot'
|
2
|
+
require 'concurrent/thread_safe/util/cheap_lockable'
|
3
|
+
|
4
|
+
# A {@link Reservoir} implementation backed by a sliding window that stores the last {@code N}
|
5
|
+
# measurements.
|
6
|
+
module Leafy
|
7
|
+
module Core
|
8
|
+
class SlidingWindowReservoir
|
9
|
+
include Concurrent::ThreadSafe::Util::CheapLockable
|
10
|
+
|
11
|
+
# Creates a new {@link SlidingWindowReservoir} which stores the last {@code size} measurements.
|
12
|
+
#
|
13
|
+
# @param size the number of measurements to store
|
14
|
+
def initialize(size)
|
15
|
+
super() # for cheap_lockable
|
16
|
+
@measurements = Array.new size
|
17
|
+
@count = 0
|
18
|
+
end
|
19
|
+
|
20
|
+
def size
|
21
|
+
cheap_synchronize do
|
22
|
+
[@count, @measurements.size].min
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def update(value)
|
27
|
+
cheap_synchronize do
|
28
|
+
@measurements[(@count % @measurements.size)] = value;
|
29
|
+
@count += 1
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def snapshot
|
34
|
+
values = nil
|
35
|
+
cheap_synchronize do
|
36
|
+
values = @measurements.dup
|
37
|
+
end
|
38
|
+
UniformSnapshot.new(*values)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Leafy
|
2
|
+
module Core
|
3
|
+
|
4
|
+
# A statistical snapshot of a {@link Snapshot}.
|
5
|
+
class Snapshot
|
6
|
+
|
7
|
+
# Returns the value at the given quantile.
|
8
|
+
#
|
9
|
+
# @param quantile a given quantile, in {@code [0..1]}
|
10
|
+
# @return the value in the distribution at {@code quantile}
|
11
|
+
def value(_quantile)
|
12
|
+
raise 'not implemented'
|
13
|
+
end
|
14
|
+
|
15
|
+
# Returns the median value in the distribution.
|
16
|
+
#
|
17
|
+
# @return the median value
|
18
|
+
def median
|
19
|
+
value(0.5)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Returns the value at the 75th percentile in the distribution.
|
23
|
+
#
|
24
|
+
# @the value at the 75th percentile
|
25
|
+
def get_75th_percentile
|
26
|
+
value(0.75)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns the value at the 95th percentile in the distribution.
|
30
|
+
#
|
31
|
+
# @the value at the 95th percentile
|
32
|
+
def get_95th_percentile
|
33
|
+
value(0.95)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Returns the value at the 98th percentile in the distribution.
|
37
|
+
#
|
38
|
+
# @the value at the 98th percentile
|
39
|
+
def get_98th_percentile
|
40
|
+
value(0.98)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Returns the value at the 99th percentile in the distribution.
|
44
|
+
#
|
45
|
+
# @the value at the 99th percentile
|
46
|
+
def get_99th_percentile
|
47
|
+
value(0.99)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Returns the value at the 99.9th percentile in the distribution.
|
51
|
+
#
|
52
|
+
# @the value at the 99.9th percentile
|
53
|
+
def get_999th_percentile
|
54
|
+
value(0.999)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require_relative 'clock'
|
2
|
+
require_relative 'meter'
|
3
|
+
require_relative 'histogram'
|
4
|
+
|
5
|
+
module Leafy
|
6
|
+
module Core
|
7
|
+
|
8
|
+
# A timer metric which aggregates timing durations and provides duration statistics, plus
|
9
|
+
# throughput statistics via {@link Meter}.
|
10
|
+
class Timer
|
11
|
+
|
12
|
+
# A timing context.
|
13
|
+
#
|
14
|
+
# @see Timer#context()
|
15
|
+
class Context
|
16
|
+
def initialize(timer, clock)
|
17
|
+
@timer = timer
|
18
|
+
@clock = clock
|
19
|
+
@startTime = clock.tick
|
20
|
+
end
|
21
|
+
|
22
|
+
# Updates the timer with the difference between current and start time. Call to this method will
|
23
|
+
# not reset the start time. Multiple calls result in multiple updates.
|
24
|
+
#
|
25
|
+
# @return the elapsed time in nanoseconds
|
26
|
+
def stop
|
27
|
+
elapsed = @clock.tick - @startTime
|
28
|
+
@timer.update(elapsed / 1000000000.0)
|
29
|
+
elapsed
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Creates a new {@link Timer} that uses the given {@link Reservoir} and {@link Clock}.
|
34
|
+
#
|
35
|
+
# @param reservoir the {@link Reservoir} implementation the timer should use
|
36
|
+
# @param clock the {@link Clock} implementation the timer should use
|
37
|
+
def initialize(reservoir = SlidingWindowReservoir, clock = Clock.default_clock)
|
38
|
+
@meter = Meter.new(clock)
|
39
|
+
@clock = clock
|
40
|
+
@histogram = Histogram.new(reservoir)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Adds a recorded duration.
|
44
|
+
#
|
45
|
+
# @param duration the length of the duration in seconds
|
46
|
+
def update(duration)
|
47
|
+
if duration >= 0
|
48
|
+
@histogram.update(duration * 1000000000.0)
|
49
|
+
@meter.mark
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Times and records the duration of event.
|
54
|
+
#
|
55
|
+
# @param event a {@link Runnable} whose {@link Runnable#run()} method implements a process
|
56
|
+
# whose duration should be timed
|
57
|
+
def time(&block)
|
58
|
+
startTime = @clock.tick
|
59
|
+
begin
|
60
|
+
block.call
|
61
|
+
ensure
|
62
|
+
update((@clock.tick - startTime) / 1000000000.0)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Returns a new {@link Context}.
|
67
|
+
#
|
68
|
+
# @return a new {@link Context}
|
69
|
+
# @see Context
|
70
|
+
def context(&block)
|
71
|
+
ctx = Context.new(self, @clock)
|
72
|
+
if block_given?
|
73
|
+
block.call ctx
|
74
|
+
ctx.stop
|
75
|
+
else
|
76
|
+
ctx
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def count
|
81
|
+
@histogram.count
|
82
|
+
end
|
83
|
+
|
84
|
+
def fifteen_minute_rate
|
85
|
+
@meter.fifteen_minute_rate
|
86
|
+
end
|
87
|
+
|
88
|
+
def five_minute_rate
|
89
|
+
@meter.five_minute_rate
|
90
|
+
end
|
91
|
+
|
92
|
+
def one_minute_rate
|
93
|
+
@meter.one_minute_rate
|
94
|
+
end
|
95
|
+
|
96
|
+
def mean_rate
|
97
|
+
@meter.mean_rate
|
98
|
+
end
|
99
|
+
|
100
|
+
def snapshot
|
101
|
+
@histogram.snapshot
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
require_relative 'snapshot'
|
2
|
+
|
3
|
+
# A statistical snapshot of a {@link UniformSnapshot}.
|
4
|
+
module Leafy
|
5
|
+
module Core
|
6
|
+
class UniformSnapshot < Snapshot
|
7
|
+
|
8
|
+
# Create a new {@link Snapshot} with the given values.
|
9
|
+
#
|
10
|
+
# @param values an unordered set of values in the reservoir that can be used by this class directly
|
11
|
+
def initialize(*values)
|
12
|
+
@values = values.dup.compact.sort
|
13
|
+
end
|
14
|
+
|
15
|
+
# Returns the value at the given quantile.
|
16
|
+
#
|
17
|
+
# @param quantile a given quantile, in {@code [0..1]}
|
18
|
+
# @return the value in the distribution at {@code quantile}
|
19
|
+
def value(quantile)
|
20
|
+
if quantile < 0.0 || quantile > 1.0
|
21
|
+
raise ArgumentError.new("#{quantile} is not in [0..1]")
|
22
|
+
end
|
23
|
+
|
24
|
+
return 0.0 if @values.empty?
|
25
|
+
|
26
|
+
pos = quantile * (values.size + 1)
|
27
|
+
index = pos.to_i
|
28
|
+
|
29
|
+
return @values[0] if index < 1
|
30
|
+
|
31
|
+
return @values.last if index >= @values.size
|
32
|
+
|
33
|
+
lower = @values[index - 1]
|
34
|
+
upper = @values[index]
|
35
|
+
lower + (pos - pos.floor) * (upper - lower)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns the number of values in the snapshot.
|
39
|
+
#
|
40
|
+
# @return the number of values
|
41
|
+
def size
|
42
|
+
@values.size
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns the entire set of values in the snapshot.
|
46
|
+
#
|
47
|
+
# @return the entire set of values
|
48
|
+
def values
|
49
|
+
@values.dup
|
50
|
+
end
|
51
|
+
|
52
|
+
# Returns the highest value in the snapshot.
|
53
|
+
#
|
54
|
+
# @return the highest value
|
55
|
+
#/
|
56
|
+
def max
|
57
|
+
return 0 if @values.empty?
|
58
|
+
@values.last
|
59
|
+
end
|
60
|
+
|
61
|
+
# Returns the lowest value in the snapshot.
|
62
|
+
#
|
63
|
+
# @return the lowest value
|
64
|
+
def min
|
65
|
+
return 0 if @values.empty?
|
66
|
+
@values.first
|
67
|
+
end
|
68
|
+
|
69
|
+
# Returns the arithmetic mean of the values in the snapshot.
|
70
|
+
#
|
71
|
+
# @return the arithmetic mean
|
72
|
+
def mean
|
73
|
+
return 0 if @values.empty?
|
74
|
+
|
75
|
+
|
76
|
+
sum = 0;
|
77
|
+
@values.each do |value|
|
78
|
+
sum += value
|
79
|
+
end
|
80
|
+
sum / @values.size
|
81
|
+
end
|
82
|
+
|
83
|
+
# Returns the standard deviation of the values in the snapshot.
|
84
|
+
#
|
85
|
+
# @return the standard deviation value
|
86
|
+
def std_dev
|
87
|
+
# two-pass algorithm for variance, avoids numeric overflow
|
88
|
+
|
89
|
+
return 0.0 if @values.size <= 1
|
90
|
+
|
91
|
+
mean = self.mean
|
92
|
+
sum = 0.0
|
93
|
+
|
94
|
+
@values.each do |value|
|
95
|
+
diff = value - mean
|
96
|
+
sum += diff * diff
|
97
|
+
end
|
98
|
+
|
99
|
+
variance = sum / (@values.size - 1)
|
100
|
+
Math.sqrt(variance)
|
101
|
+
end
|
102
|
+
|
103
|
+
# Writes the values of the snapshot to the given stream.
|
104
|
+
#
|
105
|
+
# @param output an output stream
|
106
|
+
def dump(out)
|
107
|
+
@values.each do |value|
|
108
|
+
out.printf("%d\n", value)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|