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,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
|
+
|