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 +7 -0
- data/README.md +72 -0
- data/lib/twingly/metrics/grafana_cloud_reporter.rb +240 -0
- data/lib/twingly/metrics/version.rb +7 -0
- data/lib/twingly/metrics.rb +4 -0
- metadata +58 -0
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
|
+
[](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
|
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: []
|