twingly-metrics 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c7c23b7fe7fa29534ac78882741d226c25a69629b6520329ae36d489a1ead5cc
4
+ data.tar.gz: 4750028c638903b62dd13f4911a8d15906accf0c7d7d275f1d46b81064f50858
5
+ SHA512:
6
+ metadata.gz: 843481f3ac90c856cff73cbb67adcf7be831c4aeabcd7dd41f0b996543bf35aa3519ff8367e24ecbbeee954927be24e141bc87ab1672d92286096a1445a32b2e
7
+ data.tar.gz: 9b4baf1f630f4e99b6a0118c84325a93b712e9e662d12124c14074da9554be7bdf22925eb434b9c969e8d6094c714a204b06d0235e21eb87e522315fa795d416
data/README.md ADDED
@@ -0,0 +1,72 @@
1
+ # Twingly::Metrics
2
+
3
+ [![GitHub Build Status](https://github.com/twingly/twingly-metrics/workflows/CI/badge.svg)](https://github.com/twingly/twingly-metrics/actions)
4
+
5
+ A gem for collecting and reporting metrics from our Ruby applications.
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].
8
+
9
+ The code here was inspired by the reporter in the [`metriks-librato_metrics` gem][metriks-librato_metrics].
10
+
11
+ ## Installation
12
+
13
+ Add this line to your application's Gemfile:
14
+
15
+ ```ruby
16
+ gem "twingly-metrics"
17
+ ```
18
+
19
+ Or install it yourself as:
20
+
21
+ ```shell
22
+ $ gem install twingly-metrics
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ The following example shows how to use the `Twingly::Metrics::GrafanaCloudReporter` to report metrics to Grafana Cloud using their Graphite HTTP API.
28
+
29
+ *For more information on the API endpoint and credentials, see the [Grafana Cloud documentation][grafana-cloud-graphite-api].*
30
+
31
+ ```ruby
32
+ require "twingly/metrics"
33
+
34
+ reporter = Twingly::Metrics::GrafanaCloudReporter.new(
35
+ endpoint: # The API endpoint of your Grafana Cloud Graphite HTTP API
36
+ # For example: https://<something>.grafana.net/graphite/metrics
37
+ user: # Your Grafana Cloud username for the Graphite HTTP API
38
+ token: # Your Grafana Cloud token for the Graphite HTTP API
39
+
40
+ prefix: "my_app", # Optional, can be used to add a prefix to all reported metrics,
41
+ # for example the name of your application
42
+ source: "some-example-host", # Optional, can be used for adding a "source" tag to all reported metrics,
43
+ # useful for separating metrics by host
44
+
45
+ logger: Logger.new(STDOUT), # Optional, a logger to log any errors raised in the reporter
46
+
47
+ # See the reporter class for more options
48
+ )
49
+
50
+ reporter.start # Starts the reporter thread in the background
51
+
52
+ # Create some metrics for the reporter to have something to report
53
+ Metriks.meter("test.meter").mark(1)
54
+
55
+ at_exit do
56
+ reporter.flush # Ensure all unreported metrics are sent before exiting
57
+ end
58
+ ```
59
+
60
+ ### Supported metric types
61
+
62
+ The reporter currently supports the following metric types from the `metriks` gem:
63
+
64
+ * [`Metriks.counter`](https://github.com/eric/metriks?tab=readme-ov-file#counters)
65
+ * [`Metriks.gauge`](https://github.com/eric/metriks?tab=readme-ov-file#gauges)
66
+ * [`Metriks.meter`](https://github.com/eric/metriks?tab=readme-ov-file#meters)
67
+ * [`Metriks.timer`](https://github.com/eric/metriks?tab=readme-ov-file#timers)
68
+
69
+ [metriks]: https://github.com/eric/metriks
70
+ [grafana-cloud]: https://grafana.com/products/cloud/
71
+ [grafana-cloud-graphite-api]: https://grafana.com/docs/grafana-cloud/send-data/metrics/metrics-graphite/http-api/#adding-new-data-posting-to-metrics
72
+ [metriks-librato_metrics]: https://github.com/eric/metriks-librato_metrics
@@ -0,0 +1,240 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/https"
4
+ require "json"
5
+ require "metriks"
6
+
7
+ module Twingly
8
+ module Metrics
9
+ class GrafanaCloudReporter # rubocop:disable Metrics/ClassLength
10
+ class Error < StandardError; end
11
+ class UnsupportedMetricError < Error; end
12
+
13
+ class RequestFailedError < Error
14
+ attr_reader :request, :response, :data
15
+
16
+ def initialize(request, response, data = nil)
17
+ super()
18
+
19
+ @request = request
20
+ @response = response
21
+ @data = data
22
+ end
23
+
24
+ def to_s
25
+ "#{response.code} #{response.message.dump}"
26
+ end
27
+ end
28
+
29
+ METRIC_TYPE_TO_BUILD_METHOD = {
30
+ Metriks::Meter => :build_meter_metric,
31
+ Metriks::Counter => :build_counter_metric,
32
+ Metriks::Gauge => :build_gauge_metric,
33
+ Metriks::Timer => :build_timer_metric,
34
+ }.freeze
35
+
36
+ attr_reader :endpoint
37
+ attr_reader :user
38
+ attr_reader :token
39
+ attr_reader :prefix
40
+ attr_reader :source
41
+ attr_reader :logger
42
+ attr_reader :registry
43
+ attr_reader :interval
44
+ attr_reader :timeout
45
+
46
+ def initialize(endpoint:, user:, token:, **options) # rubocop:disable Metriks/AbcSize, Metrics/MethodLength
47
+ @endpoint = endpoint
48
+ @user = user
49
+ @token = token
50
+
51
+ @prefix = options[:prefix]
52
+ @source = options[:source]
53
+ @logger = options[:logger]
54
+
55
+ @registry = options[:registry] || Metriks::Registry.default
56
+ @interval = options[:interval] || 60
57
+ @on_error = options[:on_error] || proc { |ex| @logger&.error(ex) }
58
+ @timeout = options[:timeout] || 10
59
+
60
+ @next_time = Time.now.to_f
61
+
62
+ @previous_request_values = Hash.new { |h, k| h[k] = 0 }
63
+
64
+ @percentiles = options[:percentiles] || [0.95, 0.999]
65
+
66
+ @flush_metrics_mutex = Mutex.new
67
+ @running = false
68
+ @reporter_thread = nil
69
+ end
70
+
71
+ def start
72
+ return if @reporter_thread&.alive?
73
+
74
+ @running = true
75
+ @reporter_thread = Thread.new do
76
+ while @running
77
+ sleep_until_next_flush
78
+
79
+ Thread.new do
80
+ flush
81
+ end
82
+ end
83
+ end
84
+ end
85
+
86
+ def stop
87
+ @running = false
88
+
89
+ return unless @reporter_thread
90
+
91
+ @reporter_thread.join
92
+ @reporter_thread = nil
93
+ end
94
+
95
+ def restart
96
+ stop
97
+ start
98
+ end
99
+
100
+ def flush
101
+ @flush_metrics_mutex.synchronize do
102
+ request_data = build_metrics_data_for_request
103
+
104
+ make_request(request_data)
105
+ end
106
+ rescue StandardError => e
107
+ @on_error.call(e)
108
+ end
109
+
110
+ private
111
+
112
+ def make_request(request_data) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
113
+ return if request_data.empty?
114
+
115
+ url = URI.parse(@endpoint)
116
+ request = Net::HTTP::Post.new(url.path)
117
+ request.basic_auth(@user, @token)
118
+ request.body = request_data.to_json
119
+ request.content_type = "application/json"
120
+
121
+ http = Net::HTTP.new(url.host, url.port)
122
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
123
+ http.use_ssl = true
124
+ http.open_timeout = @timeout
125
+ http.read_timeout = @timeout
126
+
127
+ case response = http.start { |http| http.request(request) }
128
+ when Net::HTTPSuccess, Net::HTTPRedirection
129
+ # OK
130
+ else
131
+ raise RequestFailedError.new(request, response, request_data.dup)
132
+ end
133
+ end
134
+
135
+ def build_metrics_data_for_request
136
+ time = now_floored
137
+
138
+ request_data = @registry.each.map do |name, metric|
139
+ next if name.nil? || name.empty?
140
+
141
+ metric_method = METRIC_TYPE_TO_BUILD_METHOD.fetch(metric.class) do
142
+ handle_unsupported_metric(name, metric)
143
+ end
144
+
145
+ next unless metric_method
146
+
147
+ send(metric_method, name, metric, time)
148
+ end
149
+
150
+ request_data.compact.flatten
151
+ end
152
+
153
+ def handle_unsupported_metric(name, metric)
154
+ error = UnsupportedMetricError.new("Unsupported metric type: #{metric.class.name} for metric '#{name}'")
155
+ @on_error.call(error)
156
+ end
157
+
158
+ def build_meter_metric(name, metric, time)
159
+ current_count = metric.count
160
+ previous_count = @previous_request_values[name]
161
+
162
+ @previous_request_values[name] = current_count
163
+
164
+ build_grafana_metric(name, current_count - previous_count, time)
165
+ end
166
+
167
+ def build_counter_metric(name, metric, time)
168
+ build_grafana_metric(name, metric.count, time)
169
+ end
170
+
171
+ def build_gauge_metric(name, metric, time)
172
+ build_grafana_metric(name, metric.value, time)
173
+ end
174
+
175
+ def build_timer_metric(name, metric, time)
176
+ [].tap do |time_metrics|
177
+ previous_count = @previous_request_values[name]
178
+ current_count = metric.count
179
+ @previous_request_values[name] = current_count
180
+
181
+ time_metrics << build_grafana_metric("#{name}.count", current_count - previous_count, time)
182
+
183
+ snapshot = metric.snapshot
184
+
185
+ time_metrics << build_grafana_metric("#{name}.median", snapshot.median, time)
186
+
187
+ build_timer_metric_percentiles(name, snapshot, time) { |metric| time_metrics << metric }
188
+ end
189
+ end
190
+
191
+ def build_timer_metric_percentiles(name, snapshot, time)
192
+ @percentiles.each do |percentile|
193
+ percentile_name = percentile_to_metric_name(percentile)
194
+
195
+ yield build_grafana_metric("#{name}.#{percentile_name}", snapshot.value(percentile), time)
196
+ end
197
+ end
198
+
199
+ def percentile_to_metric_name(percentile)
200
+ number = (percentile * 100).to_f.to_s.gsub(/0+$/, "").gsub(".", "")
201
+
202
+ "#{number}th_percentile"
203
+ end
204
+
205
+ def build_grafana_metric(name, value, time)
206
+ name = "#{prefix}.#{name}" if prefix
207
+
208
+ graphite_data = {
209
+ name: name,
210
+ interval: @interval,
211
+ value: value,
212
+ time: time.to_i,
213
+ }
214
+
215
+ graphite_data[:tags] = ["source=#{@source}"] unless @source.to_s.empty?
216
+
217
+ graphite_data
218
+ end
219
+
220
+ def sleep_until_next_flush
221
+ sleep_time = next_time - Time.now.to_f
222
+
223
+ return if sleep_time <= 0
224
+
225
+ sleep sleep_time
226
+ end
227
+
228
+ def now_floored
229
+ time = Time.now.to_i
230
+ time - (time % @interval)
231
+ end
232
+
233
+ def next_time
234
+ now = Time.now.to_f
235
+ @next_time = now if @next_time <= now
236
+ @next_time += @interval - (@next_time % @interval)
237
+ end
238
+ end
239
+ end
240
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twingly
4
+ module Metrics
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "metrics/version"
4
+ require_relative "metrics/grafana_cloud_reporter"
metadata ADDED
@@ -0,0 +1,58 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: twingly-metrics
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Twingly AB
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 2025-07-15 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: metriks
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.9'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.9'
26
+ description: Collect and report metrics in Ruby applications, and report them to Graphite.
27
+ email:
28
+ - support@twingly.com
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - README.md
34
+ - lib/twingly/metrics.rb
35
+ - lib/twingly/metrics/grafana_cloud_reporter.rb
36
+ - lib/twingly/metrics/version.rb
37
+ homepage: https://github.com/twingly/twingly-metrics
38
+ licenses: []
39
+ metadata:
40
+ rubygems_mfa_required: 'true'
41
+ rdoc_options: []
42
+ require_paths:
43
+ - lib
44
+ required_ruby_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: 3.0.0
49
+ required_rubygems_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ requirements: []
55
+ rubygems_version: 3.6.5
56
+ specification_version: 4
57
+ summary: Ruby library for collecting and reporting metrics.
58
+ test_files: []