twingly-metrics 0.5.0 → 0.6.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a1f844f321fabb39da0b4fb3bc79df69a6d16586d16675b3625df796bc58c6ca
4
- data.tar.gz: 9373cdfe43cc5637803e1d87999944372e8d1260b24ae8964b16fd3d49642f8e
3
+ metadata.gz: 3d5cfa50a735b939760dc8242470280217152c47bca4bc4e0de092d405529399
4
+ data.tar.gz: e754a813aeeb419c14c33c342f04781c6cd544af203b3a2c2f74e9d999a2260e
5
5
  SHA512:
6
- metadata.gz: '0845dcb417c724ab4e0c921c4a5d15a0bb17edf12c1e34543a5f42462309b076d178deb85f71962bfd8a88c8a838379e3f74a23c8cdfbedc05278d19e6eba5ee'
7
- data.tar.gz: 362a8ff8636fecf2306ca58a9e69cfd97112a388eef9d8bab1549bf1c9c2b261416eeb5915fbc62d80c40347c3ab56d3db0d48f4b6f50c5d8603458fb917a56d
6
+ metadata.gz: 9d41769505fd9f03261afc987f33cb469b61ca5273aeedb624718062848c05866dde2abc28afbe9ec409ab5406aa21c087da0e663515cfdb921bd1cefb962699
7
+ data.tar.gz: 0bf2a9be9289336f4cd04be6d1af51f23125c20c986768be19d74e613790427d3b8cd4704f813539f38df8ecc5593da24b145091d43ef27099399dccdd5ff1e5
data/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  A gem for collecting and reporting metrics from our Ruby applications.
6
6
 
7
- This gem currently relies on the [`metriks` gem][metriks] for collecting metrics. The reason for this is that we're already using that gem in our projects. What the `twingly-metrics` gem currently adds in functionality, is the ability to report metrics to [Grafana cloud][grafana-cloud], using their [Graphite HTTP API][grafana-cloud-graphite-api], along with some convenience methods for working with metrics.
7
+ This gem provides its own implementation of common metric types and the ability to report them to [Grafana Cloud][grafana-cloud] using their [Graphite HTTP API][grafana-cloud-graphite-api].
8
8
 
9
9
  The code here was inspired by the reporter in the [`metriks-librato_metrics` gem][metriks-librato_metrics].
10
10
 
@@ -86,7 +86,6 @@ Twingly::Metrics.mark("my_meter", 1)
86
86
  Twingly::Metrics.time("my_timer") { ... }
