prometheus_exporter-ext 0.2.4 → 0.3.2
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 +4 -4
- data/CHANGELOG.md +28 -0
- data/LICENSE.txt +21 -0
- data/README.md +273 -0
- data/Rakefile +12 -0
- data/lib/prometheus_exporter/ext/instrumentation/base_stats.rb +45 -0
- data/lib/prometheus_exporter/ext/instrumentation/periodic_stats.rb +48 -0
- data/lib/prometheus_exporter/ext/instrumentation/proc_cpu.rb +35 -0
- data/lib/prometheus_exporter/ext/metric/gauge_with_expire.rb +117 -0
- data/lib/prometheus_exporter/ext/proc_self_stat.rb +74 -0
- data/lib/prometheus_exporter/ext/rspec/matchers.rb +38 -0
- data/lib/prometheus_exporter/ext/rspec/metric_matcher.rb +52 -0
- data/lib/prometheus_exporter/ext/rspec/send_metrics_matcher.rb +89 -0
- data/lib/prometheus_exporter/ext/rspec/test_client.rb +30 -0
- data/lib/prometheus_exporter/ext/rspec.rb +17 -0
- data/lib/prometheus_exporter/ext/server/base_collector_methods.rb +103 -0
- data/lib/prometheus_exporter/ext/server/expired_stats_collector.rb +57 -0
- data/lib/prometheus_exporter/ext/server/proc_cpu_collector.rb +12 -0
- data/lib/prometheus_exporter/ext/server/singleton_web_server.rb +33 -0
- data/lib/prometheus_exporter/ext/server/stats_collector.rb +45 -0
- data/lib/prometheus_exporter/ext/version.rb +7 -0
- data/lib/prometheus_exporter/ext.rb +3 -0
- metadata +25 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9f48189e693644e88572d2c0e53d88b5e3b24ff0d5fbf5a81ab83917a9a33611
|
|
4
|
+
data.tar.gz: 54899812b42e64fd0de8b68bde3ac516be1312d0d83bec943aa1aac6b61c93ae
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 293fff8a0de37c36dcfbec873546b8416443ac6717eaf2c1c112104a8bd117a115ccf9af9a793e3bb92d243cbe88e8709763b109deec2628b88f1f44651c4da9
|
|
7
|
+
data.tar.gz: 85f241c1fd687dd12648b37bba25d6608353ac0e8f493427ed870500ced02e3eb6931271b1b5023d86d4d3c1467bdefe8e58fbb339aef6a15801154e863707fe
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
## [Unreleased]
|
|
2
|
+
|
|
3
|
+
## [0.3.2] - 2026-06-11
|
|
4
|
+
- fix gemspec missing `spec.files` so the published gem actually ships its source files (0.3.1 and earlier shipped empty)
|
|
5
|
+
|
|
6
|
+
## [0.3.1] - 2024-05-04
|
|
7
|
+
- ProcCpu add hostname label
|
|
8
|
+
|
|
9
|
+
## [0.3.0] - 2024-05-04
|
|
10
|
+
- replace ProcStat with ProcCpu
|
|
11
|
+
|
|
12
|
+
## [0.2.4] - 2024-05-03
|
|
13
|
+
- add PrometheusExporter::Ext::Server::ProcStatCollector
|
|
14
|
+
|
|
15
|
+
## [0.2.3] - 2024-05-03
|
|
16
|
+
- add PrometheusExporter::Ext::Instrumentation::ProcStat
|
|
17
|
+
|
|
18
|
+
## [0.2.2] - 2023-12-08
|
|
19
|
+
- fix send_metrics RSpec matcher
|
|
20
|
+
|
|
21
|
+
## [0.2.1] - 2023-12-06
|
|
22
|
+
- add gauge_with_expire, add tests, update readme
|
|
23
|
+
|
|
24
|
+
## [0.2.0] - 2023-12-06
|
|
25
|
+
- remove gauge_with_time, add expired_stats_collector, add tests
|
|
26
|
+
|
|
27
|
+
## [0.1.0] - 2023-11-26
|
|
28
|
+
- Initial release
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Denis Talakevich
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# PrometheusExporter::Ext
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
Extension for [Ruby Prometheus Exporter](https://github.com/discourse/prometheus_exporter).
|
|
6
|
+
Adds DSL for building your custom Prometheus instrumentations and collectors.
|
|
7
|
+
Allow to remove/zero expired gauge metrics in a collector.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
Install the gem and add to the application's Gemfile by executing:
|
|
12
|
+
|
|
13
|
+
$ bundle add prometheus_exporter-ext
|
|
14
|
+
|
|
15
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
|
16
|
+
|
|
17
|
+
$ gem install prometheus_exporter-ext
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
### When metrics should be send on particular event
|
|
22
|
+
create instrumentation
|
|
23
|
+
```ruby
|
|
24
|
+
# lib/prometheus/my_instrumentation.rb
|
|
25
|
+
require 'prometheus_exporter/ext'
|
|
26
|
+
require 'prometheus_exporter/ext/instrumentation/base_stats'
|
|
27
|
+
|
|
28
|
+
module Prometheus
|
|
29
|
+
class MyInstrumentation < ::PrometheusExporter::Ext::Instrumentation::BaseStats
|
|
30
|
+
self.type = 'my'
|
|
31
|
+
|
|
32
|
+
def collect(duration, operation)
|
|
33
|
+
collect_data(
|
|
34
|
+
labels: { operation_name: operation },
|
|
35
|
+
last_duration_seconds: duration,
|
|
36
|
+
duration_seconds_sum: duration,
|
|
37
|
+
duration_seconds_count: 1
|
|
38
|
+
)
|
|
39
|
+
rescue StandardError => e
|
|
40
|
+
Rails.logger.error("Failed to send metrics Prometheus #{self.class.name} #{e}")
|
|
41
|
+
Rails.error.report(e, handled: true, severity: :error, context: { prometheus: self.class.name })
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
then send metrics from your code
|
|
48
|
+
```ruby
|
|
49
|
+
time_start = Time.current.to_i
|
|
50
|
+
begin
|
|
51
|
+
MyOperation.run
|
|
52
|
+
ensure
|
|
53
|
+
duration = Time.current.to_i - time_start
|
|
54
|
+
Prometheus::MyInstrumentation.new.collect(duration, 'my_operation')
|
|
55
|
+
## you can add additional labels or override client
|
|
56
|
+
Prometheus::MyInstrumentation.new(
|
|
57
|
+
client: PrometheusExporter::Client.new(...),
|
|
58
|
+
labels: { foo: 'bar' }
|
|
59
|
+
).collect(duration)
|
|
60
|
+
end
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
so metrics will be collected by
|
|
64
|
+
```ruby
|
|
65
|
+
require 'prometheus_exporter/ext'
|
|
66
|
+
require 'prometheus_exporter/ext/server/stats_collector'
|
|
67
|
+
|
|
68
|
+
module Prometheus
|
|
69
|
+
class MyCollector < ::PrometheusExporter::Server::TypeCollector
|
|
70
|
+
include ::PrometheusExporter::Ext::Server::StatsCollector
|
|
71
|
+
self.type = 'my'
|
|
72
|
+
|
|
73
|
+
# The `register_gauge_with_expire` will remove or zero expired metric.
|
|
74
|
+
# when no :strategy option passed, default is `:removing`, available options are `:removing, :zeroing`.
|
|
75
|
+
# when no :ttl option passed, default is 60, any numeric greater than 0 can be used.
|
|
76
|
+
register_gauge_with_expire :last_duration_seconds, 'duration of last operation execution', ttl: 300
|
|
77
|
+
|
|
78
|
+
register_counter :task_duration_seconds_sum, 'sum of operation execution durations'
|
|
79
|
+
register_counter :task_duration_seconds_count, 'sum of operation execution runs'
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
as alternative you can use `ExpiredStatsCollector` if you want all metric data to be removed after expiration
|
|
85
|
+
```ruby
|
|
86
|
+
require 'prometheus_exporter/ext'
|
|
87
|
+
require 'prometheus_exporter/ext/server/stats_collector'
|
|
88
|
+
|
|
89
|
+
module Prometheus
|
|
90
|
+
class MyCollector < ::PrometheusExporter::Server::TypeCollector
|
|
91
|
+
include ::PrometheusExporter::Ext::Server::ExpiredStatsCollector
|
|
92
|
+
self.type = 'my'
|
|
93
|
+
self.ttl = 300 # default 60
|
|
94
|
+
|
|
95
|
+
# Optionally you can expire old_metric when specific new metric is collected.
|
|
96
|
+
# If this block returns true then old_metric will be removed.
|
|
97
|
+
unique_metric_by do |new_metric, old_metric|
|
|
98
|
+
new_metric['labels'] == old_metric['labels']
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
register_gauge :last_duration_seconds, 'duration of last operation execution'
|
|
102
|
+
register_counter :task_duration_seconds_sum, 'sum of operation execution durations'
|
|
103
|
+
register_counter :task_duration_seconds_count, 'sum of operation execution runs'
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
````
|
|
107
|
+
|
|
108
|
+
### When metrics should be send periodically with given frequency
|
|
109
|
+
create instrumentation
|
|
110
|
+
```ruby
|
|
111
|
+
# lib/prometheus/my_instrumentation.rb
|
|
112
|
+
require 'prometheus_exporter/ext'
|
|
113
|
+
require 'prometheus_exporter/ext/instrumentation/periodic_stats'
|
|
114
|
+
|
|
115
|
+
module Prometheus
|
|
116
|
+
class MyPeriodicInstrumentation < ::PrometheusExporter::Ext::Instrumentation::PeriodicStats
|
|
117
|
+
self.type = 'my'
|
|
118
|
+
|
|
119
|
+
def collect
|
|
120
|
+
count = MyItem.processed.count
|
|
121
|
+
last_duration = MyItem.processed.last&.duration
|
|
122
|
+
collect_data(
|
|
123
|
+
labels: { some_label: 'some_value' },
|
|
124
|
+
last_processed_duration: last_duration || 0,
|
|
125
|
+
processed_count: count
|
|
126
|
+
)
|
|
127
|
+
rescue StandardError => e
|
|
128
|
+
Rails.logger.error("Failed to send metrics Prometheus #{self.class.name} #{e}")
|
|
129
|
+
Rails.error.report(e, handled: true, severity: :error, context: { prometheus: self.class.name })
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
then send metrics from your code
|
|
136
|
+
```ruby
|
|
137
|
+
Prometheus::MyInstrumentation.start
|
|
138
|
+
## you can override frequency in seconds
|
|
139
|
+
Prometheus::MyInstrumentation.start(frequency: 60)
|
|
140
|
+
## also you can add additional labels or override client
|
|
141
|
+
Prometheus::MyInstrumentation.start(
|
|
142
|
+
client: PrometheusExporter::Client.new(...),
|
|
143
|
+
labels: { foo: 'bar' }
|
|
144
|
+
)
|
|
145
|
+
# to stop instrumentation call `Prometheus::MyInstrumentation.stop`
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
so metrics will be collected by
|
|
149
|
+
```ruby
|
|
150
|
+
require 'prometheus_exporter/ext'
|
|
151
|
+
require 'prometheus_exporter/ext/server/stats_collector'
|
|
152
|
+
|
|
153
|
+
module Prometheus
|
|
154
|
+
class MyCollector < ::PrometheusExporter::Server::TypeCollector
|
|
155
|
+
include ::PrometheusExporter::Ext::Server::StatsCollector
|
|
156
|
+
self.type = 'my'
|
|
157
|
+
|
|
158
|
+
# Default ttl 60, default strategy `:removing`.
|
|
159
|
+
register_gauge_with_expire :last_processed_duration, 'duration of last processed record'
|
|
160
|
+
register_metric :processed_count, :gauge_with_time, 'count of processed records'
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
as alternative you can use `ExpiredStatsCollector` if you want all metric data to be removed after expiration
|
|
166
|
+
```ruby
|
|
167
|
+
require 'prometheus_exporter/ext'
|
|
168
|
+
require 'prometheus_exporter/ext/server/stats_collector'
|
|
169
|
+
|
|
170
|
+
module Prometheus
|
|
171
|
+
class MyCollector < ::PrometheusExporter::Server::TypeCollector
|
|
172
|
+
include ::PrometheusExporter::Ext::Server::ExpiredStatsCollector
|
|
173
|
+
self.type = 'my'
|
|
174
|
+
# By default ttl is 60
|
|
175
|
+
# By default deletes old metrics only when it's expired
|
|
176
|
+
|
|
177
|
+
register_gauge :last_processed_duration, 'duration of last processed record'
|
|
178
|
+
register_metric :processed_count, :gauge_with_time, 'count of processed records'
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
````
|
|
182
|
+
|
|
183
|
+
### You also can easily test your instrumentations and collectors using new matchers
|
|
184
|
+
|
|
185
|
+
instrumentation test
|
|
186
|
+
```ruby
|
|
187
|
+
require 'prometheus_exporter/ext/rspec'
|
|
188
|
+
|
|
189
|
+
RSpec.describe Prometheus::MyInstrumentation do
|
|
190
|
+
describe '#collect' do
|
|
191
|
+
subject { described_class.new.collect(duration, operation) }
|
|
192
|
+
let(:duration) { 1.23 }
|
|
193
|
+
let(:operation) { 'test' }
|
|
194
|
+
|
|
195
|
+
it 'sends prometheus metrics' do
|
|
196
|
+
expect { subject }.to send_metrics(
|
|
197
|
+
[
|
|
198
|
+
type: 'my',
|
|
199
|
+
labels: { operation_name: operation },
|
|
200
|
+
last_duration_seconds: duration,
|
|
201
|
+
duration_seconds_sum: duration,
|
|
202
|
+
duration_seconds_count: 1
|
|
203
|
+
]
|
|
204
|
+
)
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
collector test
|
|
211
|
+
```ruby
|
|
212
|
+
RSpec.describe Prometheus::MyCollector do
|
|
213
|
+
describe '#collect' do
|
|
214
|
+
subject do
|
|
215
|
+
collector.metrics
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
let(:collector) { described_class.new }
|
|
219
|
+
let(:metric) do
|
|
220
|
+
{
|
|
221
|
+
type: 'my',
|
|
222
|
+
labels: { operation_name: 'test' },
|
|
223
|
+
last_duration_seconds: 1.2,
|
|
224
|
+
duration_seconds_sum: 3.4,
|
|
225
|
+
duration_seconds_count: 1
|
|
226
|
+
}
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
let(:collect_data) do
|
|
230
|
+
collector.collect(metric.deep_stringify_keys)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
it 'observes prometheus metrics' do
|
|
234
|
+
subject
|
|
235
|
+
expect(collector.metrics).to contain_exactly(
|
|
236
|
+
a_gauge_with_expire_metric('my_last_duration_seconds').with(1.2, metric[:labels]),
|
|
237
|
+
a_counter_metric('my_duration_seconds_sum').with(3.4, metric[:labels]),
|
|
238
|
+
a_counter_metric('my_duration_seconds_count').with(1, metric[:labels])
|
|
239
|
+
)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
context 'when collected data is expired' do
|
|
243
|
+
let(:collect_data) do
|
|
244
|
+
super()
|
|
245
|
+
sleep 60.1 # when gauge_with_expire ttl is 60
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
it 'observes empty prometheus metrics' do
|
|
249
|
+
subject
|
|
250
|
+
expect(collector.metrics).to contain_exactly(
|
|
251
|
+
a_gauge_with_expire_metric('my_last_duration_seconds').empty,
|
|
252
|
+
a_counter_metric('my_duration_seconds_sum').with(3.4, metric[:labels]),
|
|
253
|
+
a_counter_metric('my_duration_seconds_count').with(1, metric[:labels])
|
|
254
|
+
)
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## Development
|
|
262
|
+
|
|
263
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
264
|
+
|
|
265
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
266
|
+
|
|
267
|
+
## Contributing
|
|
268
|
+
|
|
269
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/didww/prometheus_exporter-ext.
|
|
270
|
+
|
|
271
|
+
## License
|
|
272
|
+
|
|
273
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'prometheus_exporter/client'
|
|
4
|
+
require_relative '../../ext'
|
|
5
|
+
|
|
6
|
+
module PrometheusExporter::Ext::Instrumentation
|
|
7
|
+
class BaseStats
|
|
8
|
+
class << self
|
|
9
|
+
attr_accessor :type
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(client: PrometheusExporter::Client.default, labels: {})
|
|
13
|
+
@labels = labels.transform_keys(&:to_sym)
|
|
14
|
+
@client = client
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def type
|
|
18
|
+
self.class.type
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def collect
|
|
22
|
+
raise NotImplementedError
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
# @param data [Array,Hash]
|
|
28
|
+
def collect_data(data)
|
|
29
|
+
data_list = data.is_a?(Array) ? data : [data]
|
|
30
|
+
metrics = data_list.map { |data_item| build_metric(data_item) }
|
|
31
|
+
metrics.map { |metric| @client.send_json(metric) }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @param data [Hash]
|
|
35
|
+
# @return [Hash]
|
|
36
|
+
def build_metric(data)
|
|
37
|
+
metric = data.transform_keys(&:to_sym)
|
|
38
|
+
metric[:type] = type
|
|
39
|
+
metric[:labels] ||= {}
|
|
40
|
+
metric[:labels].transform_keys!(&:to_sym)
|
|
41
|
+
metric[:labels].merge!(@labels)
|
|
42
|
+
metric
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base_stats'
|
|
4
|
+
|
|
5
|
+
module PrometheusExporter::Ext::Instrumentation
|
|
6
|
+
class PeriodicStats < BaseStats
|
|
7
|
+
class << self
|
|
8
|
+
def start(frequency: 30, client: PrometheusExporter::Client.default, **)
|
|
9
|
+
raise ArgumentError, 'Expected frequency to be a number' unless frequency.is_a?(Numeric)
|
|
10
|
+
raise ArgumentError, 'Expected frequency to be a positive number' if frequency.negative?
|
|
11
|
+
|
|
12
|
+
klass = self
|
|
13
|
+
stop
|
|
14
|
+
instrumentation = new(client:, **)
|
|
15
|
+
@stop_thread = false
|
|
16
|
+
|
|
17
|
+
@thread = Thread.new do
|
|
18
|
+
until @stop_thread
|
|
19
|
+
begin
|
|
20
|
+
instrumentation.collect
|
|
21
|
+
rescue StandardError => e
|
|
22
|
+
client.logger.error("#{klass} Prometheus Exporter Failed To Collect Stats")
|
|
23
|
+
client.logger.error("#{e.class} #{e.backtrace&.join("\n")}")
|
|
24
|
+
ensure
|
|
25
|
+
sleep frequency
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def started?
|
|
32
|
+
!!@thread&.alive?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def stop
|
|
36
|
+
# to avoid a warning
|
|
37
|
+
@thread = nil unless defined?(@thread)
|
|
38
|
+
|
|
39
|
+
if @thread&.alive?
|
|
40
|
+
@stop_thread = true
|
|
41
|
+
@thread.wakeup
|
|
42
|
+
@thread.join
|
|
43
|
+
end
|
|
44
|
+
@thread = nil
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'periodic_stats'
|
|
4
|
+
require_relative '../proc_self_stat'
|
|
5
|
+
|
|
6
|
+
module PrometheusExporter::Ext::Instrumentation
|
|
7
|
+
class ProcCpu < PeriodicStats
|
|
8
|
+
self.type = 'proc_cpu'
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
def start(type:, labels: {}, **)
|
|
12
|
+
labels = labels.merge(type:)
|
|
13
|
+
|
|
14
|
+
super(labels:, **)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def initialize(...)
|
|
19
|
+
@last_cpu_time = 0.0
|
|
20
|
+
super
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def collect
|
|
24
|
+
stat = PrometheusExporter::Ext::ProcSelfStat.get
|
|
25
|
+
collect_data(
|
|
26
|
+
labels: {
|
|
27
|
+
pid: stat.pid,
|
|
28
|
+
hostname: ::PrometheusExporter.hostname
|
|
29
|
+
},
|
|
30
|
+
usage_seconds_total: stat.cpu_time - @last_cpu_time
|
|
31
|
+
)
|
|
32
|
+
@last_cpu_time = stat.cpu_time
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'prometheus_exporter/metric'
|
|
4
|
+
require_relative '../../ext'
|
|
5
|
+
|
|
6
|
+
module PrometheusExporter::Ext::Metric
|
|
7
|
+
class GaugeWithExpire < PrometheusExporter::Metric::Gauge
|
|
8
|
+
NULLIFY_STRATEGY_UPDATE = ->(labels) { expiration_times[labels] = now_time + ttl }.freeze
|
|
9
|
+
NULLIFY_STRATEGY_EXPIRE = ->(labels) { data.delete(labels) }.freeze
|
|
10
|
+
ZEROING_STRATEGY_UPDATE = ->(labels) do
|
|
11
|
+
if data[labels].zero?
|
|
12
|
+
expiration_times.delete(labels)
|
|
13
|
+
else
|
|
14
|
+
expiration_times[labels] = now_time + ttl
|
|
15
|
+
end
|
|
16
|
+
end.freeze
|
|
17
|
+
ZEROING_STRATEGY_EXPIRE = ->(labels) { data[labels] = 0 }.freeze
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
# @return [Hash]
|
|
21
|
+
# key - strategy name
|
|
22
|
+
# value [Hash] with keys: :on_update, :on_expire
|
|
23
|
+
# :on_update [Proc] yieldparam labels [Hash] - updates expiration_times after data was updated (instance exec)
|
|
24
|
+
# :on_expire [Proc] yieldparam labels [Hash] - updates data after expiration_times was expired (instance exec)
|
|
25
|
+
def strategies
|
|
26
|
+
{
|
|
27
|
+
removing: { on_update: NULLIFY_STRATEGY_UPDATE, on_expire: NULLIFY_STRATEGY_EXPIRE },
|
|
28
|
+
zeroing: { on_update: ZEROING_STRATEGY_UPDATE, on_expire: ZEROING_STRATEGY_EXPIRE }
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def default_ttl
|
|
33
|
+
60
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
attr_reader :ttl, :expiration_times
|
|
38
|
+
|
|
39
|
+
def initialize(name, help, opts = {})
|
|
40
|
+
super(name, help)
|
|
41
|
+
@ttl = opts[:ttl] || self.class.default_ttl
|
|
42
|
+
raise ArgumentError, ':ttl must be numeric' unless ttl.is_a?(Numeric)
|
|
43
|
+
raise ArgumentError, ":ttl must be greater than zero: #{ttl.inspect}" unless ttl.positive?
|
|
44
|
+
|
|
45
|
+
@strategy = self.class.strategies.fetch(opts[:strategy] || :removing) do
|
|
46
|
+
raise ArgumentError, "Unknown strategy: #{opts[:strategy].inspect}"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def reset!
|
|
51
|
+
@expiration_times = {}
|
|
52
|
+
super
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def metric_text
|
|
56
|
+
expire
|
|
57
|
+
super
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def remove(labels)
|
|
61
|
+
result = super
|
|
62
|
+
remove_expired_at(labels)
|
|
63
|
+
result
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def observe(value, labels = {})
|
|
67
|
+
result = super
|
|
68
|
+
value.nil? ? remove_expired_at(labels) : update_expired_at(labels)
|
|
69
|
+
result
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def increment(labels = {}, value = 1)
|
|
73
|
+
result = super
|
|
74
|
+
update_expired_at(labels)
|
|
75
|
+
result
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def decrement(labels = {}, value = 1)
|
|
79
|
+
result = super
|
|
80
|
+
update_expired_at(labels)
|
|
81
|
+
result
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def expire
|
|
85
|
+
now = now_time
|
|
86
|
+
expiration_times.each do |labels, expired_at|
|
|
87
|
+
if expired_at < now
|
|
88
|
+
remove_data_when_expired(labels)
|
|
89
|
+
remove_expired_at(labels)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def to_h
|
|
95
|
+
expire
|
|
96
|
+
super
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def remove_data_when_expired(labels)
|
|
102
|
+
instance_exec(labels, &@strategy[:on_expire])
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def update_expired_at(labels)
|
|
106
|
+
instance_exec(labels, &@strategy[:on_update])
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def remove_expired_at(labels)
|
|
110
|
+
expiration_times.delete(labels)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def now_time
|
|
114
|
+
::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PrometheusExporter::Ext
|
|
4
|
+
# https://man7.org/linux/man-pages/man5/proc_pid_stat.5.html
|
|
5
|
+
class ProcSelfStat
|
|
6
|
+
KERNEL_PAGE_SIZE = `getconf PAGESIZE`.chomp.to_i rescue 4096 # rubocop:disable Style/RescueModifier
|
|
7
|
+
TICKS_PER_SEC = Etc.sysconf(Etc::SC_CLK_TCK)
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
def get
|
|
11
|
+
stat = File.read('/proc/self/stat')
|
|
12
|
+
parts = stat.match(/\A(\d+)\s\((.*)\)\s([A-Z])\s(.*)/)[1..]
|
|
13
|
+
rest = parts.pop.split
|
|
14
|
+
new(*parts, *rest)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
attr_reader :pid,
|
|
19
|
+
:comm,
|
|
20
|
+
:state,
|
|
21
|
+
:utime,
|
|
22
|
+
:stime,
|
|
23
|
+
:starttime,
|
|
24
|
+
:vsize,
|
|
25
|
+
:rss
|
|
26
|
+
|
|
27
|
+
def initialize(*fields)
|
|
28
|
+
@pid = fields[0]
|
|
29
|
+
@comm = fields[1]
|
|
30
|
+
@state = fields[2]
|
|
31
|
+
@utime = Integer(fields[13])
|
|
32
|
+
@stime = Integer(fields[14])
|
|
33
|
+
@starttime = Integer(fields[21])
|
|
34
|
+
@vsize = Integer(fields[22])
|
|
35
|
+
@rss = Integer(fields[23])
|
|
36
|
+
@fields = fields
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @return [Float]
|
|
40
|
+
def cpu_time
|
|
41
|
+
(utime + stime).to_f / TICKS_PER_SEC
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @return [Integer]
|
|
45
|
+
def rss_bytes
|
|
46
|
+
rss * KERNEL_PAGE_SIZE
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def to_a
|
|
50
|
+
@fields.dup
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def to_h
|
|
54
|
+
{
|
|
55
|
+
pid:,
|
|
56
|
+
comm:,
|
|
57
|
+
state:,
|
|
58
|
+
utime:,
|
|
59
|
+
stime:,
|
|
60
|
+
starttime:,
|
|
61
|
+
vsize:,
|
|
62
|
+
rss:
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def to_s
|
|
67
|
+
"#<#{self.class.name} #{to_h.map { |k, v| "#{k}=#{v.inspect}" }.join(' ')}>"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def inspect
|
|
71
|
+
to_s
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'date'
|
|
4
|
+
require_relative 'metric_matcher'
|
|
5
|
+
require_relative 'send_metrics_matcher'
|
|
6
|
+
|
|
7
|
+
module PrometheusExporter::Ext::RSpec
|
|
8
|
+
module Matchers
|
|
9
|
+
def send_metrics(*expected)
|
|
10
|
+
expected = nil if expected.empty?
|
|
11
|
+
PrometheusExporter::Ext::RSpec::SendMetricsMatcher.new(expected)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def a_prometheus_metric(klass, name)
|
|
15
|
+
MetricMatcher.new(klass, name)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def a_gauge_metric(name)
|
|
19
|
+
a_prometheus_metric(PrometheusExporter::Metric::Gauge, name)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def a_gauge_with_expire_metric(name)
|
|
23
|
+
a_prometheus_metric(PrometheusExporter::Ext::Metric::GaugeWithExpire, name)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def a_counter_metric(name)
|
|
27
|
+
a_prometheus_metric(PrometheusExporter::Metric::Counter, name)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def a_histogram_metric(name)
|
|
31
|
+
a_prometheus_metric(PrometheusExporter::Metric::Histogram, name)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def a_summary_metric(name)
|
|
35
|
+
a_prometheus_metric(PrometheusExporter::Metric::Summary, name)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PrometheusExporter::Ext::RSpec
|
|
4
|
+
class MetricMatcher
|
|
5
|
+
include RSpec::Matchers::DSL::DefaultImplementations
|
|
6
|
+
include RSpec::Matchers
|
|
7
|
+
include RSpec::Matchers::Composable
|
|
8
|
+
|
|
9
|
+
attr_reader :metric_class, :metric_name, :metric_payload, :actual
|
|
10
|
+
|
|
11
|
+
def initialize(metric_class, metric_name)
|
|
12
|
+
@metric_class = metric_class
|
|
13
|
+
@metric_name = metric_name.to_s
|
|
14
|
+
@metric_payload = nil
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def name
|
|
18
|
+
'be a prometheus metric'
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def expected
|
|
22
|
+
"#{metric_class}(name=#{metric_name}, to_h=#{description_of(metric_payload)})"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def matches?(actual)
|
|
26
|
+
@actual = actual
|
|
27
|
+
|
|
28
|
+
return false unless values_match?(metric_class, actual.class)
|
|
29
|
+
return false unless values_match?(metric_name, actual.name.to_s)
|
|
30
|
+
|
|
31
|
+
actual_payload = actual.to_h.transform_keys { |labels| labels.transform_keys(&:to_s) }
|
|
32
|
+
return false if !metric_payload.nil? && !values_match?(metric_payload, actual_payload)
|
|
33
|
+
|
|
34
|
+
true
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def with(value, labels)
|
|
38
|
+
@metric_payload ||= {}
|
|
39
|
+
metric_payload[labels.transform_keys(&:to_s)] = value
|
|
40
|
+
self
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def empty
|
|
44
|
+
@metric_payload = {}
|
|
45
|
+
self
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def description_of(object)
|
|
49
|
+
RSpec::Support::ObjectFormatter.new(nil).format(object)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PrometheusExporter::Ext::RSpec
|
|
4
|
+
class SendMetricsMatcher
|
|
5
|
+
include RSpec::Matchers::DSL::DefaultImplementations
|
|
6
|
+
include RSpec::Matchers
|
|
7
|
+
include RSpec::Matchers::Composable
|
|
8
|
+
|
|
9
|
+
attr_reader :expected, :actual
|
|
10
|
+
|
|
11
|
+
# @param expected [Array<Hash(Symbol)>,nil]
|
|
12
|
+
def initialize(expected)
|
|
13
|
+
@expected = expected
|
|
14
|
+
@ordered = false
|
|
15
|
+
@times = nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def name
|
|
19
|
+
'sends metrics to prometheus'
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def supports_block_expectations?
|
|
23
|
+
true
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def matches?(actual_proc)
|
|
27
|
+
raise ArgumentError, "#{name} matcher supports only block expectations" unless actual_proc.is_a?(Proc)
|
|
28
|
+
|
|
29
|
+
metrics_before = PrometheusExporter::Ext::RSpec::TestClient.instance.metrics
|
|
30
|
+
actual_proc.call
|
|
31
|
+
metrics_after = PrometheusExporter::Ext::RSpec::TestClient.instance.metrics - metrics_before
|
|
32
|
+
@actual = metrics_after.map { |metric| deep_symbolize_keys(metric) }
|
|
33
|
+
|
|
34
|
+
if expected
|
|
35
|
+
expected_value = @ordered ? expected : match_array(expected)
|
|
36
|
+
values_match?(expected_value, actual)
|
|
37
|
+
elsif @times
|
|
38
|
+
values_match?(@times, actual.size)
|
|
39
|
+
else
|
|
40
|
+
actual.size >= 1
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def failure_message
|
|
45
|
+
if expected
|
|
46
|
+
expected_value = @ordered ? expected : match_array(expected)
|
|
47
|
+
+"expected #{name} to receive #{description_of(expected_value)}, but got\n #{description_of(actual)}"
|
|
48
|
+
elsif @times
|
|
49
|
+
values_match?(@times, actual.size)
|
|
50
|
+
+"expected #{name} to receive #{@times} metrics, but got #{actual.size}\n #{description_of(actual)}"
|
|
51
|
+
else
|
|
52
|
+
actual.size
|
|
53
|
+
+"expected #{name} to receive more than 1 metric, but got #{actual.size}\n #{description_of(actual)}"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def ordered
|
|
58
|
+
raise ArgumentError, 'ordered cannot be when expected not provided' if expected.nil?
|
|
59
|
+
raise ArgumentError, 'ordered cannot be used with times' if @times
|
|
60
|
+
|
|
61
|
+
@ordered = true
|
|
62
|
+
self
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def times(qty)
|
|
66
|
+
raise ArgumentError, 'times argument must be an integer' unless qty.is_a?(Integer)
|
|
67
|
+
raise ArgumentError, 'times argument must be >= 1' unless qty >= 1
|
|
68
|
+
raise ArgumentError, 'ordered cannot be when expected is provided' unless expected.nil?
|
|
69
|
+
raise ArgumentError, 'ordered cannot be used with times' if @ordered
|
|
70
|
+
|
|
71
|
+
@times = qty
|
|
72
|
+
self
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def description_of(object)
|
|
76
|
+
RSpec::Support::ObjectFormatter.new(nil).format(object)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def deep_symbolize_keys(hash)
|
|
82
|
+
new_hash = {}
|
|
83
|
+
hash.each do |k, v|
|
|
84
|
+
new_hash[k.to_sym] = v.is_a?(Hash) ? deep_symbolize_keys(v) : v
|
|
85
|
+
end
|
|
86
|
+
new_hash
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'singleton'
|
|
4
|
+
|
|
5
|
+
module PrometheusExporter::Ext::RSpec
|
|
6
|
+
class TestClient
|
|
7
|
+
include Singleton
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
super
|
|
11
|
+
reset
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def metrics
|
|
15
|
+
@metrics.dup
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def send_json(data)
|
|
19
|
+
@metrics << data
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def reset
|
|
23
|
+
@metrics = []
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def stop
|
|
27
|
+
nil
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'prometheus_exporter/client'
|
|
4
|
+
require_relative 'rspec/matchers'
|
|
5
|
+
require_relative 'rspec/test_client'
|
|
6
|
+
|
|
7
|
+
# Setups default client before it used anywhere.
|
|
8
|
+
# use `include_examples :observes_prometheus_metrics` in specs
|
|
9
|
+
PrometheusExporter::Client.default = PrometheusExporter::Ext::RSpec::TestClient.instance
|
|
10
|
+
|
|
11
|
+
RSpec.configure do |config|
|
|
12
|
+
config.include PrometheusExporter::Ext::RSpec::Matchers
|
|
13
|
+
|
|
14
|
+
config.before do
|
|
15
|
+
PrometheusExporter::Ext::RSpec::TestClient.instance.reset
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'prometheus_exporter/metric'
|
|
4
|
+
require_relative '../metric/gauge_with_expire'
|
|
5
|
+
|
|
6
|
+
module PrometheusExporter::Ext::Server
|
|
7
|
+
module BaseCollectorMethods
|
|
8
|
+
class << self
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def included(klass)
|
|
12
|
+
super
|
|
13
|
+
klass.singleton_class.attr_accessor :type, :registered_metrics
|
|
14
|
+
klass.registered_metrics = {}
|
|
15
|
+
klass.extend ClassMethods
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
module ClassMethods
|
|
20
|
+
# Registers metric observer.
|
|
21
|
+
# @param name [Symbol] metric name.
|
|
22
|
+
# @param help [String] metric description.
|
|
23
|
+
# @param metric_class [Class<PrometheusExporter::Metric::Base>] observer class.
|
|
24
|
+
# @param args [Array] additional arguments for observer class.
|
|
25
|
+
# rubocop:disable Metrics/ParameterLists
|
|
26
|
+
def register_metric(name, help, metric_class, *args)
|
|
27
|
+
# rubocop:enable Metrics/ParameterLists
|
|
28
|
+
name = name.to_s
|
|
29
|
+
raise ArgumentError, "metric #{name} is already registered" if registered_metrics.key?(name)
|
|
30
|
+
|
|
31
|
+
registered_metrics[name] = { help:, metric_class:, args: }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Registers PrometheusExporter::Metric::Counter observer.
|
|
35
|
+
# @param name [Symbol] metric name.
|
|
36
|
+
# @param help [String] metric description.
|
|
37
|
+
def register_counter(name, help)
|
|
38
|
+
register_metric(name, help, PrometheusExporter::Metric::Counter)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Registers PrometheusExporter::Metric::Gauge observer.
|
|
42
|
+
# @param name [Symbol] metric name.
|
|
43
|
+
# @param help [String] metric description.
|
|
44
|
+
def register_gauge(name, help)
|
|
45
|
+
register_metric(name, help, PrometheusExporter::Metric::Gauge)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Registers PrometheusExporter::Metric::Summary observer.
|
|
49
|
+
# @param name [Symbol] metric name.
|
|
50
|
+
# @param help [String] metric description.
|
|
51
|
+
# @param opts [Hash] additional options, supports `quantiles` key.
|
|
52
|
+
def register_summary(name, help, opts = {})
|
|
53
|
+
register_metric(name, help, PrometheusExporter::Metric::Summary, opts)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Registers PrometheusExporter::Metric::Histogram observer.
|
|
57
|
+
# @param name [Symbol] metric name.
|
|
58
|
+
# @param help [String] metric description.
|
|
59
|
+
# @param opts [Hash] additional options, supports `buckets` key.
|
|
60
|
+
def register_histogram(name, help, opts = {})
|
|
61
|
+
register_metric(name, help, PrometheusExporter::Metric::Histogram, opts)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @return [String]
|
|
66
|
+
def type
|
|
67
|
+
self.class.type
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
# Adds metrics to observers with matched name.
|
|
73
|
+
# @param observers [Hash] returned by #build_observers.
|
|
74
|
+
# @param obj [Hash] metric data.
|
|
75
|
+
def fill_observers(observers, obj)
|
|
76
|
+
observers.each do |name, observer|
|
|
77
|
+
value = obj[name]
|
|
78
|
+
observer.observe(value, obj['labels']) if value
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Generally metrics sent via PrometheusExporter::Ext::Instrumentation::BaseStats populate labels to `labels` key.
|
|
83
|
+
# But PrometheusExporter::Client populate it's own labels to `custom_labels` key.
|
|
84
|
+
# Here we merge them into single `labels` key.
|
|
85
|
+
# @param obj [Hash]
|
|
86
|
+
# @return [Hash]
|
|
87
|
+
def normalize_labels(obj)
|
|
88
|
+
obj['labels'] ||= {}
|
|
89
|
+
custom_labels = obj.delete('custom_labels')
|
|
90
|
+
obj['labels'].merge!(custom_labels) if custom_labels
|
|
91
|
+
obj
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# @return [Hash] key is metric name, value is observer.
|
|
95
|
+
def build_observers
|
|
96
|
+
observers = {}
|
|
97
|
+
self.class.registered_metrics.each do |name, metric|
|
|
98
|
+
observers[name] = metric[:metric_class].new("#{type}_#{name}", metric[:help], *metric[:args])
|
|
99
|
+
end
|
|
100
|
+
observers
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'prometheus_exporter/server/metrics_container'
|
|
4
|
+
require_relative 'base_collector_methods'
|
|
5
|
+
|
|
6
|
+
module PrometheusExporter::Ext::Server
|
|
7
|
+
module ExpiredStatsCollector
|
|
8
|
+
class << self
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def included(klass)
|
|
12
|
+
super
|
|
13
|
+
klass.include BaseCollectorMethods
|
|
14
|
+
klass.singleton_class.attr_accessor :filter, :ttl
|
|
15
|
+
klass.ttl = 60
|
|
16
|
+
klass.extend ClassMethods
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
module ClassMethods
|
|
21
|
+
# Defines a rule how old metric will be replaced with new one.
|
|
22
|
+
# @yield compare new metric with existing one.
|
|
23
|
+
# @yieldparam new_metric [Hash] new metric data.
|
|
24
|
+
# @yieldparam old_metric [Hash] existing metric data.
|
|
25
|
+
# @yieldreturn [Boolean] if true existing metric will be replaced with new one.
|
|
26
|
+
def unique_metric_by(&block)
|
|
27
|
+
@filter = block
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def initialize
|
|
32
|
+
super
|
|
33
|
+
@data = PrometheusExporter::Server::MetricsContainer.new(
|
|
34
|
+
ttl: self.class.ttl,
|
|
35
|
+
filter: self.class.filter
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Returns all metrics collected by this collector.
|
|
40
|
+
# @return [Array<PrometheusExporter::Metric::Base>]
|
|
41
|
+
def metrics
|
|
42
|
+
observers = build_observers
|
|
43
|
+
@data.each do |obj|
|
|
44
|
+
fill_observers(observers, obj)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
observers.values
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Collects metric data received from client.
|
|
51
|
+
# @param obj [Hash] metric data.
|
|
52
|
+
def collect(obj)
|
|
53
|
+
normalize_labels(obj)
|
|
54
|
+
@data << obj
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'stats_collector'
|
|
4
|
+
|
|
5
|
+
module PrometheusExporter::Ext::Server
|
|
6
|
+
class ProcCpuCollector < PrometheusExporter::Server::TypeCollector
|
|
7
|
+
include ::PrometheusExporter::Ext::Server::StatsCollector
|
|
8
|
+
self.type = 'proc_cpu'
|
|
9
|
+
|
|
10
|
+
register_counter :usage_seconds_total, 'Cumulative CPU time consumed by the process in core-seconds'
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'prometheus_exporter/server/web_server'
|
|
4
|
+
require_relative '../../ext'
|
|
5
|
+
|
|
6
|
+
module PrometheusExporter::Ext::Server
|
|
7
|
+
class SingletonWebServer < PrometheusExporter::Server::WebServer
|
|
8
|
+
class << self
|
|
9
|
+
attr_accessor :server
|
|
10
|
+
|
|
11
|
+
def start(opts)
|
|
12
|
+
self.server = new(opts)
|
|
13
|
+
server.start
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def stop
|
|
17
|
+
server.stop
|
|
18
|
+
self.server = nil
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def build_htpasswd(htpasswd_path, username:, password:)
|
|
22
|
+
htpasswd = WEBrick::HTTPAuth::Htpasswd.new(htpasswd_path)
|
|
23
|
+
htpasswd.set_passwd PrometheusExporter::DEFAULT_REALM, username, password
|
|
24
|
+
htpasswd.flush
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @yieldparam collector [PrometheusExporter::Server::Collector]
|
|
28
|
+
def configure_collector
|
|
29
|
+
yield server.collector
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base_collector_methods'
|
|
4
|
+
|
|
5
|
+
module PrometheusExporter::Ext::Server
|
|
6
|
+
module StatsCollector
|
|
7
|
+
class << self
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def included(klass)
|
|
11
|
+
super
|
|
12
|
+
klass.include BaseCollectorMethods
|
|
13
|
+
klass.extend ClassMethods
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
module ClassMethods
|
|
18
|
+
# Registers PrometheusExporter::Metric::GaugeWithExpire observer.
|
|
19
|
+
# @param name [Symbol] metric name.
|
|
20
|
+
# @param help [String] metric description.
|
|
21
|
+
# @param opts [Hash] additional options, supports `ttl` and `strategy` keys.
|
|
22
|
+
def register_gauge_with_expire(name, help, opts = {})
|
|
23
|
+
register_metric(name, help, PrometheusExporter::Ext::Metric::GaugeWithExpire, opts)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def initialize
|
|
28
|
+
super
|
|
29
|
+
@observers = build_observers
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Returns all metrics collected by this collector.
|
|
33
|
+
# @return [Array<PrometheusExporter::Metric::Base>]
|
|
34
|
+
def metrics
|
|
35
|
+
@observers.values
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Collects metric data received from client.
|
|
39
|
+
# @param obj [Hash] metric data.
|
|
40
|
+
def collect(obj)
|
|
41
|
+
obj = normalize_labels(obj)
|
|
42
|
+
fill_observers(@observers, obj)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: prometheus_exporter-ext
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2
|
|
4
|
+
version: 0.3.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Denis Talakevich
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-06-11 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: prometheus_exporter
|
|
@@ -30,7 +30,28 @@ email:
|
|
|
30
30
|
executables: []
|
|
31
31
|
extensions: []
|
|
32
32
|
extra_rdoc_files: []
|
|
33
|
-
files:
|
|
33
|
+
files:
|
|
34
|
+
- CHANGELOG.md
|
|
35
|
+
- LICENSE.txt
|
|
36
|
+
- README.md
|
|
37
|
+
- Rakefile
|
|
38
|
+
- lib/prometheus_exporter/ext.rb
|
|
39
|
+
- lib/prometheus_exporter/ext/instrumentation/base_stats.rb
|
|
40
|
+
- lib/prometheus_exporter/ext/instrumentation/periodic_stats.rb
|
|
41
|
+
- lib/prometheus_exporter/ext/instrumentation/proc_cpu.rb
|
|
42
|
+
- lib/prometheus_exporter/ext/metric/gauge_with_expire.rb
|
|
43
|
+
- lib/prometheus_exporter/ext/proc_self_stat.rb
|
|
44
|
+
- lib/prometheus_exporter/ext/rspec.rb
|
|
45
|
+
- lib/prometheus_exporter/ext/rspec/matchers.rb
|
|
46
|
+
- lib/prometheus_exporter/ext/rspec/metric_matcher.rb
|
|
47
|
+
- lib/prometheus_exporter/ext/rspec/send_metrics_matcher.rb
|
|
48
|
+
- lib/prometheus_exporter/ext/rspec/test_client.rb
|
|
49
|
+
- lib/prometheus_exporter/ext/server/base_collector_methods.rb
|
|
50
|
+
- lib/prometheus_exporter/ext/server/expired_stats_collector.rb
|
|
51
|
+
- lib/prometheus_exporter/ext/server/proc_cpu_collector.rb
|
|
52
|
+
- lib/prometheus_exporter/ext/server/singleton_web_server.rb
|
|
53
|
+
- lib/prometheus_exporter/ext/server/stats_collector.rb
|
|
54
|
+
- lib/prometheus_exporter/ext/version.rb
|
|
34
55
|
homepage: https://github.com/senid231/prometheus_exporter-ext
|
|
35
56
|
licenses:
|
|
36
57
|
- MIT
|
|
@@ -54,7 +75,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
54
75
|
- !ruby/object:Gem::Version
|
|
55
76
|
version: '0'
|
|
56
77
|
requirements: []
|
|
57
|
-
rubygems_version: 3.5.
|
|
78
|
+
rubygems_version: 3.5.16
|
|
58
79
|
signing_key:
|
|
59
80
|
specification_version: 4
|
|
60
81
|
summary: Extended Prometheus Exporter for Ruby
|