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