leafy 0.0.3 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+