87
87
  ```
88
88
 
89
- [metriks]: https://github.com/eric/metriks
90
89
  [grafana-cloud]: https://grafana.com/products/cloud/
91
90
  [grafana-cloud-graphite-api]: https://grafana.com/docs/grafana-cloud/send-data/metrics/metrics-graphite/http-api/#adding-new-data-posting-to-metrics
92
91
  [metriks-librato_metrics]: https://github.com/eric/metriks-librato_metrics
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "atomic"
4
+
5
+ module Twingly
6
+ module Metrics
7
+ # Public: Counters are one of the simplest metrics whose only operations
8
+ # are increment and decrement.
9
+ class Counter
10
+ # Public: Initialize a new Counter.
11
+ def initialize
12
+ @count = Atomic.new(0)
13
+ end
14
+
15
+ # Public: Reset the counter back to 0
16
+ #
17
+ # Returns nothing.
18
+ def clear
19
+ @count.value = 0
20
+ end
21
+
22
+ # Public: Increment the counter.
23
+ #
24
+ # incr - The value to add to the counter.
25
+ #
26
+ # Returns nothing.
27
+ def increment(incr = 1)
28
+ @count.update { |v| v + incr }
29
+ end
30
+
31
+ # Public: Decrement the counter.
32
+ #
33
+ # decr - The value to subtract from the counter.
34
+ #
35
+ # Returns nothing.
36
+ def decrement(decr = 1)
37
+ @count.update { |v| v - decr }
38
+ end
39
+
40
+ # Public: The current count.
41
+ #
42
+ # Returns the count.
43
+ def count
44
+ @count.value
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "atomic"
4
+
5
+ module Twingly
6
+ module Metrics
7
+ class EWMA
8
+ INTERVAL = 5.0
9
+ SECONDS_PER_MINUTE = 60.0
10
+
11
+ ONE_MINUTE = 1
12
+ FIVE_MINUTES = 5
13
+ FIFTEEN_MINUTES = 15
14
+
15
+ M1_ALPHA = 1 - Math.exp(-INTERVAL / SECONDS_PER_MINUTE / ONE_MINUTE)
16
+ M5_ALPHA = 1 - Math.exp(-INTERVAL / SECONDS_PER_MINUTE / FIVE_MINUTES)
17
+ M15_ALPHA = 1 - Math.exp(-INTERVAL / SECONDS_PER_MINUTE / FIFTEEN_MINUTES)
18
+
19
+ def self.new_m1
20
+ new(M1_ALPHA, INTERVAL)
21
+ end
22
+
23
+ def self.new_m5
24
+ new(M5_ALPHA, INTERVAL)
25
+ end
26
+
27
+ def self.new_m15
28
+ new(M15_ALPHA, INTERVAL)
29
+ end
30
+
31
+ def initialize(alpha, interval)
32
+ @alpha = alpha
33
+ @interval = interval
34
+
35
+ @initialized = false
36
+ @rate = Atomic.new(0.0)
37
+ @uncounted = Atomic.new(0)
38
+ end
39
+
40
+ def clear
41
+ @initialized = false
42
+ @rate.value = 0.0
43
+ @uncounted.value = 0
44
+ end
45
+
46
+ def update(value)
47
+ @uncounted.update { |v| v + value }
48
+ end
49
+
50
+ def tick
51
+ count = @uncounted.swap(0)
52
+ instant_rate = count / @interval.to_f
53
+
54
+ if @initialized
55
+ @rate.update { |v| v + (@alpha * (instant_rate - v)) }
56
+ else
57
+ @rate.value = instant_rate
58
+ @initialized = true
59
+ end
60
+ end
61
+
62
+ def rate
63
+ @rate.value
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "atomic"
4
+ require "red_black_tree"
5
+ require_relative "snapshot"
6
+
7
+ module Twingly
8
+ module Metrics
9
+ class ExponentiallyDecayingSample
10
+ RESCALE_THRESHOLD = 60 * 60 # 1 hour
11
+
12
+ def initialize(reservoir_size, alpha, values = nil)
13
+ @values = values || ConcurrentRedBlackTree.new
14
+ @count = Atomic.new(0)
15
+ @next_scale_time = Atomic.new(0)
16
+ @alpha = alpha
17
+ @reservoir_size = reservoir_size
18
+ @mutex = Mutex.new
19
+ clear
20
+ end
21
+
22
+ def clear
23
+ @mutex.synchronize do
24
+ @values.clear
25
+ @count.value = 0
26
+ @next_scale_time.value = Time.now + RESCALE_THRESHOLD
27
+ @start_time = Time.now
28
+ end
29
+ end
30
+
31
+ def size
32
+ count = @count.value
33
+ [count, @reservoir_size].min
34
+ end
35
+
36
+ def snapshot
37
+ @mutex.synchronize do
38
+ Snapshot.new(@values.values)
39
+ end
40
+ end
41
+
42
+ def update(value, timestamp = Time.now) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
43
+ @mutex.synchronize do
44
+ priority = weight(timestamp - @start_time) / rand
45
+ priority = Float::MAX if priority.infinite?
46
+ new_count = @count.update { |v| v + 1 }
47
+
48
+ if priority.nan?
49
+ warn "ExponentiallyDecayingSample found priority of NaN. " \
50
+ "timestamp: #{timestamp.to_f} start_time: #{@start_time.to_f}"
51
+ return
52
+ end
53
+
54
+ if new_count <= @reservoir_size
55
+ @values[priority] = value
56
+ else
57
+ first_priority = @values.first[0]
58
+ if first_priority < priority
59
+ unless @values[priority] # rubocop:disable Style/SoleNestedConditional
60
+ @values[priority] = value
61
+
62
+ first_priority = @values.first[0] until @values.delete(first_priority) # rubocop:disable Metrics/BlockNesting
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ now = Time.new
69
+ next_time = @next_scale_time.value
70
+ return unless now >= next_time
71
+
72
+ rescale(now, next_time)
73
+ end
74
+
75
+ def weight(time)
76
+ Math.exp(@alpha * time)
77
+ end
78
+
79
+ def rescale(now, next_time) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
80
+ return unless @next_scale_time.compare_and_swap(next_time, now + RESCALE_THRESHOLD)
81
+
82
+ @mutex.synchronize do
83
+ old_start_time = @start_time
84
+ @start_time = Time.now
85
+ @values.keys.each do |key|
86
+ value = @values.delete(key)
87
+ new_key = key * Math.exp(-@alpha * (@start_time - old_start_time))
88
+
89
+ if key.nan?
90
+ warn "ExponentiallyDecayingSample found a key of NaN. " \
91
+ "old_start_time: #{old_start_time.to_f} start_time: #{@start_time.to_f}"
92
+ next
93
+ end
94
+
95
+ if new_key.nan?
96
+ warn "ExponentiallyDecayingSample found a new_key of NaN. " \
97
+ "key: #{key} old_start_time: #{old_start_time.to_f} start_time: #{@start_time.to_f}"
98
+ next
99
+ end
100
+
101
+ @values[new_key] = value
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "atomic"
4
+
5
+ module Twingly
6
+ module Metrics
7
+ class Gauge
8
+ # Public: Initialize a new Gauge.
9
+ def initialize(callable = nil, &block)
10
+ @gauge = Atomic.new(nil)
11
+ @callback = callable || block
12
+ end
13
+
14
+ # Public: Set a new value.
15
+ #
16
+ # val - The new value.
17
+ #
18
+ # Returns nothing.
19
+ def set(val)
20
+ @gauge.value = val
21
+ end
22
+
23
+ # Public: The current value.
24
+ #
25
+ # Returns the gauge value.
26
+ def value
27
+ @callback ? @callback.call : @gauge.value
28
+ end
29
+ end
30
+ end
31
+ end
@@ -2,8 +2,8 @@
2
2
 
3
3
  require "net/https"
4
4
  require "json"
5
- require "metriks"
6
5
  require "socket"
6
+ require_relative "registry"
7
7
 
8
8
  module Twingly
9
9
  module Metrics
@@ -28,10 +28,10 @@ module Twingly
28
28
  end
29
29
 
30
30
  METRIC_TYPE_TO_BUILD_METHOD = {
31
- Metriks::Meter => :build_meter_metric,
32
- Metriks::Counter => :build_counter_metric,
33
- Metriks::Gauge => :build_gauge_metric,
34
- Metriks::Timer => :build_timer_metric,
31
+ Twingly::Metrics::Meter => :build_meter_metric,
32
+ Twingly::Metrics::Counter => :build_counter_metric,
33
+ Twingly::Metrics::Gauge => :build_gauge_metric,
34
+ Twingly::Metrics::Timer => :build_timer_metric,
35
35
  }.freeze
36
36
 
37
37
  attr_reader :endpoint
@@ -55,7 +55,7 @@ module Twingly
55
55
 
56
56
  log_missing_credentials unless credentials_provided?
57
57
 
58
- @registry = options[:registry] || Metriks::Registry.default
58
+ @registry = options[:registry] || Twingly::Metrics::Registry.default
59
59
  @interval = options[:interval] || 60
60
60
  @on_error = options[:on_error] || proc { |ex| @logger&.error(ex) }
61
61
  @timeout = options[:timeout] || 10
@@ -158,7 +158,7 @@ module Twingly
158
158
 
159
159
  begin
160
160
  yield
161
- rescue RequestFailedError => e
161
+ rescue RequestFailedError, Net::OpenTimeout, Net::ReadTimeout => e
162
162
  retries += 1
163
163
 
164
164
  raise if retries > max_retries
@@ -223,7 +223,7 @@ module Twingly
223
223
 
224
224
  time_metrics << build_grafana_metric("#{name}.median", snapshot.median, time)
225
225
 
226
- build_timer_metric_percentiles(name, snapshot, time) { |metric| time_metrics << metric }
226
+ build_timer_metric_percentiles(name, snapshot, time) { |percentile_metric| time_metrics << percentile_metric }
227
227
  end
228
228
  end
229
229
 
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "atomic"
4
+ require_relative "snapshot"
5
+ require_relative "uniform_sample"
6
+ require_relative "exponentially_decaying_sample"
7
+
8
+ module Twingly
9
+ module Metrics
10
+ class Histogram
11
+ DEFAULT_SAMPLE_SIZE = 1028
12
+ DEFAULT_ALPHA = 0.015
13
+
14
+ def self.new_uniform
15
+ new(Twingly::Metrics::UniformSample.new(DEFAULT_SAMPLE_SIZE))
16
+ end
17
+
18
+ def self.new_exponentially_decaying
19
+ new(Twingly::Metrics::ExponentiallyDecayingSample.new(DEFAULT_SAMPLE_SIZE, DEFAULT_ALPHA))
20
+ end
21
+
22
+ def initialize(sample)
23
+ @sample = sample
24
+ @count = Atomic.new(0)
25
+ @min = Atomic.new(nil)
26
+ @max = Atomic.new(nil)
27
+ @sum = Atomic.new(0)
28
+ @variance = Atomic.new([-1, 0])
29
+ end
30
+
31
+ def clear
32
+ @sample.clear
33
+ @count.value = 0
34
+ @min.value = nil
35
+ @max.value = nil
36
+ @sum.value = 0
37
+ @variance.value = [-1, 0]
38
+ end
39
+
40
+ def update(value)
41
+ @count.update { |v| v + 1 }
42
+ @sample.update(value)
43
+ self.max = value
44
+ self.min = value
45
+ @sum.update { |v| v + value }
46
+ update_variance(value)
47
+ end
48
+
49
+ def snapshot
50
+ @sample.snapshot
51
+ end
52
+
53
+ def count
54
+ @count.value
55
+ end
56
+
57
+ def sum
58
+ @sum.value
59
+ end
60
+
61
+ def max
62
+ count.positive? ? @max.value : 0.0
63
+ end
64
+
65
+ def min
66
+ count.positive? ? @min.value : 0.0
67
+ end
68
+
69
+ def mean
70
+ count.positive? ? @sum.value / count : 0.0
71
+ end
72
+
73
+ def stddev
74
+ count.positive? ? variance**0.5 : 0.0
75
+ end
76
+
77
+ def variance
78
+ count <= 1 ? 0.0 : @variance.value[1] / (count - 1)
79
+ end
80
+
81
+ def max=(potential_max)
82
+ done = false
83
+
84
+ until done
85
+ current_max = @max.value
86
+ done = (!current_max.nil? && current_max >= potential_max) || @max.compare_and_swap(current_max,
87
+ potential_max)
88
+ end
89
+ end
90
+
91
+ def min=(potential_min)
92
+ done = false
93
+
94
+ until done
95
+ current_min = @min.value
96
+ done = (!current_min.nil? && current_min <= potential_min) || @min.compare_and_swap(current_min,
97
+ potential_min)
98
+ end
99
+ end
100
+
101
+ def update_variance(value) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
102
+ @variance.update do |old_values|
103
+ new_values = Array.new(2)
104
+ if old_values[0] == -1
105
+ new_values[0] = value
106
+ new_values[1] = 0
107
+ else
108
+ old_m = old_values[0]
109
+ old_s = old_values[1]
110
+
111
+ new_m = old_m + ((value - old_m) / count)
112
+ new_s = old_s + ((value - old_m) * (value - new_m))
113
+
114
+ new_values[0] = new_m
115
+ new_values[1] = new_s
116
+ end
117
+
118
+ new_values
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "atomic"
4
+
5
+ require_relative "ewma"
6
+
7
+ module Twingly
8
+ module Metrics
9
+ class Meter
10
+ TICK_INTERVAL = 5.0
11
+
12
+ def initialize(averager_klass = Twingly::Metrics::EWMA)
13
+ @count = Atomic.new(0)
14
+ @start_time = Time.now.to_f
15
+ @last_tick = Atomic.new(@start_time)
16
+
17
+ @m1_rate = averager_klass.new_m1
18
+ @m5_rate = averager_klass.new_m5
19
+ @m15_rate = averager_klass.new_m15
20
+ end
21
+
22
+ def clear
23
+ @count.value = 0
24
+ @start_time = Time.now.to_f
25
+ @last_tick.value = @start_time
26
+ @m1_rate.clear
27
+ @m5_rate.clear
28
+ @m15_rate.clear
29
+ end
30
+
31
+ def tick
32
+ @m1_rate.tick
33
+ @m5_rate.tick
34
+ @m15_rate.tick
35
+ end
36
+
37
+ def tick_if_nessesary
38
+ old_tick = @last_tick.value
39
+ new_tick = Time.new.to_f
40
+ age = new_tick - old_tick
41
+ return unless age > TICK_INTERVAL && @last_tick.compare_and_swap(old_tick, new_tick)
42
+
43
+ required_ticks = age / TICK_INTERVAL
44
+ required_ticks.to_i.times do
45
+ tick
46
+ end
47
+ end
48
+
49
+ def mark(val = 1)
50
+ tick_if_nessesary
51
+ @count.update { |v| v + val }
52
+ @m1_rate.update(val)
53
+ @m5_rate.update(val)
54
+ @m15_rate.update(val)
55
+ end
56
+
57
+ def count
58
+ @count.value
59
+ end
60
+
61
+ def one_minute_rate
62
+ tick_if_nessesary
63
+ @m1_rate.rate
64
+ end
65
+
66
+ def five_minute_rate
67
+ tick_if_nessesary
68
+ @m5_rate.rate
69
+ end
70
+
71
+ def fifteen_minute_rate
72
+ tick_if_nessesary
73
+ @m15_rate.rate
74
+ end
75
+
76
+ def mean_rate
77
+ return 0.0 if count.zero?
78
+
79
+ elapsed = Time.now.to_f - @start_time
80
+ count / elapsed
81
+ end
82
+
83
+ def stop; end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,209 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "counter"
4
+ require_relative "timer"
5
+ require_relative "utilization_timer"
6
+ require_relative "meter"
7
+ require_relative "gauge"
8
+
9
+ module Twingly
10
+ module Metrics
11
+ # Public: A collection of metrics
12
+ class Registry
13
+ # Public: The default registry for the process.
14
+ #
15
+ # Returns the default Registry for the process.
16
+ def self.default
17
+ @default ||= new
18
+ end
19
+
20
+ # Public: Initializes a new Registry.
21
+ def initialize
22
+ @mutex = Mutex.new
23
+ @metrics = {}
24
+ end
25
+
26
+ # Public: Clear all of the metrics in the Registry. This ensures all
27
+ # metrics that have been added are stopped.
28
+ #
29
+ # Returns nothing.
30
+ def clear
31
+ @mutex.synchronize do
32
+ @metrics.each_value do |metric|
33
+ metric.stop if metric.respond_to?(:stop)
34
+ end
35
+
36
+ @metrics = {}
37
+ end
38
+ end
39
+
40
+ # Public: Clear all of the metrics in the Registry. This has the same
41
+ # effect as calling #clear.
42
+ #
43
+ # Returns nothing.
44
+ def stop
45
+ clear
46
+ end
47
+
48
+ # Public: Iterate over all of the counters.
49
+ #
50
+ # Examples
51
+ #
52
+ # registry.each do |name, metric|
53
+ # puts name
54
+ # end
55
+ #
56
+ # Returns nothing.
57
+ def each(&block)
58
+ metrics = @mutex.synchronize do
59
+ @metrics.dup
60
+ end
61
+
62
+ metrics.each(&block)
63
+ end
64
+
65
+ # Public: Fetch or create a new counter metric. Counters are one of the
66
+ # simplest metrics whose only operations are increment and decrement.
67
+ #
68
+ # name - The String name of the metric to define or fetch
69
+ #
70
+ # Examples
71
+ #
72
+ # registry.counter('method.calls')
73
+ #
74
+ # Returns the Metrics::Counter identified by the name.
75
+ def counter(name)
76
+ add_or_get(name, Metrics::Counter)
77
+ end
78
+
79
+ # Public: Fetch or create a new gauge metric.
80
+ #
81
+ # name - The String name of the metric to define or fetch
82
+ #
83
+ # Examples
84
+ #
85
+ # registry.gauge('disk_space.used') { 1 }
86
+ #
87
+ # Returns the Metrics::Gauge identified by the name.
88
+ def gauge(name, callable = nil, &block)
89
+ add_or_get(name, Metrics::Gauge) do
90
+ Metrics::Gauge.new(callable, &block)
91
+ end
92
+ end
93
+
94
+ # Public: Fetch or create a new meter metric. Meters are a counter that
95
+ # tracks throughput along with the count.
96
+ #
97
+ # name - The String name of the metric to define or fetch
98
+ #
99
+ # Examples
100
+ #
101
+ # registry.meter('resque.calls')
102
+ #
103
+ # Returns the Metrics::Meter identified by the name.
104
+ def meter(name)
105
+ add_or_get(name, Metrics::Meter)
106
+ end
107
+
108
+ # Public: Fetch or create a new timer metric. Timers provide the means to
109
+ # time the execution of a method including statistics on the number of
110
+ # invocations, average length of time, throughput.
111
+ #
112
+ # name - The String name of the metric to define or fetch
113
+ #
114
+ # Examples
115
+ #
116
+ # registry.timer('resque.worker')
117
+ #
118
+ # Returns the Metrics::Timer identified by the name.
119
+ def timer(name)
120
+ add_or_get(name, Metrics::Timer)
121
+ end
122
+
123
+ # Public: Fetch or create a new utilization timer metric.
124
+ #
125
+ # Utilization timers are a specialized version of a timer that calculate
126
+ # the percentage of wall-clock time (between 0 and 1) that was spent in
127
+ # the method. This metric is most valuable in a single-threaded
128
+ # environment where a processes is waiting on an external resource like a
129
+ # message queue or HTTP server.
130
+ #
131
+ # name - The String name of the metric to define or fetch
132
+ #
133
+ # Examples
134
+ #
135
+ # registry.utilization_timer('rack.utilization')
136
+ #
137
+ # Returns the Metrics::UtilizationTimer identified by the name.
138
+ def utilization_timer(name)
139
+ add_or_get(name, Metrics::UtilizationTimer)
140
+ end
141
+
142
+ # Public: Fetch or create a new histogram metric. Histograms record values
143
+ # and expose statistics about the distribution of the data like median and
144
+ # 95th percentile.
145
+ #
146
+ # name - The String name of the metric to define or fetch
147
+ #
148
+ # Examples
149
+ #
150
+ # registry.histogram('backlog.wait')
151
+ #
152
+ # Returns the Metrics::Histogram identified by the name.
153
+ def histogram(name)
154
+ add_or_get(name, Metrics::Histogram) do
155
+ Metrics::Histogram.new_exponentially_decaying
156
+ end
157
+ end
158
+
159
+ # Public: Fetch an existing metric.
160
+ #
161
+ # name - The String name of the metric to fetch
162
+ #
163
+ # Examples
164
+ #
165
+ # registry.get('rack.utilization')
166
+ #
167
+ # Returns the metric or nil.
168
+ def get(name)
169
+ @mutex.synchronize do
170
+ @metrics[name]
171
+ end
172
+ end
173
+
174
+ # Public: Add a new metric.
175
+ #
176
+ # name - The String name of the metric to add
177
+ # metric - The metric instance to add
178
+ #
179
+ # Examples
180
+ #
181
+ # registry.add('method.calls', Metrics::Counter.new)
182
+ #
183
+ # Returns nothing.
184
+ # Raises RuntimeError if the metric name is already defined
185
+ def add(name, metric)
186
+ @mutex.synchronize do
187
+ raise "Metric '#{name}' already defined" if @metrics[name]
188
+
189
+ @metrics[name] = metric
190
+ end
191
+ end
192
+
193
+ protected
194
+
195
+ def add_or_get(name, klass, &create_metric)
196
+ @mutex.synchronize do
197
+ if (metric = @metrics[name])
198
+ raise "Metric already defined as '#{metric.class}'" unless metric.is_a?(klass)
199
+
200
+ return metric
201
+
202
+ else
203
+ @metrics[name] = create_metric ? create_metric.call : klass.new
204
+ end
205
+ end
206
+ end
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "atomic"
4
+
5
+ module Twingly
6
+ module Metrics
7
+ class SimpleMovingAverage
8
+ INTERVAL = 5.0
9
+ SECONDS_PER_MINUTE = 60.0
10
+
11
+ ONE_MINUTE = 1
12
+ FIVE_MINUTES = 5
13
+ FIFTEEN_MINUTES = 15
14
+
15
+ def self.new_m1
16
+ new(ONE_MINUTE * SECONDS_PER_MINUTE, INTERVAL)
17
+ end
18
+
19
+ def self.new_m5
20
+ new(FIVE_MINUTES * SECONDS_PER_MINUTE, INTERVAL)
21
+ end
22
+
23
+ def self.new_m15
24
+ new(FIFTEEN_MINUTES * SECONDS_PER_MINUTE, INTERVAL)
25
+ end
26
+
27
+ def initialize(duration, interval)
28
+ @interval = interval
29
+ @duration = duration
30
+
31
+ @values = Array.new((duration / interval).to_i) { Atomic.new(nil) }
32
+ @index = Atomic.new(0)
33
+ end
34
+
35
+ def clear
36
+ @values.each do |value|
37
+ value.value = nil
38
+ end
39
+ @index.value = 0
40
+ end
41
+
42
+ def update(value)
43
+ @values[@index.value].update { |v| v ? v + value : value }
44
+ end
45
+
46
+ def tick
47
+ next_index = @index.update { |v| v < @values.length - 1 ? v + 1 : 0 }
48
+ @values[next_index].value = nil
49
+ end
50
+
51
+ def rate
52
+ num = 0.0
53
+ count = 0.0
54
+
55
+ @values.each do |value|
56
+ if (v = value.value)
57
+ num += v
58
+ count += 1
59
+ end
60
+ end
61
+
62
+ return 0.0 if count.zero?
63
+
64
+ num / count / @interval.to_f
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twingly
4
+ module Metrics
5
+ class Snapshot
6
+ MEDIAN_Q = 0.5
7
+ P75_Q = 0.75
8
+ P95_Q = 0.95
9
+ P98_Q = 0.98
10
+ P99_Q = 0.99
11
+ P999_Q = 0.999
12
+
13
+ attr_reader :values
14
+
15
+ def initialize(values)
16
+ @values = values.sort
17
+ end
18
+
19
+ def value(quantile) # rubocop:disable Metrics/AbcSize
20
+ raise ArgumentError, "quantile must be between 0.0 and 1.0" if quantile < 0.0 || quantile > 1.0
21
+
22
+ return 0.0 if @values.empty?
23
+
24
+ pos = quantile * (@values.length + 1)
25
+
26
+ return @values.first if pos < 1
27
+ return @values.last if pos >= @values.length
28
+
29
+ lower = @values[pos.to_i - 1]
30
+ upper = @values[pos.to_i]
31
+ lower + ((pos - pos.floor) * (upper - lower))
32
+ end
33
+
34
+ def size
35
+ @values.length
36
+ end
37
+
38
+ def median
39
+ value(MEDIAN_Q)
40
+ end
41
+
42
+ # rubocop:disable Naming/AccessorMethodName
43
+ def get_75th_percentile
44
+ value(P75_Q)
45
+ end
46
+
47
+ def get_95th_percentile
48
+ value(P95_Q)
49
+ end
50
+
51
+ def get_98th_percentile
52
+ value(P98_Q)
53
+ end
54
+
55
+ def get_99th_percentile
56
+ value(P99_Q)
57
+ end
58
+
59
+ def get_999th_percentile
60
+ value(P999_Q)
61
+ end
62
+ # rubocop:enable Naming/AccessorMethodName
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twingly
4
+ module Metrics
5
+ class TimeTracker
6
+ def initialize(interval)
7
+ @interval = interval
8
+ @next_time = Time.now.to_f
9
+ end
10
+
11
+ def sleep
12
+ sleep_time = next_time - Time.now.to_f
13
+ return unless sleep_time.positive?
14
+
15
+ Kernel.sleep(sleep_time)
16
+ end
17
+
18
+ def now_floored
19
+ time = Time.now.to_i
20
+ time - (time % @interval)
21
+ end
22
+
23
+ def next_time
24
+ now = Time.now.to_f
25
+ @next_time = now if @next_time <= now
26
+ @next_time += @interval - (@next_time % @interval)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "atomic"
4
+
5
+ require_relative "meter"
6
+ require_relative "histogram"
7
+
8
+ module Twingly
9
+ module Metrics
10
+ class Timer
11
+ class Context
12
+ def initialize(timer)
13
+ @timer = timer
14
+ @interval = Process.clock_gettime(Process::CLOCK_MONOTONIC)
15
+ end
16
+
17
+ def restart
18
+ @interval = Process.clock_gettime(Process::CLOCK_MONOTONIC)
19
+ end
20
+
21
+ def stop
22
+ duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - @interval
23
+ @timer.update(duration)
24
+ end
25
+ end
26
+
27
+ def initialize(histogram = Twingly::Metrics::Histogram.new_exponentially_decaying)
28
+ @meter = Twingly::Metrics::Meter.new
29
+ @histogram = histogram
30
+ end
31
+
32
+ def clear
33
+ @meter.clear
34
+ @histogram.clear
35
+ end
36
+
37
+ def update(duration)
38
+ return unless duration >= 0
39
+
40
+ @meter.mark
41
+ @histogram.update(duration)
42
+ end
43
+
44
+ def time(callable = nil, &block)
45
+ callable ||= block
46
+ context = Context.new(self)
47
+
48
+ return context if callable.nil?
49
+
50
+ begin
51
+ callable.call
52
+ ensure
53
+ context.stop
54
+ end
55
+ end
56
+
57
+ def snapshot
58
+ @histogram.snapshot
59
+ end
60
+
61
+ def count
62
+ @histogram.count
63
+ end
64
+
65
+ def sum
66
+ @histogram.sum
67
+ end
68
+
69
+ def one_minute_rate
70
+ @meter.one_minute_rate
71
+ end
72
+
73
+ def five_minute_rate
74
+ @meter.five_minute_rate
75
+ end
76
+
77
+ def fifteen_minute_rate
78
+ @meter.fifteen_minute_rate
79
+ end
80
+
81
+ def mean_rate
82
+ @meter.mean_rate
83
+ end
84
+
85
+ def min
86
+ @histogram.min
87
+ end
88
+
89
+ def max
90
+ @histogram.max
91
+ end
92
+
93
+ def mean
94
+ @histogram.mean
95
+ end
96
+
97
+ def stddev
98
+ @histogram.stddev
99
+ end
100
+
101
+ def stop
102
+ @meter.stop
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "atomic"
4
+ require_relative "snapshot"
5
+
6
+ module Twingly
7
+ module Metrics
8
+ class UniformSample
9
+ def initialize(reservoir_size)
10
+ @values = Array.new(reservoir_size, 0)
11
+ @count = Atomic.new(0)
12
+ end
13
+
14
+ def clear
15
+ @values.length.times do |idx|
16
+ @values[idx] = 0
17
+ end
18
+ @count.value = 0
19
+ end
20
+
21
+ def size
22
+ count = @count.value
23
+ [count, @values.length].min
24
+ end
25
+
26
+ def snapshot
27
+ Snapshot.new(@values.slice(0, size))
28
+ end
29
+
30
+ def update(value)
31
+ new_count = @count.update { |v| v + 1 }
32
+
33
+ if new_count <= @values.length
34
+ @values[new_count - 1] = value
35
+ else
36
+ idx = rand(new_count)
37
+ @values[idx] = value if idx < @values.length
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "timer"
4
+
5
+ module Twingly
6
+ module Metrics
7
+ class UtilizationTimer < Metrics::Timer
8
+ def initialize
9
+ super
10
+ @duration_meter = Metrics::Meter.new
11
+ end
12
+
13
+ def clear
14
+ super
15
+ @duration_meter.clear
16
+ end
17
+
18
+ def update(duration)
19
+ return unless duration >= 0
20
+
21
+ super
22
+ @duration_meter.mark(duration)
23
+ end
24
+
25
+ def one_minute_utilization
26
+ @duration_meter.one_minute_rate
27
+ end
28
+
29
+ def five_minute_utilization
30
+ @duration_meter.five_minute_rate
31
+ end
32
+
33
+ def fifteen_minute_utilization
34
+ @duration_meter.fifteen_minute_rate
35
+ end
36
+
37
+ def mean_utilization
38
+ @duration_meter.mean_rate
39
+ end
40
+
41
+ def stop
42
+ super
43
+ @duration_meter.stop
44
+ end
45
+ end
46
+ end
47
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Twingly
4
4
  module Metrics
5
- VERSION = "0.5.0"
5
+ VERSION = "0.6.0"
6
6
  end
7
7
  end
@@ -1,40 +1,57 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "metriks"
4
-
5
3
  require_relative "metrics/version"
4
+ require_relative "metrics/registry"
6
5
  require_relative "metrics/grafana_cloud_reporter"
6
+ require_relative "metrics/counter"
7
+ require_relative "metrics/timer"
8
+ require_relative "metrics/utilization_timer"
9
+ require_relative "metrics/meter"
10
+ require_relative "metrics/gauge"
11
+ require_relative "metrics/histogram"
7
12
 
8
13
  module Twingly
9
14
  module Metrics
15
+ def self.get(name)
16
+ Registry.default.get(name)
17
+ end
18
+
10
19
  def self.counter(name)
11
- Metriks.counter(name)
20
+ Registry.default.counter(name)
12
21
  end
13
22
 
14
23
  def self.gauge(name, callable = nil, &block)
15
- Metriks.gauge(name, callable, &block)
24
+ Registry.default.gauge(name, callable, &block)
16
25
  end
17
26
 
18
27
  def self.timer(name)
19
- Metriks.timer(name)
28
+ Registry.default.timer(name)
29
+ end
30
+
31
+ def self.utilization_timer(name)
32
+ Registry.default.utilization_timer(name)
20
33
  end
21
34
 
22
35
  def self.meter(name)
23
- Metriks.meter(name)
36
+ Registry.default.meter(name)
24
37
  end
25
38
 
26
39
  def self.mark(name, val = 1)
27
40
  meter(name).mark(val)
28
41
  end
29
42
 
43
+ def self.set(name, val)
44
+ gauge(name).set(val)
45
+ end
46
+
30
47
  def self.time(name, duration = nil, &block)
31
48
  return timer(name).update(duration) if duration
32
49
 
33
50
  timer(name).time(&block)
34
51
  end
35
52
 
36
- def self.set(name, val)
37
- gauge(name).set(val)
53
+ def self.histogram(name)
54
+ Registry.default.histogram(name)
38
55
  end
39
56
  end
40
57
  end
metadata CHANGED
@@ -1,28 +1,42 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: twingly-metrics
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Twingly AB
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-09-15 00:00:00.000000000 Z
10
+ date: 2026-07-13 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
- name: metriks
13
+ name: atomic
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: '0.9'
18
+ version: '1.0'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: '0.9'
25
+ version: '1.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: avl_tree
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: 1.2.0
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: 1.2.0
26
40
  description: Collect and report metrics in Ruby applications, and report them to Graphite.
27
41
  email:
28
42
  - support@twingly.com
@@ -32,7 +46,20 @@ extra_rdoc_files: []
32
46
  files:
33
47
  - README.md
34
48
  - lib/twingly/metrics.rb
49
+ - lib/twingly/metrics/counter.rb
50
+ - lib/twingly/metrics/ewma.rb
51
+ - lib/twingly/metrics/exponentially_decaying_sample.rb
52
+ - lib/twingly/metrics/gauge.rb
35
53
  - lib/twingly/metrics/grafana_cloud_reporter.rb
54
+ - lib/twingly/metrics/histogram.rb
55
+ - lib/twingly/metrics/meter.rb
56
+ - lib/twingly/metrics/registry.rb
57
+ - lib/twingly/metrics/simple_moving_average.rb
58
+ - lib/twingly/metrics/snapshot.rb
59
+ - lib/twingly/metrics/time_tracker.rb
60
+ - lib/twingly/metrics/timer.rb
61
+ - lib/twingly/metrics/uniform_sample.rb
62
+ - lib/twingly/metrics/utilization_timer.rb
36
63
  - lib/twingly/metrics/version.rb
37
64
  homepage: https://github.com/twingly/twingly-metrics
38
65
  licenses: []
@@ -52,7 +79,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
52
79
  - !ruby/object:Gem::Version
53
80
  version: '0'
54
81
  requirements: []
55
- rubygems_version: 3.6.5
82
+ rubygems_version: 3.6.2
56
83
  specification_version: 4
57
84
  summary: Ruby library for collecting and reporting metrics.
58
85
  test_files: []