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.
@@ -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
+